Java并发基础概念

线程的同步与互斥

线程通信

并发编程中有一个关键问题就是线程间如何进行通信。通信指的是线程间以何种机制来交换信息,常见的解决方式有两种,一种是共享内存,一种是消息传递。
Java的并发采用的是共享内存模型,我们设置一个共享变量,然后多个线程去操作同一个共享变量,从而达到了线程间通信的目的,这种通信是隐式的。而消息传递采用的线程间的直接通信,不同的线程通过显式的发送消息来达到交互目的,这种方式最有名的的实现就是Actor模型(akka框架有实现),各个actor都有一个消息队列来接受其他actor传递的消息,actor根据消息来维护自己的状态。

线程同步

是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。

线程互斥

是指对于共享资源,在各线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。

两者的区别

互斥是通过竞争对资源的独占使用,彼此之间不需要知道对方的存在,执行顺序是一个乱序。同步是协调多个相互关联线程合作完成任务,彼此之间知道对方存在,执行顺序往往是有序的。

对于Java锁的理解

也许Java语法层面将锁包装成了sycnchronized或者明确的XXXLock,但是底层都是一样的。无非就是哪种写起来方便而已。锁就是锁而已,避免多个线程对同一个共享的数据并发修改带来的数据混乱。
锁要解决的大概就只有这4个问题 :

  1. 谁拿到了锁“这个信息存哪里(可以是当前class,当前instance的markword,还可以是某个具体的Lock的实例)
  2. 谁能抢到锁的规则(只能一个人抢到 - Mutex;能抢有限多个数量 - Semphore;自己可以反复抢 - 重入锁;读可以反复抢到但是写独占 - 读写锁……)
  3. 抢不到时怎么办(抢不到玩命抢;抢不到暂时睡着,等一段时间再试/等通知再试;或者二者的结合,先玩命抢几次,还没抢到就睡着)
  4. 如果锁被释放了还有其他等待锁的怎么办(不管,让等的线程通过超时机制自己抢;按照一定规则通知某一个等待的线程;通知所有线程唤醒他们,让他们一起抢……)

有了这些选择,你就可以按照业务需求组装出你需要锁。

恰当的使用锁,可以解决同步或者互斥的问题。你可以说Mutex是专门被设计来解决互斥的;Barrier,Semphore是专门来解决同步的。但是这些都离不开上述对上述4个问题的处理。同时,如果遇到了其他的具体的并发问题,你也可以定制一个锁来满足需要。

Java内存模型(JMM)

JMM抽象图示

图片来自:http://www.importnew.com/24082.html
Java并发基础概念_第1张图片
线程之间的共享变量存储在主内存中,每个线程都有一个私有的工作内存,工作内存中存储了该线程以读/写共享变量的副本。工作内存是JMM的一个抽象概念,它涵盖了缓存、写缓冲区、寄存器以及其他硬件和编译器优化。如果上图的线程A想要和线程B进行通信,那么线程A先要将自己的工作内存更新过的共享变量刷新到主内存中去,然后线程B到主内存取出该变量并保存到自己的工作内存中。

从运行时数据区理解JMM

图片来自:http://tutorials.jenkov.com/java-concurrency/java-memory-model.html
Java并发基础概念_第2张图片
这张图片解释的更加细致,是从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种:

  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原则(先行发生原则):

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

参考资料:

  • 线程通信机制—共享内存:消息传递
  • Java中线程同步锁和互斥锁有啥区别? - chen Kingwen的回答 - 知乎 (解释了互斥和同步的含义)
  • Java中线程同步锁和互斥锁有啥区别? - 大宽宽的回答 - 知乎(通俗的解释了锁的内涵)
  • Java Concurrency(Java并发教学网站,非常棒,需要反复看反复学习)
  • Java多线程中提到的原子性和可见性、有序性

你可能感兴趣的:(Java精华笔记)