Java并发-深刻理解synchronized

synchronized基础用法

synchronized的三种应用方式:java

  1. 做用于实例方法,当前实例加锁,进入同步代码前要得到当前实例的锁;
  2. 做用于静态方法,当前类加锁,进去同步代码前要得到当前类对象的锁;
  3. 做用于代码块,这须要指定加锁的对象,对所给的指定对象加锁,进入同步代码前要得到指定对象的锁。

用法总结以下:安全

clipboard.png

注:并发

  1. 不管synchronized关键字加在方法上仍是对象上,若是它做用的对象是非静态的,则它取得的锁是对象;若是synchronized做用的对象是一个静态方法或一个类,则它取得的锁是对类,该类全部的对象同一把锁;
  2. 每一个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就能够运行它所控制的那段代码;
  3. 不管是方法正常执行完毕或者方法抛出异常,都会释放锁;
  4. synchronized不能够被继承,父类某个方法加了synchronized,若子类覆写了该方法,子类要想同步还得在子类方法上加上synchronized关键字。

synchronized与wait、nofity、nofityAll配合使用

【问题】实现一个容器,提供两个方法,add,size。写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束。
public class MyContainer {app

private static List<Integer> lists = new ArrayList<>();

public static void main(String[] args) {
    final Object lock = new Object();

    //监控线程
    new Thread(()->{
        synchronized (lock) {
            System.out.println("thread 2 start...");
            if(lists.size() != 5) {
                try {
                    lock.wait();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            System.out.println("thread 2 end.");
            lock.notify();
        }
    }, "t2").start();
    
    new Thread(()->{
        synchronized (lock) {
            for(int i = 0; i < 10; i++) {
                System.out.println("thread 1, add " + i);
                lists.add(i);

                if(lists.size() == 5) {
                    lock.notify();
                    try {
                        lock.wait();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }, "t1").start();
}

}性能

注:
wait()会马上释放synchronized(obj)中的obj锁,以便其余线程能够执行obj.notify(),可是notify()不会马上马上释放sycronized(obj)中的obj锁,必需要等notify()所在线程执行完synchronized(obj)块中的全部代码才会释放这把锁。因此,在t1中,调用了lock对象的notify方法以后,再调用lock的wait方法释放锁,而在t2被唤醒以后,继续执行,最后还要调用lock对象的notify方法去唤醒此时处在wait状态的t1优化

synchronized原理

原理概述

先经过下面简单的例子看下:spa

public class Synchronize {操作系统

public static void main(String[] args) {
    synchronized (Synchronize.class){
        System.out.println("Synchronize");
    }
}

}.net

使用 javap -c Synchronize 能够查看编译以后的具体信息线程

clipboard.png

从编译后的结果能够看到:在同步方法调用前加了一个 monitorenter 指令,在退出方法和异常处插入了 monitorexit 的指令

实现原理:JVM 是经过进入、退出对象监视器( Monitor )来实现对方法、同步块的同步的,具体实现是在编译以后同步代码块采用添加moniterenter、moniterexit,同步方法使用ACC_SYNCHRONIZED标记符隐式实现。每一个对象都有一个monitor与之关联,运行到moniterenter时尝试获取对应monitor的全部权,获取成功就将monitor的进入数加1(因此是可重入锁,也被称为重量级锁),不然就阻塞,拥有monitor的线程运行到moniterexit时进入数减1,为0时释放monitor。其本质就是对一个对象监视器( Monitor )进行获取,而这个获取过程具备排他性从而达到了同一时刻只能一个线程访问的目的。Java内置的synchronized关键字能够认为是管程模型中的MESA模型的简化版。

Java对象如何与Monitor关联

Java对象与Monitor关联关系示意图以下:

图片描述

JVM堆中存放的是对象实例,每个对象都有对象头,对象头里有Mark Word,里面存储着对象的hashCode、GC分代年龄以及锁信息。如图所示,重量级锁中存有指向monitor的指针。
其中ObjectMonitor中几个关键字段的含义以下:
_count:记录owner线程获取锁的次数。这句话很好理解,这也决定了synchronized是可重入的。
_owner:指向拥有该对象的线程
_WaitSet:主要存放全部wait的线程的对象,也就是说若是有线程处于wait状态,将被挂入这个队列,调用了wait()方法线程会进入该队列
_EntryList:全部在等待获取锁的线程的对象,也就是说若是有线程处于等待获取锁的状态的时候,将被挂入这个队列。

图片描述

详情请参考:https://www.jianshu.com/p/32e...

Monitor加锁及解锁过程

图片描述

  1. 当多个线程同时访问一段同步代码时,首先会进入_EntryList队列中,当某个线程获取到对象的monitor后进入_Owner区域并把monitor中的_owner变量设置为当前线程,同时monitor中的计数器_count加1。即得到对象锁;
  2. 若持有monitor的线程调用wait()方法,将释放当前持有的monitor,_owner变量恢复为null,_count自减1,同时该线程进入_WaitSet集合中等待被唤醒;
  3. 当程序里其余线程调用了notify/notifyAll方法时,就会唤醒_waitSet中的某个线程,这个线程就会再次尝试获取monitor锁。若是成功,则就会成为monitor的owner;
  4. 若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其余线程进入获取。

详细过程请参考:https://www.hollischuang.com/...

加锁和解锁的内存语义

Java内存模型:

clipboard.png

Java内存模型虽然有助加快执行速度,可是也带来了新的问题。不一样线程之间是没法之间访问对方工做内存中的变量,线程间的变量值传递均须要经过主内存来完成,那线程的操做结果怎么让其余线程可见呢?这便须要先行发生(happens-before)原则来保证了。

先行发生规则中有以下2条:

  1. 对同一个监视器的解锁,happens-before于对该监视器的加锁
  2. 若是A happens-before B,则A的执行结果对B可见,而且A的执行顺序先于B

即:若是有2个线程A和B,则根据规则1,A线程释放锁 happens-before B线程获取锁,根据规则2,那A线程的操做结果对B线程是可见的。

clipboard.png

clipboard.png

从上图能够看出,线程A会首先先从主内存中读取共享变量a=0的值而后将该变量拷贝到本身的本地内存,进行加1操做后,再将该值刷新到主内存,整个过程即为线程A 加锁-->执行临界区代码-->释放锁相对应的内存语义。线程B获取锁的时候一样会从主内存中读取共享变量a的值,这个时候就是最新的值1,而后将该值拷贝到线程B的工做内存中去,释放锁的时候一样会重写到主内存中。
即:释放锁的时候会将值刷新到主内存中,其余线程获取锁时会强制从主内存中获取最新的值。这也验证了A happens-before B,A的执行结果对B是可见的。

详情请参考:https://www.jianshu.com/p/151...

锁的优化

高效并发是从JDK 1.5 到 JDK 1.6的一个重要改进,HotSpot虚拟机开发团队在这个版本中花费了很大的精力去对Java中的锁进行优化,如适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等。这些技术都是为了在线程之间更高效的共享数据,以及解决竞争问题。但对Java开发者而言,只须要知道想在加锁的时候使用synchronized就能够了,具体的锁的优化是虚拟机根据竞争状况自行决定的
因为Java的线程是映射到操做系统原生线程之上的,若是要阻塞或唤醒一个线程就须要操做系统的帮忙,这就要从用户态转换到内核态,所以状态转换须要花费不少的处理器时间,因此优化的想法主要是能不阻塞线程就不阻塞。

  1. 适应性自旋:所谓的自旋锁就是让线程不停地执行循环体,不进行线程状态的改变。若是在锁被占用的时间很短的状况下,自旋等待的效果会很是好,反之,若是锁被占用的时间很长,自旋就会浪费CPU,因此自旋要有必定限度。在JDK1.6后,自旋的时间再也不固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这即是自适应自旋了。
  2. 锁消除:经过逃逸分析,判断出代码块中不存在多个线程共享的数据,便会在编译后将锁去掉。好比:咱们常常在代码中使用StringBuffer做为局部变量,而StringBuffer中的append是线程安全的,有synchronized修饰的,可是做为局部变量并不须要共享,因此这个时候便会进行锁消除的优化。
  3. 锁粗化:须要加锁的时候,咱们提倡尽可能减少锁的粒度,这样能够避免没必要要的阻塞。可是若是一系列的连续操做都对同一个对象反复加锁和解锁,甚至加锁操做是出如今循环体中的,那便是没有线程竞争,频繁地进行互斥同步操做也会致使没必要要的性能损耗。因此当虚拟机探测到这样的状况时,就会把加锁的范围扩大
    如如下代码:

    clipboard.png
    会被粗化成:

    clipboard.png

  4. 轻量级锁:其实就是指经过CAS操做尝试把monitor的_owner字段设置为当前线程,若是更新成功了,那么代表这个线程就拥有了该对象的锁,并将对象头的Mark Word的锁标志位转变为"00",即表明此对象处于轻量级锁定状态。若是更新失败,则膨胀为重量级锁,等待锁的线程须要进入阻塞状态。经过ObjectMonitor类的源码能够看出:

    clipboard.png

  5. 偏向锁:意思是这个锁会偏向于第一个得到它的线程,若是在接下来执行过程当中,该锁没有被其余线程获取,则持有偏向锁的线程将永远不须要再进行同步。若是说轻量级锁是在无竞争的状况下使用CAS操做去消除同步使用的互斥量,那么偏向锁就是在无竞争的状况下把整个同步都消除掉,连CAS操做都不作了。但这一切都是在无竞争的状况下,若是有另一个线程尝试去获取这个锁,那偏向模式便宣告结束。

细节请参考:https://www.hollischuang.com/...

总结

本文从synchronized的用法开始,而后逐步深刻介绍synchronized的实现原理,其实质是对管程模型的一种实现。虽然在用的时候就是一个关键字,但背后的内容却十分丰富,写本文的过程当中,参考了许多大牛的博客,受益良多。

参考

https://blog.csdn.net/weixin_...
https://www.jianshu.com/p/32e...
https://www.jianshu.com/p/d53...
https://www.hollischuang.com/...
https://www.hollischuang.com/...
https://www.hollischuang.com/...
https://www.jianshu.com/p/e62...
https://www.jianshu.com/p/27f...
https://www.jianshu.com/p/151...

相关文章
相关标签/搜索