八、JVM视角浅理解并发和锁
根据《深入理解java虚拟机》这本书总结
提到java的并发和锁,第一反应可能回想到多线程、synchronized关键字等,那么对于jvm虚拟机,这些是如何实现的呢?或者用的什么思想实现的?
一、JAVA内存模型
为什么要谈到内存模型?并发编程和锁要解决的问题就是同步的问题,抛开java代码,虚拟机自身是如何实现单线程甚至是多线程保证同步的。这就需要对内存模型又了解,虚拟机如何读取、修改、保存数据,在多线程的情况下,又是如何使这些操作安全,这就是了解内存模型的出发点。
内存模型?jvm运行时内存?这里的java内存模型跟jvm内存时的区域划分个人感觉也是有一些联系的。java内存模型主要分:主内存和工作内存。其中主内存属于线程共享的区域,可以跟运行内存的堆和方法去对应,工作内存则是线程运行时的私有内存,可以跟运行时内存的栈对应起来。
主内存?主内存是所有线程共享的,存储一些变量(不包括方法的局部变量和方法的入参数,因为这些是线程内部私有的并不会涉及到并发问题,只包括成员变量、实例变量、静态变量等),线程不可以直接对主内存中的数据进行读写的操作。
工作内存?工作内存是线程私有的,当线程需要使用一个变量时,不能直接读写主内存的变量,而是给线程自身设置了一个专属的工作内存,当前线程会将需要用的变量read到工作线程,然后通过load操作在工作内存中放入主内存的变量副本。线程只能对这些变量副本进行操作,操作完毕后,需要store该变量到主内存中,然后通过write操作将变量的值放入变量中。
二、主内存和工作内存的交互操作
上面粗略的讲了主内存和工作内存对变量的操作。那么这些操作中都涉及到哪些交互动作呢?
1、lock:作用于主内存变量,将一个变量标示为一个线程独占的状态。
2、unlock:将lock操作后锁定状态的变量释放出来,只有unlock后变量才能被其他线程锁定。
3、read:读取主内存变量,并传输到工作内存,供load操作载入
4、load:载入read读取到的数据,将主内存传入的数据,放入工作内存的变量副本中。
5、use:在工作内存中使用该变量,将该变量传给执行引擎
6、assign:工作内存操作变量的值
7、store:将工作内存的变量传输给主内存
8、write:将store传输的变量,写入主内存对应的变量中
在这些操作中,虚拟机指定了一些规则,如3456操作不能单独出现,不允许工作线程未进行6操作的情况下执行78操作,在对对象进行2操作之前,必须已经执行78操作等等。另外也有一些特殊情况,如被volatile修饰的变量,在工作线程中修改,其余线程也能察觉到。又比如对于long和double的变量在64位系统中,可以将读写操作分为两次32位的操作进行等等。这些就不具体理解..
上面的操作都是原子操作(特殊情况如long、double除外),当两个线程同时运行时,在没有进行lock的情况下,如果同时read变量i值为1,放入工作内存,并且在工作内存中分别+1,在最后78操作后,最终值为2,但是两个线程分别+1,结果应该为3,这就是多线程的情况下没有同步导致的问题。下面会阐述,java提供了哪些规则/思想,在单线程和多线程来保证数据的同步。
三、单线程的代码执行
我们写一段代码,对于当前线程来说代码是按照一行一行,更精确的讲是按照逻辑一步一步运行的,脱离这个线程,站在其他线程的的角度看,代码确不是一步一步的执行的,有可能后面的逻辑先行执行,执行顺序是没有保证的,但是java是在经过计算,保证在结果一致的情况下去执行指令,所以对于单线程,我们是完全感知不到的。
四、多线程的同步
1、互斥同步:多线程并发访问共享数据,保证共享数据在同一时刻只有一个线程能访问,最基本的互斥同步就是synchronize关键字,经过代码编译后,在需要同步的前后分别形成monitorenter和monitorexit两个字节码指令,这两个字节码都需要对于的现场来锁定和解锁。另外synchronize对于同一个线程是可以重入的,如果当前线程再次执行同步方法,在原先获取锁的计数上加1,当然解锁的时候-1,只有减到0的时候,才能真正的释放锁。
juc包中lock下reentrantlock也是一种互斥同步锁,这种锁比synchronize更加轻量级,并且更加灵活,synchronize修饰,获取不到锁的时候,需要将线程挂起,这就需要切换到操作系统内核去执行,开销也是很大的,并且reentrantlock提供了很多条件,如果获取不到锁,可以进行别的动作。
2、非堵塞同步:上面说到的互斥同步,主要的缺点就是线程进行堵塞和唤醒导致性能问题,因此在处理方式上来说,互斥同步锁可以理解成一种悲观的并发策略,因为他每次都考虑会有多个线程对资源进行竞争,必须要在获得锁的情况下才去操作。而这边的非堵塞同步锁,则可以理解成是一种乐观引发策略。就是先进行操作,如果没有其他线程对资源竞争,则操作成功,否则操作失败,当然这里也要有标志条件去标记有没有其余线程的竞争操作。这里用到了cas的操作,比较当前值是否是预想的值,如果是,设置为新值,否则失败。
3、另外还有几个概念:自旋锁(获取不到锁的情况,不进行挂起,线程自旋,在一定时间内还获取不到锁再挂起)、自适应自旋锁(根据之前获取该锁通过自旋方式的成功率,自适应是否要进行自旋操作)。