zoukankan      html  css  js  c++  java
  • JAVA并发(三):关键词synchronized、锁

    JAVA并发(三):关键词synchronized

    整理自https://www.pdai.tech/md/java/thread/java-thread-x-key-synchronized.html

    1 本文将介绍以下问题

    • Synchronized可以作用在哪里? 分别通过对象锁和类锁进行举例。

    • Synchronized本质上是通过什么保证线程安全的? 分三个方面回答:加锁和释放锁的原理,可重入原理,保证可见性原理。

    • Synchronized由什么样的缺陷? Java Lock是怎么弥补这些缺陷的。

    • Synchronized和Lock的对比,和选择?

    • Synchronized在使用时有何注意事项?

    • Synchronized修饰的方法在抛出异常时,会释放锁吗?

    • 多个线程等待同一个snchronized锁的时候,JVM如何选择下一个获取锁的线程?

    • Synchronized使得同时只有一个线程可以执行,性能比较差,有什么提升的方法?

    • 我想更加灵活地控制锁的释放和获取(现在释放锁和获取锁的时机都被规定死了),怎么办?

    • 什么是锁的升级和降级? 什么是JVM里的偏斜锁、轻量级锁、重量级锁?

    • 不同的JDK中对Synchronized有何优化?

    2 synchronized的使用

    基本规则有:

    • 一把锁只能同时被一个线程获取,没有获得锁的线程只能等待;

    • 对象锁:每个实例都对应有自己的一把锁(this),不同实例之间互不影响;

      类锁:锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象公用同一把锁

    • synchronized修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁

    2.1 对象锁

    2.1.1 同步代码块锁

    手动指定锁定对象,也可是是this,也可以是自定义的锁

    case1:指定this

    public class SynchronizedObjectExample implements Runnable{
    
        @Override
        public void run() {
            // 同步代码块形式——锁为this,两个线程使用的锁是一样的,线程1必须要等到线程0释放了该锁后,才能执行
            synchronized (this) {
                System.out.println("我是线程" + Thread.currentThread().getName());
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "结束");
            }
        }
    
        public static void main(String[] args) {
            SynchronizedObjectExample instance = new SynchronizedObjectExample();
            Thread t1 = new Thread(instance);
            Thread t2 = new Thread(instance);
            t1.start();
            t2.start();
        }
    }
    // 我是线程Thread-0
    // Thread-0结束
    // 我是线程Thread-1
    // Thread-1结束
    

    case2:指定不同对象

    public class SynchronizedObjectExample implements Runnable{
        Object lock;
        public SynchronizedObjectExample(Object lock){
            this.lock = lock;
        }
    
        @Override
        public void run() {
            synchronized (lock) {
                System.out.println("我是线程" + Thread.currentThread().getName());
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "结束");
            }
        }
    
        public static void main(String[] args) {
            SynchronizedObjectExample instance1 = new SynchronizedObjectExample(new Object());
            SynchronizedObjectExample instance2 = new SynchronizedObjectExample(new Object());
            Thread t1 = new Thread(instance1);
            Thread t2 = new Thread(instance2);
            t1.start();
            t2.start();
        }
    }
    // 我是线程Thread-0
    // 我是线程Thread-1
    // Thread-1结束
    // Thread-0结束
    

    2.1.2 方法锁(默认锁对象为this,当前实例对象)

    public synchronized void method() {
    	//...
    }
    

    等效于

    public void method() {
        synchronized(this){
            //...
        }
    }
    

    2.2 类锁

    指synchronize修饰静态的方法或指定锁对象为Class对象

    2.2.1 synchronized指定锁对象为Class对象

    public class SynchronizedObjectExample implements Runnable{
    
        @Override
        public void run() {
            synchronized (SynchronizedExample.class) {
                System.out.println("我是线程" + Thread.currentThread().getName());
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "结束");
            }
        }
    
        public static void main(String[] args) {
            SynchronizedObjectExample instance1 = new SynchronizedObjectExample();
            SynchronizedObjectExample instance2 = new SynchronizedObjectExample();
            Thread t1 = new Thread(instance1);
            Thread t2 = new Thread(instance2);
            t1.start();
            t2.start();
        }
    }
    // 我是线程Thread-0
    // Thread-0结束
    // 我是线程Thread-1
    // Thread-1结束
    

    2.2.2 synchronized修时静态方法

    public static synchronized void method() {
        //...
    }
    

    3 synchronized原理分析

    3.1 加锁和释放锁原理

    创建以下代码

    public class SynchronizedDemo2 {
        Object object = new Object();
        public void method1() {
            synchronized (object) {
    
            }
        }
    }
    

    使用javac命令进行编译生成.class文件

    javac SynchronizedDemo2.java
    

    使用javap命令反编译查看.class文件的信息

    javap -verbose SynchronizedDemo2.class
    

    image-20210120162209633

    关注红色方框里的monitorentermonitorexit即可。

    MonitorenterMonitorexit指令,会让对象在执行,使其锁计数器加1或者减1。每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得,一个对象在尝试获得与这个对象相关联的Monitor锁的所有权的时候,monitorenter指令会发生如下3中情况之一:

    • monitor计数器为0,意味着目前还没有被获得,那这个线程就会立刻获得然后把锁计数器+1,一旦+1,别的线程再想获取,就需要等待
    • 如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加
    • 这把锁已经被别的线程获取了,等待锁释放

    monitorexit指令:释放对于monitor的所有权,释放过程很简单,就是讲monitor的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该monitor的所有权,即释放锁

    有点类似于PV信号量

    3.2 保证可见性的原理:内存模型和happens-before规则

    Synchronized的happens-before规则,即监视器锁规则:对同一个监视器的解锁,happens-before于对该监视器的加锁。继续来看代码:

    public class MonitorDemo {
        private int a = 0;
    
        public synchronized void writer() {     // 1
            a++;                                // 2
        }                                       // 3
    
        public synchronized void reader() {    // 4
            int i = a;                         // 5
        }                                      // 6
    }
    

    image-20210120162504730

    在图中每一个箭头连接的两个节点就代表之间的happens-before关系,黑色的是通过程序顺序规则推导出来,红色的为监视器锁规则推导而出:线程A释放锁happens-before线程B加锁,蓝色的则是通过程序顺序规则和监视器锁规则推测出来happens-befor关系,通过传递性规则进一步推导的happens-before关系。现在我们来重点关注2 happens-before 5,通过这个关系我们可以得出什么?

    根据happens-before的定义中的一条:如果A happens-before B,则A的执行结果对B可见,并且A的执行顺序先于B。线程A先对共享变量A进行加一,由2 happens-before 5关系可知线程A的执行结果对线程B可见即线程B所读取到的a的值为1

    4 JVM对锁的优化

    简单来说在JVM中monitorenter和monitorexit字节码依赖于底层的操作系统的Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的;然而在现实中的大部分情况下,同步方法是运行在单线程环境(无锁竞争环境)如果每次都调用Mutex Lock那么将严重的影响程序的性能。

    不过在jdk1.6中对锁的实现引入了大量的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销

    • 锁粗化(Lock Coarsening):也就是减少不必要的紧连在一起的unlock,lock操作,将多个连续的锁扩展成一个范围更大的锁。
    • 锁消除(Lock Elimination):通过运行时JIT编译器的逃逸分析来消除一些没有在当前同步块以外被其他线程共享的数据的锁保护,通过逃逸分析也可以在线程本地Stack上进行对象空间的分配(同时还可以减少Heap上的垃圾收集开销)。
    • 轻量级锁(Lightweight Locking):这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(即单线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁,取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。当存在锁竞争的情况下,执行CAS指令失败的线程将调用操作系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒(具体处理步骤下面详细讨论)。
    • 偏向锁(Biased Locking):是为了在无锁竞争的情况下避免在锁获取过程中执行不必要的CAS原子指令,因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟。
    • 适应性自旋(Adaptive Spinning):当线程在获取轻量级锁的过程中执行CAS操作失败时,在进入与monitor相关联的操作系统重量级锁(mutex semaphore)前会进入忙等待(Spinning)然后再次尝试,当尝试一定的次数后如果仍然没有成功则调用与该monitor关联的semaphore(即互斥锁)进入到阻塞状态。

    4.1 锁消除

    锁消除时指虚拟机即时编译器再运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持。意思就是:JVM会判断再一段程序中的同步明显不会逃逸出去从而被其他线程访问到,那JVM就把它们当作栈上数据对待,认为这些数据时线程独有的,不需要加同步。此时就会进行锁消除。

    当然在实际开发中,我们很清楚的知道那些地方时线程独有的,不需要加同步锁,但是在Java API中有很多方法都是加了同步的,那么此时JVM会判断这段代码是否需要加锁。如果数据并不会逃逸,则会进行锁消除。比如如下操作:在操作String类型数据时,由于String是一个不可变类,对字符串的连接操作总是通过生成的新的String对象来进行的。因此Javac编译器会对String连接做自动优化。在JDK 1.5之前会使用StringBuffer对象的连续append()操作,在JDK 1.5及以后的版本中,会转化为StringBuidler对象的连续append()操作

    public static String test03(String s1, String s2, String s3) {
        String s = s1 + s2 + s3;
        return s;
    }
    

    image-20210120163521030

    众所周知,StringBuilder不是安全同步的,但是在上述代码中,JVM判断该段代码并不会逃逸,则将该代码带默认为线程独有的资源,并不需要同步,所以执行了锁消除操作。(还有Vector中的各种操作也可实现锁消除。在没有逃逸出数据安全防卫内)

    4.2 锁粗化

    思想:在加同步锁时,尽可能的将同步块的作用范围限制到尽量小的范围(只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小。在存在锁同步竞争中,也可以使得等待锁的线程尽早的拿到锁)

    如果存在连串的一系列操作都对同一个对象反复加锁和解锁,甚至加锁操作时出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要地性能操作

    public static String test04(String s1, String s2, String s3) {
        StringBuilder sb = new StringBuilder();
        sb.append(s1);
        sb.append(s2);
        sb.append(s3);
        return sb.toString();
    }
    

    在上述地连续append()操作中就属于这类情况。JVM会检测到这样一连串地操作都是对同一个对象加锁,那么JVM会将加锁同步地范围扩展(粗化)到整个一系列操作的 外部,使整个一连串地append()操作只需要加锁一次就可以了

    4.3 锁膨胀

    在Java SE 1.6里Synchronied同步锁,一共有四种状态:无锁偏向锁轻量级所重量级锁,它会随着竞争情况逐渐升级。锁可以升级但是不可以降级,目的是为了提供获取锁和释放锁的效率。

    锁膨胀方向: 无锁 → 偏向锁 → 轻量级锁 → 重量级锁 (此过程是不可逆的)

    偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
    轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
    重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低

    4.4 自适应自旋锁

    背景:大家都知道,在没有加入锁优化时,Synchronized是一个非常“胖大”的家伙。在多线程竞争锁时,当一个线程获取锁时,它会阻塞所有正在竞争的线程,这样对性能带来了极大的影响。在挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作对系统的并发性能带来了很大的压力。同时HotSpot团队注意到在很多情况下,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和回复阻塞线程并不值得。在如今多处理器环境下,完全可以让另一个没有获取到锁的线程在门外等待一会(自旋),但不放弃CPU的执行时间。等待持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需要让线程执行一个忙循环(自旋),这便是自旋锁由来的原因。

    自旋锁早在JDK1.4 中就引入了,只是当时默认时关闭的。在JDK 1.6后默认为开启状态

    • 如果锁占用的时间非常的短,那么自旋锁的新能会非常的好
    • 相反,其会带来更多的性能开销(因为在线程自旋时,始终会占用CPU的时间片,如果锁占用的时间太长,那么自旋的线程会白白消耗掉CPU资源)。
    • 因此自旋等待的时间必须要有一定的限度,如果自选超过了限定的次数仍然没有成功获取到锁,就应该使用传统的方式去挂起线程了,在JDK定义中,自旋锁默认的自旋次数为10次,用户可以使用参数-XX:PreBlockSpin来更改

    自旋锁有一个问题,如果有个线程在自旋刚刚结束,挂起以后,等待的锁被释放了,那会有些得不偿失。所以这时候我们需要更加聪明的锁来实现更加灵活的自旋。来提高并发的性能。

    在JDK 1.6中引入了自适应自旋锁

    • 自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。
    • 如果在同一个锁对象上,自旋等待刚刚成功获取过锁,并且持有锁的线程正在运行中,那么JVM会认为该锁自旋获取到锁的可能性很大,会自动增加等待时间。比如增加到100此循环。
    • 相反,如果对于某个锁,自旋很少成功获取锁。那再以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。
    • 有了自适应自旋,JVM对程序的锁的状态预测会越来越准备,JVM也会越来越聪明
  • 相关阅读:
    win10下python环境变量设置
    c++ primer第15章这几个例子中的构造函数形式不太理解
    ++与*
    C++符号优先级
    56-Remove Linked List Elements
    55. Binary Tree Preorder Traversal
    54. Flatten Binary Tree to Linked List
    野指针--内存泄漏--缓存区溢出--栈溢出
    数组指针和指针数组的区别
    53-Linked List Cycle II
  • 原文地址:https://www.cnblogs.com/cpaulyz/p/14305889.html
Copyright © 2011-2022 走看看