说说synchronized关键字的底层原理

        synchronized关键字是java语言进行多线程编程,非常常用的关键字。

一、加锁原理

        使用了synchronized关键字后,在底层编译后的JVM指令中,会有monitorenter和monitorexit两个关键字。这两条指令是成对出现的。

  •         进入同步代码段,执行monitorenter,加锁。
  •         离开同步代码段,执行monitorexit,释放锁。

        每个对象都会关联一个monitor对象,要对某个对象加锁,必须获取这个对象关联的monitor对象的lock锁。monitor对象里面有一个从0开始的计数器,如果线程1要获得monitor的锁,先要看计数器是否为0,如果是0,说明没有其它线程获得锁,那么线程1就可以获得锁了,同时会对计数器加1,monitor对象里面还包含了一个owner指针,指向了持有锁的线程 。如果此时线程2想给这个对象加锁,它查看这个对象关联的monitor对象的计数器,发现大于0,说明别的线程已经获得锁了,此时线程2就会进入block阻塞状态进行等待。monitor对象里有一个entrylist,想要加锁的线程全部先进入这个entrylist等待获取机会尝试加锁,如果线程2加锁失败会进入entrylist等待。

        线程1获得了锁后,是还可以再次加锁的,出现这种情况其实也很常见。比如,synchronized关键字嵌套、synchronized方法递归调用等。这种现象叫锁的可重入,需要注意的是,每加一次锁(执行monitorenter),计数器就会加1。每执行一次加锁必须对应执行一次对应的释放锁,每执行一次释放锁(执行monitorexit),计数器就会减1。当线程1的锁全部释放,monitor对象的计数器归0,此时线程2就可以对该对象进行加锁了。JDK1.6以后对synchronized关键字做了很大优化,内部也是使用CAS来进行加锁操作count计数器。

说说synchronized关键字的底层原理_第1张图片

        wait和notify关键字的实现也是依托于monitor实现的,有线程执行wait之后,自己会加入monitor对象的一个waitset中等待唤醒获取锁,notifyall操作会从monitor的waitset中唤醒所有的线程,让他们竞争获取锁 。

说说synchronized关键字的底层原理_第2张图片

二、synchronized关键字,同时可以保证原子性、可见性以及有序性

1、原子性

        基本的赋值写操作都是可以保证原子性的,复杂的操作是无法保证原子性的。synchronized关键字,有一个加锁和释放锁的机制,加锁了之后,同一段代码就只有一个可以执行了,实际上就是串行执行,从而保证了原子性。

2、可见性

        MESI协议、flush、refresh,配合起来,才可以解决可见性。

        根据底层硬件不同,MESI协议的实现是有区别的。为了实现MESI协议,有两个配套机制:flush和refresh。

        flush处理器缓存,是把自己更新的值刷新到高速缓存(或主内存),因为必须刷新才能在后续通过一些机制让其它处理器的高速缓存读取到更新的值。flush后还会发送一个消息到总线(bus),通知其它处理器,某个变量值被修改了。

        refresh处理器缓存,是处理器中的线程在读取一个变量值的时候,如果发现其它处理器线程更新了变量值,必须从其它处理器的高速缓存(或主内存)去读取这个变量最新的值,更新到自己的高速缓存中。

        综上,flush强制刷新数据到高速缓存(或主内存),不要仅仅停留在自己的写缓冲器里面;refresh从总线嗅探机制发现某个变量被修改,强制从其它处理器的高速缓存(或主内存)去读取这个变量最新的值,更新到自己的高速缓存中。

        内存屏障的使用,在底层硬件级别其实就是执行flush和refresh来实现的。按可见性划分,内存屏障可以分为Load屏障和Store屏障。Load屏障的作用其实就是refresh处理器缓存操作,Store屏障的作用其实就是flush处理器缓存操作。在monitorenter指令之后会有一个Load屏障执行refresh处理器缓存操作,在monitorexit指令之后会有一个Store屏障执行flush处理器缓存操作。

        synchronized关键字就是通过加入Load屏障和Store屏障保证可见性的。代码块对变量做的写操作,都会在释放锁的时候,全部强制执行flush操作,在进入同步代码块的时候,对变量的读操作,全部会强制执行refresh的操作。从而保证可见性,也就是更新的数据,别的线程只要进入代码块,就一定可以读到。

3、有序性

        指令重排会造成其它线程的指令执行顺序的视觉假象。synchronized关键字也是通过加内存屏障来防止指令重排,保证有序性的。按有序性保障划分,内存屏障可以分为Acquire屏障和Release屏障。

         synchronized关键字在monitorenter指令之后,Load屏障之后,会加一个Acquire屏障。这个屏障的作用是禁止读操作与读写操作之间发生指令重排。在monitorexit指令之前,会加一个Release屏障,这个屏障的作用是禁止写操作与读写操作之间发生指令重排。通过Acquire屏障和Release屏障保证只有synchronized代码段内部的指令可以重排,但内部指令绝不会和外部指令发生重排,从而保证有序性。

你可能感兴趣的:(java,开发语言)