并发编程的关键——LOCK
- 锁的分类
- synchronized
- 万物即可为锁
- synchronized的实现
- 锁升级
- Lock
- AQS
- LockSupport
- CLH
- CAS
- Lock实现
- ReentrantLock
- 阻塞方法acquire
- ReadWriteLock
- ReentrantReadWriteLock
- StampedLock
锁的分类
- 公平锁/非公平锁:
– 公平的意思是多个线程按照申请锁的顺序来获取锁。
– 公平锁通常会造成系统更低的吞吐量
– 非公平锁可能会出现线程饥饿的现象
– ReentranLock:默认是非公平锁,可以通过构造参数指定为公平锁。
– synchronized:默认是非公平锁,并且不能变为公平锁。 - 独享锁/共享锁:
– 独享的意思是一个锁只能被一个线程持有。
– ReentranLock是独享锁
– ReadWriteLocK的读锁是共享锁,写锁是独享锁。
– synchronized是独享锁 - 互斥锁/读写锁: 独享锁和共享锁的具体实现。
- 乐观锁/悲观锁:
– 乐观锁认为数据不一定会被修改,所以不会加锁,而是不断尝试更新。
– 悲观锁认为数据会被修改,所以采用加锁的方式实现同步。
– 乐观锁适合读操作多余写操作的系统。 - 分段锁:一种锁的设计,例如ConcurentHashMap就使用了分段锁。
- 偏向锁/轻量级锁/重量级锁:synchronized的三种状态
– 偏向锁指同一段同步代码一直被一个线程访问,那么该线程就会自动获取这个锁,以降低获取锁的代价。
– 轻量级锁:当前锁是偏向锁,并且被另一个线程访问,偏向锁会升级成轻量级锁,其他线程会通过自旋(CAS)的形式尝试获取锁。不会阻塞其他线程。
– 重量级锁:当前锁是轻量级锁,另一个线程自旋到一定次数的时候还没获取到该锁,轻量级锁就会升级为重量级锁,会阻塞其他线程。 - 自旋锁/调度锁:
– 自旋锁:反复参数直到满足条件。
– 调度锁:跟系统调度机构交互,实现并发控制。 - 可重入锁:指同一线程再外层方法获取锁的时候,进入内层方法时会自动获取锁。
synchronized
synchronized是基于JVM实现的锁。
万物即可为锁
//给对象上锁----锁对象
synchronized Object obj = new Object();
//给方法上锁---锁对象
public synchronized void function(){}
//给静态方法上锁---锁class对象
public synchronized static void funciton(){}
同一个实例的两个方法都有synchronize锁,当方法1被一个线程上锁后,另一个线程能进入方法2吗?
方法上加锁,锁的是对象,静态方法上加锁,锁的是class对象,既然如此,答案肯定是不能进入。代码验证:
public class LockService {
public synchronized void lock1(){
System.out.println(Thread.currentThread().getName()+"进入lock1");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"离开lock1");
}
public synchronized void lock2(){
System.out.println(Thread.currentThread().getName()+"进入lock2");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"离开lock2");
}
}
public class LockTest {
public static void main(String[] args) {
LockService lockService = new LockService();
new Thread(lockService::lock1).start();
new Thread(lockService::lock2).start();
System.out.println("等待任务执行完成");
}
}
Thread-0进入lock1
等待任务执行完成
Thread-0离开lock1
Thread-1进入lock2
Thread-1离开lock2
疑问?
synchronized(this) {}
是锁代码块?这不是锁当前对象吗?
synchronized的实现
synchronized是通过对象头的锁标志位实现对象加锁的。
1.5以前synchronized是重量级锁,悲观锁,来就会加锁。
1.6以后优化了,有了锁升级的过程。
锁升级
1.6以后被synchronized标记的对象存在一个锁升级的过程,依次是:
注意:锁升级是按
无锁>偏向锁>轻量级锁>重量级锁
进行升级的,并且除了偏向锁可以重置为无锁外,锁是无法降级的。
Lock
lock是一个接口,用代码来实现锁。
AQS
AQS是jdk种Lock的实现基础,AQS使用模板方法模式,提供一个框架来实现依赖于FIFO等待队列的阻塞锁和相关的同步器(信号量、事件等),底层实现是volatile修饰的state和CAS。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
/**
* 等待队列的NODE节点类,等待队列是LCH锁队列的一种变种。CLH锁通常用于自旋锁。
* 相反,等待队列将它们用于阻塞同步器,但使用相同的基本策略,即在上一节点中保留有关线程的一些控制信息。
* 当一个线程在等待某个资源时,它会被放入等待队列中,等待队列中的每个元素都是一个NODE对象。
* +------+ prev +-----+ +-----+
* head | | <---- | | <---- | | tail
* +------+ +-----+ +-----+
*/
static final class Node{
/** 用于指示节点正在共享模式下等待的标记 */
static final Node SHARED = new Node();
/** 用于指示节点正在独占模式下等待的标记 */
static final Node EXCLUSIVE = null;
/**
* waitStatus是AQS的重要属性,表示当前节点的状态。有五种取值:
* CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
* SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
* CONDITION(-2):表示节点在条件等待队列中,当其他线程调用了Condition的signal()方法之后,该节点就会从条件等待队列移动到同步队列中。
* PROPAGATE(-3):表示当前节点需要被传播或者传递到下一个节点。
* 当一个线程在获取资源时,如果当前节点不是头节点,并且前继节点的状态为SIGNAL或CONDITION,那么当前节点的状态将被更新为PROPAGATE。
* 这个状态的作用是让当前节点能够将前继节点的状态传递下去,从而让下一个节点有机会获取资源。
* 0:表示线程节点进入就绪状态,可以继续尝试获取锁了。
*/
volatile int waitStatus;
//连接到前一个节点,当前节点的waitStatus检查依赖前一节点。入队时分配,出队时设为null(为了GC) 。
volatile Node prev;
//连接到下一节点,
volatile Node next;
//将此节点排入队列的线程。在构造时初始化,使用后为null
volatile Thread thread;
Node nextWaiter;
}
//等待队列的头和尾
private transient volatile Node head;
private transient volatile Node tail;
/**
* 同步状态,有三种状态:
* 0:没有线程持有
* 1:已被线程持有
* >1: 被线程多次持有
* 当一个线程来尝试加锁时,会对state进行CASE操作,将state从0改为1,如果操作成功,则将线程信息设置成自己。如果操作失败,则会进入等待队列。如果线程需要释放锁时会对state进行减1,如果减1后为0,则会彻底释放锁,将加锁线程置为null,然后从等待队列唤醒下一个线程。
*/
private volatile int state;
/**
* Condition同样时一个队列,用于维护等待队列(条件队列)。Condition是基于Node实现的。
* 每一个Condition都拥有一个等待队列(NODE队列),一个Lock可以有多个Condition,每一个Condition都是对应了一个单独的条件。
*/
public class ConditionObject implements Condition, java.io.Serializable {
private transient Node firstWaiter;
private transient Node lastWaiter;
}
}
LockSupport
LockSupport通过许可(permit)实现线程挂起、挂起线程唤醒功能。许可可以理解为每个已启动线程维持的一个int类型状态位counter。线程分别通过执行LockSupport静态方法park()、unPark()方法来完成挂起、唤醒操作2。LockSupport通常不会被直接使用,更多是作为锁实现的基础工具类1。
CLH
CLH是Craig、Landin、Hagersten三人发明的一种基于双向链表数据结构的队列,是FIFO先入先出线程等待队列,Java中的CLH队列是原CLH队列的一个变种,线程由原自旋机制改为阻塞机制1。
CAS
CAS全称为Compare And Swap,是unsafe的一个方法。CAS机制的原理是利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法,该操作时原子的。
CAS操作包括三个操作数:内存位置V、预期原值A和新值B。当内存位置V的值与预期原值A相匹配时,将内存位置V的值设置为新值B,并且返回true;否则,不做任何操作,并返回false。
Lock实现
上面对AQS的概念还是比较模糊,下面将从锁的具体实现来理解下AQS
ReentrantLock
//提供锁实现的同步基础,分为以下公平和非公平版本。使用AQS状态表示锁上的挂起次数。
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
//……
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
//非公平锁
static final class NonfairSync extends Sync {
/**
* 立即尝试获取锁,否则以独占锁形式进入队列获取锁。
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
//公平锁
static final class FairSync extends Sync {
/**
* 直接进入队列获取锁
*/
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
public void lock() {
sync.lock();
}
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
注意看lock()方法,非公平锁和公平锁的差异就是:
- 非公平锁会在lock的时候不管三七二十一,先尝试获取锁一次,如果获取失败,则会再查看下state是不是0,如果是0,再抢一次锁。如果没抢到才会乖乖入队。
- 而公平锁则是查看state是否为0,并且是否其他线程在等待,如果没有才会尝试加锁。
lock和tryLock的区别是:
- lock:获取锁失败会阻塞。
- tryLock:获取锁失败则返回false,成功则返回true。不会阻塞。
阻塞方法acquire
/*********AQS*****************/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
/**
* 在一个死循环里面,不断检测上一个节点的状态,直到上一个节点状态为SIGNAL则将当前线程挂起来等待唤醒或被中断
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
/**
*shouldParkAfterFailedAcquire只有检测到前一个节点状态为SIGNAL才会返回true,此时当前节点才能安全的挂起。
*parkAndCheckInterrupt负责将当前线程挂起,并判断是否中断。如果线程未中断,则继续阻塞;如果线程中断,则抛出InterruptedException异常
*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
ReadWriteLock
ReadWriteLock读写锁是一个接口,定义了两个获取锁的接口readLock
和writeLock
,在jdk里面其有两个实现:
ReentrantReadWriteLock
ReentrantReadWriteLock故名思意,可重入读写锁。
读写锁的读锁和写锁的关系是什么?
- 读锁之间不互斥,可多个线程同时获取;
- 读锁与写锁互斥,即写锁已经被某个线程获取时,其他线程不能获取读锁;读锁被某个线程获取时,其他线程不能获取写锁。
- 写锁之间互斥,即一次只能有一个线程获取写锁;
- 这两种锁之间会有优先级,当有等待的线程时,写锁锁住优先于读锁锁住;
锁降级是什么?
锁降级是说一个线程在拥有写锁的情况下可以不释放写锁,直接获取读锁,然后再释放写锁。这个过程就完成了锁降级。锁降级的好处是可以在写操作后直接进行读操作,避免了无谓的锁竞争和线程阻塞,提高了程序的并发性能。需要注意的是,锁降级是可逆的,即读锁可以再升级为写锁。但是锁升级的操作会导致死锁的风险,因此在使用锁降级时需要谨慎处理锁的获取和释放顺序。
StampedLock
StampedLock是JDK1.8中新增的一个读写锁,也是对JDK1.5中的读写锁ReentrantReadWriteLock的读性能的优化。
StampedLock有一个stamp变量(戳记,long类型)代表了锁的状态。当stamp返回零时,表示线程获取锁失败。并且,当释放锁或者转换锁的时候,都要传入最初获取的stamp值1。StampedLock有以下3种模式:
- 悲观读锁。与ReadWriteLock的读锁类似(这里的读锁不可重入),多个线程可以同时获取悲观读锁,悲观读锁是一个共享锁。
- 乐观读锁。相当于直接操作数据,不加任何锁,连读锁都不要。在操作数据前并没有通过CAS设置锁的状态,仅仅通过位运算测试。
- 写锁。与ReadWriteLock的写锁类似,写锁和悲观读锁是互斥的。