6. 深入理解线程通信【举例说明】
6.1 线程之间共享数据
Java 里面进行多线程通信的主要方式就是共享内存的方式,共享内存主要的关注点有两个:可见 性和有序性原子
性。Java 内存模型(JMM)解决了可见性和有序性的问题,而锁解决了原子性的 问题,理想情况下我们希望做到
“同步”和“互斥”。有以下常规实现方法:
6.1.1 将数据抽象成一个类,并将数据的操作作为这个类的方法
Data类
/**
* 多个线程共享数据
* i++
* i--
*/
public class Data {
private int i = 0;
public synchronized void add(){
i++;
System.out.println("线程::"+Thread.currentThread().getName()+"正在操作数值i="+i);
}
public synchronized void dec(){
i--;
System.out.println("线程::"+Thread.currentThread().getName()+"正在操作数值i="+i);
}
public int getI() {
return i;
}
}
AddThread
public class AddThread implements Runnable{
Data data;
public AddThread(Data data) {
this.data = data;
}
@Override
public void run() {
data.add();
}
}
DecThread
public class DecThread implements Runnable {
Data data;
public DecThread(Data data) {
this.data = data;
}
@Override
public void run() {
data.dec();
}
}
MainClass
public class MainClass {
public static void main(String[] args) {
// 多线程需要共享的数据
Data data = new Data();
Runnable addThread = new AddThread(data);
Runnable decThread = new DecThread(data);
for (int i = 0; i < 3; i++) {
new Thread(addThread,"增加线程").start();
new Thread(decThread,"减少线程").start();
}
}
}
/*
运行结果:
线程::增加线程正在操作数值i=1
线程::增加线程正在操作数值i=2
线程::减少线程正在操作数值i=1
线程::增加线程正在操作数值i=2
线程::减少线程正在操作数值i=1
线程::减少线程正在操作数值i=0
*/
6.1.2 Runnable 对象作为一个类的内部类
Data类
/**
* 多个线程共享数据
* i++
* i--
*/
public class Data {
private int i = 0;
public synchronized void add(){
i++;
System.out.println("线程::"+Thread.currentThread().getName()+"正在操作数值i="+i);
}
public synchronized void dec(){
i--;
System.out.println("线程::"+Thread.currentThread().getName()+"正在操作数值i="+i);
}
public int getI() {
return i;
}
}
ShareDataThread
public class ShareDataThread {
public static void main(String[] args) {
// 共享数据
Data data = new Data();
for (int i = 0; i < 2; i++) {
// 匿名内部类共享数据
new Thread(new Runnable() {
@Override
public void run() {
data.add();
}
},"增加线程").start();
new Thread(new Runnable() {
@Override
public void run() {
data.dec();
}
},"减少线程").start();
}
}
}
/*
运行结果:
线程::增加线程正在操作数值i=1
线程::减少线程正在操作数值i=0
线程::增加线程正在操作数值i=1
线程::减少线程正在操作数值i=0
*/
6.2 CountDownLatch 闭锁
CountDownLatch是 java.util.concurrent 包下一个同步工具类,用来协调多个线程之间的同步。这个工具通常用
来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。利用它可以实现**类似计数器**的功能。比
如有 一个任务 A,它要等待其他 4 个任务执行完毕之后才能执行,此时就可以利用 CountDownLatch 来实现这种
功能了。
CountDownLatch 一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直
等待。以下的应用场景是可以使用闭锁来实现的
-
闭锁可以延迟线程的进度直到其到达终止状态
-
闭锁可以用来确保某些活动直到其他活动都完成才继续执行:
-
确保某个计算在其需要的所有资源都被初始化之后才继续执行;
-
确保某个服务在其依赖的所有其他服务都已经启动之后才启动;
-
等待直到某个操作所有参与者都准备就绪再继续执行。
//参数 count 为计数值
public CountDownLatch(int count) { };
//调用 await()方法的线程会被挂起,它会等待直到 count 值为 0 才继续执行
public void await() throws InterruptedException { };
//和 await()类似,只不过等待一定的时间后 count 值还没变为 0 的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };
//将 count 值减 1
public void countDown() { };
/**
* 有三个线程,要求线程A在其他线程都执行完成在执行
*/
public class TestCountDownLatch {
public static void main(String[] args) {
// 闭锁计数器
CountDownLatch latch = new CountDownLatch(2);
long start= System.currentTimeMillis();
// 主线程A
new Thread(()->{
try {
// 等待计数器为0执行
latch.await();
long end= System.currentTimeMillis();
System.out.println("线程全部执行共耗时::"+(end-start)+"毫秒");
System.out.println("主线程A已经开始执行了...");
Thread.sleep((long)Math.random()*1000);
System.out.println("主线程A执行完成!!!");
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
// 子线程B
new Thread(()->{
try {
System.out.println("线程B已经开始执行了...");
Thread.sleep((long)Math.random()*1000);
System.out.println("线程B执行完成!!!");
// 计数器减一
latch.countDown();
} catch (Exception e) {
e.printStackTrace();
}
},"子线程B").start();
// 子线程C
new Thread(()->{
try {
System.out.println("线程C已经开始执行了...");
Thread.sleep((long)Math.random()*1000);
System.out.println("线程C执行完成!!!");
// 计数器减一
latch.countDown();
} catch (Exception e) {
e.printStackTrace();
}
},"子线程B").start();
}
}
/*
运行结果:
线程B已经开始执行了...
线程C已经开始执行了...
线程C执行完成!!!
线程B执行完成!!!
线程全部执行共耗时::54毫秒
主线程A已经开始执行了...
主线程A执行完成!!!
*/
CountDownLatch 也是基于 AQS(AbstractQueuedSynchronizer) 实现的
-
初始化一个 CountDownLatch 时告诉并发的线程,然后在每个线程处理完毕之后调用 countDown() 方法。
-
该方法会将 AQS 内置的一个 state 状态 -1 。
-
最终在主线程调用 await() 方法,它会阻塞直到 state == 0 的时候返回。
6.3 CyclicBarrier 循环栅栏
CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,
但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的
字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步
点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用
await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
CyclicBarrier(回环栅栏-等待至 barrier 状态再全部同时执行)字面意思回环栅栏,通过它可以实现让一组线
程等待至某个状态之后再全部同时执行。叫做回环 是因为当所有等待线程都被释放以后, CyclicBarrier 可以被重
用。我们暂且把这个状态就叫做 barrier,当调用 await()方法之后,线程就处于 barrier 了。CyclicBarrier 中最重
要的方法就是 await 方法,它有 2 个重载版本:
-
public int await():用来挂起当前线程,直至所有线程都到达 barrier 状态再同时执行后续任 务;
-
public int await(long timeout, TimeUnit unit):让这些线程等待至一定的时间,如果还有 线程没有到达 barrier 状态
就直接让到达 barrier 的线程执行后续任务。具体使用如下,另外 CyclicBarrier 是可以重用的
举例:公司组织周末聚餐吃饭,首先员工们(线程)各自从家里到聚餐地点,全部到齐之后,才开始一起吃东西
(同步点)。假如人员没有到齐(阻塞),到的人只能够等待,直到所有人都到齐之后才开始吃饭
CyclicBarrier(可循环的障碍物)
场景:在多线程计算数据之后,最后需要合并结果。
/**
* 3个员工,周末聚餐,当三个人全部到达目的地开始吃饭
* 如果一个人不到达,那么其他人玩手机等待
* 主任务:人员到齐,拍照吃饭
* 子任务:到达目的地,等吃饭,吃完回家
*/
public class CyclicBarrierDemo {
public static void main(String[] args) throws InterruptedException {
int eleNum = 5;
CyclicBarrier cb = new CyclicBarrier(eleNum,()->{
System.out.println("人员全部到齐,先集体拍照留念");
});
// 员工的线程
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < eleNum; i++) {
int userNumber = i+1;
Runnable r = new Runnable() {
@Override
public void run() {
// 员工线程的业务逻辑
try {
Thread.sleep((long) (Math.random()*1000));
System.out.println(userNumber+"号员工已经到达聚餐地点,当前已经有"+cb.getNumberWaiting()+"人到达了");
// 到达等待(阻塞)
cb.await();
System.out.println("拍照结束,"+userNumber+"号员工开始吃饭");
Thread.sleep((long) (Math.random()*1000));
System.out.println(userNumber+"号员工吃完饭回家...");
} catch (Exception e) {
e.printStackTrace();
}
}
};
pool.submit(r);
}
Thread.sleep(10000L);
pool.shutdownNow();
}
}
/*
运行结果:
1号员工已经到达聚会地点,已到达1人
2号员工已经到达聚会地点,已到达2人
3号员工已经到达聚会地点,已到达3人
员工都到齐了,开始拍照...
1号员工拍照完毕,开始吃饭...
2号员工拍照完毕,开始吃饭...
3号员工拍照完毕,开始吃饭...
吃饭完毕,1号员工回家
吃饭完毕,2号员工回家
吃饭完毕,3号员工回家
*/
6.4 Semaphore 信号量
限流、需要控制连接数的稀缺资源等
Semaphore(信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访
问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
Semaphore 可以控制同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release()释
放一个许可。
Semaphore 类中比较重要的几个方法:
-
public void acquire(): 用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许 可。
-
public void acquire(int permits):获取 permits 个许可
-
public void release() { } :释放许可。注意,在释放许可之前,必须先获获得许可。
-
public void release(int permits) { }:释放 permits 个许可
上面 4 个方法都会被阻塞,如果想立即得到执行结果,可以使用下面几个方法
Semaphore 是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信 号,做完自己的
申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore 可以用来 构建一些对象池,资源池之类
的,比如数据库连接池
-
public boolean tryAcquire():尝试获取一个许可,若获取成功,则立即返回 true,若获取失 败,则立即返回 false
-
public boolean tryAcquire(long timeout, TimeUnit unit):尝试获取一个许可,若在指定的 时间内获取成功,
则立即返回 true,否则则立即返回 false
-
public boolean tryAcquire(int permits):尝试获取 permits 个许可,若获取成功,则立即返 回 true,若获取
失败,则立即返回 false
-
public boolean tryAcquire(int permits, long timeout, TimeUnit unit): 尝试获取 permits 个许可,若在指定的时间
内获取成功,则立即返回 true,否则则立即返回 false
- 还可以通过 availablePermits()方法得到可用的许可数目。
例子:若一个工厂有 5 台机器,但是有 8 个工人,一台机器同时只能被一个工人使用,只有使用完了,其他工人
才能继续使用。那么我们就可以通过 Semaphore 来实现:
public class TestSemaphore {
public static void main(String[] args) {
int wokerNumber = 8;
// 共享资源信号量
Semaphore semaphore = new Semaphore(5);
for (int i = 0; i < wokerNumber; i++) {
new Thread(new Woker(semaphore,i+1)).start();
}
}
}
class Woker implements Runnable{
// 线程信号量
private Semaphore semaphore;
// 员工编号
private int number;
public Woker(Semaphore semaphore, int number) {
this.semaphore = semaphore;
this.number = number;
}
@Override
public void run() {
try {
// 申请资源
semaphore.acquire();
System.out.println(number+"号员工申请到了一台机器...");
Thread.sleep(2000);
// 归还资源
System.out.println(number+"号员工释放了一台机器...");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/*
运行结果:
1号员工申请到了一台机器...
2号员工申请到了一台机器...
3号员工申请到了一台机器...
4号员工申请到了一台机器...
5号员工申请到了一台机器...
5号员工释放了一台机器...
2号员工释放了一台机器...
1号员工释放了一台机器...
4号员工释放了一台机器...
6号员工申请到了一台机器...
7号员工申请到了一台机器...
8号员工申请到了一台机器...
3号员工释放了一台机器...
6号员工释放了一台机器...
8号员工释放了一台机器...
7号员工释放了一台机器...
*/
6.4.1 实现互斥锁(计数器为 1)
我们也可以创建计数为 1 的 Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,
表示两种互斥状态。
它的用法如下:
// 创建一个计数阈值为 5 的信号量对象 // 只能 5 个线程同时访问
Semaphore semp = new Semaphore(5);
try {
// 申请许可
semp.acquire();
try {
// 业务逻辑
} catch (Exception e) {
//处理异常
} finally {
// 释放许可
semp.release();
}
} catch (InterruptedException e) {
}
6.4.2 Semaphore 与 ReentrantLock
Semaphore 基本能完成 ReentrantLock 的所有工作,使用方法也与之类似,通过 acquire()与 release()方法
来获得和释放临界资源。经实测,Semaphone.acquire()方法默认为可响应中断锁,与
ReentrantLock.lockInterruptibly()作用效果一致,也就是说在等待临界资源的过程中可以被 Thread.interrupt()方
法中断。
此外,Semaphore 也实现了可轮询的锁请求与定时锁的功能,除了方法名 tryAcquire 与 tryLock 不同,其使
用方法与 ReentrantLock 几乎一致。Semaphore 也提供了公平与非公平锁的机制,也 可在构造函数中进行设
定。Semaphore 的锁释放操作也由手动进行,因此与 ReentrantLock 一样,为避免线程因抛出异常而无法正常释
放的情况发生,释放锁的操作也必须在 finally 代码块中完成。
6.5 Exchanger(一手交钱,一手交货)
业务需求三:绑架案,目的是为了用钱来赎人
张三团伙绑架了小乔,放言要 1000 万来赎人,
张三团伙和周瑜同时到达约定的地点,然后一手交钱,一手交人
两个线程,在同一个点(阻塞点),交换数据
Exchanger 两个线程进行交换数据
场景:1.用于 2 个线程交换数据
2.校对工作
public class TestSemaphore {
public static void main(String[] args) {
//交换器,交换 String 类型
Exchanger<String> ec = new Exchanger<>();
ExecutorService pool = Executors.newCachedThreadPool();
// 张三团伙
pool.submit(()->{
try {
String str = ec.exchange("小乔");
System.out.println("绑架者用小乔交换回:" + str);
} catch (Exception e) {
e.printStackTrace();
}
});
// 周瑜
pool.submit(()->{
try {
String str = ec.exchange("1000w");
System.out.println("周瑜用1000w交换回:" + str);
} catch (Exception e) {
e.printStackTrace();
}
});
pool.shutdown();
}
}
/*
运行结果:
周瑜用1000w交换回:小乔
绑架者用小乔交换回:1000w
*/