在并发编程中,需要处理的两个关键问题:线程之间如何通信以及线程之间如何同步

通信是指线程之间以或者机制交换信息,java的并发采用的是共享内存模型,线程之间共享程序的公共状态,通过读写内存中的公共状态进行隐式通信。

同步是是指程序中用于控制不同线程间操作发生相对顺序的机制。

 

最开始首先应该知道计算机中的缓存在其中起的作用

CPU Cache(高速缓存):由于计算机的存储设备与处理器的处理设备有着几个数量级的差距,所以现代计 算机都会加入一层读写速度与处理器处理速度接近相同的高级缓存来作为内存与处理器之间的缓冲,将运 算使用到的数据复制到缓存中,让运算能够快速的执行,当运算结束后,再从缓存同步到内存之中,这 样,CPU就不需要等待缓慢的内存读写了。

主(内)存:一个计算机包含一个主存,所有的CPU都可以访问主 存,主存比缓存容量大的多(CPU访问缓存层的速度快于访问主存的速度!但通常比访问内存寄存器的速度还是要慢点)

运作原理:通常情况下,当一个CPU要读取主存(RAM - Main Mernory)的时候,他会将主存中的数据读 取到CPU缓存中,甚至将缓存内容读到内部寄存器里面,然后再寄存器执行操作,当运行结束后,会 将寄存器中的值刷新回缓存中,并在某个时间点将值刷新回主存。

线程安全—可见性和有序性

为什么需要CPU Cache?

 答:CPU 的频率太快了,快到主存跟不上,这样在处理器时钟周期内,CPU常常需要等待主存,浪费资源。 所以cache 的出现,是为了缓解 CPU 和内存之间速度的不匹配问题 结构:cpu-> cache-> memory).

 

什么是java的内存模型?

共享变量:一个变量可以被多个线程使用,那么这个变量就是这几个线程的共享变量。
Java Memory Model (JAVA 内存模型)描述线程之间如何通过内存(memory)来进行交互,描述了java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节。 
具体说来, JVM中存在一个主存区(Main Memory或Java Heap Memory),对于所有线程进行共享,但线程不能直接操作主内存中的变量,每个线程都有自己独立的工作内存(Working Memory),里面保存该线程使用到的变量的副本( 主内存中该变量的一份拷贝 )
规定:线程对共享变量的读写都必须在自己的工作内存中进行,而不能直接在主内存中读写。不同线程不能直接访问其他线程的工作内存中的变量,线程间变量值的传递需要主内存作为桥梁。 
线程安全—可见性和有序性
 
Heap(堆):Java里的堆是一个运行时的数据区,堆是由垃圾回收来负责的。实例域、静态域、和数组元素都存储在堆内存中。
Stack(栈):栈的优势是存取速度比堆要快,仅次于计算机里的寄存器。局部变量、方法参数、对象的引用存储在栈中。
 
 
java内存模型的抽象结构图:
 
 
线程安全—可见性和有序性

 每个线程之间共享变量都存放在主内存里面,每个线程都有一个私有的本地内存,本地内存是Java内存模型中抽象的概念,并不是真实存在的(他涵盖了缓存写缓冲区。寄存器,以及其他硬件的优化) 本地内存中存储了以读或者写共享变量的拷贝的一个副本。

注意:由于工作内存(缓冲区)仅对自己的处理器可见,它会导致处理器质质性内存操作的顺序可能会与内存实际的操作顺序不一致,内存的操作顺序被重排序了,这是与后面讲的指令重排序不同的另一种重排序。

 
什么是内存的可见性?
可见性:一个线程对共享变量值得修改,能够及时的被其他线程看到 
线程可见性原理: 
线程一对共享变量的改变想要被线程二看见,就必须执行下面两个步骤:
①将工作内存1中的共享变量的改变更新到主内存中
②将主内存中最新的共享变量的变化更新到工作内存2中。
 
线程安全—可见性和有序性
指令重排序:代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化。

1.编译器优化的重排序(编译器优化)

2.指令级并行重排序(处理器优化)

3.内存系统的重排序(处理器优化)

是不是所有的语句的执行顺序都可以重排呢?

答案是否定的。为了讲清楚这个问题,先讲解另一个概念:数据依赖性

什么是数据依赖性?

如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖。数据依赖分下列三种类型:

名称 代码示例 说明
写后读 a = 1;b = a; 写一个变量之后,再读这个位置。
写后写 a = 1;a = 2; 写一个变量之后,再写这个变量。
读后写 a = b;b = 1; 读一个变量之后,再写这个变量。

上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果将会被改变。所以,编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。也就是说:在单线程环境下,指令执行的最终效果应当与其在顺序执行下的效果一致,否则这种优化便会失去意义。这句话有个专业术语叫做as-if-serial semantics (as-if-serial语义)

int num1=1;//第一行
int num2=2;//第二行
int sum=num1+num;//第三行

  • 单线程:第一行和第二行可以重排序,但第三行不行
  • 重排序不会给单线程带来内存可见性问题
  • 多线程中程序交错执行时,重排序可能会照成内存可见性问题。

可见性分析:

导致共享变量在线程间不可见的原因:

  1. 线程的交叉执行
  2. 重排序结合线程交叉执行
  3. 共享变量更新后的值没有在工作内存与主内存间及时更新

 

重排序对多线程的影响
class ReorderExample {
    int a = 0;
    boolean flag = false;
 
    public void writer() {
        a = 1;          // 1
        flag = true;    // 2
    }
 
    public void reader() {
        if (flag) {            // 3
            int i = a * a; // 4
        }
    }
}
flag变量是个标记,用来标识变量a是否已被写入。这里假设有两个线程A和B,A首先执行writer()方法,随后B线程接着执行reader()方法。线程B在执行操作4时,能否看到线程A在操作1对共享变量a的写入?

答案是:不一定能看到。

由于操作1和操作2没有数据依赖关系,编译器和处理器可以对这两个操作重排序;同样,操作3和操作4没有数据依赖关系,编译器和处理器也可以对这两个操作重排序。让我们先来看看,当操作1和操作2重排序时,可能会产生什么效果?

执行顺序是:2 -> 3 -> 4 -> 1 (这是完全存在并且合理的一种顺序,如果你不能理解,请先了解CPU是如何对多个线程进行时间分配的)
 

操作3和操作4重排序后,因为操作3和操作4存在控制依赖关系。当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜测(Speculation)执行来克服控制相关性对并行度的影响。以处理器的猜测执行为例,执行线程B的处理器可以提前读取并计算a*a,然后把计算结果临时保存到一个名为重排序缓冲(reorder buffer ROB)的硬件缓存中。当接下来操作3的条件判断为真时,就把该计算结果写入变量i中。

我们可以看出,猜测执行实质上对操作3和4做了重排序。重排序在这里破坏了多线程程序的语义!

 
同步(synchronization)就是指一个线程访问数据时,其它线程不得对同一个数据进行访问,即同一时刻只能有一个线程访问该数据,当这一线程访问结束时其它线程才能对这它进行访问。
线程安全—可见性和有序性
package com.xidian.count;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

import com.xidian.annotations.ThreadSafe;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@ThreadSafe
public class CountExample3 {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    public static int count = 0;

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count);
    }

    private synchronized static void add() {
        count++;
    }
}
View Code

相关文章: