Java 中的锁分类与应用
- Java 锁的品种
在笔者面试过程时,常常会被问到各种各样的锁,如乐观锁、读写锁等等,十分繁多,在此做一个总结。介绍的内容如下:
- 乐观锁 / 乐观锁
- 独享锁 / 共享锁
- 互斥锁 / 读写锁
- 可重入锁
- 偏心锁 / 非偏心锁
- 分段锁
- 偏差锁 / 轻量级锁 / 重量级锁
- 自旋锁
以上是一些锁的名词,这些分类并不是全是指锁的状态,有的指锁的个性,有的指锁的设计,上面总结的内容是对每个锁的名词进行肯定的解释。
1.1 乐观锁 / 乐观锁
乐观锁与乐观锁并不是特指某两种类型的锁,是人们定义进去的概念或思维,次要是指对待并发同步的角度。
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为他人不会批改,所以不会上锁,然而在更新的时候会判断一下在此期间他人有没有去更新这个数据,能够应用版本号等机制。乐观锁实用于多读的利用类型,这样能够进步吞吐量,在 Java 中 java.util.concurrent.atomic 包上面的原子变量类就是应用了乐观锁的一种实现形式 CAS(Compare and Swap 比拟并替换)实现的。
乐观锁:总是假如最坏的状况,每次去拿数据的时候都认为他人会批改,所以每次在拿数据的时候都会上锁,这样他人想拿这个数据就会阻塞直到它拿到锁。比方 Java 外面的同步原语 synchronized 关键字的实现就是乐观锁。
乐观锁适宜写操作十分多的场景,乐观锁适宜读操作十分多的场景,不加锁会带来大量的性能晋升。
乐观锁在 Java 中的应用,就是利用各种锁。
乐观锁在 Java 中的应用,是无锁编程,经常采纳的是 CAS 算法,典型的例子就是原子类,通过 CAS 自旋实现原子操作的更新。
1.1.1 乐观锁
乐观锁总是认为不存在并发问题,每次去取数据的时候,总认为不会有其余线程对数据进行批改,因而不会上锁。然而在更新时会判断其余线程在这之前有没有对数据进行批改,个别会应用“数据版本机制”或“CAS 操作”来实现。
(1)数据版本机制
实现数据版本个别有两种,第一种是应用版本号,第二种是应用工夫戳。以版本号形式为例。
版本号形式:个别是在数据表中加上一个数据版本号 version 字段,示意数据被批改的次数,当数据被批改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若方才读取到的 version 值为以后数据库中的 version 值相等时才更新,否则重试更新操作,直到更新胜利。
外围 SQL 代码:
1 update table set xxx=#{xxx}, version=version+1 where id=#{id} and version=#{version};
(2)CAS 操作
CAS(Compare and Swap 比拟并替换),当多个线程尝试应用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并能够再次尝试。
CAS 操作中蕴含三个操作数——须要读写的内存地位 (V)、进行比拟的预期原值(A) 和拟写入的新值(B)。如果内存地位 V 的值与预期原值 A 相匹配,那么处理器会主动将该地位值更新为新值 B,否则处理器不做任何操作。
1.2 乐观锁
乐观锁认为对于同一个数据的并发操作,肯定会产生批改的,哪怕没有批改,也会认为批改。因而对于同一份数据的并发操作,乐观锁采取加锁的模式。乐观的认为,不加锁并发操作肯定会出问题。
在对任意记录进行批改前,先尝试为该记录加上排他锁(exclusive locking)。
如果加锁失败,阐明该记录正在被批改,那么以后查问可能要期待或者抛出异样。具体响应形式由开发者依据理论须要决定。
如果胜利加锁,那么就能够对记录做批改,事务实现后就会解锁了。
期间如果有其余对该记录做批改或加排他锁的操作,都会期待咱们解锁或间接抛出异样。
1.2 独享锁 / 共享锁
独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。
对于 Java ReentrantLock 而言,其是独享锁。然而对于 Lock 的另一个实现类 ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保障并发读是十分高效的,读写,写读,写写的过程是互斥的。
独享锁与共享锁也是通过 AQS 来实现的,通过实现不同的办法,来实现独享或者共享。
对于 Synchronized 而言,当然是独享锁。
1.3 互斥锁 / 读写锁
下面讲的独享锁 / 共享锁就是一种狭义的说法,互斥锁 / 读写锁就是具体的实现。
互斥锁在 Java 中的具体实现就是 ReentrantLock。
读写锁在 Java 中的具体实现就是 ReadWriteLock。
1.4 可重入锁
可重入锁又名递归锁,是指在同一个线程在外层办法获取锁的时候,在进入内层办法会主动获取锁。说的有点形象,上面会有一个代码的示例。
对于 Java ReetrantLock 而言,从名字就能够看出是一个重入锁,其名字是 Re entrant Lock 从新进入锁。
对于 Synchronized 而言,也是一个可重入锁。可重入锁的一个益处是可肯定水平防止死锁。
; “ 复制代码 ”)
1 synchronized void setA() throws Exception{ 2 Thread.sleep(1000); 3 setB(); 4} 5
6 synchronized void setB() throws Exception{ 7 Thread.sleep(1000); 8 }
; “ 复制代码 ”)
下面的代码就是一个可重入锁的一个特点。如果不是可重入锁的话,setB 可能不会被以后线程执行,可能造成死锁。
1.5 偏心锁 / 非偏心锁
偏心锁是指多个线程依照申请锁的程序来获取锁。
非偏心锁是指多个线程获取锁的程序并不是依照申请锁的程序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿景象。
对于 Java ReetrantLock 而言,通过构造函数指定该锁是否是偏心锁,默认是非偏心锁。非偏心锁的长处在于吞吐量比偏心锁大。
对于 Synchronized 而言,也是一种非偏心锁。因为其并不像 ReentrantLock 是通过 AQS 的来实现线程调度,所以并没有任何方法使其变成偏心锁。
1.6 分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,对于 ConcurrentHashMap 而言,其并发的实现就是通过分段锁的模式来实现高效的并发操作。
咱们以 ConcurrentHashMap 来说一下分段锁的含意以及设计思维,ConcurrentHashMap 中的分段锁称为 Segment,它即相似于 HashMap(JDK7 和 JDK8 中 HashMap 的实现)的构造,即外部领有一个 Entry 数组,数组中的每个元素又是一个链表;同时又是一个 ReentrantLock(Segment 继承了 ReentrantLock)。
当须要 put 元素的时候,并不是对整个 hashmap 进行加锁,而是先通过 hashcode 来晓得他要放在哪一个分段中,而后对这个分段进行加锁,所以当多线程 put 的时候,只有不是放在一个分段中,就实现了真正的并行的插入。
然而,在统计 size 的时候,可就是获取 hashmap 全局信息的时候,就须要获取所有的分段锁能力统计。
分段锁的设计目标是细化锁的粒度,当操作不须要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
1.7 偏差锁 / 轻量级锁 / 重量级锁
这三种锁是指锁的状态,并且是针对 Synchronized。在 Java 5 通过引入锁降级的机制来实现高效 Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏差锁是指一段同步代码始终被一个线程所拜访,那么该线程会主动获取锁。升高获取锁的代价。
轻量级锁是指当锁是偏差锁的时候,被另一个线程所拜访,偏差锁就会降级为轻量级锁,其余线程会通过自旋的模式尝试获取锁,不会阻塞,进步性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程尽管是自旋,但自旋不会始终继续上来,当自旋肯定次数的时候,还没有获取到锁,就会进入阻塞,该锁收缩为重量级锁。重量级锁会让他申请的线程进入阻塞,性能升高。
1.8 自旋锁
在 Java 中,自旋锁是指尝试获取锁的线程不会立刻阻塞,而是采纳循环的形式去尝试获取锁,这样的益处是缩小线程上下文切换的耗费,毛病是循环会耗费 CPU。
2. 锁的应用
2.1 准备常识
2.1.1 AQS
AbstractQueuedSynchronized 形象队列式的同步器,AQS 定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如罕用的 ReentrantLock/Semaphore/CountDownLatch…
AQS 保护了一个 volatile int state(代表共享资源)和一个 FIFO 线程期待队列(多线程争用资源被阻塞时会进入此队列)。
state 的拜访形式有三种:
1 getState() 2 setState() 3 compareAndSetState()
AQS 定义两种资源共享形式:Exclusive(独占,只有一个线程能执行,如 ReentrantLock)和 Share(共享,多个线程可同时执行,如 Semaphore/CountDownLatch)。
不同的自定义同步器争用共享资源的形式也不同。自定义同步器在实现时只须要实现共享资源 state 的获取与开释形式即可,至于具体线程期待队列的保护(如获取资源失败入队 / 唤醒出队等),AQS 曾经在顶层实现好了。自定义同步器实现时次要实现以下几种办法:
1 isHeldExclusively(): 该线程是否正在独占资源。只有用到 condition 才须要去实现它。2 tryAquire(int): 独占形式。尝试获取资源,胜利则返回 true,失败则返回 false。3 tryRelease(int): 独占形式。尝试开释资源,胜利则返回 true,失败则返回 false。4 tryAcquireShared(int): 共享形式。尝试获取资源。正数示意失败;0 示意胜利,但没有残余可用资源;负数示意胜利,且有残余资源。5 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 减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,而后主调用线程就会 await()函数返回,持续后余动作。
一般来说,自定义同步器要么是独占办法,要么是共享形式,他们也只需实现 tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared 中的一种即可。但 AQS 也反对自定义同步器同时实现独占和共享两种形式,如 ReentrantReadWriteLock。
2.1.2 CAS
CAS(Compare and Swap 比拟并替换)是乐观锁技术,当多个线程尝试应用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其余线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并能够再次尝试。
CAS 操作中蕴含三个操作数——须要读写的内存地位(V)、进行比拟的预期原值(A)和拟写入的新值(B)。如果内存地位 V 的值与预期原值 A 相匹配,那么处理器会主动将该地位值更新为新值 B,否则处理器不做任何操作。无论哪种状况,它都会在 CAS 指令之前返回该地位的值(在 CAS 的一些非凡状况下将仅返回 CAS 是否胜利,而不提取以后值)。CAS 无效地阐明了“我认为地位 V 应该蕴含值 A;如果蕴含该值,则将 B 放到这个地位;否则,不要更改该地位,只通知我这个地位当初的值即可”。这其实和乐观锁的抵触查看 + 数据更新的原理是一样的。
JAVA 对 CAS 的反对:
在 JDK1.5 中新增 java.util.concurrent 包就是建设在 CAS 之上的。绝对于 synchronized 这种阻塞算法,CAS 是非阻塞算法的一种常见实现。所以 java.util.concurrent 包中的 AtomicInteger 为例,看一下在不应用锁的状况下是如何保障线程平安的。次要了解 getAndIncrement 办法,该办法的作用相当于 ++ i 操作。
; “ 复制代码 ”)
1 public class AtomicInteger extends Number implements java.io.Serializable{2 private volatile int value; 3 public final int get(){4 return value; 5}
6
7 public final int getAndIncrement(){ 8 for (;;){9 int current = get(); 10 int next = current + 1; 11 if (compareAndSet(current, next)) 12 return current; 13 } 14 } 15
16 public final boolean compareAndSet(int expect, int update){17 return unsafe.compareAndSwapInt(this, valueOffset, expect, update); 18 } 19 }
; “ 复制代码 ”)
2.2 实战
2.2.1 synchronized
synchronized 可重入锁验证
; “ 复制代码 ”)
1 public class MyLockTest implements Runnable {2 public synchronized void get() {3 System.out.println(“2 enter thread name–>” + Thread.currentThread().getName()); 4 //reentrantLock.lock();
5 System.out.println(“3 get thread name–>” + Thread.currentThread().getName()); 6 set();
7 //reentrantLock.unlock();
8 System.out.println(“5 leave run thread name–>” + Thread.currentThread().getName()); 9 } 10
11 public synchronized void set() { 12 //reentrantLock.lock();
13 System.out.println(“4 set thread name–>” + Thread.currentThread().getName()); 14 //reentrantLock.unlock();
15 } 16
17 @Override 18 public void run() { 19 System.out.println(“1 run thread name–>” + Thread.currentThread().getName()); 20 get(); 21} 22
23 public static void main(String[] args) {24 MyLockTest test = new MyLockTest(); 25 for (int i = 0; i < 10; i++) {26 new Thread(test, “thread-” + i).start(); 27} 28 } 29
30 }
; “ 复制代码 ”)
运行后果
; “ 复制代码 ”)
1 1 run thread name–>thread-0
2 2 enter thread name–>thread-0
3 3 get thread name–>thread-0
4 1 run thread name–>thread-1
5 1 run thread name–>thread-2
6 4 set thread name–>thread-0
7 5 leave run thread name–>thread-0
8 1 run thread name–>thread-3
9 2 enter thread name–>thread-2
10 3 get thread name–>thread-2
11 4 set thread name–>thread-2
12 5 leave run thread name–>thread-2
13 2 enter thread name–>thread-1
14 3 get thread name–>thread-1
15 4 set thread name–>thread-1
16 5 leave run thread name–>thread-1
17 2 enter thread name–>thread-3
18 3 get thread name–>thread-3
19 4 set thread name–>thread-3
20 5 leave run thread name–>thread-3
21 1 run thread name–>thread-5
22 2 enter thread name–>thread-5
23 3 get thread name–>thread-5
24 4 set thread name–>thread-5
25 5 leave run thread name–>thread-5
26 1 run thread name–>thread-7
27 1 run thread name–>thread-6
28 2 enter thread name–>thread-7
29 3 get thread name–>thread-7
30 4 set thread name–>thread-7
31 1 run thread name–>thread-4
32 5 leave run thread name–>thread-7
33 1 run thread name–>thread-8
34 2 enter thread name–>thread-8
35 3 get thread name–>thread-8
36 4 set thread name–>thread-8
37 5 leave run thread name–>thread-8
38 1 run thread name–>thread-9
39 2 enter thread name–>thread-4
40 3 get thread name–>thread-4
41 4 set thread name–>thread-4
42 5 leave run thread name–>thread-4
43 2 enter thread name–>thread-6
44 3 get thread name–>thread-6
45 4 set thread name–>thread-6
46 5 leave run thread name–>thread-6
47 2 enter thread name–>thread-9
48 3 get thread name–>thread-9
49 4 set thread name–>thread-9
50 5 leave run thread name–>thread-9
; “ 复制代码 ”)
get()办法中顺利进入了 set()办法,阐明 synchronized 确实是可重入锁。剖析打印 Log,thread- 0 先进入 get 办法体,这个时候 thread-1、thread-2、thread- 3 期待进入,但当 thread- 0 来到时,thread- 2 却先进入了办法体,没有依照 thread-1、thread-2、thread- 3 的程序进入 get 办法体,阐明 sychronized 确实是非偏心锁。而且在一个线程进入 get 办法体后,其余线程只能期待,无奈同时进入,验证了 synchronized 是独占锁。
2.2.2 ReentrantLock
ReentrantLock 既能够结构偏心锁又能够结构非偏心锁,默认为非偏心锁,将下面的代码改为用 ReentrantLock 实现,再次运行。
; “ 复制代码 ”)
1 import java.util.concurrent.locks.ReentrantLock; 2
3 public class MyLockTest implements Runnable {4
5 private ReentrantLock reentrantLock = new ReentrantLock(); 6
7 public void get() { 8 System.out.println(“2 enter thread name–>” + Thread.currentThread().getName()); 9 reentrantLock.lock(); 10 System.out.println(“3 get thread name–>” + Thread.currentThread().getName()); 11 set(); 12 reentrantLock.unlock(); 13 System.out.println(“5 leave run thread name–>” + Thread.currentThread().getName()); 14 } 15
16 public void set() { 17 reentrantLock.lock(); 18 System.out.println(“4 set thread name–>” + Thread.currentThread().getName()); 19 reentrantLock.unlock(); 20} 21
22 @Override 23 public void run() { 24 System.out.println(“1 run thread name–>” + Thread.currentThread().getName()); 25 get(); 26} 27
28 public static void main(String[] args) {29 MyLockTest test = new MyLockTest(); 30 for (int i = 0; i < 10; i++) {31 new Thread(test, “thread-” + i).start(); 32} 33 } 34
35 }
; “ 复制代码 ”)
运行后果
; “ 复制代码 ”)
1 1 run thread name–>thread-0
2 2 enter thread name–>thread-0
3 1 run thread name–>thread-1
4 2 enter thread name–>thread-1
5 3 get thread name–>thread-0
6 4 set thread name–>thread-0
7 1 run thread name–>thread-3
8 2 enter thread name–>thread-3
9 3 get thread name–>thread-3
10 4 set thread name–>thread-3
11 5 leave run thread name–>thread-3
12 1 run thread name–>thread-4
13 2 enter thread name–>thread-4
14 3 get thread name–>thread-4
15 4 set thread name–>thread-4
16 5 leave run thread name–>thread-4
17 1 run thread name–>thread-5
18 2 enter thread name–>thread-5
19 3 get thread name–>thread-5
20 4 set thread name–>thread-5
21 5 leave run thread name–>thread-5
22 1 run thread name–>thread-7
23 2 enter thread name–>thread-7
24 3 get thread name–>thread-7
25 4 set thread name–>thread-7
26 5 leave run thread name–>thread-7
27 5 leave run thread name–>thread-0
28 3 get thread name–>thread-1
29 4 set thread name–>thread-1
30 5 leave run thread name–>thread-1
31 1 run thread name–>thread-2
32 2 enter thread name–>thread-2
33 3 get thread name–>thread-2
34 4 set thread name–>thread-2
35 5 leave run thread name–>thread-2
36 1 run thread name–>thread-9
37 2 enter thread name–>thread-9
38 3 get thread name–>thread-9
39 4 set thread name–>thread-9
40 5 leave run thread name–>thread-9
41 1 run thread name–>thread-6
42 1 run thread name–>thread-8
43 2 enter thread name–>thread-8
44 3 get thread name–>thread-8
45 4 set thread name–>thread-8
46 5 leave run thread name–>thread-8
47 2 enter thread name–>thread-6
48 3 get thread name–>thread-6
49 4 set thread name–>thread-6
50 5 leave run thread name–>thread-6
; “ 复制代码 ”)
确实如其名,可重入锁,当然默认确实是非偏心锁。thread- 0 持有锁期间,thread- 1 期待领有锁,当 thread- 0 开释锁时 thread- 3 先获取到锁,并非依照先后顺序获取锁的。
将其结构为偏心锁,看看运行后果是否合乎预期。查看源码结构偏心锁很简略,只有在结构器传入 boolean 值 true 即可。
; “ 复制代码 ”)
1 /**
2 Creates an instance of {@code ReentrantLock} with the 3 given fairness policy. 4 5 @param fair {@code true} if this lock should use a fair ordering policy 6 */
7 public ReentrantLock(boolean fair) {8 sync = fair ? new FairSync() : new NonfairSync(); 9}
; “ 复制代码 ”)
批改下面例程的代码构造方法为:
1 ReentrantLock reentrantLock = new ReentrantLock(true);
ReentrantLock 实现偏心锁。
; “ 复制代码 ”)
1 import java.util.concurrent.locks.ReentrantLock; 2
3 public class MyLockTest implements Runnable {4
5 private ReentrantLock reentrantLock = new ReentrantLock(true);
6
7 public void get() { 8 System.out.println(“2 enter thread name–>” + Thread.currentThread().getName()); 9 reentrantLock.lock(); 10 System.out.println(“3 get thread name–>” + Thread.currentThread().getName()); 11 set(); 12 reentrantLock.unlock(); 13 System.out.println(“5 leave run thread name–>” + Thread.currentThread().getName()); 14 } 15
16 public void set() { 17 reentrantLock.lock(); 18 System.out.println(“4 set thread name–>” + Thread.currentThread().getName()); 19 reentrantLock.unlock(); 20} 21
22 @Override 23 public void run() { 24 System.out.println(“1 run thread name–>” + Thread.currentThread().getName()); 25 get(); 26} 27
28 public static void main(String[] args) {29 MyLockTest test = new MyLockTest(); 30 for (int i = 0; i < 10; i++) {31 new Thread(test, “thread-” + i).start(); 32} 33 } 34
35 }
; “ 复制代码 ”)
运行后果
; “ 复制代码 ”)
1 1 run thread name–>thread-0
2 2 enter thread name–>thread-0
3 3 get thread name–>thread-0
4 1 run thread name–>thread-2
5 2 enter thread name–>thread-2
6 4 set thread name–>thread-0
7 1 run thread name–>thread-3
8 2 enter thread name–>thread-3
9 1 run thread name–>thread-1
10 2 enter thread name–>thread-1
11 1 run thread name–>thread-5
12 2 enter thread name–>thread-5
13 3 get thread name–>thread-2
14 4 set thread name–>thread-2
15 5 leave run thread name–>thread-2
16 5 leave run thread name–>thread-0
17 3 get thread name–>thread-3
18 4 set thread name–>thread-3
19 5 leave run thread name–>thread-3
20 1 run thread name–>thread-9
21 2 enter thread name–>thread-9
22 3 get thread name–>thread-1
23 4 set thread name–>thread-1
24 5 leave run thread name–>thread-1
25 3 get thread name–>thread-5
26 4 set thread name–>thread-5
27 5 leave run thread name–>thread-5
28 3 get thread name–>thread-9
29 4 set thread name–>thread-9
30 5 leave run thread name–>thread-9
31 1 run thread name–>thread-6
32 2 enter thread name–>thread-6
33 3 get thread name–>thread-6
34 4 set thread name–>thread-6
35 1 run thread name–>thread-7
36 5 leave run thread name–>thread-6
37 2 enter thread name–>thread-7
38 3 get thread name–>thread-7
39 4 set thread name–>thread-7
40 5 leave run thread name–>thread-7
41 1 run thread name–>thread-4
42 2 enter thread name–>thread-4
43 3 get thread name–>thread-4
44 1 run thread name–>thread-8
45 2 enter thread name–>thread-8
46 4 set thread name–>thread-4
47 5 leave run thread name–>thread-4
48 3 get thread name–>thread-8
49 4 set thread name–>thread-8
50 5 leave run thread name–>thread-8
; “ 复制代码 ”)
偏心锁在多个线程想要同时获取锁的时候,会发现再排队,依照先来后到的程序进行。
2.2.3 ReentrantReadWriteLock
读写锁的性能都会比排他锁要好,因为大多数场景读是多于写的。在读多于写的状况下,读写锁可能提供比排它锁更好的并发性和吞吐量。Java 并发包提供读写锁的实现是 ReentrantReadWriteLock。
个性
阐明
公平性抉择
反对非偏心 (默认) 和偏心的锁获取形式,吞吐量还是非偏心优于偏心
重进入
该锁反对重进入,以读写线程为例:读线程在获取了读锁之后,可能再次获取读锁。而写线程在获取了写锁之后可能再次获取写锁,同时也能够获取读锁
锁降级
遵循获取写锁、获取读锁再开释写锁的秩序,写锁可能降级成为读锁
; “ 复制代码 ”)
1 import java.util.HashMap; 2 import java.util.Map; 3 import java.util.concurrent.locks.Lock; 4 import java.util.concurrent.locks.ReentrantReadWriteLock; 5
6 public class MyLockTest {7
8 public static void main(String[] args) {9 for (int i = 0; i < 10; i++) {10 new Thread(new Runnable() {11 @Override 12 public void run() {13 Cache.put(“key”, new String(Thread.currentThread().getName() + ” joke”)); 14 } 15 }, “threadW-” + i).start(); 16 new Thread(new Runnable() {17 @Override 18 public void run() {19 System.out.println(Cache.get(“key”)); 20 } 21 }, “threadR-” + i).start(); 22 new Thread(new Runnable() {23 @Override 24 public void run() {25 Cache.clear(); 26 } 27 }, “threadC-” + i).start(); 28} 29 } 30 } 31
32 class Cache {33 static Map<String, Object> map = new HashMap<String, Object>(); 34 static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); 35 static Lock r = rwl.readLock(); 36 static Lock w = rwl.writeLock(); 37
38 // 获取一个 key 对应的 value
39 public static final Object get(String key) {40 r.lock(); 41 try {42 System.out.println(“get ” + Thread.currentThread().getName()); 43 return map.get(key); 44 } finally {45 r.unlock(); 46 } 47 } 48
49 // 设置 key 对应的 value,并返回旧有的 value
50 public static final Object put(String key, Object value) {51 w.lock(); 52 try {53 System.out.println(“put ” + Thread.currentThread().getName()); 54 return map.put(key, value); 55 } finally {56 w.unlock(); 57 } 58 } 59
60 // 清空所有的内容
61 public static final void clear() { 62 w.lock(); 63 try {64 System.out.println(“clear ” + Thread.currentThread().getName()); 65 map.clear(); 66} finally {67 w.unlock(); 68 } 69 } 70 }
; “ 复制代码 ”)
运行后果
; “ 复制代码 ”)
1 put threadW-0
2 clear threadC-1
3 put threadW-1
4 get threadR-1
5 threadW-1 joke 6 put threadW-2
7 get threadR-0
8 threadW-2 joke 9 clear threadC-0
10 get threadR-2
11 null
12 clear threadC-4
13 clear threadC-2
14 clear threadC-3
15 put threadW-4
16 put threadW-3
17 get threadR-3
18 threadW-3 joke 19 put threadW-5
20 get threadR-4
21 threadW-5 joke 22 clear threadC-5
23 put threadW-6
24 put threadW-7
25 get threadR-7
26 threadW-7 joke 27 get threadR-5
28 threadW-7 joke 29 get threadR-6
30 threadW-7 joke 31 clear threadC-6
32 clear threadC-7
33 put threadW-8
34 clear threadC-8
35 put threadW-9
36 get threadR-9
37 threadW-9 joke 38 clear threadC-9
39 get threadR-8
40 null
; “ 复制代码 ”)
可看到一般 HashMap 在多线程中数据可见性失常。