并发编程之锁(一)--volatile与synchronized

2019-05-07  本文已影响0人  夏目手札

前言

本文是对并发编程中的锁一个系统性总结。

什么是死锁

1. 定义:
theadA已经持有了资源2,同时还想申请资源1,theadB已经持有了资源1,同时还想申请资源2,所以theadA与theadB因为相互等待对方已经持有的资源进入死锁状态。
2. 死锁的四个条件
互斥条件:指线程对已经获取到的资源进行排他性使用。
请求并持有条件:指一个线程已经持有至少一个资源同时又想申请新的资源,但是新的资源被其他线程占有,所以该线程会被阻塞,但是阻塞的同时并不释放自己已经获取的资源。
不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有等待其使用完了。
环路等待条件:指发生死锁时,必然存在一个线程-资源的环形链。

什么是线程安全

当多个线程同时运行某段代码时,如果每次运行的结果与单线程运行的结果一致,而且其他的变量值也与预期的一致,那么我们就说这段代码是线程安全的。

相关知识

volatile关键字

上面介绍了多线程相关的三个知识,下面介绍的volatile关键字只具备了其中的可见性和有序性,而并不具备原子性。
1. 使用介绍
有volatile关键字修饰的共享变量会在每次更改变量后回写至内存,从而导致其他线程的该变量缓存无效,进而保证了共享变量对所有线程可见。
下面的方法如果不使用volatile关键字,则永远不会退出,因为主线程对共享变量isStop不可见。

private static volatile boolean isStop = false;
public static void stop(){
    isStop=true;
}
static class Worker implements Runnable{


    @Override
    public void run() {
        try {
            java.lang.Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        stop();
    }
}

public static void main(String[] args) {
    new Thread(new Worker()).start();
    while(!isStop){
        //println方法使用了synchronized关键字,如果在这里面打印不用volatile关键字也会退出
        //System.out.println("continue....");
    }
    System.out.println("stop");
}

2. 原理分析
volatile 的底层实现,是通过插入内存屏障。但是对于编译器来说,发现一个最优布置来最小化插入内存屏障的总数几乎是不可能的,所以,JVM 采用了保守策略。
策略如下:

原因如下:

synchronized关键字

1. 使用介绍
synchronized一直是元老级别的存在,是重量级的锁,它比volatile高级,它满足上面的三个特性。一般有如下三种表现形式:

public static void main(String[] args) throws InterruptedException {
        //1.对于普通方法上加锁,锁是该对象,如果操作的是同一个对象该锁是有左右的
//      Counter1 counter1 = new Counter1();
//      Thread thread1 = new Thread(counter1);
//      Thread thread2 = new Thread(counter1);
//      thread1.start();
//      thread2.start();
//      thread1.join();
//      thread2.join();
//      System.out.println(Counter1.i);
//      此时锁并不生效
//      Counter1 counter11 = new Counter1();
//      Counter1 counter12 = new Counter1();
//      Thread thread3 = new Thread(counter11);
//      Thread thread4 = new Thread(counter12);
//      thread3.start();
//      thread4.start();
//      thread3.join();
//      thread4.join();
//      System.out.println(Counter1.i);
        //2.静态方法锁
//      Counter2 counter21 = new Counter2();
//      Counter2 counter22 = new Counter2();
//      Thread thread5 = new Thread(counter21);
//      Thread thread6 = new Thread(counter22);
//      thread5.start();
//      thread6.start();
//      thread5.join();
//      thread6.join();
//      System.out.println(Counter2.i);
//      Counter2 counter21 = new Counter2();
//      Thread thread5 = new Thread(counter21);
//      Thread thread6 = new Thread(new Runnable() {
//          @Override
//          public void run() {
//              for (int i = 0; i < 1000; i++) {
//                  counter21.addNoSyn();
//              }
//          }
//      });
//      thread5.start();
//      thread6.start();
//      thread5.join();
//      thread6.join();
//      System.out.println(Counter2.i);
        //3.同步代码块
        Counter3 counter31 = new Counter3();
        Counter3 counter32 = new Counter3();
        Thread thread7 = new Thread(counter31);
        Thread thread8 = new Thread(counter32);
        thread7.start();
        thread8.start();
        thread7.join();
        thread8.join();
        System.out.println(Counter3.i);

    }
    /**
     * 普通方法锁
     */
    static class  Counter1 implements Runnable{
        static int i=0;
        synchronized void add(){
            i++;
        }
        @Override
        public void run() {
            for (int j = 0; j < 1000000; j++) {
                add();
            }
        }
    }

    /**
     * 静态方法锁
     */
    static class  Counter2 implements Runnable{
        static int i=0;
        static synchronized void add(){
            i++;
        }
        synchronized void addNoSyn(){
            i++;
        }
        @Override
        public void run() {
            for (int j = 0; j < 1000000; j++) {
                add();
            }
        }
    }
    //锁代码块
    static class  Counter3 implements Runnable{
        static String syn = "true";
        static int i=0;
        @Override
        public void run() {
            synchronized (syn){
                for (int j = 0; j < 1000000; j++) {
                    i++;
                }
            }
        }
    }

以上三种其实就是所谓的对象锁与类锁。
第二个方法也证明了类锁和对象锁互不干涉,add方法锁的是类锁,而addNoSyn锁的是对象锁,两个并不是同一把锁,所以不存在竞争关系。
2. 原理分析
使用Classpy工具打开上面我们写的demo(Classpy工具可以从github上下载):
TestSync$Counter1.class

TestSync$Counter1.class
TestSync$Counter3.class
TestSync$Counter3.class
我们可以看到:

下面我们进一步来分析synchronized的实现:
2.1 对象头
synchronized用的锁是存在Java对象头里的。对象头中的数据:

2.2 Monitor
Monitor其实是一种同步工具,也可以说是一种同步机制,它通常被描述为一个对象,主要特点是互斥和信号机制:

Monitor Record是Java线程私有的数据结构,每一个线程都有一个可用MR列表,同时还有一个全局的可用列表,其中:

其结构如下:


Monitor Record结构图

3. 锁优化
synchronized是重量级锁,在JDK1.6中对synchronized的实现进行了各种优化,比如锁粗化、锁消除、锁升级、自旋锁、适应性自旋锁等技术来减少锁操作的开销。下面我们来看看:
3.1 锁粗化

for(int i=0;i<size;i++){
    synchronized(lock){
    }
}
//锁粗化之后
synchronized(lock){
    for(int i=0;i<size;i++){
    }
}

JVM 检测到对同一个对象(lock)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到 for 循环之外。这个例子只是一个抽象的概念,实际上这种写法JVM并不会进行所优化,我们来看一个实际的例子:

public static class CoarsingTest implements Runnable {
        public static String name = "Tom";

        @Override
        public void run() {
            //#System.out.println()是加锁的,锁粗化后,name变量具有可见性
            while(!"Bob".equals(name)) {
                System.out.println("我不是Bob");
            }
            //这种写法,反编译后,#System.out.println()是在循环外面的,所以name是不可见的
//            while (true) {
//                if ("Bob".equals(name)) {
//                    System.out.println(name);
//                    break;
//                }
//            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        CoarsingTest coarsingTest = new CoarsingTest();
        Thread thread = new Thread(coarsingTest);
        thread.start();
        Thread.sleep(1000);
        CoarsingTest.name = "Bob";
    }

3.2 锁消除

public static void main(String[] args) throws InterruptedException {
        long tsStart = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            getString("AB", "CD");
        }
        //-XX:+DoEscapeAnalysis -XX:+EliminateLocks 开启锁消除模式下999ms
        //-XX:+DoEscapeAnalysis -XX:-EliminateLocks 关闭锁消除模式下1447ms
        System.out.println((System.currentTimeMillis() - tsStart) + " ms");
    }

    public static String getString(String s1, String s2) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb.toString();
    }

根据逃逸分析,变量sb没有逃逸出方法#getString(),所以JVM可以大胆的将StringBuffer内部的锁消除掉。
3.3 锁升级
锁的4中状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高)

3.4 自旋锁
定义:所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。其实轻量级锁就是一种自旋锁。
在 JDK1.6 中默认开启。同时自旋的默认次数为 10 次,可以通过参数 -XX:PreBlockSpin 来调整。
但是这种手动设置自旋次数也不太合理,所以JDK 1.6 引入了更加聪明的自旋锁,即自适应自旋锁。即自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

总结

volatile相对于synchronized稍微轻量些,在某些场合它可以替代synchronized,但是又不能完全取代synchronized 。
volatile经常使用的场景:状态标记变量。

参考资料

  1. 《深入理解Java虚拟机》
  2. 《Java并发编程的艺术》
  3. Java 8 并发篇 - 冷静分析 Synchronized(下)
  4. 通过踩坑带你读透虚拟机的“锁粗化”
  5. Java锁消除和锁粗化
  6. Java并发——Synchronized关键字和锁升级,详细分析偏向锁和轻量级锁的升级
上一篇下一篇

猜你喜欢

热点阅读