并发编程中有一个关键问题就是线程间如何进行通信。通信指的是线程间以何种机制来交换信息,常见的解决方式有两种,一种是共享内存,一种是消息传递。
Java的并发采用的是共享内存模型,我们设置一个共享变量,然后多个线程去操作同一个共享变量,从而达到了线程间通信的目的,这种通信是隐式的。而消息传递采用的线程间的直接通信,不同的线程通过显式的发送消息来达到交互目的,这种方式最有名的的实现就是Actor模型(akka框架有实现),各个actor都有一个消息队列来接受其他actor传递的消息,actor根据消息来维护自己的状态。
是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
是指对于共享资源,在各线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。
互斥是通过竞争对资源的独占使用,彼此之间不需要知道对方的存在,执行顺序是一个乱序。同步是协调多个相互关联线程合作完成任务,彼此之间知道对方存在,执行顺序往往是有序的。
也许Java语法层面将锁包装成了sycnchronized或者明确的XXXLock,但是底层都是一样的。无非就是哪种写起来方便而已。锁就是锁而已,避免多个线程对同一个共享的数据并发修改带来的数据混乱。
锁要解决的大概就只有这4个问题 :
有了这些选择,你就可以按照业务需求组装出你需要锁。
恰当的使用锁,可以解决同步或者互斥的问题。你可以说Mutex是专门被设计来解决互斥的;Barrier,Semphore是专门来解决同步的。但是这些都离不开上述对上述4个问题的处理。同时,如果遇到了其他的具体的并发问题,你也可以定制一个锁来满足需要。
图片来自:http://www.importnew.com/24082.html
线程之间的共享变量存储在主内存中,每个线程都有一个私有的工作内存,工作内存中存储了该线程以读/写共享变量的副本。工作内存是JMM的一个抽象概念,它涵盖了缓存、写缓冲区、寄存器以及其他硬件和编译器优化。如果上图的线程A想要和线程B进行通信,那么线程A先要将自己的工作内存更新过的共享变量刷新到主内存中去,然后线程B到主内存取出该变量并保存到自己的工作内存中。
图片来自:http://tutorials.jenkov.com/java-concurrency/java-memory-model.html
这张图片解释的更加细致,是从JVM运行时数据区的角度进行的解释:上文中的共享内存实际上就是JVM运行时数据区中的堆;每个线程都会有自己的虚拟机栈,线程中的方法在执行的时候会在该线程的栈中新建一个栈帧,用于存储局部变量表、操作数栈等,这个部分对应上文中提到的每个线程的工作内存。
Java内存模型的关键技术点都是围绕着多线程的原子性、可见性、有序性来建立的。接下来进行简单的介绍:
一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
x = 10; //语句1
y = x; //语句2
x++; //语句3
x = x + 1; //语句4
这些例子是最常见的原子性问题,上面的语句中只有语句1是原子的;语句2包含了读x和写y两个操作;语句3最容易产生混淆,i++等价于i=i+1,和语句4是相同的,它们分别包含3个操作,读i,对i进行加1,然后写i。
总结来说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。
可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。常用的用来保证可见性的方法是volatile关键字,它可以保证被volatile关键字修饰的共享变量被修改后,立即写回主存,其他线程在读取这个volatile修饰的变,量的时候,不会去工作线程中读取,而是直接从主存中读。
除了volatile之外,Java还有两个关键字能实现可见性,它们是synchronized,Lock:同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)”这条规则获得的。而final关键字的可见性是指,被final修饰的字段是构造器一旦初始化完成,并且构造器没有把“this”引用传递出去,那么在其它线程中就能看见final字段的值。
有序性是指程序执行的顺序按照代码的先后顺序执行。
int i = 0;
boolean flag = false;
i = 1; //语句1
flag = true; //语句2
上面的这个例子中,语句1不一定在语句2之前执行,因为会发生指令重排。
重排序分为3种:
指令重排不会影响单个线程的执行,但是会影响到线程并发执行的正确性。在Java中可以通过synchronized和Lock来保证有序性,它们保证了每个时刻只有线程执行同步代码,相当于让线程顺序执行同步代码,自然保证了有序性。
volatile可以保证部分的有序性,例如:
int a=1;
int b=2;
volatile int c=3;
int d=4;
a,b一定会发生在c之前,因为volatile会添加内存屏障,保证了程序部分的有序性,为什么说是部分,因为a,b之间的发生顺序是不确定的。
Java内存模型具备一些先天的“有序性”,这个有序性称为happens-before原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么就无法保证它们的有序性。
happens-before原则(先行发生原则):
参考资料: