黄小华的个人网站
熬过无人问津的日子才有诗和远方!
浅析Synchronized关键字和Lock

一 Synchronized简介

Synchronized是java内置的关键字。代表这个方法加锁,相当于不管哪一个线程,运行到这个方法时,都要检查有没有其它线程正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程运行完这个方法后再运行此线程,没有的话,锁定调用线程,然后直接运行。
Synchronized是同步锁,保障原子性、可见性、有序性
1 原子性
指一个操作一旦开始,就不会被其他线程所干扰,不可中断。
2.可见性
指一个线程对共享变量进行修改,另一个能立刻获取到修改后的最新值。synchronized的原理就是清空自己工作内存上的值,通过将主内存最新值刷新到工作内存中,让各个线程能互相感知修改。
3.有序性
指令有序执行。synchronize有序性是由“一个变量在同一时刻只允许一条线程对其进行 lock 操作“,volatile是自身就禁止指令重排。

二 Synchronized的使用

1.修饰实例方法,对当前实例对象this加锁

public class SynchronizedDemo {
    
    public synchronized void methodOne() {
    
    }
}

2.修饰静态方法,对当前类的Class字节码对象加锁(所有通过该类new出的实例对象都要同步,用的是同一个锁)

public class SynchronizedDemo {

    public static synchronized void methodTwo() {

    }
}

3.修饰代码块,指定加锁对象,对给定对象加锁

public class SynchronizedDemo {
    public void methodThree() {
        // 对当前实例对象this加锁
        synchronized (this) {
        
        }
    }
    public void methodFour() {
        // 对class对象加锁
        synchronized (SynchronizedDemo.class) {
        
        }
    }
}

三 synchronized实现原理

synchronized是我们用过的第一个并发关键字,不过很多人还停留在使用层面,对其底层原理和优化不理解。 在JDK1.6之前使用synchronized是直接给对象加重量级锁的,JDK1.6给它底层进行了优化,极大地提高了synchronized的性能,1.6是增加了偏向锁和轻量级锁。

四.synchronized锁升级

无锁 ->偏向锁 ->轻量级锁 ->重量级锁
68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f31312f32382f313637353964643162306239363236383f773d37323026683d32353026663d6a70656726733d3337323831.jpg

synchronized关键字就像汽车的自动档,一脚油门踩下去,synchronized会从无锁升级为偏向锁,再升级为轻量级锁,最后升级为重量级锁,就像自动换挡一样。那么自旋锁在哪里呢?这里的轻量级锁就是一种自旋锁。

偏向锁

初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁
线程A第一次执行完同步代码块后,当线程B尝试获取锁的时候,发现是偏向锁,会判断线程A是否仍然存活。如果线程A仍然存活,将线程A暂停,此时偏向锁升级为轻量级锁,之后线程A继续执行,线程B自旋。但是如果判断结果是线程A不存在了,则线程B持有此偏向锁,锁不升级。
如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。(大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。)

轻量级锁

一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。这里要明确一下什么是锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。

在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。

长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销,追求响应时间 同步块执行速度非常快。

显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。 synchronsized除重量级锁外都是在JDK层面做了处理。
重量级锁会造成用户态和内核态的切换,这个开销是很大的。但是吞吐量可以很高。只是同步块执行速度较长。

一个锁只能按照 偏向锁->轻量级锁->量级锁的顺序逐渐升级,不允许降级。 v274d97c1d2f70a2a626aebf1c6ed00192_r.jpg

五 Lock

v2ddb71ab0b68d65ae70244bfdeb0d6704_r.jpg
JDK1.5在java.util.concurrent.locks包下加了另外一种方式来实现锁,那就是Lock接口,这个接口的实现类在代码层面实现了锁的功能,具体细节不在本文展开,有兴趣可以研究下AbstractQueuedSynchronizer类,写得可以说是牛逼爆了。
ReentrantLock、ReadLock、WriteLock 是Lock接口最重要的三个实现类。对应了“可重入锁”、“读锁”和“写锁”,后面会讲它们的用途。

ReadWriteLock其实是一个工厂接口,而ReentrantReadWriteLock是ReadWriteLock的实现类,它包含两个静态内部类ReadLock和WriteLock。这两个静态内部类又分别实现了Lock接口。

public interface Lock {
    // 加锁
    void lock();
    // 能够响应中断
    void lockInterruptibly() throws InterruptedException;
    // 非阻塞获取锁
    boolean tryLock();
    // 非阻塞超时获取锁
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    // 解锁
    void unlock();
    // 定义阻塞条件
    Condition newCondition();
}

可以看到Lock接口相比synchronized多了很多特性,详细解释一下方法

lock()方法,用来获取锁,如果锁被其他线程获得则进行等待,需要和unlock方法配合主动释放锁。发生异常时,不会主动释放锁,所以释放锁的操作放在finally块中

lockInterruptibly()方法,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程

tryLock()方法,用来尝试获取锁,如果获取成功,则返回true。如果获取失败则返回false。也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待

tryLock(long time, TimeUnit unit)方法,和tryLock()类似。只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true

unlock()方法,解锁

newCondition()方法,定义条件

六 synchronized和ReentrantLock的异同

1.ReentrantLock支持非阻塞的方式获取锁,能够响应中断,而synchronized不可中断

2.ReentrantLock必须手动获取和释放锁,而synchronized不需要

3.ReentrantLock可以是公平锁(先来先拿锁)或者非公平锁(后申请的线程可能先获取到锁,是随机或者按照其他优先级排序的),而synchronized只能是非公平锁

4.synchronized在发生异常的时候,会自动释放线程占有的锁,而ReentrantLock在发生异常时,如果没有通过unlock去释放锁,很有可能造成死锁,因此需要在finally块中释放锁

5.synchronized和ReentrantLock都是可重入锁(允许同一个线程多次获取同一把锁)