1.守护线程和用户线程区别
JVM会等待非守护线程完成后关闭, 但不会等待守护线程
2.线程的生命周期
- 五个状态:新建,可运行(就绪),运行,阻塞,死亡
- 三种阻塞原因:sleep,wait,suspend
- 生命周期
以及
以及
3.如何结束一个一直运行的线程
场景一: 中断处于运行状态的线程
通常,我们通过“标记”方式终止处于“运行状态”的线程
- 中断标记
@Override
public void run() {
while (!isInterrupted()) {
// 执行任务...
}
}
说明:isInterrupted()是判断线程的中断标记是不是为true。当线程处于运行状态,并且我们需要终止它时;可以调用线程的interrupt()方法,使用线程的中断标记为true,即isInterrupted()会返回true。此时,就会退出while循环。 注意:interrupt()并不会终止处于“运行状态”的线程!它会将线程的中断标记设为true。`
- 额外标记(自定义标记)
private volatile boolean flag= true;
@Override
public void run() {
while (flag) {
// 执行任务...
}
}
注意:将flag定义为volatile类型,是为了保证flag的可见性。即其它线程通过stopTask()修改了flag之后,本线程能看到修改后的flag的值。
场景二: 中断处于阻塞状态的线程
使用interrupt()
当线程由于被调用了sleep(), wait(), join()等方法而进入阻塞状态;若此时调用线程的interrupt()将线程的中断标记设为true。由于处于阻塞状态,中断标记会被清除,同时产生一个InterruptedException异常。将InterruptedException放在适当的为止就能终止线程,形式如下:
@Override
public void run() {
try {
while (true) {
// 执行任务...
}
} catch (InterruptedException ie) {
// 由于产生InterruptedException异常,退出while(true)循环,线程终止!
}
}
参考 https://www.cnblogs.com/skywang12345/p/3479949.html
4.一个线程运行时发生异常会怎样
如果这个异常没有被捕获的话,这个线程就停止执行了。
另外重要的一点是:如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放
5.创建线程的方式及实现
- 方式一,继承 Thread 类创建线程类。
- 方式二,通过 Runnable 接口创建线程类。
此方式可以使用线程池 - 方式三,通过 Callable 和 Future 创建线程。
此方式可以使用线程池
6.start 和 run 方法有什么区别
调用start()将创建新线程,run()将被执行;
直接调用run(),则不会创建线程,run()将作为普通方法执行
7.如何使用 wait + notify 实现通知机制
- wait和notify都属于Object类的,故所有类都可以调用这两个方法。
- 对象调用wait会阻塞当前线程,并释放对象锁
- 调用notify则不会释放对象锁,但是会随机唤醒一个线程,等当前线程继续执行完notify()之后,
synchronized
之内的代码。唤醒的线程则需要抢到锁才能执行。 - 其他通信机制:Condition,CountDownLatch,Queue,Future等
8.Thread类的 sleep 方法和对象的 wait 方法都可以让线程暂停执行,它们有什么区别
- sleep()是Thread的静态方法,调用sleep会让当前线程让出CPU给其他线程,但是并不释放持有的锁。休眠时间结束后当前线程进入就绪状态等待CPU时间片。
- wait()是Object的方法,调用对象的wait()会让当前线程释放对象的锁,当前线程将进入对象等待池,只有调用对象的
notify()
或notifyAll()
,才能唤醒等待线程,如果线程再次获取了锁就可以进入就绪状态。
为什么你应该在循环中检查等待条件
https://blog.csdn.net/qq_35181209/article/details/77362297
sleep、join、yield 方法有什么区别
- sleep让出当前线程CPU并进入休眠,让其他线程有机会继续执行,但当前线程并不释放对象锁
- yield和 sleep 方法类似,也不会释放“锁标志”。区别在于它没有参数,不能设置休眠时间,所以休眠线程可能又马上能进入执行状态。同时yield只会让出CPU给同优先级或高优先级线程。在实际场景下很少用yield.
- 让一个线程 B “加入”到另外一个线程 A 的尾部
sleep(0) 有什么用途
线程暂时放弃 CPU
你如何确保 main 方法所在的线程是 Java 程序最后结束的线程
使用 Thread 类的 #join() 方法
interrupt ,interrupted 和 isInterrupted 方法的区别
- interrupt:调用该方法的线程的状态为将被置为”中断”状态
- interrupted: 查询当前线程的中断状态,会清除状态
- isInterrupted:查询当前线程的中断状态,不会清除状态
Servlet 是线程安全吗
Servlet 是单实例多线程的,不安全
单例模式的线程安全性
单例模式的线程安全是指:某个单例类的实例在多线程环境下只会被创建一个。
- 饿汉式单例模式的写法:线程安全
public class Singleton {
private static final Singleton INSTANCE=new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}
优点:类加载即创建单例,整个过程不会有第二个对象,单例线程安全。
缺点:过早创建,影响性能。
- 懒汉式单例模式的写法:非线程安全
public class Singleton{
private static Singleton instance = null;
private Singleton(){}
public static Singleton newInstance(){
if(null == instance){
instance = new Singleton();
}
return instance;
}
}
优点:需要时才创建
缺点:多线程环境会创建多个对象
改造:
public class Singleton {
private static Singleton instance;
private Singleton(){
}
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
说明:可以保存单例多线程安全,但是synchronized
方法效率低
- 双检锁(DCL)单例模式的写法:线程安全
public class Singleton {
/* 注意这个写法在JDK1.5之前仍然不能保证安全单例,因为JDK1.之前并没有实现volatile禁止指令重排的语义。*/
private Volatitle static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
单例模式参考 https://www.jianshu.com/p/12d1a151982e
什么是 ThreadLocal 变量
- ThreadLocal作用
ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量 - 底层原理
ThreadLocal内部用一个内部ThreadLocalMap类型的成员变量threadLocals来保存变量副本,ThreadLocalMap内部定义了一个Entity静态内部类,使用ThreadLocal实例作为key,与set(value)设置的value关联起来 - 应用场景
ThreadLocal 很适合实现线程级的单例(数据库连接/Session等) - InheritableThreadLocal
是 ThreadLocal 类的子类,与 ThreadLocal 不同的是,InheritableThreadLocal 允许一个线程以及该线程创建的所有子线程都可以访问它保存的值。
在多线程环境下,SimpleDateFormat 是线程安全的吗
不安全的,解决办法:
- 使用ThreadLocal包装
- 定义为局部变量
- 使用joda-time类
什么是Java timer 类
java.util.Timer ,是一个工具类,可以用于安排一个线程在未来的某个特定时间执行
java.util.TimerTask ,是一个实现了 Runnable 接口的抽象类,我们需要去继承这个类来创建我们自己的定时任务并使用 Timer 去安排它的执行
java.util.TimerTask ,是一个实现了 Runnable 接口的抽象类,我们需要去继承这个类来创建我们自己的定时任务并使用 Timer 去安排它的执行
你有哪些多线程开发良好的实践
尽可能使用更高层次的并发工具而非 wait 和 notify 方法来实现线程通信
例如 CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作
synchronized 的原理是什么
- 作用
synchronized 可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。 - 原理
synchronized
是通过对象锁实现同步,具体为:
- 普通同步方法,锁是当前实例对象
同步方法是在JVM中实现,依靠的是方法修饰符上的ACC_SYNCHRONIZED实现。 - 静态同步方法,锁是当前类的 class 对象
- 同步代码块锁是括号里面的对象
同步代码块是使用monitorenter
和monitorexit
指令实现的。任何对象都有一个Monitor
与之相关联,线程执行到 monitorenter 指令时,将会尝试获取对象所对应的 Monitor 所有权,即尝试获取对象的锁。
-
底层实现
- 对象头
Hotspot 虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针),Mark Word 用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志
、线程持有的锁
、偏向线程 ID、偏向时间戳等等 - monitor
互斥:一个 Monitor 锁在同一时刻只能被一个线程占用
Mesa派的signal机制(notify):占有Monitor锁的线程发出释放通知时,不会立即失去锁,而是让其他线程等待在队列中,重新竞争锁
这种机制里,等待者拿到锁后不能确定在这个时间差里是否有别的等待者进入过Monitor,因此不能保证谓词(条件)一定为真,所以对条件的判断必须使用while - Monitor Record (MR)
每一个线程都有一个MR列表,一个被锁住的对象就会和线程的MR列表关联。MR中有个Owner字段,存放线程ID,表示线程抢到了对象锁。
- 对象头
-
JDK1.6中的synchronized优化
JDK1.6之前,monitorenter 和 monitorexit依赖操作系统的信号量实现,需要挂起当前线程,进入内核态来执行,这种切换代价高昂。 因此JDK1.6对锁进行了大量优化,包括自旋锁
、自适应自旋锁
、锁消除
、锁粗化
、偏向锁
、轻量级锁
等。
自旋锁原理
- 痛点:多线程wait/notify同步机制需要让CPU在用户态和内核态切换,效率低下。
- 原理:通常对象锁的锁状态只会持续很短时间,没必要频繁阻塞和唤醒线程。可以让等待线程执行一段无意义的空循环,保证线程不被挂起,同时等待锁的释放,从而避免通通过阻塞(CPU内核态?)来获取锁,提高了性能。故自旋的本质是避免了操作系统级别的锁。
- 隐患:如果锁一直不释放,则自旋线程会持续占用CPU执行无意义的循环。
- 措施:设置自旋次数或时间限制,超过限制则挂起或者阻塞线程。可以通过启动参数来配置自旋次数。
自适应自旋锁原理
- 痛点:自旋锁的自旋次数是固定的,缺乏个性化,产生不必要的自旋
- 原理:将自旋次数设置为动态,增加自旋成功线程的自旋次数,因为他们更容易成功,减少自旋失败的线程的自旋次数,甚至直接挂起,因为他们失败率高,继续自旋也没有意义。
轻量级锁
- 痛点
重量级锁会发生用户态到内核态切换而消耗大量性能 - 原理
轻量级锁是JDK1.6加入的新型锁机制。轻量级是相对于使用操作系统互斥量实现的传统重量级锁而言的。它是基于CAS操作避免了内核切换。 - 应用场景 经验说明,绝大部分的锁,在同步周期都不存在竞争。这种情况适合使用轻量级锁
- 实现原理
- 在线程栈简历锁记录表(Lock Record),用来存储竞争对象的Mark Word
- JVM使用CAS操作将对象的Mark Word更新指向线程栈的Lock Record,若成功就将锁标志位更新为"00",表示线程获得了锁
- 如果有两条以上线程争用同一个对象锁,则轻量级锁不再有效,要锁膨胀为重量级锁,即锁标志位变为"10",等待中的线程将进入阻塞
- 释放锁时,用CAS再次更新对象的mark word,若更新成功,则整个同步过程就完成了。若更新失败,说明有其他线程在尝试获取这个锁,于是需要在释放锁的同时,唤醒被挂起的线程。
- 缺陷
仅适用于同步周期内不存在竞争的情况,否则会膨胀为重量级锁,并且开销比重量级锁更大,因为不仅有互斥量的开销,还有CAS操作的开销。
注:
锁标志位
偏向锁
- 概念
与轻量级锁对比:轻量级锁是在无竞争情况下用CAS操作去消除重量级锁互斥量的消耗,那么偏向锁就是再无竞争情况下把整个同步都消除,连CAS都不做了。
偏向锁的"偏"指的是对象锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步。 - 原理
- 当线程第一次获取锁对象时,JVM会把对象mark word锁标志位设为01(偏向模式)。
- 用CAS操作更新对象锁mark word指向线程栈,如果成功,则持有偏向锁的这个线程以后每次进入这个锁相关的同步块时,JVM都可以不再进行任何同步操作(例如locking,unlocking,以及mark work的update等)
- 如果有另一个线程尝试获取锁,偏向锁就不再有效。则对象mark word锁标志可能就被更新为轻量锁"00"状态,即偏向锁膨胀为轻量锁,后续则进入轻量锁控制逻辑。
-
应用场景
偏向锁适合带有同步但无竞争的场景。 -
缺陷
如果程序中大多数锁总是被多个线程访问,那偏向锁就是多余的。可以通过启动参数配置禁用偏向锁(默认是开启的)以上为JDK1.6对锁优化的主要方面,用以下图总结
锁转换:
synchronized原理概要:
本节主要参考
http://www.iocoder.cn/JUC/sike/synchronized/?vip
https://juejin.im/post/5abc9de851882555770c8c72
https://www.jianshu.com/p/36eedeb3f912
以及《深入理解Java虚拟机》
volatile 实现原理
- 意义
volatile用来保证JVM层面的变量可见性
以及禁止指令重排
。所谓可见性是指,多个线程访问共享变量时,如果一个变量改变了共享变量的值,其他线程可以立即看到修改的值。所谓指令重排是指,比如i++操作,在汇编语言层面其实是三条指令,即读取,加一,赋值,但JVM可能会出于性能方面考虑,将后面两条指令顺序反过来执行。 - 保证可见性原理
被volatile修饰的共享变量一旦被一个线程修改之后,会立即同步到主存。其他线程要是用这个共享变量,会直接从主存读取。 - 底层实现
底层实现包括两方面的实现,
可见性
:即前面说的共享变量被修改立即同步到主存,其他线程从主存读取。禁止指令重排
:采用内存屏障(内存栅栏)实现,在汇编语言层面,就是给指令加了个LOCK,相当于一个屏障,不允许LOCK后面的指令放到前面执行,从而实现了禁止指令重排。
- 缺陷
volatile虽然能禁止指令重排,但是并不能保证操作的原子性,例如i++操作,用volatile就不能保存其原子性。而是用AtomicInteger
类的相关方法,例如getAndIncrement()就可以即保证可见性,又保证原子性。 - 适用场景
1写N读
volatile修饰long和dobule
多线程访问long或者dobule变量时最好将变量定义为volatile修饰,因为64位JVM读取long或double类型变量并不是原子操作,而是每次读一半(32)字节,可能会导致线程读取到一个修改了一半的long或double变量。用volatile修饰则可避免这种情况。
什么场景下可以使用 volatile 替换 synchronized
- volatile是告诉JVM在寄存器中的共享变量副本是不可靠的,要从主存读取。而synchronized则是锁定变量,只有当前线程能访问变量,其他线程则被阻塞。
- volatile仅能保证可见性,不能保证原子性。synchronized则都能保证。
- volatile不会造成线程阻塞,synchronized则可能会造成线程阻塞。
- volatile不会被JVM优化。
Java AQS
- 简介
java.util.concurrent.locks.AbstractQueuedSynchronizer 抽象类,简称 AQS ,是一个用于构建锁和同步容器的同步器。
current包中很多类都是基于AQS。例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock等。 - 使用场景
AQS不直接使用,而是通过继承方式,子类重写其抽象方法来管理同步状态。通常推荐在子类中聚合AQS类型的内部类(也就是继承方式),例如Lock的具体实现类例如ReentrantLock中,就有静态内部类Sync extends AbstractQueuedSynchronizer
也就是说,AQS通常是作为锁的底层实现,为上层Lock的实现类提供基本的线程状态控制功能。 - 实现原理
AQS使用一个int成员变量作为同步状态,使用一个队列数据结构来同步多线程,其中队头节点作为哨兵节点,不与其他线程关联。 - 主要API
重写AQS指定的方法时,需要通过AQS以下三个方法来获取/修改同步状态:
- getState
- setState
- compareAndSetState
AQS
可被重写
的主要方法
- tryAcquire(int) 独占式获取同步状态,CAS设置同步状态
- tryAcquireShared(int) 共享式获取同步状态,返回值>0表示成功
- isHeldExclusively() 当前同步器是否被当前线程占用
AQS提供的一些
模板方法
- acquire(int)独占式获取同步状态,获取失败将进入队列等待,此方法会调用重写的tryAcquire(int)方法
- acquireInterruptibly(int) 与acquire(int)功能相同,区别是线程可以被中断
tryAcquireNanos(int) 在acquireInterruptibly(int)基础上增加了超时机制,超时返回false
- 工作过程
同步队列
同步器(AQS)依赖内部双向队列进行同步状态管理,当前线程获取同步状态失败时,同步器会将当前线程包装成一个节点并加入队列,并阻塞该线程,当同步状态释放时,同步器会把队列首节点唤醒,使其尝试获取同步状态。
同步队列节点保存等待状态等信息,其中等待状态包含CANCELLED
(中断或超时), SIGNAL
(节点释放/中断/超时,用来唤醒后继节点), CONDITION
(等待其他线程调用singal()方法,则当前线程从等待队列进入同步队列), PROPAGATE
(共享式同步状态无条件传播), INITAL
(0)等。
独占式同步状态获取与释放
获取同步状态过程
通过调用同步器的acquire(int)可以获取同步器状态(即锁),代码为:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
主要逻辑为,调用重写过的tryAcquire()来进行线程安全地获取同步状态(锁),若获取失败则加入等待队列中进行自旋等待。
自旋过程
指的是加入队列中的每个节点都在自我死循环检查是否满足开始获取锁条件。条件就是当自己的前驱是头节点,且头节点释放了锁或者发生了中断,则当前节点就可以尝试将自己变为头结点,也就是结束了自旋。在自旋的整个过程中,除了跟自己的前驱节点外,基本不跟队列中其他节点通信,自旋退出条件仅仅依靠判断前驱节点是否为头节点决定当前节点是否能结束自旋,这样的设计既满足了节点按照FIFO顺序释放,也避免了过早通知(即前驱节点并不是头节点,但他发生了中断,而且唤醒了后继节点,就是过早通知)。
独占式同步状态(锁)获取,即accquire(int)流程如下
共享式同步状态获取与释放
共享式与独享式区别
共享式允许在同一时刻多个线程
同时获取到同步状态。典型场景是文件读写,其中写操作为独占式,而读操作是共享式,则读写操作获取同步状态区别如下图:
左边为共享式,所有共享式访问都被允许,独占式访问都被阻塞。
右边为独占式,只有独占式访问被允许,其他访问都被阻塞。
获取同步状态过程
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) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null;
if (interrupted) selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed) cancelAcquire(node);
}
}
上面的tryAcquireShared()
< 0 时表还没获取到同步状态,因此进入自旋,获取到同步状态并退出自旋的条件是:1.前驱节点是头节点(开始尝试获取)。2.成功获取同步状态。
需要注意的是在共享式释放同步状态时,需要CAS保证线程安全释放。
独占式超时获取同步状态
工作原理
关于响应中断
在JDK1.5之前,被synchronized阻塞在外的等待线程被中断(interrupt)时,只是设置一个中断标志位,线程依然会阻塞。JDK1.5提供了一个acquireInterruptibly(int)方法可让等待同步状态的线程被中断时,理解抛异常并返回。
超时获取同步状态的过程可被看作响应中断的增强版。
获取锁过程(自旋过程)
当节点前驱为头节点时尝试获取同步状态,成功则返回。
获取失败时则看是否超时,若未超时,将继续自旋;若超时则返回。
若自旋过程中线程被中断,则会响应中断立即返回并抛异常。
一个例子,基于AQS实现一个简单的锁(非自旋)
https://gitee.com/fysola/concurrent/blob/master/AQS_DEMO/src/Main.java
Lock接口
与synchronized相比,java.util.concurrent.locks.Lock接口提供了显示地管理锁,使得锁成为程序员可控状态,也正因为如此,Lock接口才定义了尝试非阻塞地获取锁
,能被中断地获取锁
,超时获取锁
等synchronized不具备的功能。
Lock接口主要定义了以下API:
- lock()
- lockInterruptibly() 在获取锁的过程中的阻塞线程可以响应中断退出。
- tryLock() 尝试非阻塞获取锁,无论是否获取成功都能立即返回true或false
- tryLock(long time, TimeUnit unit) 支持超时获取锁
- newCondition() 当前线程只有获取了锁,才能调用condition的wait(),调用后释放锁。
Lock的常用实现类例如ReentrantLock等,是聚合了AQS同步器(以静态内部类继承AQS方式)实现的。
什么是可重入锁(ReentrantLock)
- 原理
就是同一个线程获取了锁之后,在没释放锁之前,可以重复加锁而不会导致自己被阻塞,例如线程中调用了递归方法,方法中用lock加了锁之后,未释放之前再次加锁,如果不会阻塞,就是可重入锁。典型的可重入锁有ReentrantLock
(显式)和synchronized
(隐式)
此外,可重入锁涉及公平
和非公平
两种方式。公平即按照获取锁的线程顺序来加锁,等待最久的锁则最优先获取锁。非公平锁则不管这些,以下为ReentrantLock非公平锁的实现
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)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
上面这个方法重写了AQS中的tryAcquire(),实现了非公平锁。主要是在后面增加了判断当前获取锁的线程跟持有锁的线程是不是同一个,是的话则将状态值增加并返回,表示再次获取成功。
再看公平锁的实现,与非公平锁相比,唯一区别就是获取锁时用hasQueuedPredecessors()判断是否有前驱节点,如果有则说明有线程比当前更早去申请锁,因此要等待前驱先获取锁并释放锁,当前线程才能继续尝试获取。在整个获取锁的过程严格按照FIFO的顺序进行。代码如下
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;
}
- 公平锁和非公平锁对比
公平锁保证了获取锁按照FIFO顺序进行,但是会因此产生大量线程切换。而非公平锁切换极少,即吞吐量更大,性能更高,但是产生的副作用是线程饥饿,即有的线程可能长时间都无法获取锁。
ReadWriteLock 是什么
-
读写锁原理
读写锁ReadWriteLock是一种共享锁,它维护了一对锁,即一个读锁和一个写锁。读写锁在同一个时刻可以允许多个读线程访问,但是写线程访问时,所有读线程和其他写线程都将被阻塞。读写锁通过分离读锁和写锁,使得性能相比一般的排他锁有了很大提升。 -
读写分离模式的应用场景
例如对于一个共享的缓存,读多写少,在JDK1.5之前,需要使用通知等待(synchronized,wait,notify)机制来控制读写顺序,让读操作排在写操作之后,避免脏读。而在JDK1.5引入读写锁之后,读操作时只需要获取读锁就能实现多个线程同时读取,提高吞吐量,写操作时只要获取写锁来阻塞其他读写线程,整个设计更为简单。 -
ReentrantReadWriteLock
J.U.C提供了ReadWriteLock的实现类ReentrantReadWriteLock来实现读写锁,主要特性有:
公平性选择: 默认为非公平锁(吞吐量更大),可选择为公平锁
可重入:支持可重入锁。
锁降级:遵循获取写锁,获取读锁,再释放写锁,释放读锁的顺序,写锁能够降级为读锁。 -
内部原理
读写锁通过自定义同步器实现同步功能(静态内部类继承AQS),并将一个整型状态变量分为高16和低16位,表示读和写锁,用位操作得出结果表示读写各自的状态。 -
锁降级
锁降级指的是写锁变为读锁,但是降级过程必须要获取读锁,而不是直接释放血锁,这是为了保证可见性。
LockSupport 工具类
LockSupport是J.C.U lock包下面的一个工具类,它提供了一组park开头的方法,用来实现线程阻塞和唤醒,AQS就是聚合了它来实现阻塞。
JDK1.6提供了一组带 blocker 参数的park方法,用来标识当前线程在等待的对象(即阻塞对象),线程dump结果可以用来排查问题。
Condition 接口
简介
Condition是J.C.U下的一个接口,它提供了与Object的wait和notify类似的功能。但也有一些区别,主要是:
- Object.wait是与synchronized配合,而Condition.await是与Lock.lock配合
- synchronized只有一个Object.wait的等待队列
- Lock可以有多个Condition等待队列
- Condition支持响应中断
- Condition支持响应超时
- 调用Condition.await后,当前线程会释放锁并在此等待,其他线程调用Condition.signal,通知当前线程后,当前线程才从await返回,并且在返回前就已经获取了锁。
典型使用方法
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void conditionWait() throws InterruptedException {
lock.lock();
try {
condition.await();
} finally {
lock.unlock();
}
}
public void conditionSignal() throws InterruptedException {
lock.lock();
try {
condition.signal();
} finally {
lock.unlock();
}
}
一个例子:Condition实现有界队列
有界队列:队列空时,读线程将阻塞,队列满时,写线程将阻塞
https://gitee.com/fysola/concurrent/blob/master/Condition_DEMO/src/Application.java
Condition 内部结构
每个Condition都维护一个等待队列,而Condition又是同步器AQS的内部类,可以访问AQS实例的方法和属性。Lock以静态内部类的方式聚合了AQS,故Lock中除了有一个同步队列外,还可以有多个Condition实例,也就是可以有多个等待队列,并且同步队里二和等待队列的节点用的是相同数据类型,因为Condition的等待队列节点复用了AQS同步队列节点的数据结构。
Lock中的同步队列与Condition等待队列的关系如图:
Condition.await()原理
从队列角度看,Condition.await()方法相当于将AQS同步队列首节点(持有锁的节点)移到了Condition等待队列中。 过程如图:
Condition.signal()原理
从队列角度看,Condition.signal()方法是将Condition等待队列中的首节点(等待时间最长)唤醒(用LockSupport.parkxxx()),在唤醒之前,会将该节点加入到AQS同步队列中,并开始竞争同步状态锁,直到获取了同步状态锁,才会从Contidion.await()返回,此时该线程已经获取了锁。过程如图