|
16 | 16 | <!-- TOC depthFrom:2 depthTo:4 --> |
17 | 17 |
|
18 | 18 | - [概述](#概述) |
19 | | - - [为什么会出现 Lock?](#为什么会出现-lock) |
20 | | - - [synchronized](#synchronized) |
| 19 | + - [概念](#概念) |
| 20 | + - [公平锁/非公平锁](#公平锁非公平锁) |
| 21 | + - [可重入锁](#可重入锁) |
| 22 | + - [独享锁/共享锁](#独享锁共享锁) |
| 23 | + - [互斥锁/读写锁](#互斥锁读写锁) |
| 24 | + - [乐观锁/悲观锁](#乐观锁悲观锁) |
| 25 | + - [分段锁](#分段锁) |
| 26 | + - [偏向锁/轻量级锁/重量级锁](#偏向锁轻量级锁重量级锁) |
| 27 | + - [自旋锁](#自旋锁) |
| 28 | + - [为什么用 Lock、ReadWriteLock](#为什么用-lockreadwritelock) |
21 | 29 | - [Lock 和 ReentrantLock](#lock-和-reentrantlock) |
22 | 30 | - [要点](#要点) |
23 | 31 | - [源码](#源码) |
@@ -47,22 +55,91 @@ tags: |
47 | 55 |
|
48 | 56 | ## 概述 |
49 | 57 |
|
50 | | -### 为什么会出现 Lock? |
| 58 | +### 概念 |
51 | 59 |
|
52 | | -### synchronized |
| 60 | +#### 公平锁/非公平锁 |
53 | 61 |
|
54 | | -synchronized 是 Java 的内置锁。 |
| 62 | +公平锁是指多个线程按照申请锁的顺序来获取锁。 |
55 | 63 |
|
56 | | -如果一个代码块被 synchronized 修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况: |
| 64 | +非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。 |
57 | 65 |
|
58 | | -1. 获取锁的线程执行完了该代码块,然后线程释放对锁的占有; |
59 | | -2. 线程执行发生异常,此时 JVM 会让线程自动释放锁。 |
| 66 | +对于 Java `ReentrantLock`而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。 |
60 | 67 |
|
61 | | -如果这个获取锁的线程被阻塞了,但是又没有释放锁,其他线程就只能一直等待。 |
| 68 | +对于`Synchronized`而言,也是一种非公平锁。由于其并不像`ReentrantLock`是通过 AQS 的来实现线程调度,所以并没有任何办法使其变成公平锁。 |
62 | 69 |
|
63 | | -使用 synchronized,如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。 |
| 70 | +#### 可重入锁 |
64 | 71 |
|
65 | | -可重入锁可中断锁公平锁读写锁 |
| 72 | +可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。 |
| 73 | + |
| 74 | +说的有点抽象,下面会有一个代码的示例。对于 Java `ReentrantLock`而言, 他的名字就可以看出是一个可重入锁,其名字是`Re entrant Lock`重新进入锁。对于`Synchronized`而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。 |
| 75 | + |
| 76 | +``` |
| 77 | +synchronized void setA() throws Exception{ |
| 78 | + Thread.sleep(1000); |
| 79 | + setB(); |
| 80 | +} |
| 81 | +
|
| 82 | +synchronized void setB() throws Exception{ |
| 83 | + Thread.sleep(1000); |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +上面的代码就是一个可重入锁的一个特点,如果不是可重入锁的话,setB 可能不会被当前线程执行,可能造成死锁。 |
| 88 | + |
| 89 | +#### 独享锁/共享锁 |
| 90 | + |
| 91 | +独享锁是指该锁一次只能被一个线程所持有。 |
| 92 | + |
| 93 | +共享锁是指该锁可被多个线程所持有。 |
| 94 | + |
| 95 | +对于 Java `ReentrantLock`而言,其是独享锁。但是对于 Lock 的另一个实现类`ReadWriteLock`,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。独享锁与共享锁也是通过 AQS 来实现的,通过实现不同的方法,来实现独享或者共享。对于`Synchronized`而言,当然是独享锁。 |
| 96 | + |
| 97 | +#### 互斥锁/读写锁 |
| 98 | + |
| 99 | +上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。互斥锁在 Java 中的具体实现就是`ReentrantLock` |
| 100 | +读写锁在 Java 中的具体实现就是`ReadWriteLock` |
| 101 | + |
| 102 | +#### 乐观锁/悲观锁 |
| 103 | + |
| 104 | +乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。 |
| 105 | + |
| 106 | +从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。悲观锁在 Java 中的使用,就是利用各种锁。乐观锁在 Java 中的使用,是无锁编程,常常采用的是 CAS 算法,典型的例子就是原子类,通过 CAS 自旋实现原子操作的更新。 |
| 107 | + |
| 108 | +#### 分段锁 |
| 109 | + |
| 110 | +分段锁其实是一种锁的设计,并不是具体的一种锁,对于`ConcurrentHashMap`而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。我们以`ConcurrentHashMap`来说一下分段锁的含义以及设计思想,`ConcurrentHashMap`中的分段锁称为 Segment,它即类似于 HashMap(JDK7 与 JDK8 中 HashMap 的实现)的结构,即内部拥有一个 Entry 数组,数组中的每个元素既是一个链表;同时又是一个 ReentrantLock(Segment 继承了 ReentrantLock)。当需要 put 元素的时候,并不是对整个 hashmap 进行加锁,而是先通过 hashcode 来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程 put 的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计 size 的时候,可就是获取 hashmap 全局信息的时候,就需要获取所有的分段锁才能统计。分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。 |
| 111 | + |
| 112 | +#### 偏向锁/轻量级锁/重量级锁 |
| 113 | + |
| 114 | +这三种锁是指锁的状态,并且是针对`Synchronized`。在 Java 5 通过引入锁升级的机制来实现高效`Synchronized`。 |
| 115 | + |
| 116 | +这三种锁的状态是通过对象监视器在对象头中的字段来表明的。 |
| 117 | + |
| 118 | +偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。 |
| 119 | + |
| 120 | +轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。 |
| 121 | + |
| 122 | +重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。 |
| 123 | + |
| 124 | +#### 自旋锁 |
| 125 | + |
| 126 | +在 Java 中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗 CPU。 |
| 127 | + |
| 128 | +### 为什么用 Lock、ReadWriteLock |
| 129 | + |
| 130 | +* synchronized 的缺陷 |
| 131 | + |
| 132 | + * 被 synchronized 修饰的方法或代码块,只能被一个线程访问。如果这个线程被阻塞,其他线程也只能等待。 |
| 133 | + * synchronized 不能响应中断。 |
| 134 | + * synchronized 没有超时机制。 |
| 135 | + * synchronized 只能是非公平锁。 |
| 136 | + |
| 137 | +* Lock、ReadWriteLock 相较于 synchronized,解决了以上的缺陷: |
| 138 | + * Lock 可以手动释放锁(synchronized 获取锁和释放锁都是自动的),以避免死锁。 |
| 139 | + * Lock 可以响应中断 |
| 140 | + * Lock 可以设置超时时间,避免一致等待 |
| 141 | + * Lock 可以选择公平锁或非公平锁两种模式 |
| 142 | + * ReadWriteLock 将读写锁分离,从而使读写操作分开,有效提高并发性。 |
66 | 143 |
|
67 | 144 | ## Lock 和 ReentrantLock |
68 | 145 |
|
@@ -250,7 +327,7 @@ public class ReentrantReadWriteLockDemo { |
250 | 327 |
|
251 | 328 | ## AQS |
252 | 329 |
|
253 | | -> AQS 作为构建锁或者其他同步组件的基础框架 |
| 330 | +> AQS 作为构建锁或者其他同步组件的基础框架,有必要好好了解一下其原理。 |
254 | 331 |
|
255 | 332 | ### 要点 |
256 | 333 |
|
@@ -769,3 +846,5 @@ tryAcquireSharedNanos 方法与 tryAcquireNanos 几乎一致,不再赘述。 |
769 | 846 | * https://zhuanlan.zhihu.com/p/27134110 |
770 | 847 | * https://t.hao0.me/java/2016/04/01/aqs.html |
771 | 848 | * http://ju.outofmemory.cn/entry/353762 |
| 849 | +* https://blog.csdn.net/u012403290/article/details/64910926?locationNum=11&fps=1 |
| 850 | +* https://www.cnblogs.com/qifengshi/p/6831055.html |
0 commit comments