java 锁的分类

一、简介

锁,是一种同步机制,用于在多线程中实现对资源的控制,解决并发问题。

 

二、锁的种类

  • 公平锁/非公平锁

  • 可重入锁

  • 独享锁/共享锁

  • 互斥锁/读写锁

  • 乐观锁/悲观锁

  • 分段锁

  • 偏向锁/轻量级锁/重量级锁

  • 自旋锁

上面包含了8种锁的名词,这些分类并不全是指锁的状态,有的是指锁的特性,有的是指锁的设计。下面,将对每个锁的名词进行解释。

 

在介绍之前,说一下饥饿现象

如果一个线程的cpu执行时间都被其他线程抢占了,导致得不到cpu的分配执行,这种情况就叫做“饥饿”。解决饥饿现象的方法就是实现公平(无法实现完全100%公平),保证所有线程都公平的获得执行的机会。

 

公平锁/非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁。

非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。

对于Java ReentrantLock(可重入锁)而言,通过构造函数指定该锁是否为公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。

对于synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS来实现线程调度,所以并没有任何办法使其变成公平锁。(ReentrantLock内部提供了两种AQS的实现,一种公平模式,一种是非公平模式)

 

Synchronized锁是公平锁还是非公平锁?

在多线程的环境中,假如有7个线程,第一次抢到的可能是3号线程。3号线程释放锁之后,第二次这7个线程继续抢,有可能还是3号线程抢到,所以,当然是非公平的锁啦.而且,synchronized 是可重入锁。

 

可重入锁

可重入锁又名递归锁,是指同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁。

我的理解就是,同一线程已经获得某个锁,可以无需等待而再次获取锁,并且不会出现死锁(不同线程当然不能多次获得锁,需要等待)。

ReentrantLock和synchronized,都是可重入锁。

synchronized void setA() throws Exception{
    Thread.sleep(1000);
    setB();
}

synchronized void setB() throws Exception{
    Thread.sleep(1000);
}

上面的代码就是一个可重入锁的关键部分,如果不是可重入锁的话,setB方法就会阻塞,造成死锁。

 

独享锁/共享锁

独享锁也称独占锁,是指该锁一次只能被一个线程所持有。

共享锁是指该锁可被多个线程所持有。

对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写、写读 、写写的过程是互斥的。独享锁与共享锁是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。对于synchronized而言,是独享锁。(AQS,AbstractQueuedSynchronizer,抽象队列同步器)

 

 

互斥锁/读写锁

上面说到的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。

互斥锁在Java中的具体实现就是ReentrantLock。

读写锁在Java中的具体实现就是ReadWriteLock。其包含了读锁和写锁,其中读锁是可以多线程共享的,即共享锁,而写锁是排他锁,在更改时候不允许其他线程操作。读写锁底层是同一把锁(基于同一个AQS),所以会有同一时刻不允许读写锁共存的限制。如果只读取共享资源用「读锁」加锁,如果要修改共享资源则用「写锁」加锁。

 

乐观锁/悲观锁

乐观锁和悲观锁并不是一种真实存在的锁,而是一种思想。

乐观锁:非常“乐观”的认为在“我”之后不会再有人对该数据进行操作,所以不会加锁。但是在进行写入操作的时候会判断当前数据是否被修改过。

悲观锁:非常”悲观“的认为在”我“之后会有人对数据进行操作,为了保证数据不被别人修改,会加锁(行锁、读锁、写锁等)。

 

分段锁

分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作,ConcurrentHashMap中的分段锁称为Segment,它类似于HashMap(JDK7与JDK8)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。当需要put元素的时候,并不是对整个HashMap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计size的时候,也就是获取HashMap全局信息的时候,需要获取所有的分段锁才能统计。

分段锁设计的目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的部分进行加锁操作。

 

偏向锁/轻量级锁/重量级锁

这三种锁是指锁的状态,并且针对synchronized。在Java 5通过引入锁升级的机制来实现高效synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。(如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会尝试消除它身上的偏向锁,将锁恢复到标准的轻量级锁)

轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

锁状态的切换

synchronized 关键字之锁的升级(偏向锁->轻量级锁->重量级锁)

Java SE1.6 为了改善性能, 使得 JVM 会根据竞争情况, 使用如下 3 种不同的锁机制:

偏向锁(Biased Lock )

轻量级锁( Lightweight Lock)

重量级锁(Heavyweight Lock)

上述这三种机制的切换是根据竞争激烈程度进行的, 在几乎无竞争的条件下, 会使用偏向锁, 在轻度竞争的条件下, 会由偏向锁升级为轻量级锁, 在重度竞争的情况下, 会升级到重量级锁。

 

什么是自旋?

“自旋”可以理解为“自我旋转”,这里的“旋转”指“循环”,比如 while 循环或者 for 循环。“自旋”就是自己在这里不停地循环,直到目标达成。而不像普通的锁那样,如果获取不到锁就进入阻塞。如果获取锁失败就再次尝试,直到成功为止。

 

自旋锁

是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待并尝试获取,不断的判断锁是否能够被成功获取,直到成功获取到锁才会退出循环。

 

三、锁的使用

预备知识

AQS

AbstractQueuedSynchronized,抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch…

AQS原理

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程归为有效工作线程,并且该资源设置为锁定状态。其他线程如果要请求该共享资源,由于该资源被占有,因此无法请求成功,那么就需要一套线程阻塞等待以及唤醒时锁分配的机制。这个机制AQS使用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH同步队列是一个FIFO双向队列。

AQS维护了一个volatile int state(共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。state的访问方式有三种:

getState()

setState()

compareAndSetState()

AQS定义两种资源共享方式

Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:

  • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁

  • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的

Share(共享):多个线程可同时执行,如Semaphore、CountDownLatch、ReadWriteLock的Read锁。

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要继承AQS,实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。

tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。

tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。

tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。

tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

以可重入的独占锁ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态。

再以共享锁CountDownLatch为例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行(顺序)执行的,每个子线程执行完后countDown()一次,state会 CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()唤醒主线程,然后主线程就会从await()函数返回,继续后续动作。

一般来说,自定义同步器要么是独占方式的,要么是共享方式的,他们只需实现tryAcquire-tryRelease或tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

CAS

CAS(Compare and Swap 比较并交换)是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,其它线程都会失败,失败的线程并不会被挂起,只是被告知这次竞争中失败,并且可以再次尝试。说白了,CAS 其实是乐观锁,是一种思想。

CAS操作中包含三个操作数——需要读写的内存位置(V)、进行比较的预期原值(A)和准备写入的新值(B)。如果内存位置V的值与预期原值A相等,则将内存位置值更新为新值B,否则不更新。无论哪种情况,它都会在CAS 指令之前返回该位置的值(在CAS的一些特殊情况下将仅返回CAS是否成功,而不提取当前值)。CAS有效地说明了“ 我认为位置V应该包含值A,如果包含该值A,则将 B放到这个位置;否则,不要更改该位置的值,只告诉我这个位置现在的值即可”。这其实和乐观锁的冲突检查 + 数据更新的原理是一样的。

JAVA对CAS的支持:

在JDK1.5中新增java.util.concurrent包就是建立在CAS之上的。相对于synchronized 这种阻塞算法,CAS是非阻塞算法的一种常见实现。所以java.util.concurrent在性能上有了很大的提升。

以java.util.concurrent包中的AtomicInteger为例,看一下在不使用锁的情况下是如何保证线程安全的。主要理解 getAndIncrement方法,该方法的作用相当于 ++i 操作。

public class AtomicInteger extends Number implements java.io.Serializable {  
    private volatile int value; 

    public final int get() {  
        return value;  
    }  

    public final int getAndIncrement() {  
        for (;;) {  
            int current = get();  
            int next = current + 1;  
            if (compareAndSet(current, next))  
                return current;  
        }  
    }  

    public final boolean compareAndSet(int expect, int update) {  
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
    }  
}

 

实战

1.验证synchronized可重入独占锁

public class Test implements Runnable {
    //private ReentrantLock reentrantLock = new ReentrantLock();

    public synchronized void get(){
        System.out.println("2 get enter thread name-->" + Thread.currentThread().getName());
        //reentrantLock.lock();
        System.out.println("3 get thread name-->" + Thread.currentThread().getName());
        set();
        //reentrantLock.unlock();
        System.out.println("5 get leave run thread name-->" + Thread.currentThread().getName());
    }

    public synchronized void set(){
        //reentrantLock.lock();
        System.out.println("4 set thread name-->" + Thread.currentThread().getName());
        //reentrantLock.unlock();
    }

    @Override
    public void run() {
        System.out.println("1 run thread name-->" + Thread.currentThread().getName());
        get();
    }

    public static void main(String[] args){
        Test test = new Test();
        for(int i = 0; i < 10; i++){
            //自定义线程名式启动
            new Thread(test, "thread-" + i).start();
        }
    }
}

结果:get()方法中顺利进入了set()方法,说明synchronized的确是可重入锁。分析打印信息,进入run方法的顺序是线程1、线程2、线程0、线程4、线程3,说明sychronized的确是非公平锁。而且,每个 2、3、4、5的执行都是同一个线程完成的,说明了synchronized的确是独占锁。

2. 验证ReentrantLock可重入独占锁

ReentrantLock既可以构造公平锁又可以构造非公平锁,默认为非公平锁,将上面的代码改为用ReentrantLock实现,再次运行。

public class Test implements Runnable{

    private ReentrantLock reentrantLock = new ReentrantLock();

    public void get() {
        System.out.println("2 get enter thread name-->" + Thread.currentThread().getName());
        reentrantLock.lock();
        try{
            System.out.println("3 get thread name-->" + Thread.currentThread().getName());
            set();
        }catch (Exception e){
        }finally {
            reentrantLock.unlock();
            System.out.println("5 get leave run thread name-->" + Thread.currentThread().getName());
        }
    }

    public void set() {
        reentrantLock.lock();
        try{
            System.out.println("4 set thread name-->" + Thread.currentThread().getName());
        }catch (Exception e){
        }finally {
            reentrantLock.unlock();
        }
    }

    @Override
    public void run() {
        System.out.println("1 run thread name-->" + Thread.currentThread().getName());
        get();
    }

    public static void main(String[] args){
        Test test = new Test();
        for(int i = 0; i < 10; i++){
            //自定义线程名式启动
            new Thread(test, "thread-" + i).start();
        }
    }
}

 

结果:截图附近线程9获取锁后进行释放,是线程5还是线程4获取了锁?线程4,因为可以看到最近一次锁释放是线程4进行的。这个过程,也说明了锁是可重入的。而且,先执行get方法代码片段的线程未必先获取到锁,说明默认的确是非公平锁。

将其构造为公平锁,看看运行结果是否符合预期。查看源码构造公平锁很简单,只要在构造器传入boolean值true即可。


修改上面示例代码中的构造方法:

ReentrantLock reentrantLock = new ReentrantLock(true);

如果使用了IntelliJ IDEA IDE,可以看到在true前面有个公平fair的提示。

public class Test implements Runnable{

//    private ReentrantLock reentrantLock = new ReentrantLock();
    private ReentrantLock reentrantLock = new ReentrantLock(true);

    public void get() {
        try{
            System.out.println("2 get enter thread name-->" + Thread.currentThread().getName());
            reentrantLock.lock();//获取锁(加锁)
            System.out.println("3 get thread name-->" + Thread.currentThread().getName());
            set();
        }catch (Exception e){
        }finally {
            reentrantLock.unlock();
            System.out.println("5 get leave run thread name-->" + Thread.currentThread().getName());
        }
    }

    public void set() {
        try{
            reentrantLock.lock();
            System.out.println("4 set thread name-->" + Thread.currentThread().getName());
        }catch (Exception e){
        }finally {
            reentrantLock.unlock();
        }
    }

    @Override
    public void run() {
        System.out.println("1 run thread name-->" + Thread.currentThread().getName());
        get();
    }

    public static void main(String[] args){
        Test test = new Test();
        for(int i = 0; i < 10; i++){
            //自定义线程名式启动
            new Thread(test, "thread-" + i).start();
        }
    }
}

 结果:

1 run thread name-->thread-0
1 run thread name-->thread-1
1 run thread name-->thread-2
2 get enter thread name-->thread-1
3 get thread name-->thread-1
4 set thread name-->thread-1
5 get leave run thread name-->thread-1
1 run thread name-->thread-4
2 get enter thread name-->thread-0
1 run thread name-->thread-3
2 get enter thread name-->thread-4
2 get enter thread name-->thread-2
1 run thread name-->thread-5
2 get enter thread name-->thread-3
3 get thread name-->thread-0
4 set thread name-->thread-0
1 run thread name-->thread-6
2 get enter thread name-->thread-6
2 get enter thread name-->thread-5
1 run thread name-->thread-9
2 get enter thread name-->thread-9
1 run thread name-->thread-8
2 get enter thread name-->thread-8
3 get thread name-->thread-4
5 get leave run thread name-->thread-0
1 run thread name-->thread-7
4 set thread name-->thread-4
2 get enter thread name-->thread-7
3 get thread name-->thread-2
5 get leave run thread name-->thread-4
4 set thread name-->thread-2
5 get leave run thread name-->thread-2
3 get thread name-->thread-3
4 set thread name-->thread-3
5 get leave run thread name-->thread-3
3 get thread name-->thread-6
4 set thread name-->thread-6
5 get leave run thread name-->thread-6
3 get thread name-->thread-5
4 set thread name-->thread-5
5 get leave run thread name-->thread-5
3 get thread name-->thread-9
4 set thread name-->thread-9
5 get leave run thread name-->thread-9
3 get thread name-->thread-8
4 set thread name-->thread-8
5 get leave run thread name-->thread-8
3 get thread name-->thread-7
4 set thread name-->thread-7
5 get leave run thread name-->thread-7

 分析:可以把2语句看做申请锁的顺序,分别是线程1、线程0、线程4、线程2、线程3、线程6、线程5、线程9… 可以把3语句看做得到锁的顺序,分别是线程1、线程0、线程4、线程2、线程3、线程6、线程5、线程9… 可以看到,先申请锁的先获取锁,说明的确实现了公平锁。

3.验证ReentrantReadWriteLock可重入读写锁

ReentrantReadWriteLock有两种锁策略,公平方式和非公平方式。其读锁和写锁都支持线程重进入。

公平、非公平

指线程在获取锁时的机会是否公平。我们知道AQS中有一个FIFO的线程排队队列,那么如果所有线程想要获取锁时都来排队,大家先来后到井然有序,这就是公平;而如果每个线程都不守秩序,选择插队,而且还插队成功了,那这就是不公平。

 

但是为什么需要不公平呢?
因为效率。
有两个因素会制约公平机制下的效率:

  • 上下文切换带来的消耗

  • 依赖同步队列造成的消耗

一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。Java并发包提供读写锁的实现是ReentrantReadWriteLock,它提供的特性如下所示。

公平性选择:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。

重进入:该锁支持重进入,以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁(同一线程)。而写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁。

锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁(同一线程)。

代码实现

缓存类

public class Cache {
    static Map<String, Object> map = new HashMap<>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    /**
     * 获取用于读取操作的读锁
     */
    static Lock r = rwl.readLock();
    /**
     * 获取用于写入操作的写锁
     */
    static Lock w = rwl.writeLock();
    /**
     * 【读】获取一个key对应的value
     */
    public static final Object get(String key) {
        //加读锁
        r.lock();
        try {
            System.out.println("get 加完读锁" + Thread.currentThread().getName());
            return map.get(key);
        } finally {
            //实际unlock()要放在行首,我之所以这么放仅为了演示效果,如果该语句放在最后会误以为线程加锁后其他线程却可进入的错觉,因为可能unlock()执行完了,但是该语句并未执行。
            System.out.println("get 放读锁" + Thread.currentThread().getName());
            //释放读锁
            r.unlock();
        }
    }
    /**
     * 【写】设置或更新key对应的value,并返回旧的value(首次为null)
     */
    public static final Object put(String key, Object value) {
        //加写锁
        w.lock();
        try {
            System.out.println("put 加完写锁" + Thread.currentThread().getName());
            return map.put(key, value);
        } finally {
            //实际unlock()要放在行首,我之所以这么放仅为了演示效果,如果该语句放在最后会误以为线程加锁后其他线程却可进入的错觉,因为可能unlock()执行完了,但是该语句并未执行。
            System.out.println("put 放写锁" + Thread.currentThread().getName());
            //释放写锁
            w.unlock();
        }
    }
    /**
     * 【写】清空map所有的内容
     */
    public static final void clear() {
        //加写锁
        w.lock();
        try {
            System.out.println("clear 加完写锁" + Thread.currentThread().getName());
            map.clear();
        } finally {
            //实际unlock()要放在行首,我之所以这么放仅为了演示效果,如果该语句放在最后会误以为线程加锁后其他线程却可进入的错觉,因为可能unlock()执行完了,但是该语句并未执行。
            System.out.println("clear 放写锁" + Thread.currentThread().getName());
            //释放写锁
            w.unlock();
        }
    }
}

测试类

public class Test {
    public static void main(String[] args){
        for(int i = 0; i < 10; i++){
            /**
             * 自定义线程名式启动
             * 【写】put
             */
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Cache.put("key", new String(Thread.currentThread().getName() + " dg"));
                }
            }, "threadPUT-"+ i).start();
            /**
             * 【读】get
             */
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Cache.get("key"));
                }
            }, "threadGET-"+ i).start();
            /**
             * 【写】clear
             */
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Cache.clear();
                }
            }, "threadCLEAR-"+ i).start();
        }
    }
}

执行结果:

put 加完写锁threadPUT-0
put 放写锁threadPUT-0
clear 加完写锁threadCLEAR-5
clear 放写锁threadCLEAR-5
put 加完写锁threadPUT-6
put 放写锁threadPUT-6
put 加完写锁threadPUT-4
put 放写锁threadPUT-4
clear 加完写锁threadCLEAR-7
clear 放写锁threadCLEAR-7
put 加完写锁threadPUT-8
put 放写锁threadPUT-8
get 加完读锁threadGET-0
get 放读锁threadGET-0
threadPUT-8 dg
put 加完写锁threadPUT-5
put 放写锁threadPUT-5
clear 加完写锁threadCLEAR-0
clear 放写锁threadCLEAR-0
put 加完写锁threadPUT-1
put 放写锁threadPUT-1
put 加完写锁threadPUT-3
put 放写锁threadPUT-3
clear 加完写锁threadCLEAR-4
clear 放写锁threadCLEAR-4
get 加完读锁threadGET-1
get 放读锁threadGET-1
null
put 加完写锁threadPUT-2
put 放写锁threadPUT-2
clear 加完写锁threadCLEAR-1
clear 放写锁threadCLEAR-1
get 加完读锁threadGET-3
get 放读锁threadGET-3
null
get 加完读锁threadGET-2
get 放读锁threadGET-2
null
clear 加完写锁threadCLEAR-2
clear 放写锁threadCLEAR-2
get 加完读锁threadGET-4
get 放读锁threadGET-4
null
clear 加完写锁threadCLEAR-3
clear 放写锁threadCLEAR-3
get 加完读锁threadGET-5
get 放读锁threadGET-5
null
get 加完读锁threadGET-6
get 放读锁threadGET-6
null
clear 加完写锁threadCLEAR-6
clear 放写锁threadCLEAR-6
get 加完读锁threadGET-7
get 放读锁threadGET-7
null
put 加完写锁threadPUT-7
put 放写锁threadPUT-7
get 加完读锁threadGET-8
get 放读锁threadGET-8
threadPUT-7 dg
clear 加完写锁threadCLEAR-8
clear 放写锁threadCLEAR-8
put 加完写锁threadPUT-9
put 放写锁threadPUT-9
get 加完读锁threadGET-9
get 放读锁threadGET-9
threadPUT-9 dg
clear 加完写锁threadCLEAR-9
clear 放写锁threadCLEAR-9

分析可知:

  1. 可以看到,某个线程加锁后,其它线程处于等待阻塞,只有当该线程放锁后,其它线程才能加锁。

  2. 关于外层读值的输出(threadPUT-8 dg、null这些),这些值都是最近一次put的那个线程操作的,比如threadPUT-8 dg是线程8放入的值,null都是因为最近一次put的那个线程操作之后另一个线程进行了清除操作,所以取到的值为null(具体看下图)。

3.可以看到,某个线程加完读锁后还没放锁,其它线程可以继续加读锁(见下图)。

Ps:编码规范要求unlock()要放在行首,我之所以这么放仅为了演示效果,如果该语句放在最后会误以为线程加锁后其他线程却可进入的错觉,因为可能unlock()执行完了,但是该语句并未执行。 

读写锁的使用场景

对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了(只能自己读和写)。

 

可重入的理解

一个 Lock 对象可以被同一线程加锁多次。

 

什么时候使用读锁、写锁?

如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。也就是说,在只读数据的时候上读锁,在只写数据的时候上写锁!

 

再测试一下ReentrantReadWriteLock可重入读写锁

public class ReentrantReadWriteLockRRExample {
    public static void main(String[] args) {
        ReentrantReadWriteLock rWlock = new ReentrantReadWriteLock();
        Runnable r = () ->{
            rWlock.readLock().lock();//加读锁
//            rWlock.writeLock().lock();//加写锁
            try {
                System.out.println(Thread.currentThread().getName()+" start 当前时间戳:" + System.nanoTime());
                Thread.sleep(2000);//2秒
                //先打印输出的,其时间反而要晚,什么情况?因为线程执行获取时间后可能没有执行输出,此时另一个线程继续执行了。
                System.out.println(Thread.currentThread().getName()+" end 当前时间戳:" + System.nanoTime());
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                rWlock.readLock().unlock();//放读锁
//                rWlock.writeLock().unlock();//放写锁
            }
        };
        Thread t1 = new Thread(r,"t1");
        Thread t2 = new Thread(r,"t2");
        Thread t3 = new Thread(r,"t3");
        Thread t4 = new Thread(r,"t4");
        Thread t5 = new Thread(r,"t5");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}

 执行结果:

t1 start 当前时间戳:967753860143300
t2 start 当前时间戳:967753860247000
t5 start 当前时间戳:967753860252000
t3 start 当前时间戳:967753860257600
t4 start 当前时间戳:967753860247100
t2 end 当前时间戳:967755866153100
t3 end 当前时间戳:967755866243300
t5 end 当前时间戳:967755866154600
t1 end 当前时间戳:967755866153100
t4 end 当前时间戳:967755866151900

分析可知:

  1. 一个线程加读锁(未放锁),另一个线程可以继续加读锁。

  2. N个读锁,多个线程之间不互斥,可以实现并发读数据(并不是真正意义上的同时,而是宏观上的同时)。

  3. 一个线程加写锁(未放锁),另一个线程不可以继续加写锁。

  4. 加写锁,多个线程之间是互斥的。某个线程执行完释放锁后,另一个线程再执行并释放锁

  5. 依据读写锁的特性,利用其读锁实现并发读,利用其写锁实现唯一写(一个线程独占,自己进行写操作)。保证数据的安全性、准确性。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值