1.6 锁
1.6.1 Synchronized
1 Synchronized的使用方式
修饰实例方法:
作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
修饰静态方法:
也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
修饰代码块:
指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
2 底层原理
① synchronized 同步语句块的情况
public class Synchronized Demo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
复制代码
注意存在两次 moniterexit
② synchronized 修饰方法的的情况
public class Synchronize dDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}
复制代码
3 锁升级
无锁状态,可偏向
JVM启动一会会后,才会开启,最开始生成的对象是可偏向状态
偏向锁
偏向第一个拿到锁的线程。
当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。
轻量级锁
轻量级锁就是通过 CAS 进行加锁的。
JVM 会给线程的栈帧中创建一个叫锁记录 Lock Record 的空间,把对象头 Mark Word 复制到该空间里(Displaced Mark Word),并通过 CAS 尝试把原对象头 Mark Word 中锁记录指针指向该锁记录。如果成功,表示线程拿到了锁。如果失败,则进行自旋(自旋锁),自旋超过一定次数时升级为重量级锁,这时该线程会被内核挂起。
自旋锁
轻量级锁膨胀为重量级锁前,线程在执行 monitorenter 指令进入等待队列时,会通过自旋去尝试获得锁。
如果自旋超过一定次数时还未拿到锁(10次),就会进入阻塞状态,等待内核来调度。此时会发生内核态与用户态之间的上下文切换,所以会影响性能(引入自旋锁就是为了减少这个开销)。
因为后面的线程也先进行自旋尝试获取锁,所以这对于已被阻塞的那些线程来说,会不公平。
重量级锁
重量级锁就是通过内核来操作线程。因为频繁出现内核态与用户态的切换,会严重影响性能。
升级为重量级锁时会在堆中创建 monitor 对象,并将 Mark Word 指向该 monitor 对象,并将线程加入 EntrySet 中。
1.6.2 ReentryLock
底层主要是一个 AQS 和 condition 节点
节点状态
- int INITIAL = 0; // 初始状态
- int CANCELLED = 1; // 当前节点从同步队列中取消
- int SIGNAL = -1; // 后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继 节点的线程继续运行。
- int CONDITION = -2; // 节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了 signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中。
- int PROPAGATE = -3; // 表示下一次共享式同步状态获取将会无条件地被传播下去。
源码分析
锁获取
// 顶层入口
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
/*
tryAcquire()尝试直接去获取资源,如果成功则直接返回(这里体现了非公平锁,每个线程获取锁时会尝试直接抢占加塞一次,而CLH队列中可能还有别的线程在等待);
addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
acquireQueued()使线程阻塞在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
*/
// 产生获取锁
tryAcquire(int)
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
// 添加进入等待队列
addWaiter(Node mode)
private Node addWaiter(Node mode) {
//以给定模式构造结点。mode有两种:EXCLUSIVE(独占)和SHARED(共享)
Node node = new Node(Thread.currentThread(), mode);
//尝试快速方式直接放到队尾。
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//上一步失败则通过enq入队。
enq(node);
return node;
}
private Node enq(final Node node) {
//CAS"自旋",直到成功加入队尾
for (;;) {
Node t = tail;
if (t == null) { // 队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。
if (compareAndSetHead(new Node()))
tail = head;
} else {//正常流程,放入队尾
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
复制代码
上面的代码进入的等待队列中,需要排队拿号,去获得锁
// 接上文 第4行 的方法
// 在等待队列中排队拿号(中间没其它事干可以休息),直到拿到号后再返回
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;//标记是否成功拿到资源
try {
boolean interrupted = false;//标记等待过程中是否被中断过
//又是一个“自旋”!
for (;;) {
final Node p = node.predecessor();//拿到前驱
//如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)。
if (p == head && tryAcquire(arg)) { //tryAcquire 返回 ture 成功拿到锁
setHead(node);//拿到资源后,将head指向该结点。所以head所指的标杆结点,就是当前获取到资源的那个结点或null。
p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!
failed = false; // 成功获取资源
return interrupted;//返回等待过程中是否被中断过
}
//如果自己可以休息了,就通过park()进入waiting状态,直到被unpark()。如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
}
} finally {
if (failed) // 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。
cancelAcquire(node);
}
}
// 接 20 行
// 此方法主要用于检查状态,看看自己是否真的可以去休息了(进入waiting状态,如果线程状态转换不熟,可以参考本人上一篇写的Thread详解),万一队列前边的线程都放弃了只是瞎站着,那也说不定,对吧!
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;//拿到前驱的状态
if (ws == Node.SIGNAL)
//如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了
return true;
if (ws > 0) {
/*
* 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
* 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)!
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢!
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
// 接上第 21 行 线程进入休息
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//调用park()使线程进入waiting状态
return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。
}
复制代码
到此可以进行锁的获取
主要过程
非公平状态下,首先尝试直接获取
不能直接获取,生成节点去纳入等待队列中,需要把前面节点的状态变成sign(通知唤醒我),如果找不到,就一直向前找
通过一个死循环的自旋操作去获取锁,如果前驱是 head 节点,就尝试去获取锁,如果前去不是,就通过park()的方法把自己阻塞
锁释放
非共享模式下
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;//找到头结点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);//唤醒等待队列里的下一个线程
return true;
}
return false;
}
// 上接第 5 行, 此方法用于唤醒等待队列中下一个线程
private void unparkSuccessor(Node node) {
//这里,node一般为当前线程所在的结点。
int ws = node.waitStatus;
if (ws < 0)//置零当前线程所在的结点状态,允许失败。
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;//找到下一个需要唤醒的结点s
if (s == null || s.waitStatus > 0) {//如果为空或已取消
s = null;
for (Node t = tail; t != null && t != node; t = t.prev) // 从后向前找。
if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);//唤醒
}
复制代码
共享模式下
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);//加入队列尾部
boolean failed = true;//是否成功标志
try {
boolean interrupted = false;//等待过程中是否被中断过的标志
for (;;) {
final Node p = node.predecessor();//前驱
if (p == head) {//如果到head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可能是head用完资源来唤醒自己的
int r = tryAcquireShared(arg);//尝试获取资源
if (r >= 0) {//成功
setHeadAndPropagate(node, r);//将head指向自己,还有剩余资源可以再唤醒之后的线程
p.next = null; // help GC
if (interrupted)//如果等待过程中被打断过,此时将中断补上。
selfInterrupt();
failed = false;
return;
}
}
//判断状态,寻找安全点,进入waiting状态,等着被unpark()或interrupt()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// 上接第 16 行,将头节点设置位自己,唤醒后面的线程,
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);//head指向自己
//如果还有剩余量,继续唤醒下一个邻居线程
if (propagate > 0 || h == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
// 释放锁
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {//尝试释放资源
doReleaseShared();//唤醒后继结点
return true;
}
return false;
}
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h);//唤醒后继
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)// head发生变化
break;
}
}
复制代码
共享和非共享的主要区别在于:当head 节点释放了锁,唤醒后续节点的时候,共享模式会一直持续唤醒后面点,而非共享模式不会
1.6.3 相关组件
Semaphore (信号量):
允许多个线程同时访问: synchronized 和 ReentrantLock 都是⼀次只允许⼀个线程访问某个资源, Semaphore (信号量)可以指定多个线程同时访问某个资源。
CountDownLatch (倒计时器):
CountDownLatch 是⼀个同步⼯具类,⽤来协调多个线程之间的同步。这个⼯具通常⽤来控制线程等待,它可以让某⼀个线程等待直到倒计时结
束,再开始执⾏。
public class CountDownLatchExample1 {
// 处理⽂件的数量
private static final int threadCount = 6;
public static void main(String[] args) throws InterruptedException {
// 创建⼀个具有固定线程数量的线程池对象(推荐使⽤构造⽅法创建)
ExecutorService threadPool = Executors.newFixedThreadPool(10);
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
final int threadnum = i;
threadPool.execute(() -> {
try {
//处理⽂件的业务操作
......
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//表示⼀个⽂件已经被完成
countDownLatch.countDown();
}
});
}
countDownLatch.await();
threadPool.shutdown();
System.out.println("finish");
}
复制代码
CyclicBarrier (循环栅栏):
CyclicBarrier 和 CountDownLatch ⾮常类似,它也可以实现线程间的技术等待,但是它的功能⽐ CountDownLatch 更加复杂和强⼤。主要应⽤场景和
CountDownLatch 类似。 CyclicBarrier 的字⾯意思是可循环使⽤( Cyclic )的屏障( Barrier )。它要做的事情是,让⼀组线程到达⼀个屏障(也可以叫同步点)时被阻塞,直到最后⼀个线程到达屏障时,屏障才会开⻔,所有被屏障拦截的线程才会继续⼲活。 CyclicBarrier 默认的构造⽅法是 CyclicBarrier(int parties) ,其参数表示屏障拦截的线程数量,每个线程调⽤ await() ⽅法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
ReadWriteLock(读写锁)
public ReentrantReadWriteLock() {
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
复制代码
将锁的state 状态变成高16位和低16位,分别表示锁状态
公平模式 ====》都是入队
非公平模式 ====》
由于获取读锁的逻辑比较复杂,我们在这里先简单进行归纳:
a. 如果当前全局处于无锁状态,则当前线程获取读锁
b. 如果当前全局处于读锁状态,且队列中没有等待线程,则当前线程获取读锁
c. 如果当前全局处于写锁占用状态(并且不是当前线程占有),则当前线程入队尾
d. 如果当前全局处于读锁状态,且等待队列中第一个等待线程想获取写锁,那么当前线程能够获取到读锁的条件为:当前线程获取了写锁,还未释放;当前线程获取了读锁,这一次只是重入读锁而已;其它情况当前线程入队尾。之所以这样处理一方面是为了效率,一方面是为了避免想获取写锁的线程饥饿,老是得不到执行的机会
e. 如果当前全局处于读锁状态,且等待队列中第一个等待线程不是写锁,则当前线程可以抢占读锁
获取写锁相对就比较简单了,规则如下:
h. 如果当前处于无锁状态,则当前线程获取写锁
i. 如果当前全局处于读锁状态,当前线程入队尾
j. 如果当前全局处于写锁状态,除非是重入获取写锁,否则入队尾
锁降级
假设A线程获得了写锁,在释放写锁之前A线程可以继续获得读锁。这个时候A线成同时拥有读锁和写锁。A线程的写业务完成后,先释放了写锁,但没有释放读锁。这个时候对于A线程来说就是锁降级了。需要注意的是,如果A线程首先获得的是读锁,这个时候是不能直接升级到写锁的。需要先把读锁释放了,再去尝试获得写锁
锁升级===》不可以
可重入===》 一旦发生可重入操作,可以直接获取,不需要排队
1.6.4 ReentrantLock 和 synchronized 的区别
1. 锁的实现
synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
2. 性能
新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
3. 等待可中断
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock 可中断,而 synchronized 不行。
4. 公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
5. 锁绑定多个条件
一个 ReentrantLock 可以同时绑定多个 Condition 对象。
1.6.5 interrupted、interrupt、isInterrupted
interrupt():
方法是用于中断线程的,调用该方法的线程的状态将被置为”中断”状态。
注意:调用interrupt()方法仅仅是在当前线程中打了一个停止的标记,并不是真的停止线程,需要用户自己去监视线程的状态为并做处理</font>。这一方法实际上完成的是,在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态
。
更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,那么,它将接收到一个中断异常(InterruptedException),从而提早地终结被阻塞状态。
interrupted():
测试当前线程(当前线程是指运行interrupted()方法的线程)是否已经中断,且清除中断状态。
isInterrupted():
isInterrupted():测试线程(*调用该方法的线程*)是否已经中断,不清除中断状态。
public class InterruptionSleepThread implements Runnable {
@Override
public void run() {
try {
System.out.println("本大爷要休眠50秒,你能奈我何?");
Thread.sleep(50000);//休眠50秒
} catch (InterruptedException e) {
System.out.println("休眠的线程收到中断信号,利用抛异常的方式将本大爷从梦中叫醒,结束线程。");
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new InterruptionSleepThread());
thread.start();
Thread.sleep(2000);
thread.interrupt();
System.out.println("主线程结束");
}
}
复制代码