文章目录
1. 相关背景
1.1 线程状态
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
1.2 管程
即 Monitor 监视器,是一种同步机制,保证同一时间,只有一个线程能去访问被保护的代码块
JVM 的同步是基于进入和退出,使用管程对象来实现的
1.3 多线程编程步骤
- 创建资源类,在资源类创建属性和操作方法
- 创建多个线程,调用资源类的操作方法
public class Ticket {
private int number = 100;
public synchronized void sale(){
if (number > 0) {
System.out.println(Thread.currentThread().getName() + ":卖出一张票,还剩" + --number + "张票");
}
}
}
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{
for (int i = 0; i < 100; i++) {
ticket.sale();
}
}, "售票员1").start();
new Thread(()->{
for (int i = 0; i < 100; i++) {
ticket.sale();
}
}, "售票员2").start();
new Thread(()->{
for (int i = 0; i < 100; i++) {
ticket.sale();
}
}, "售票员3").start();
}
}
输出结果:
......
售票员1:卖出一张票,还剩6张票
售票员2:卖出一张票,还剩5张票
售票员3:卖出一张票,还剩4张票
售票员2:卖出一张票,还剩3张票
售票员2:卖出一张票,还剩2张票
售票员2:卖出一张票,还剩1张票
售票员2:卖出一张票,还剩0张票
2. JUC 原子类
JUC 中多数类是通过 volatile
和 CAS 来实现的,CAS 本质上提供的是一种无锁方案,而 synchronized
和 Lock
是互斥锁方案; java 原子类本质上使用的是 CAS,而 CAS 底层是通过 Unsafe
类实现的。
2.1 CAS
CAS 的全称为 Compare-And-Swap,直译就是对比交换。是一条 CPU 的原子指令,其作用是让 CPU 先进行比较两个值是否相等,然后原子地更新某个位置的值,经过调查发现,其实现方式是基于硬件平台的汇编指令,就是说 CAS 是靠硬件实现的,JVM 只是封装了汇编调用,那些 AtomicInteger
类便是使用了这些封装后的接口。
简单解释:CAS 操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。
CAS 操作是原子性的,所以多线程并发使用 CAS 更新数据时,可以不使用锁。JDK 中大量使用了 CAS 来更新数据而防止加锁(synchronized
重量级锁)来保持原子更新。
2.1.1 CAS 使用示例
如果不使用 CAS,在高并发下,多线程同时修改一个变量的值我们需要 synchronized
加锁(可能有人说可以用 Lock
加锁,Lock
底层的 AQS 也是基于 CAS 进行获取锁的)。
以前的写法:
public class Test {
private int i=0;
public synchronized int add(){
return i++;
}
}
利用 CAS 的写法:
java 中为我们提供了 AtomicInteger
原子类(底层基于 CAS 进行更新数据的),不需要加锁就在多线程并发场景下实现数据的一致性。
public class Test {
private AtomicInteger i = new AtomicInteger(0);
public int add(){
return i.addAndGet(1);
}
}
2.1.2 CAS 问题
CAS 方式为乐观锁,synchronized
为悲观锁。因此使用 CAS 解决并发问题通常情况下性能更优。
但使用 CAS 方式也会有几个问题:
2.1.2.1 ABA 问题
因为 CAS 需要在操作值的时候,检查值有没有发生变化,比如没有发生变化则更新,但是如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行检查时则会发现它的值没有发生变化,但是实际上却变化了。
ABA 问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加 1,那么 A->B->A 就会变成 1A->2B->3A。
从 Java 1.5开始,JDK 的 Atomic 包里提供了一个类 AtomicStampedReference
来解决 ABA 问题。这个类的 compareAndSet
方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
public static void main(String[] args) {
AtomicStampedReference<Integer> n = new AtomicStampedReference<>(0, 0);
n.compareAndSet(0,5,0,3);
System.out.println(n.getReference()); // 5
System.out.println(n.getStamp()); // 3
}
2.1.2.2 循环时间长开销大
自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。如果 JVM 能支持处理器提供的 pause 指令,那么效率会有一定的提升。pause 指令有两个作用:第一,它可以延迟流水线执行命令(de-pipeline),使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起 CPU 流水线被清空(CPU Pipeline Flush),从而提高 CPU 的执行效率。
2.1.2.3 只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。
还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量 i = 2,j = a
,合并一下 ij = 2a
,然后用 CAS 来操作 ij
。
从 Java 1.5 开始,JDK 提供了 AtomicReference
类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行 CAS 操作。
class Sample {
private int n;
private String s;
public Sample() {
}
public Sample(int n, String s) {
this.n = n;
this.s = s;
}
public int getN() {
return n;
}
public String getS() {
return s;
}
public void setN(int n) {
this.n = n;
}
public void setS(String s) {
this.s = s;
}
@Override
public String toString() {
return "Sample{" +
"n=" + n +
", s='" + s + '\'' +
'}';
}
}
public static void main(String[] args) {
Sample sample = new Sample(0, "");
AtomicReference<Sample> ar = new AtomicReference<>(sample);
ar.compareAndSet(sample, new Sample(10, "ice"));
System.out.println(ar.get()); // Sample{n=10, s='ice'}
ar.lazySet(new Sample(100, "亚索"));
System.out.println(ar.get()); // Sample{n=100, s='亚索'}
System.out.println(ar); // Sample{n=100, s='亚索'}
Sample update = ar.getAndUpdate(x -> {
x.setS("瑞雯");
return x;
});
System.out.println(update); // Sample{n=100, s='瑞雯'}
}
2.2 UnSafe 类
Unsafe
是位于 sun.misc
包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。但由于 Unsafe
类使 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用 Unsafe
类会使得程序出错的概率变大,使得 Java 这种安全的语言变得不再“安全”,因此对 Unsafe
的使用一定要慎重。
这个类尽管里面的方法都是 public 的,但是并没有办法使用它们,JDK API 文档也没有提供任何关于这个类的方法的解释。总而言之,对于 Unsafe 类的使用都是受限制的,只有授信的代码才能获得该类的实例,当然 JDK 库里面的类是可以随意使用的。
先来看下这张图,对 UnSafe
类总体功能:
2.3 AtomicInteger
以 AtomicInteger
为例,常用 API:
public final int get():获取当前的值
public final int getAndSet(int newValue):获取当前的值,并设置新的值
public final int getAndIncrement():获取当前的值,并自增
public final int getAndDecrement():获取当前的值,并自减
public final int getAndAdd(int delta):获取当前的值,并加上预期的值
void lazySet(int newValue): 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
相比 Integer
的优势,多线程中让变量自增:
private volatile int count = 0;
// 若要线程安全执行执行 count++,需要加锁
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
使用 AtomicInteger
后:
private AtomicInteger count = new AtomicInteger();
public void increment() {
count.incrementAndGet();
}
// 使用 AtomicInteger 后,不需要加锁,也可以实现线程安全
public int getCount() {
return count.get();
}
源码解析:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
//用于获取value字段相对当前对象的“起始地址”的偏移量
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
//返回当前值
public final int get() {
return value;
}
//递增加detla
public final int getAndAdd(int delta) {
//三个参数,1、当前的实例 2、value实例变量的偏移量 3、当前value要加上的数(value+delta)。
return unsafe.getAndAddInt(this, valueOffset, delta);
}
//递增加1
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
......
}
我们可以看到 AtomicInteger
底层用的是 volatile
的变量和 CAS 来进行更改数据的。
volatile
保证线程的可见性,多线程并发时,一个线程修改数据,可以保证其它线程立马看到修改后的值- CAS 保证数据更新的原子性
2.4 延伸到所有原子类:共 13 个
2.4.1 原子更新基本类型
使用原子的方式更新基本类型,Atomic 包提供了以下 3 个类。
AtomicBoolean
: 原子更新布尔类型AtomicInteger
: 原子更新整型AtomicLong
: 原子更新长整型
以上 3 个类提供的方法几乎一模一样,可以参考上面 AtomicInteger
中的相关方法。
2.4.2 原子更新数组
通过原子的方式更新数组里的某个元素,Atomic 包提供了以下的 3 个类:
AtomicIntegerArray
: 原子更新整型数组里的元素AtomicLongArray
: 原子更新长整型数组里的元素AtomicReferenceArray
: 原子更新引用类型数组里的元素。
这三个类的最常用的方法是如下两个方法:
get(int index)
:获取索引为index
的元素值compareAndSet(int i,E expect,E update)
: 如果当前值等于预期值,则以原子方式将数组位置i
的元素设置为update
值
举个 AtomicIntegerArray
例子:
import java.util.concurrent.atomic.AtomicIntegerArray;
public class Demo5 {
public static void main(String[] args) throws InterruptedException {
AtomicIntegerArray array = new AtomicIntegerArray(new int[] { 0, 0 });
System.out.println(array);
System.out.println(array.getAndAdd(1, 2));
System.out.println(array);
}
}
输出结果:
[0, 0]
0
[0, 2]
2.2.3 原子更新引用类型
Atomic 包提供了以下三个类:
AtomicReference
: 原子更新引用类型AtomicStampedReference
: 原子更新引用类型, 内部使用Pair
来存储元素值及其版本号AtomicMarkableReferce
: 原子更新带有标记位的引用类型
这三个类提供的方法都差不多,首先构造一个引用对象,然后把引用对象 set 进 Atomic
类,然后调用 compareAndSet
等一些方法去进行原子操作,原理都是基于 Unsafe
实现,但 AtomicReferenceFieldUpdater
略有不同,更新的字段必须用 volatile
修饰。
举个 AtomicReference
例子:
import java.util.concurrent.atomic.AtomicReference;
public class AtomicReferenceTest {
public static void main(String[] args){
// 创建两个Person对象,它们的id分别是101和102。
Person p1 = new Person(101);
Person p2 = new Person(102);
// 新建AtomicReference对象,初始化它的值为p1对象
AtomicReference ar = new AtomicReference(p1);
// 通过CAS设置ar。如果ar的值为p1的话,则将其设置为p2。
ar.compareAndSet(p1, p2);
Person p3 = (Person)ar.get();
System.out.println("p3 is "+ p3);
System.out.println("p3.equals(p1)=" + p3.equals(p1));
}
}
class Person {
volatile long id;
public Person(long id) {
this.id = id;
}
public String toString() {
return "id:" + id;
}
}
结果输出:
p3 is id:102
p3.equals(p1)=false
结果说明:
- 新建
AtomicReference
对象ar
时,将它初始化为p1
- 紧接着,通过 CAS 函数对它进行设置。如果
ar
的值为p1
的话,则将其设置为p2
- 最后,获取
ar
对应的对象,并打印结果。p3.equals(p1)
的结果为false
,这是因为Person
并没有覆盖equals()
方法,而是采用继承自Object.java
的equals()
方法;而Object.java
中的equals()
实际上是调用==
去比较两个对象,即比较两个对象的地址是否相等
2.2.4 原子更新字段类
Atomic 包提供了四个类进行原子字段更新:
AtomicIntegerFieldUpdater
: 原子更新整型的字段的更新器。AtomicLongFieldUpdater
: 原子更新长整型字段的更新器。AtomicStampedFieldUpdater
: 原子更新带有版本号的引用类型。AtomicReferenceFieldUpdater
: 上面已经说过此处不在赘述
这四个类的使用方式都差不多,是基于反射的原子更新字段的值。要想原子地更新字段类需要两步:
- 因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法
newUpdater()
创建一个更新器,并且需要设置想要更新的类和属性 - 第二步,更新类的字段必须使用
public volatile
修饰
举个例子:
public class TestAtomicIntegerFieldUpdater {
public static void main(String[] args){
TestAtomicIntegerFieldUpdater tIA = new TestAtomicIntegerFieldUpdater();
tIA.doIt();
}
public AtomicIntegerFieldUpdater<DataDemo> updater(String name){
return AtomicIntegerFieldUpdater.newUpdater(DataDemo.class,name);
}
public void doIt(){
DataDemo data = new DataDemo();
System.out.println("publicVar = "+updater("publicVar").getAndAdd(data, 2));
/*
* 由于在DataDemo类中属性value2/value3,在TestAtomicIntegerFieldUpdater中不能访问
*/
//System.out.println("protectedVar = "+updater("protectedVar").getAndAdd(data,2));
//System.out.println("privateVar = "+updater("privateVar").getAndAdd(data,2));
//System.out.println("staticVar = "+updater("staticVar").getAndIncrement(data));//报java.lang.IllegalArgumentException
/*
* 下面报异常:must be integer
*/
//System.out.println("integerVar = "+updater("integerVar").getAndIncrement(data));
//System.out.println("longVar = "+updater("longVar").getAndIncrement(data));
}
}
class DataDemo{
public volatile int publicVar=3;
protected volatile int protectedVar=4;
private volatile int privateVar=5;
public volatile static int staticVar = 10;
//public final int finalVar = 11;
public volatile Integer integerVar = 19;
public volatile Long longVar = 18L;
}
再说下对于 AtomicIntegerFieldUpdater
的使用稍微有一些限制和约束,约束如下:
- 字段必须是
volatile
类型的,在线程之间共享变量时保证立即可见. e.g. :volatile int value = 3
- 字段的描述类型(修饰符
public
/protected
/default
/private
)是与调用者与操作对象字段的关系一致。也就是说调用者能够直接操作对象字段,那么就可以反射进行原子操作。但是对于父类的字段,子类是不能直接操作的,尽管子类可以访问父类的字段 - 只能是实例变量,不能是类变量,也就是说不能加
static
关键字 - 只能是可修改变量,不能使
final
变量,因为final
的语义就是不可修改。实际上final
的语义和volatile
是有冲突的,这两个关键字不能同时存在 - 对于
AtomicIntegerFieldUpdater
和AtomicLongFieldUpdater
只能修改int
/long
类型的字段,不能修改其包装类型(Integer
/Long
)。如果要修改包装类型就需要使用AtomicReferenceFieldUpdater
3. AQS
3.1 简介
AQS (AbstractQueuedSynchronizer
) 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock
,Semaphore
,其他的诸如 ReentrantReadWriteLock
,SynchronousQueue
,FutureTask
等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。
3.2 AQS 核心思想
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH (Craig,Landin,and Hagersten) 队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点 (Node) 来实现锁的分配。
AQS 使用一个 int
成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。
private volatile int state; //共享变量,使用volatile修饰保证线程可见性
状态信息通过 protected
类型的 getState
,setState
,compareAndSetState
进行操作
//返回同步状态的当前值
protected final int getState() {
return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
3.3 AQS 对资源的共享方式
AQS 定义两种资源共享方式
- Exclusive(独占):只有一个线程能执行,如
ReentrantLock
。又可分为公平锁和非公平锁:- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
- Share(共享):多个线程可同时执行,如
Semaphore
/CountDownLatch
。Semaphore
、CountDownLatCh
、CyclicBarrier
、ReadWriteLock
ReentrantReadWriteLock
可以看成是组合式,因为 ReentrantReadWriteLock
也就是读写锁允许多个线程同时对某一资源进行读。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在上层已经帮我们实现好了。
4. Lock 接口
4.1 什么是 Lock
Lock
锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。 Lock
提供了比 synchronized
更多的功能。
4.2 Lock 详解
package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
下面来逐个讲述 Lock
接口中每个方法的使用
4.2.1 lock() 与 unlock()
lock()
方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
采用 Lock
,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用 Lock
必须在 try{}catch{}
块中进行,并且将释放锁的操作放在 finally
块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用 Lock
来进行同步的话,是以下面这种形式去使用的:
Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
4.2.2 newCondition()
关键字 synchronized
与 wait()
/ notify()
这两个方法一起使用可以实现 等待 / 通知 模式, Lock
锁的 newContition()
方法返回 Condition
对象,Condition
类也可以实现 等待 / 通知 模式。
用 notify()
通知时,JVM 会随机唤醒某个等待的线程,使用 Condition
类可以进行选择性通知,Condition` 比较常用的两个方法:
await()
会使当前线程等待,同时会释放锁,当其他线程调用signal()
时,线程会重新获得锁并继续执行signal()
用于唤醒一个等待的线程
注意:在调用
Condition
的 await()
/signal()
方法前,也需要线程持有相关的Lock
锁,调用await()
后线程会释放这个锁,在singal()
调用后会从当前Condition
对象的等待队列中,唤醒 一个线程,唤醒的线程尝试获得锁, 一旦获得锁成功就继续执行。
4.3 ReentrantLock
ReentrantLock
,意思是“可重入锁”。
ReentrantLock
是唯一实现了 Lock
接口的类,并且 ReentrantLock
提供了更多的方法。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Ticket {
private int number = 100;
private final Lock lock = new ReentrantLock();
public void sale(){
lock.lock();
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + ":卖出一张票,还剩" + --number + "张票");
}
}finally {
lock.unlock();
}
}
}
4.4 ReadWriteLock
ReadWriteLock
也是一个接口,在它里面只定义了两个方法:
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}
一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成 2 个锁来分配给线程,从而使得多个线程可以同时进行读操作。
下面的 ReentrantReadWriteLock
实现了 ReadWriteLock
接口。
ReentrantReadWriteLock
里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()
和 writeLock()
用来获取读锁和写锁。
public class Test {
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();
}
public void get(Thread thread) {
rwl.readLock().lock();
try {
long start = System.currentTimeMillis();
while(System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName()+"正在进行读操作");
}
System.out.println(thread.getName()+"读操作完毕");
} finally {
rwl.readLock().unlock();
}
}
}
此时 thread1
和 thread2
在同时进行读操作。这样就大大提升了读操作的效率。
- 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
- 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
4.5 小结
Lock
是一个接口,而synchronized
是 Java 中的关键字,synchronized
是 JVM 实现的synchronized
在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock
在发生异常时,如果没有主动通过unLock()
去释放锁,则很可能造成死锁现象,因此使用Lock
时需要在finally
块中释放锁Lock
可以让等待锁的线程响应中断,而synchronized
却不行,使用synchronized
时,等待的线程会一直等待下去,不能够响应中断- 通过
Lock
可以知道有没有成功获取锁,而synchronized
却无法办到 Lock
可以提高多个线程进行读操作的效率
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时 Lock
的性能要远远优于synchronized
。
5. 线程间通信
线程间通信的模型有两种:共享内存和消息传递,以下方式都是基本这两种模型来实现的。我们来一道面试常见的题目来分析:
场景:两个线程,一个线程对当前数值加 1,另一个线程对当前数值减 1,要求用线程间通信
5.1 synchronized 方案
public class Share {
private int number = 0;
public synchronized void incr() {
if (number != 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
++number;
System.out.println(Thread.currentThread().getName() + " :: " + number);
this.notifyAll();
}
public synchronized void decr() {
if (number != 1) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
--number;
System.out.println(Thread.currentThread().getName() + " :: " + number);
this.notifyAll();
}
}
public class ThreadDemo01 {
public static void main(String[] args) {
Share share = new Share();
new Thread(
() -> {
for (int i = 1; i <= 10; i++) {
share.incr();
}
}, "AA"
).start();
new Thread(
() -> {
for (int i = 1; i <= 10; i++) {
share.decr();
}
}, "BB"
).start();
}
}
输出结果:
AA :: 1
BB :: 0
AA :: 1
BB :: 0
AA :: 1
BB :: 0
AA :: 1
BB :: 0
AA :: 1
BB :: 0
AA :: 1
BB :: 0
AA :: 1
BB :: 0
AA :: 1
BB :: 0
AA :: 1
BB :: 0
AA :: 1
BB :: 0
Question:这样看上去好像没问题,那如果用四个线程呢?
public class ThreadDemo01 {
public static void main(String[] args) {
Share share = new Share();
new Thread(
() -> {
for (int i = 1; i <= 10; i++) {
share.incr();
}
}, "AA"
).start();
new Thread(
() -> {
for (int i = 1; i <= 10; i++) {
share.decr();
}
}, "BB"
).start();
new Thread(
() -> {
for (int i = 1; i <= 10; i++) {
share.incr();
}
}, "CC"
).start();
new Thread(
() -> {
for (int i = 1; i <= 10; i++) {
share.decr();
}
}, "DD"
).start();
}
}
输出结果:
......
BB :: 0
CC :: 1
AA :: 2
CC :: 3
DD :: 2
CC :: 3
DD :: 2
CC :: 3
DD :: 2
CC :: 3
DD :: 2
......
显然,出问题了!这就是虚假唤醒问题
关键在于这么一段代码:
if (number != 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
这里的逻辑是,首先判断是否满足条件,不满足则执行 wait()
方法,但是当被唤醒后,它没有再次判断是否符合条件,直接执行下面的代码了,所以 wait()
方法应该在循环里使用。
while (number != 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
5.2 Lock 方案
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Share {
private int number = 0;
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void incr() {
lock.lock();
try {
while (number != 0) {
condition.await();
}
++number;
System.out.println(Thread.currentThread().getName() + " :: " + number);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decr() {
lock.lock();
try {
while (number != 1) {
condition.await();
}
--number;
System.out.println(Thread.currentThread().getName() + " :: " + number);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
其他代码一样,这也解决了线程间的通信。
5.3 线程间定制化通信
问题: A 线程打印 5 次 A,B 线程打印 10 次 B,C 线程打印 15 次 C,按照此顺序循环 10 轮
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ShareResource {
private int flag = 1; // 1:AA,2:BB,3:CC
private final Lock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
public void print5(int loop) {
lock.lock();
try {
while (flag != 1) {
c1.await();
}
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + " :: 第" + loop + "论,第" + i + "次");
}
flag = 2;
c2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print10(int loop) {
lock.lock();
try {
while (flag != 2) {
c2.await();
}
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " :: 第" + loop + "论,第" + i + "次");
}
flag = 3;
c3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print15(int loop) {
lock.lock();
try {
while (flag != 3) {
c3.await();
}
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName() + " :: 第" + loop + "论,第" + i + "次");
}
flag = 1;
c1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class ThreadTest {
public static void main(String[] args) {
ShareResource sr = new ShareResource();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
sr.print5(i);
}
}, "AA").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
sr.print10(i);
}
}, "BB").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
sr.print15(i);
}
}, "CC").start();
}
}
6. 集合的线程安全
6.1 集合线程不安全示例
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class ThreadDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
多试几次,会发现可能出现并发修改异常 java.util.ConcurrentModificationException
:
Exception in thread "2" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1012)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:966)
at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:456)
at java.base/java.lang.String.valueOf(String.java:3388)
at java.base/java.io.PrintStream.println(PrintStream.java:1047)
at lock.ThreadDemo.lambda$main$0(ThreadDemo.java:13)
at java.base/java.lang.Thread.run(Thread.java:832)
6.2 方案 1: Vector (不推荐)
import java.util.List;
import java.util.UUID;
import java.util.Vector;
public class ThreadDemo {
public static void main(String[] args) {
List<String> list = new Vector<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
6.3 方案 2: Collections (不推荐)
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
public class ThreadDemo {
public static void main(String[] args) {
List<String> list = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < 30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
6.4 方案 3: CopyOnWriteArrayList
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
public class ThreadDemo {
public static void main(String[] args) {
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
简单看下 CopyOnWriteArrayList
的 add()
方法源码:
public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}
final void setArray(Object[] a) {
array = a;
}
可以看到,本质上就是复制个数组,然后在复制的数组上写入修改。最后将引用指向复制的数组
CopyOnWriteArrayList
相当于线程安全的 ArrayList
,和 ArrayList
一样,它是个可变数组,但是和 ArrayList
不同的是,它具有以下特性:
- 它最适合于具有以下特征的应用程序:
List
大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突 - 它是线程安全的
- 因为通常需要复制整个基础数组,所以可变操作(
add()
、set()
和remove()
等等)的开销很大 - 迭代器支持
hasNext()
,next()
等不可变操作,但不支持可变remove()
等操作 - 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突,在构造迭代器时,迭代器依赖于不变的数组快照
CopyOnWriteArrayList
的思想:
-
独占锁效率低:采用读写分离思想解决
-
写线程获取到锁,其他写线程阻塞
-
复制思想:
当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
这时候会抛出来一个新的问题,也就是数据不一致的问题。如果写线程还没来得及写会内存,其他的线程就会读到了脏数据。
CopyOnWriteArrayList
的原理:
- “动态数组”机制
- 它内部有个 “
volatile
数组” (array) 来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给 “volatile
数组”, 这就是它叫做CopyOnWriteArrayList
的原因。 - 由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,
CopyOnWriteArrayList
效率很低;但是单单只是进行遍历查找的话,效率比较高。
- 它内部有个 “
- “线程安全”机制
- 通过
volatile
和互斥锁来实现的。 - 通过 “
volatile
数组”来保存数据的。一个线程读取volatile
数组时,总能看到其它线程对该volatile
变量最后的写入;就这样,通过volatile
提供了“读取到的数据总是最新的”这个机制的保证。 - 通过互斥锁来保护数据。在“添加/修改/删除”数据时,会先“获取互斥锁”,再修改完毕之后,先将数据更新到“volatile数组”中,然后再“释放互斥锁”,就达到了保护数据的目的。
- 通过
7. Callable 创建线程
7.1 Callable 接口
目前我们学习了有两种创建线程的方法-一种是通过创建 Thread
类,另一种是通过使用 Runnable
创建线程。但是,Runnable
缺少的一项功能是,当线程终止时(即 run()
完成时),我们无法使线程返回结果。为了支持此功能,Java 中提供了 Callable
接口。
Callable
接口的特点如下:
- 为了实现
Runnable
,需要实现不返回任何内容的run()
方法,而对于Callable
,需要实现在完成时返回结果的call()
方法 call()
方法可以引发异常,而run()
则不能- 为实现
Callable
而必须重写call()
方法 - 不能直接替换
Runnable
,因为Thread
类的构造方法根本没有Callable
import java.util.concurrent.Callable;
public class Resource implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 100;
}
}
7.2 Future 接口
当 call()
方法完成时,结果必须存储在主线程已知的对象中,以便主线程可以知道该线程返回的结果。为此,可以使用 Future
对象。
将 Future
视为保存结果的对象——它可能暂时不保存结果,但将来会保存(一旦 Callable
返回)。Future
基本上是主线程可以跟踪进度以及其他线程的结果的一种方式。要实现此接口,必须重写 5 种方法,这里列出了重要的方法,,如下:
// 如果尚未启动,它将停止任务。如果已启动,则仅在 mayInterrupt 为 true 时才会中断任务。
boolean cancel(boolean mayInterruptIfRunning);
// 如果任务完成,它将立即返回结果,否则将等待任务完成,然后返回结果
boolean isCancelled();
// 如果任务完成,则返回true,否则返回false
boolean isDone();
// 返回多线程任务的结果,必要时等待线程计算完毕
V get() throws InterruptedException, ExecutionException;
// 如果有必要,最多等待给定的时间来完成计算,然后检索其结果(如果可用)。
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
7.3 FutureTask
Java 库具有具体的 FutureTask
类型,该类型实现 Runnable
和 Future
,并方便地将两种功能组合在一起。 可以通过为其构造函数提供 Callable
来创建 FutureTask
。然后,将 FutureTask
对象提供给 Thread
的构造函数以创建 Thread
对象。因此,间接地使用 Callable
创建线程。
在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给 Future
对象在后台完成。
-
当主线程将来需要时,就可以通过
Future
对象获得后台作业的计算结果或者执行状态 -
一般
FutureTask
多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果 -
仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞
get()
方法 -
一旦计算完成,就不能再重新开始或取消计算
-
get()
方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常 -
get()
只计算一次,因此get()
方法放到最后
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Demo {
public static void main(String[] args) {
FutureTask<Integer> task = new FutureTask<>(() -> {
System.out.println(Thread.currentThread().getName() + " come in callable");
return 100;
});
new Thread(task,"lucy").start();
while (!task.isDone()){
System.out.println("wait...");
}
try {
System.out.println(task.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}finally {
System.out.println(Thread.currentThread().getName() + " come over");
}
}
}
......
wait...
lucy come in callable
wait...
wait...
......
100
main come over
8. JUC 辅助类
8.1 CountDownLatch
CountDownLatch
类可以设置一个计数器,然后通过 countDown()
方法来进行减 1 的操作,使用 await()
方法等待计数器不大于 0,然后继续执行 await()
方法之后的语句。
CountDownLatch
主要有两个方法,当一个或多个线程调用await()
方法时,这些线程会阻塞- 其它线程调用
countDown()
方法会将计数器减 1 (调用countDown()
方法的线程不会阻塞) - 当计数器的值变为 0 时,因
await()
方法阻塞的线程会被唤醒,继续执行
场景: 6 个同学陆续离开教室后值班同学才可以关门
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "号同学离开了教室");
countDownLatch.countDown();
}, String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "班长锁门走人了");
}
}
输出结果:
1号同学离开了教室
5号同学离开了教室
3号同学离开了教室
2号同学离开了教室
4号同学离开了教室
6号同学离开了教室
main班长锁门走人了
8.2 CyclicBarrier
CyclicBarrier
看英文单词可以看出大概就是循环阻塞的意思,在使用中 CyclicBarrier
的构造方法第一个参数是目标障碍数,每执行 CyclicBarrier
一次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await()
之后的语句。可以将 CyclicBarrier
理解为加 1 操作。
场景: 集齐 7 颗龙珠就可以召唤神龙
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
private static final int NUMBER = 7;
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, () -> {
System.out.println("集齐 7 颗龙珠召唤神龙");
});
for (int i = 1; i <= 7; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 星龙珠收集到了");
cyclicBarrier.await();
} catch (BrokenBarrierException | InterruptedException e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
输出结果:
2 星龙珠收集到了
5 星龙珠收集到了
4 星龙珠收集到了
1 星龙珠收集到了
3 星龙珠收集到了
7 星龙珠收集到了
6 星龙珠收集到了
集齐 7 颗龙珠召唤神龙
8.3 Semaphore
Semaphore
的构造方法中传入的第一个参数是最大信号量(可以看成最大线程池),每个信号量初始化为一个最多只能分发一个许可证。使用 acquire()
方法获得许可证,release()
方法释放许可。
场景: 抢车位, 6 部汽车 3 个停车位
import java.util.Random;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 抢到了车位");
// 随机停车时间
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(Thread.currentThread().getName() + " -----离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}, String.valueOf(i)).start();
}
}
}
输出结果:
1 抢到了车位
3 抢到了车位
2 抢到了车位
3 -----离开了车位
5 抢到了车位
2 -----离开了车位
4 抢到了车位
1 -----离开了车位
5 -----离开了车位
6 抢到了车位
6 -----离开了车位
4 -----离开了车位
9. 读写锁
9.1 读写锁介绍
现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。
针对这种场景,JAVA 的并发包提供了读写锁 ReentrantReadWriteLock
,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁。
线程进入读锁的前提条件:
- 没有其他线程的写锁
- 没有写请求, 或者有写请求,但调用线程和持有锁的线程是同一个(可重入锁)
线程进入写锁的前提:
- 没有其他线程的写锁
- 没有其他线程的读锁
读写锁有以下三个重要的特性:
- 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平
- 重进入:读锁和写锁都支持线程重进入
- 锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁
9.2 ReentrantReadWriteLock
资源类:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void put(String key, Object value) {
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "正在写操作:" + key);
TimeUnit.MICROSECONDS.sleep(300);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写完了:" + key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
public Object get(String key) {
readWriteLock.readLock().lock();
Object o = null;
try {
System.out.println(Thread.currentThread().getName() + "正在读取操作:" + key);
TimeUnit.MICROSECONDS.sleep(300);
o = map.get(key);
System.out.println(Thread.currentThread().getName() + "取完了:" + key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
return o;
}
}
测试类:
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache cache = new MyCache();
for (int i = 1; i <= 5; i++) {
int num = i;
new Thread(() -> {
cache.put(num + "", num + "");
}, String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
int num = i;
new Thread(() -> {
cache.get(num + "");
}, String.valueOf(i)).start();
}
}
}
输出结果:
1正在写操作:1
1写完了:1
4正在写操作:4
4写完了:4
5正在写操作:5
5写完了:5
2正在写操作:2
2写完了:2
3正在写操作:3
3写完了:3
3正在读取操作:3
4正在读取操作:4
2正在读取操作:2
1正在读取操作:1
5正在读取操作:5
1取完了:1
5取完了:5
2取完了:2
4取完了:4
3取完了:3
10. BlockingQueue
java.util.concurrent
包中,BlockingQueue
很好的解决了多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利。
- 先进先出
- 队列为空时,获取元素将被阻塞,直到插入新的元素
- 队列为满时,添加元素将被阻塞,直到队列不为空
10.1 BlockingQueue 核心方法
方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除 | remove() | poll() | take() | poll(time,unit) |
检查 | element() | peek() | 不可用 | 不可用 |
-
抛出异常
当阻塞队列满时,再往队列里
add()
插入元素会抛出IlleagalStateException:Queue full
当阻塞队列空时,再往队列里
remove()
移除元素会抛出NoSuchElementException
-
特殊值
插入方法,成功为
true
,失败为false
移除方法,返回出队列的元素,如果队列为空就返回
null
-
一直阻塞
当阻塞队列满时,生产者线程继续往队列里
put()
元素,队列会一直阻塞生产者线程直到put()
数据或响应中断退出当阻塞队列为空时,消费者线程试图从队列里
take()
元素,队列会一直阻塞消费者线程直到队列可用 -
超时退出
当阻塞队列满时,队列会阻塞生产者线程一定时间,超过限时后生产者线程会退出
10.2 数组阻塞队列 ArrayBlockingQueue
基于数组的阻塞队列实现,在 ArrayBlockingQueue
内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue
内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。
ArrayBlockingQueue
在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于 LinkedBlockingQueue
;按照实现原理来分析,ArrayBlockingQueue
完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。Doug Lea 之所以没这样去做,也许是因为 ArrayBlockingQueue
的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。
ArrayBlockingQueue
和 LinkedBlockingQueue
间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的 Node
对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于 GC 的影响还是存在一定的区别。而在创建 ArrayBlockingQueue
时,我们还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁。
10.3 链阻塞队列 LinkedBlockingQueue
基于链表的阻塞队列,同 ArrayBlockingQueue
类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue
可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而 LinkedBlockingQueue
之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
ArrayBlockingQueue
和 LinkedBlockingQueue
是两个最普通也是最常用的阻塞队列,一般情况下,在处理多线程间的生产者消费者问题,使用这两个类足以。
10.4 延迟队列 DelayQueue
DelayQueue
中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue
是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
10.5 具有优先级的阻塞队列 PriorityBlockingQueue
基于优先级的阻塞队列(优先级的判断通过构造函数传入的 Compator
对象来决定),但需要注意的是 PriorityBlockingQueue
并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。
因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。
在实现 PriorityBlockingQueue
时,内部控制线程同步的锁采用的是公平锁。
10.6 同步队列 SynchronousQueue
一种无缓冲的等待队列,类似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去集市找到所要商品的直接生产者,如果一方没有找到合适的目标,那么对不起,大家都在集市等待。相对于有缓冲的 BlockingQueue
来说,少了一个中间经销商的环节(缓冲区),如果有经销商,生产者直接把产品批发给经销商,而无需在意经销商最终会将这些产品卖给那些消费者,由于经销商可以库存一部分商品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖);但另一方面,又因为经销商的引入,使得产品从生产者到消费者中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。
声明一个 SynchronousQueue
有两种不同的方式,它们之间有着不太一样的行为。
公平模式和非公平模式的区别:
-
公平模式
SynchronousQueue
会采用公平锁,并配合一个 FIFO 队列来阻塞多余的生产者和消费者,从而体系整体的公平策略 -
非公平模式(
SynchronousQueue
默认)SynchronousQueue
采用非公平锁,同时配合一个 LIFO 队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。
10.7 生产者/消费者模型
package queue;
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
class Producer implements Runnable {
private final BlockingQueue<String> queue;
Producer(BlockingQueue<String> q) {
queue = q;
}
public void run() {
try {
for (int i = 1; i <= 10; i++) {
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println("\033[1;31m生产者生产包子" + i + "\033[0m");
queue.put("包子" + i);
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
class Consumer implements Runnable {
private final BlockingQueue<String> queue;
private final int n;
Consumer(BlockingQueue<String> q, int n) {
queue = q;
this.n = n;
}
public void run() {
try {
for (int i = 1; i <= 5; i++) {
String baozi = queue.take();
System.out.println("\033[1;32m消费者" + n + "消费" + baozi + "\033[0m");
TimeUnit.SECONDS.sleep(new Random().nextInt(3));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class QueueSemo {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
Producer producer = new Producer(queue);
Consumer consumer1 = new Consumer(queue, 1);
Consumer consumer2 = new Consumer(queue, 2);
new Thread(producer).start();
new Thread(consumer1).start();
new Thread(consumer2).start();
}
}
输出结果:
生产者生产包子1
消费者1消费包子1
生产者生产包子2
消费者2消费包子2
生产者生产包子3
消费者1消费包子3
生产者生产包子4
消费者2消费包子4
生产者生产包子5
消费者1消费包子5
生产者生产包子6
生产者生产包子7
消费者2消费包子6
生产者生产包子8
消费者1消费包子7
消费者1消费包子8
生产者生产包子9
消费者2消费包子9
生产者生产包子10
消费者2消费包子10
11. 线程池
11.1 线程池概述
线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。
线程池的优势: 线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
Java 中的线程池是通过 Executor
框架实现的,该框架中用到了 Executor
,Executors
,ExecutorService
,ThreadPoolExecutor
这几个类。
11.2 FixedThreadPool
创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。
特征:
- 线程池中的线程处于一定的量,可以很好的控制线程的并发量
- 线程可以重复被使用,在显示关闭之前,都将一直存在
- 超出一定量的线程被提交时候需在队列中等待
场景: 适用于可以预测线程数量的业务中,或者服务器负载较重,对线程数有严格限制的场景
11.3 SingleThreadExecutor
创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执行后续的任务)。可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的 newFixedThreadPool()
不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。
特征:线程池中最多执行 1 个线程,之后提交的线程活动将会排在队列中以此执行
11.4 CachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程.
特点:
- 线程池中数量没有固定,可达到最大值(
Interger. MAX_VALUE
) - 线程池中的线程可进行缓存重复利用和回收(回收默认时间为 1 分钟)
- 当线程池中,没有可用线程,会重新创建一个线程
11.5 ThreadPoolExecutor
上面三个常用线程池的底层都是返回一个 ThreadPoolExecutor
对象,我们现在看看他的构造函数的参数情况:
/**
* corePoolSize:常驻线程数量
* maximumPoolSize:最大线程数量
* keepAliveTime:线程存活时间
* unit:存活时间的单位
* workQueue:存放提交但未执行任务的队列
* threadFactory:用于创建线程工厂
* handler:等待队列满后的拒绝策略
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
11.6 拒绝策略
-
AbortPolicy (默认)
丢弃任务,并抛出拒绝执行
RejectedExecutionException
异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。 -
CallerRunsPolicy
当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大。
-
DiscardOldestPolicy
当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入
-
DiscardPolicy
直接丢弃,其他啥都没有
11.7 自定义线程池
实际使用都是自定义的
import java.util.concurrent.*;
public class ThreadDemo {
public static void main(String[] args) {
ExecutorService executor = new ThreadPoolExecutor(
2,
5,
3L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
try {
for (int i = 0; i < 10; i++) {
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + "办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
}
输出结果:
pool-1-thread-1办理业务
pool-1-thread-4办理业务
pool-1-thread-5办理业务
......
12. Fork/Join 分支合并框架
12.1 概述
Fork/Join它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出。Fork/Join框架要完成两件事情:
- Fork:把一个复杂任务进行分拆,大事化小
- Join:把分拆任务的结果进行合并
12.1.1 三个模块及关系
Fork/Join框架主要包含三个模块:
- 任务对象:
ForkJoinTask
(包括RecursiveTask
、RecursiveAction
和CountedCompleter
) - 执行Fork/Join任务的线程:
ForkJoinWorkerThread
- 线程池:
ForkJoinPool
这三者的关系是: ForkJoinPool
可以通过池中的 ForkJoinWorkerThread
来处理 ForkJoinTask
任务。
// from 《A Java Fork/Join Framework》Dong Lea
Result solve(Problem problem) {
if (problem is small)
directly solve problem
else {
split problem into independent parts
fork new subtasks to solve each part
join all subtasks
compose result from subresults
}
}
ForkJoinPool
只接收 ForkJoinTask
任务(在实际使用中,也可以接收 Runnable
/Callable
任务,但在真正运行时,也会把这些任务封装成 ForkJoinTask
类型的任务),RecursiveTask
是 ForkJoinTask
的子类,是一个可以递归执行的 ForkJoinTask
,RecursiveAction
是一个无返回值的 RecursiveTask
,CountedCompleter
在任务完成执行后会触发执行一个自定义的钩子函数。
在实际运用中,我们一般都会继承 RecursiveTask
、RecursiveAction
或 CountedCompleter
来实现我们的业务需求,而不会直接继承 ForkJoinTask
类。
12.1.2 核心思想: 分治算法(Divide-and-Conquer)
分治算法(Divide-and-Conquer)把任务递归的拆分为各个子任务,这样可以更好的利用系统资源,尽可能的使用所有可用的计算能力来提升应用性能。首先看一下 Fork/Join 框架的任务运行机制:
12.1.3 核心思想: work-stealing(工作窃取)算法
work-stealing(工作窃取)算法: 线程池内的所有工作线程都尝试找到并执行已经提交的任务,或者是被其他活动任务创建的子任务(如果不存在就阻塞等待)。这种特性使得 ForkJoinPool
在运行多个可以产生子任务的任务,或者是提交的许多小任务时效率更高。尤其是构建异步模型的 ForkJoinPool
时,对不需要合并(join)的事件类型任务也非常适用。
在 ForkJoinPool
中,线程池中每个工作线程(ForkJoinWorkerThread
)都对应一个任务队列(WorkQueue
),工作线程优先处理来自自身队列的任务(LIFO 或 FIFO 顺序,参数 mode 决定),然后以 FIFO 的顺序随机窃取其他队列中的任务。
具体思路如下:
- 每个线程都有自己的一个
WorkQueue
,该工作队列是一个双端队列 - 队列支持三个功能 push、pop、poll
- push/pop 只能被队列的所有者线程调用,而poll可以被其他线程调用
- 划分的子任务调用 fork 时,都会被 push 到自己的队列中
- 默认情况下,工作线程从自己的双端队列获出任务并执行
- 当自己的队列为空时,线程随机从另一个线程的队列末尾调用 poll 方法窃取任务
12.1.4 Fork/Join 框架的执行流程
12.2 一些Fork/Join例子
12.2.1 采用Fork/Join来异步计算1+2+3+…+10000的结果
public class Test {
static final class SumTask extends RecursiveTask<Integer> {
private static final long serialVersionUID = 1L;
final int start; //开始计算的数
final int end; //最后计算的数
SumTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
//如果计算量小于1000,那么分配一个线程执行if中的代码块,并返回执行结果
if(end - start < 1000) {
System.out.println(Thread.currentThread().getName() + " 开始执行: " + start + "-" + end);
int sum = 0;
for(int i = start; i <= end; i++)
sum += i;
return sum;
}
//如果计算量大于1000,那么拆分为两个任务
SumTask task1 = new SumTask(start, (start + end) / 2);
SumTask task2 = new SumTask((start + end) / 2 + 1, end);
//执行任务
task1.fork();
task2.fork();
//获取任务执行的结果
return task1.join() + task2.join();
}
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask<Integer> task = new SumTask(1, 10000);
pool.submit(task);
System.out.println(task.get());
}
}
执行结果:
ForkJoinPool-1-worker-1 开始执行: 1-625
ForkJoinPool-1-worker-7 开始执行: 6251-6875
ForkJoinPool-1-worker-6 开始执行: 5626-6250
ForkJoinPool-1-worker-10 开始执行: 3751-4375
ForkJoinPool-1-worker-13 开始执行: 2501-3125
ForkJoinPool-1-worker-8 开始执行: 626-1250
ForkJoinPool-1-worker-11 开始执行: 5001-5625
ForkJoinPool-1-worker-3 开始执行: 7501-8125
ForkJoinPool-1-worker-14 开始执行: 1251-1875
ForkJoinPool-1-worker-4 开始执行: 9376-10000
ForkJoinPool-1-worker-8 开始执行: 8126-8750
ForkJoinPool-1-worker-0 开始执行: 1876-2500
ForkJoinPool-1-worker-12 开始执行: 4376-5000
ForkJoinPool-1-worker-5 开始执行: 8751-9375
ForkJoinPool-1-worker-7 开始执行: 6876-7500
ForkJoinPool-1-worker-1 开始执行: 3126-3750
50005000
12.2.2 实现斐波那契数列
public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool(4); // 最大并发数4
Fibonacci fibonacci = new Fibonacci(20);
long startTime = System.currentTimeMillis();
Integer result = forkJoinPool.invoke(fibonacci);
long endTime = System.currentTimeMillis();
System.out.println("Fork/join sum: " + result + " in " + (endTime - startTime) + " ms.");
}
//以下为官方API文档示例
static class Fibonacci extends RecursiveTask<Integer> {
final int n;
Fibonacci(int n) {
this.n = n;
}
@Override
protected Integer compute() {
if (n <= 1) {
return n;
}
Fibonacci f1 = new Fibonacci(n - 1);
f1.fork();
Fibonacci f2 = new Fibonacci(n - 2);
return f2.compute() + f1.join();
}
}
当然你也可以两个任务都 fork
,要注意的是两个任务都 fork
的情况,必须按照f1.fork()
,f2.fork()
, f2.join()
,f1.join()
这样的顺序,不然有性能问题,详见上面注意事项中的说明。