JAVA SDK并发包通过lock和condition两个接口来实现管程,其中lock用于解决互斥,condition用于解决同步问题。
1.6之前的synchronized互斥锁,线程未获取到锁就进入阻塞状态。Lock针对这个问题做了一些设计来解决。如下:
再造管程的设计理念:
- 能够响应中断。synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了。
- 支持超时。如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
- 非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
重入锁: ReentrantLock,指的是线程可以重复获取同一把锁。
公平锁和非公平锁:ReentrantLock有两个构造器,一个无参,一个传入fair参数。fair代表是否公平策略,传入true是公平锁,传入false是非公平锁。公平锁的意义在于当有线程释放锁的时候,就需要从等待队列中唤醒一个等待的线程。如果是公平锁,唤醒的策略就是谁等待的时间长,就唤醒谁;如果是非公平锁,有可能等待时间短的线程反而先被唤醒。
重入锁比较灵活,可以通过lock,unlock trylock等方法实现锁的细节控制。公平性是true的话,会倾向于把锁赋值给等待时间久的线程,这种事减少线程饥饿的方法。但是引入公平锁会产生一定的开销,会导致吞吐量下降。
再从重入锁出发,分场景:
读多写少的场景: 读写锁,readWriteLock。读写锁的特点:. 1 允许多个线程同时读共享变量; 2. 只允许一个线程写共享变量; 3. 如果一个写线程正在执行写操作,此时禁止读线程读共享变量。读写锁支持重入锁所有的方法。都实现了LOCK接口的方法。
读写锁喝互斥锁的最大区别:读写锁允许多个线程同时读共享变量,互斥锁是不允许的。但是读写锁的写操作是互斥的,当一个线程在进行写操作时,是不允许其他线程来进行读操作和写操作的。
如下是读写锁来实现一个缓存工具类:
final Map<String, String> m = new HashMap<>();
final ReadWriteLock rwl = new ReentrantReadWriteLock();
// 读锁
final Lock r = rwl.readLock();
// 写锁
final Lock w = rwl.writeLock();
String get(String key) {
String v = null;
// 读缓存
r.lock();
try {
v = m.get(key);
} finally {
r.unlock();
}
// 缓存中存在,返回
if (v != null) {
return v;
}
// 缓存中不存在,查询数据库
w.lock();
try {
// 避免高并发场景下其他线程重复查询数据库。
v = m.get(key);
if (v == null) {
// 查询数据库
m.put(key, v);
}
} finally {
w.unlock();
}
return v;
}
// 写缓存
String put(String key, String v) {
w.lock();
try {
return m.put(key, v);
} finally {
w.unlock();
}
}
这边首先获取缓存初始化的数据。缓存的初始化,分为一次性加载和按需加载,即懒加载,按数据的量决定。
读写锁不支持锁的升级(读锁里嵌套了写锁),读锁还没有释放掉的情况下获取写锁,会导致读锁一直处于阻塞状态。但是读写锁支持锁的降级,获取读锁的时候线程还持有写锁。
AQS abstractQueuedSynchronieer 同步发生器。重入锁,读写锁都是基于AQS的。通过FIFO同步队列来实现线程争夺资源的。线程之间通过指针连接起来。前面指针指向后面指针的后继,后面指针指向前面的前驱。还有一个头结点同步器,主要是用来指向头部线程和尾部线程的。这就是CLH同步队列。 实际工作原理是 线程会先获取state值 state为0时没有线程占用获取锁,state大于0时有线程占用,线程进入同步队列中。state也是volatile修饰的。每个线程通过自旋的方式去获取锁。
semaphore
Semaphore,普遍翻译为“信号量”。
信号量模型还是很简单的,可以简单概括为:一个计数器,一个等待队列,三个方法。在信号量模型里,计数器和等待队列对外是透明的,所以只能通过信号量模型提供的三个方法来访问它们,这三个方法分别是:init()、down() 和 up()。
- init():设置计数器的初始值。
- down():计数器的值减 1;如果此时计数器的值小于 0,则当前线程将被阻塞,否则当前线程可以继续执行。
- up():计数器的值加 1;如果此时计数器的值小于或者等于 0,则唤醒等待队列中的一个线程,并将其从等待队列中移除。
活锁:各个线程之间互相谦让,都让对方线程去获取资源。
线程饥饿: 线程A进入等待队列,获得锁的线程执行完任务后把CPU资源给了线程B,B释放锁后又给了C,就一直没有给到线程A。