并发编程专题
第一章 线程入门
基本概念
串行
Ø 一个任务执行完再去执行另一个任务
并行
Ø 用齐头并进的方式去完成任务,其实只是时间片的切换,离开时间片,无意义
并发
Ø 一段时间内以交替的方式去完成任务
进程
Ø 资源分配的最小单位
线程
Ø CPU调度的最小单位,共享进程中的资源,必须依附于进程,不能独立存在
任务
Ø 线程要完成特定计算的任务
并发编程意义
Ø 充分利用CPU资源
Ø 加快用户响应的时候
Ø 模块化,异步化
创建线程
Ø Extends Thread : 重写run方法
Ø Implement Runable: 实现run方法,可以共享变量
Ø Implement Callable:带返回值
线程启动
Ø Run方法 是对象本身方法
Ø Start方法 是启动线程的方法
线程生命周期
Ø 新建:new的时候
Ø 就绪:调用start方法等待CPU的调度
Ø 运行:获得CPU调度的权利
Ø 阻塞:主要有被动的或者主动的放弃了CPU的调度权
>> 线程调用sleep方法主动放弃所占有的CPU资源
>> 调用IO阻塞方法,在该方法返回之前,该线程被阻塞
>> 线程试图获取一个同步监视器,但该监视器正被其它线程锁持有
>> 线程在等待某个通知,比如notify,signal等
>> 程序调用了线程的suspend,使该线程挂起
Ø 死亡
>> 线程调用run方法正常运行且结束、
>> 线程抛出一个未捕获的异常或者错误,被迫结束
>> 直接调用了线程的stop方法
线程上下文切换
Ø Wait:等待
线程会放弃对象锁
Ø Yield:暂停
是让当前线程让出cpu时间,os依然可以选中当前线程
Ø Sleep:休眠
让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态,线程不会释放对象锁
Ø Join:等待
当前线程A等待thread线程终止之后才从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(longmillis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时时间里没有终止,那么将会从该超时方法中返回
Ø Stw: GC出现的stop the word
线程故障
死锁
Ø 有两个以上的线程或者进程发生了资源的争抢
活锁
Ø X线程之间学会了谦让,谁都不愿意去争抢资源
饥饿锁
Ø 线程因为CPU时间全部被其它线程抢走而得不到CPU运行时间
线程调度
Ø 公平调度
Ø 非公平调度
线程中断
停止一个正在运行的线程,目前不推荐使用stop方法或者supend方法,而是使用interupt方法,看一下代码,针对runable接口和thread类是有区别的,下面看
实现runable接口
public class EndRunnable {
private static class UseRunnable implements Runnable{
@Override
public void run() {
while(!Thread.currentThread().isInterrupted()) {//改为true,线程不会中断;改为Thread.interrupted(),标志位被复位
System.out.println(Thread.currentThread().getName()
+ " I am implements Runnable.");
}
System.out.println(Thread.currentThread().getName()
+" interrupt flag is "+Thread.currentThread().isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
UseRunnable useRunnable = new UseRunnable();
Thread endThread = new Thread(useRunnable,"endThread");
endThread.start();
Thread.sleep(20);
endThread.interrupt();
}
继承thread
public class HasInterrputException {
private static class UseThread extends Thread{
public UseThread(String name) {
super(name);
}
@Override
public void run() {
while(!isInterrupted()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()
+" in InterruptedException interrupt flag is "
+isInterrupted());
interrupt();
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ " I am extends Thread.");
}
System.out.println(Thread.currentThread().getName()
+" interrupt flag is "+isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
Thread endThread = new UseThread("HasInterrputEx");
endThread.start();
Thread.sleep(500);
endThread.interrupt();
}
}
守护线程
Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。我们一般用不上,比如垃圾回收线程就是Daemon线程。
Daemon线程被用作完成支持性工作,但是在Java虚拟机退出时Daemon线程中的finally块并不一定会执行。在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。
看一下这段代码
Finally代码不一定会执行,setDaemon方法一定要放在start方法调用之前设置
线程特性
Ø 原子性:
java对变量的操作都是原子性
保证读取到的值要不是初始化的,要不就是最新更新的值
Ø 可见性
修改变量对其它线程是可见的
Ø 有序性
指令重排序
第二章 线程安全
安全定义
类,多线程下访问,不管调用者调用这个类,这个类总是表现出个正确的行为
不安全原因
主要是JMM模型导致,总结如下
1、 数据不一致,缓存不一致
2、 指令的重排序
不安全表现
数据不一致
伪共享
多个线程不同的变量正位于同一个缓存行
CPU缓存
CPU缓存系统中是以缓存行(cache line)为单位存储的。目前主流的CPU Cache的Cache Line大小都是64Bytes
CPU Cache分成了三个级别:L1,L2,L3。级别越小越接近CPU, 所以速度也更快, 同时也代表着容量越小
CPU获取数据回依次从L1,L2,L3中查找,如果都找不到则会直接向内存查找
解决方案
jdk8以前需要自己实现:缓存填充行
jdk8以后需要自己实现:添加注解
@sun.misc.Contended, 此注解默认是无效的,需要在jvm启动时设置-XX:-RestrictContended才会生效
死锁
通常来说是发生在系统有好几个进程或者线程同时争抢某个资源而发生了死锁,
它严重的时候会消耗整个CPU资源,而是程序瘫痪,所以平时编程的时候一定要注意死锁的发生
public class NormalDeadLock {
private static Object valueFirst = new Object();//第一个锁
private static Object valueSecond = new Object();//第二个锁
//先拿第一个锁,再拿第二个锁
private static void fisrtToSecond() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (valueFirst){
System.out.println(threadName+" get valueSecond");
Thread.sleep(100);
synchronized (valueSecond){
System.out.println(threadName+" get valueFirst");
}
}
}
//先拿第二个锁,再拿第一个锁
private static void SecondToFisrt() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (valueSecond){
System.out.println(threadName+" get valueSecond");
Thread.sleep(100);
synchronized (valueFirst){
System.out.println(threadName+" get valueFirst");
}
}
}
private static class TestThread extends Thread{
private String name;
public TestThread(String name) {
this.name = name;
}
public void run(){
Thread.currentThread().setName(name);
try {
SecondToFisrt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread.currentThread().setName("TestDeadLock");
TestThread testThread = new TestThread("SubTestThread");
testThread.start();
try {
fisrtToSecond();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
安全编程
Synchroized
基于JVM级别的,可以用来同步方法块,指定同步快,在高并发的情况下,性能能比Lock稍微差点
模拟两个线程分别对一个数字做同样的加法和减法,最终打印的结果应该是一致的
public class AddSubstractionTest {
private int age=100;
private static class AddSubThread extends Thread{
private AddSubstractionTest synTest;
public AddSubThread(AddSubstractionTest synTest) {
this.synTest = synTest;
}
@Override
public void run() {
for(int i=0;i<1000;i++) {//递增100000
synTest.add();
}
System.out.println(Thread.currentThread().getName()
+" age = "+synTest.getAge());
}
}
/**
* 加法
*/
public synchronized void add() {
age++;
}
/**
* 减法
* @return
*/
public synchronized void muls() {
age--;
}
public int getAge() {
return age;
}
public static void main(String[] args) {
AddSubstractionTest test = new AddSubstractionTest();
AddSubThread thread = new AddSubThread(test);
thread.start();
for (int i=0;i<1000;i++){
test.muls();
}
}
}
如果去掉synchroized关键字,会发现结果每次都是不正确且还是不一样的,这就说明了数据出现了不安全,也就是我们所说的线程不安全,加上这个关键字线程就安全了,但是加这个关键字要考虑实际业务的需求,加到不同的位置.
Ø 直接加方法上,粒度比较大,性能也比较差
Ø 加指定需要的同步块,粒度变小,性能也较好
但这里需要注意的,这里同步的对象不能是基本数值类型,比如int,而是要换成引用对象,Integer
Lock
锁是基于类级别的,正常来说性能会比synchroized关键字要好点,但是它编程会相对麻烦点,从使用的角度来说又可以分为专门的普通锁和读写锁
ReetrantLock
普通的锁,有三个常用方法
方法 | 说明 |
---|---|
lock | 锁住当前对象 |
unLock | 释放锁,这个方法一定是会执行的 |
tryLock | 尝试获取锁,可以指定时间 |
lockInterruptibly | 获取锁时候,响应中断,直接抛出异常 |
看一个简单实用的例子 做加减法的功能
public class LockCase {
private Lock lock = new ReentrantLock();
private int age = 100000;//初始100000
private static class TestThread extends Thread{
private LockCase lockCase;
public TestThread(LockCase lockCase,String name) {
super(name);
this.lockCase = lockCase;
}
@Override
public void run() {
for(int i=0;i<100000;i++) {//递增100000
lockCase.add();
}
System.out.println(Thread.currentThread().getName()
+" age = "+lockCase.getAge());
}
}
public void add() {
lock.lock();
try {
age++;
} finally {
lock.unlock();
}
}
public void mlus() {
lock.lock();
try {
age--;
} finally {
lock.unlock();
}
}
public int getAge() {
return age;
}
public static void main(String[] args) throws InterruptedException {
LockCase lockCase = new LockCase();
Thread endThread = new TestThread(lockCase,"endThread");
endThread.start();
for(int i=0;i<100000;i++) {//递减100000
lockCase.mlus();
}
System.out.println(Thread.currentThread().getName()
+" age = "+lockCase.getAge());
}
}
ReetrantReaderWriteLock
读写锁,它把锁分的更细了,区别了读锁和写锁,如果在读多写少的场景,这个性能比普通的锁要好很多.
Ø readLock 获取读锁
Ø writeLock 获取写锁
实现一个商品数量的读取和修改的例子
定义商品服务接口
public interface GoodsService {
public GoodsInfo getNum();//读取商品数量
public void setNum(int number);//设置商品数量
}
定义服务接口的实现
在这里插入代码片public class UseRwLock implements GoodsService {
private GoodsInfo goodsInfo;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();//get
private final Lock writeLock = lock.writeLock();//set
public UseRwLock(GoodsInfo goodsInfo) {
this.goodsInfo = goodsInfo;
}
/**
* 获取商品数量
* @return
*/
@Override
public GoodsInfo getNum() {
readLock.lock();
SleepTools.ms(5);
try {
return this.goodsInfo;
} finally {
readLock.unlock();
}
}
/**
* 设置商品数量
* @param number
*/
@Override
public void setNum(int number) {
writeLock.lock();
try {
SleepTools.ms(50);
this.goodsInfo.changeNumber(number);
} finally {
writeLock.unlock();
}
}
}
定义读线程和写线程以及测试
public class BusiApp {
static final int readWriteRatio = 10;//读写线程的比例
static final int minthreadCount = 3;//最少线程数
static CountDownLatch latch= new CountDownLatch(1);
/**
* 读取线程
*/
private static class GetThread implements Runnable{
private GoodsService goodsService;
public GetThread(GoodsService goodsService) {
this.goodsService = goodsService;
}
@Override
public void run() {
try {
latch.await();//让读写线程同时运行
} catch (InterruptedException e) {
}
long start = System.currentTimeMillis();
for(int i=0;i<100;i++){//操作100次
goodsService.getNum();
}
System.out.println(Thread.currentThread().getName()+"读取商品数据耗时:"
+(System.currentTimeMillis()-start)+"ms");
}
}
/**
* 写入线程
*/
private static class SetThread implements Runnable{
private GoodsService goodsService;
public SetThread(GoodsService goodsService) {
this.goodsService = goodsService;
}
@Override
public void run() {
try {
latch.await();//让读写线程同时运行
} catch (InterruptedException e) {
}
long start = System.currentTimeMillis();
Random r = new Random();
for(int i=0;i<10;i++){//操作10次
SleepTools.ms(50);
goodsService.setNum(r.nextInt(10));
}
System.out.println(Thread.currentThread().getName()
+"写商品数据耗时:"+(System.currentTimeMillis()-start)+"ms---------");
}
}
public static void main(String[] args) throws InterruptedException {
GoodsInfo goodsInfo = new GoodsInfo("Cup",100000,10000);
GoodsService goodsService = new UseRwLock(goodsInfo);
for(int i = 0;i<minthreadCount;i++){
Thread setT = new Thread(new SetThread(goodsService));
for(int j=0;j<readWriteRatio;j++) {
Thread getT = new Thread(new GetThread(goodsService));
getT.start();
}
setT.start();
}
latch.countDown();
}
}
可以对比普通的锁,性能就比较明显了
public class UseSyn implements GoodsService {
private GoodsInfo goodsInfo;
public UseSyn(GoodsInfo goodsInfo) {
this.goodsInfo = goodsInfo;
}
@Override
public synchronized GoodsInfo getNum() {
SleepTools.ms(5);
return this.goodsInfo;
}
@Override
public synchronized void setNum(int number) {
SleepTools.ms(5);
goodsInfo.changeNumber(number);
}
}
锁的使用场景
1、 尝试非阻塞获取锁
2、 可以被中断的获取锁
3、 超时获取锁
CAS
概括
比较和交换,有三个操作符
Ø V:内存地址
Ø A:一个期望的旧值
Ø B:一个新值
结论:
操作的时候如果这个内存地址上存放的值等于期望的值A,那么就将内存地址上的值变为新值B,否则不做任何操作
问题
Ø ABA:本来被子里面放了一杯水,走了以后被别人喝了,然后别人又加上了,这个时候你还以为被子里面的水没有动过,其实也就不是之前的水了.
上面的本质和这个是一样的,可以通过版本号来解决.
Ø 循环的时间长开销大,自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
Ø 往往只能保证一个共享变量的原子操作
线程可见性
Voliate
它的本质,一句话概括,就是强制线程每次读取该值的时候都去“主内存”中取值,线程其实不是直接修改堆内存中的值,而是修改对应的副本值,完了之后,再把线程的副本的值写回堆内存,这样堆内存中的值就发生了变化
优点
Ø 可见性
Ø 有序性
Ø 变量操作的读写的原子性,但不一定保证
Ø 禁止重排序
Ø JIT编译器不会对相应代码做优化
缺点
Ø 读取成本比读取普通变量要稍微高
Ø 它被存储在高速缓存或主内存中,而不是寄存器中
使用场景
Ø 作为状态标志
Ø 保障可见性
Ø 替代锁
举例 线程可见性
public class VolatileTest extends Thread {
//线程可见性
volatile boolean flag = false;
//如果没有加volatile关键字发现程序也能正常退出,那是因为默认的JVM是以客户端形式运行的,要改成服务端模式,具体如何修改可以参考这个博客
https://www.cnblogs.com/wxw7blog/p/7221756.html
int i = 0;
public void run() {
while (!flag) {
i++;
}
}
public static void main(String[] args) throws Exception {
VolatileTest vt = new VolatileTest();
vt.start();
Thread.sleep(2000);
vt.flag = true;
System.out.println("stope" + vt.i);
}
}
线程会退出的.
例子 无法提供原子性
public class VolatileUnsafe {
private static class VolatileVar implements Runnable {
private volatile int a = 0;
@Override
public void run() {
a = a + 1;
System.out.println(Thread.currentThread().getName() + ":----" + a);
SleepTools.ms(100);
a = a + 1;
System.out.println(Thread.currentThread().getName() + ":----" + a);
}
}
public static void main(String[] args) {
VolatileVar v = new VolatileVar();
Thread t1 = new Thread(v);
Thread t2 = new Thread(v);
Thread t3 = new Thread(v);
Thread t4 = new Thread(v);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
打印的结果是会有重复的,并不是按照理想的顺序打印的
线程原子性
保证线程的操作是不可分割的
类型
Ø 基本类型: AtomicInteger/Long/Boolean
这个一本结合volatlie关键字一起使用,做一些计数器之类的或者标识位判断.
Ø 数组类型: AtomicIntergerArray/LongArray
Ø 引用类型: AtomicReference/ AtomicStampedReference
Ø 字段更新类型: AtomicInteger/LongFieldUpdate
这里面有解决ABA的叫AtomicStampedReference
实例演示 引用类型的使用
public class UseAtomicReference {
public static AtomicReference<UserInfo> atomicUserRef = new AtomicReference<UserInfo>();//原子引用类型的实例
public static void main(String[] args) {
UserInfo user = new UserInfo("Mark", 15);//要修改的实体的实例
// V 设置 A
atomicUserRef.set(user);//用原子引用类型包装
UserInfo updateUser = new UserInfo("Bill", 17);//要变化成为的新实例
// 调用CAS 来判断 V设置B
atomicUserRef.compareAndSet(user, updateUser);
atomicUserRef.compareAndSet(updateUser, user);
System.out.println("ref:"+atomicUserRef.get().getName()+":"+atomicUserRef.get().getAge());
System.out.println("org:"+user.getName()+":"+user.getAge());
}
//定义一个实体类
static class UserInfo {
private String name;
private int age;
public UserInfo(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
}
实例演示 解决ABA问题
public class UseAtomicStampedReference {
static AtomicStampedReference<String> asr = new AtomicStampedReference("mark", 0);
public static void main(String[] args) throws InterruptedException {
final int oldStamp = asr.getStamp();//拿初始版本0
final String oldReference = asr.getReference();//初始值
System.out.println(oldReference+"============"+oldStamp);
Thread rightStampThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":当前变量值:"
+oldReference + "-当前版本戳:" + oldStamp + "-"
+ asr.compareAndSet(oldReference,
oldReference + "+Java", oldStamp, oldStamp + 1));
}
});
Thread errorStampThread = new Thread(new Runnable() {
@Override
public void run() {
String reference = asr.getReference();//变量的最新值
System.out.println(Thread.currentThread().getName()+":当前变量值:"
+reference + "-当前版本戳:" + asr.getStamp() + "-"
+ asr.compareAndSet(reference,
reference + "+C", oldStamp, oldStamp + 1));
}
});
rightStampThread.start();
rightStampThread.join();
errorStampThread.start();
errorStampThread.join();
System.out.println(asr.getReference()+"============"+asr.getStamp());
}
}
常用方法
Ø getAndIncrement() 增加1,返回增加前的数据
Ø incrementAndGet() 增加1,返回增加后的值
Ø getAndDecrement() 减少1,返回减少前的数据
Ø decrementAndGet() 减少1,返回减少后的值
Ø getAndAdd(int) 增加指定的数据,返回变化前的数据
Ø compareAndSet(int, int) 尝试新增后对比,若增加成功则返回true否则返回false
第三章 线程协调
多个线程之间协同工作,一般指两个以上工作的线程
Wait VS Notify
Wait是object的方法,
使用
object.wait:当前正在执行的线程被阻塞
object.notify/notifyall:通知正在等待唤醒的线程
问题
Ø 过早唤醒
Ø 欺骗唤醒
Ø 信号丢失
Ø 无法区分是否是等待超时
Notify VS Notifyall
Notify:只能被唤醒所有等待线程中的一个,至于是哪一个完全是随机的
Notifyall:唤醒所有被等待的线程
建议使用notifyall
代码实例
public class Express { //快递的里程数和公里数之间存在互相通知
public final static String CITY = "ShangHai";
private int km;/*快递运输里程数*/
private String site;/*快递到达地点*/
public Express() {
}
public Express(int km, String site) {
this.km = km;
this.site = site;
}
/* 变化公里数,然后通知处于wait状态并需要处理公里数的线程进行业务处理*/
public synchronized void changeKm(){
this.km = 101;
notifyAll();
}
/* 变化地点,然后通知处于wait状态并需要处理地点的线程进行业务处理*/
public synchronized void changeSite(){
this.site = "BeiJing";
notifyAll();
}
public synchronized void waitKm(){
while(this.km<=100){//公里数小于100不做处理
try {
wait();
System.out.println("Check Km thread["+Thread.currentThread().getId()
+"] is be notified");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("the Km is "+this.km+",I will change db");
}
public synchronized void waitSite(){
while(this.site.equals(CITY)){//快递到达目的地
try {
wait();
System.out.println("Check Site thread["+Thread.currentThread().getId()
+"] is be notified");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("the site is "+this.site+",I will call user");
}
}
测试
public class TestWN {
private static Express express = new Express(0,Express.CITY);
/*检查里程数变化的线程,不满足条件,线程一直等待*/
private static class CheckKm extends Thread{
@Override
public void run() {
express.waitKm();
}
}
/*检查地点变化的线程,不满足条件,线程一直等待*/
private static class CheckSite extends Thread{
@Override
public void run() {
express.waitSite();
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<3;i++){
new CheckSite().start();
}
for(int i=0;i<3;i++){
new CheckKm().start();
}
Thread.sleep(1000);
express.changeKm();//快递地点变化
}
}
Condition VS Signal
它不是object的方法,它是基于Lock获取的,想必wait有一定的优点
创建condition
ReentrantLock.newCondition
常用方法
condition.awatiunit可以解决是否超时
condition.awatit等于object.wait
condition.singal/singall相当于object.notify/notifyall
优点
Ø 解决过早唤醒通知
Ø 实现多路通知
代码实例
ublic class ExpressCond {
public final static String CITY = "ShangHai";
private int km;/*快递运输里程数*/
private String site;/*快递到达地点*/
private Lock kmLock = new ReentrantLock();
private Lock siteLock = new ReentrantLock();
private Condition kmCond = kmLock.newCondition();
private Condition siteCond = siteLock.newCondition();
public ExpressCond() {
}
public ExpressCond(int km, String site) {
this.km = km;
this.site = site;
}
/* 变化公里数,然后通知处于wait状态并需要处理公里数的线程进行业务处理*/
public void changeKm(){
kmLock.lock();
try {
this.km = 101;
kmCond.signal();
} finally {
kmLock.unlock();
}
}
/* 变化地点,然后通知处于wait状态并需要处理地点的线程进行业务处理*/
public synchronized void changeSite(){
siteLock.lock();
try {
this.site = "BeiJing";
siteCond.signal();
} finally {
siteLock.unlock();
}
}
public void waitKm(){
kmLock.lock();
try {
while(this.km<=100){//公里数小于100不做处理
try {
kmCond.await();
System.out.println("Check Km thread["
+Thread.currentThread().getId()+"] is be signal");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
kmLock.unlock();
}
System.out.println("the Km is "+this.km+",I will change db");
}
public void waitSite(){
siteLock.lock();
try {
while(this.site.equals(CITY)){//快递到达目的地
try {
siteCond.await();
System.out.println("Check Site thread["
+Thread.currentThread().getId()+"] is be signal");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
siteLock.unlock();
}
System.out.println("the site is "+this.site+",I will call user");
}
}
测试效果
public class TestCond {
private static ExpressCond express = new ExpressCond(0,ExpressCond.CITY);
/*检查里程数变化的线程,不满足条件,线程一直等待*/
private static class CheckKm extends Thread{
@Override
public void run() {
express.waitKm();
}
}
/*检查地点变化的线程,不满足条件,线程一直等待*/
private static class CheckSite extends Thread{
@Override
public void run() {
express.waitSite();
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<3;i++){
new CheckSite().start();
}
for(int i=0;i<3;i++){
new CheckKm().start();
}
Thread.sleep(1000);
express.changeKm();//快递地点变化
}
}
第四章 并发工具类
CountDownLatch
闭锁,CountDownLatch这个类能够使一个线程等待其他线程完成各自的工作后再执行。例如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有的框架服务之后再执行。CountDownLatch是通过一个计数器来实现的,计数器的初始值为初始任务的数量。每当完成了一个任务后,计数器的值就会减1(CountDownLatch.countDown()方法)。当计数器值到达0时,它表示所有的已经完成了任务,然后在闭锁上等待CountDownLatch.await()方法的线程就可以恢复执行任务。实现最大的并行性:有时我们想同时启动多个线程,实现最大程度的并行性。例如,我们想测试一个单例类。如果我们创建一个初始计数为1的CountDownLatch,并让所有线程都在这个锁上等待,那么我们可以很轻松地完成测试。我们只需调用 一次countDown()方法就可以让所有的等待线程同时恢复执行。开始执行前等待n个线程完成各自任务:例如应用程序启动类要确保在处理用户请求前,所有N个外部系统已经启动和运行了,例如处理excel中多个表单。
本质
一个线程等待其他线程完成各自的工作之后,再执行(一般阻塞主线程)
方法
Ø countdown 计数器减1
Ø await 等待
注意
Ø 一个实例只能执行一次通知或者执行
Ø 确保countDown方法一定被执行
代码实例
主线程要等A和B两个子线程执行完了,再执行
public class CountDownLatchDemo {
public static void main(String[] args)throws Exception {
CountDownLatch latch = new CountDownLatch(2);
new Thread(new Runnable() {
@Override
public void run() {
latch.countDown();
System.out.println("执行子线程A");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
latch.countDown();
System.out.println("执行子线程B");
}
}).start();
latch.await();
System.out.println("执行主线程");
}
}
模拟服务启动检查,所有都启动成功才执行主业务,否则退出虚拟机
public class ServerStartMainThread {
public static void main(String[] args) {
ServerStart.start();
Runtime.getRuntime().addShutdownHook(new CleanResourceThread());
}
/**
* 清理服务资源
*/
static class CleanResourceThread extends Thread{
@Override
public void run() {
Debug.info("服务正在退出,清理相关资源...");
}
}
/**
* 定义服务接口
*/
interface Service {
void start();
boolean isStarted();
}
/**
* 服务的默认实现
*/
static abstract class AbstractService implements Service{
protected final CountDownLatch countDownLatch ;
protected boolean isStarted = false;
public AbstractService(CountDownLatch countDownLatch){
this.countDownLatch = countDownLatch;
}
@Override
public void start() {
new StartServiceThread().start();
}
@Override
public boolean isStarted() {
return isStarted;
}
/**
* 启动服务
*/
public abstract void doStart();
/**
* 实现服务启动的真正线程
*/
class StartServiceThread extends Thread{
@Override
public void run() {
final String serviceName =AbstractService.this.getClass()
.getSimpleName();
Debug.info("Starting %s", serviceName);
try {
doStart();
isStarted = true;
} catch (Exception e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
Debug.info("Done Starting %s", serviceName);
}
}
}
}
/**
* 启动DB数据初始化服务
*/
static class DbDataInitService extends AbstractService{
public DbDataInitService(CountDownLatch countDownLatch) {
super(countDownLatch);
}
@Override
public void doStart() {
Tools.randomPause(1000);
System.out.println("SampleServiceA 正在执行数据库初始化任务");
}
}
/**
* 启动日志恢复服务
*/
static class LogFileRecoverService extends AbstractService{
public LogFileRecoverService(CountDownLatch countDownLatch) {
super(countDownLatch);
}
@Override
public void doStart() {
Tools.randomPause(2000);
System.out.println("SampleServiceC 正在启动日志恢复服务");
}
}
/**
* 启动缓存初始化服务
*/
static class CacheInitService extends AbstractService{
public CacheInitService(CountDownLatch countDownLatch) {
super(countDownLatch);
}
@Override
public void doStart() {
Tools.randomPause(2000);
System.out.println("SampleServiceB 正在启动缓存预热服务");
//throw new RuntimeException("缓存为空");
}
}
/**
* 服务启动管理
*/
static class ServiceManager{
static volatile CountDownLatch countDownLatch;
static Set<Service>services;
/**
* 初始化要启动的服务
* @return
*/
static Set<Service> getServices(){
Set<Service>serviceSet = new HashSet<Service>();
countDownLatch = new CountDownLatch(3);
serviceSet.add(new DbDataInitService(countDownLatch));
serviceSet.add(new LogFileRecoverService(countDownLatch));
serviceSet.add(new CacheInitService(countDownLatch));
return serviceSet;
}
/**
* 启动所有的服务
*/
public static void startServices(){
services = getServices();
for (Service service: services){
service.start();
}
}
/**
* 检查服务启动的状态
*/
public static boolean checkServiceState(){
boolean isStarted = true;//默认是成功启动的
try {
countDownLatch.await(); //等待服务启动完毕
} catch (InterruptedException e) {
e.printStackTrace();
}
//遍历每个服务的启动状态 有一个没有启动就返回false
for (Service service : services){
if (!service.isStarted()){
isStarted = false;
break;
}
}
return isStarted;
}
}
/**
* 封装服务启动
*/
static class ServerStart{
public static void start(){
// 启动服务
ServiceManager.startServices();
// 检查服务启动状态
boolean isOK = ServiceManager.checkServiceState();
if (isOK){
System.out.println("All services were sucessfully started!");
}
else {
// 个别服务启动失败,退出JVM
System.err.println("Some service(s) failed to start,exiting JVM...");
System.exit(1);
}
}
}
}
CyclicBarrier
CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
CyclicBarrier还提供一个更高级的构造函数CyclicBarrier(int parties,Runnable barrierAction),用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景。
CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。
本质
一组线程到达一个屏障是被阻塞,知道最后一个线程也达到,屏障才会开门
(一般阻塞工作线程)
方法
Await:阻塞当前运行线程
注意
它可以循环使用
代码实例
累加求和,比如开启3个线程计算1-9之间的和
public class SumThread {
//存放子线程工作结果的容器
private static ConcurrentHashMap<String,Integer> resultMap = new ConcurrentHashMap<>();
// 启动三个子线程来分别计算 ,最后使用一个结果汇总线程
private static CyclicBarrier barrier = new CyclicBarrier(3,new CollectorThread());
public static void main(String[] args) throws Exception {
new AddThread(1,3).start();
new AddThread(3,6).start();
new AddThread(6,9).start();
}
//对每个线程结果进行汇总
static class CollectorThread extends Thread{
int sum =0;
@Override
public void run() {
for (Map.Entry<String, Integer> map : resultMap.entrySet()) {
sum += map.getValue();
}
System.out.println("thread compute summary result is "+sum);
}
}
//求和线程
static class AddThread extends Thread{
private int start;
private int end;
public AddThread(int start, int end) {
this.start = start;
this.end = end;
}
int sum;
@Override
public void run() {
System.out.println("start compute"+" ["+ start+" to "+end +"]");
for (int i=start;i<=end;i++){
sum+=i;
}
System.out.println("start compute "+"["+ start + "to"+end +"]"+"result is "+sum);
resultMap.put(this.getName(),sum);
try {
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
CyclicBarrier和CountDownLatch的区别
CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置,CountDownLatch.await一般阻塞主线程,所有的工作线程执行countDown,而CyclicBarrierton通过工作线程调用await从而阻塞工作线程,直到所有工作线程达到屏障。
Semaphore
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。应用场景Semaphore可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接。假如有一个需求,几十个线程并发地存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有10个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,就可以使用Semaphore来做流量控制。
本质
保证在多线程情况下,合理利用公共资源
方法
Ø acquire 获取许可证
Ø relase 释放许可证
Ø tryAcquire()方法尝试获取许可证
Ø availablePermits 当前可用的许可证数
Ø getQueueLength 正在等待获取许可证的线程数
Ø hasQueuedThreads是否有线程正在等待获取许可证
Ø reducePermits减少reduction个许可证
Ø getQueuedThreads返回所有等待获取许可证的线程集合
场景
流程控制,连接控制,共享控制
代码实例
模拟多线程获取数据库的连接
public class DBPoolSemaphore {
private final static int POOL_SIZE = 10;
private final Semaphore useful,useless;//两个指示器,分别表示池子还有可用连接和已用连接
//存放数据库连接的容器 使用链表
private static LinkedList<Connection> pool = new LinkedList<Connection>();
//初始化池
static {
for (int i = 0; i < POOL_SIZE; i++) {
pool.addLast(SqlConnectImpl.fetchConnection());
}
}
public DBPoolSemaphore() {
this.useful = new Semaphore(10);
this.useless = new Semaphore(0);
}
/*归还连接*/
public void returnConnect(Connection connection) throws InterruptedException {
if(connection!=null) {
System.out.println("当前有"+useful.getQueueLength()+"个线程等待数据库连接!!"
+"可用连接数:"+useful.availablePermits());
useless.acquire();
synchronized (pool) {
pool.addLast(connection);
}
useful.release();
}
}
/*从池子拿连接*/
public Connection takeConnect() throws InterruptedException {
useful.acquire();
Connection connection;
synchronized (pool) {
connection = pool.removeFirst();
}
useless.release();
return connection;
}
}
测试
public class AppTest {
private static DBPoolSemaphore dbPool = new DBPoolSemaphore();
private static class BusiThread extends Thread{
@Override
public void run() {
Random r = new Random();//让每个线程持有连接的时间不一样
long start = System.currentTimeMillis();
try {
Connection connect = dbPool.takeConnect();
System.out.println("Thread_"+Thread.currentThread().getId()
+"_获取数据库连接共耗时【"+(System.currentTimeMillis()-start)+"】ms.");
SleepTools.ms(100+r.nextInt(100));//模拟业务操作,线程持有连接查询数据
System.out.println("查询数据完成,归还连接!");
dbPool.returnConnect(connect);
} catch (InterruptedException e) {
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
Thread thread = new BusiThread();
thread.start();
}
}
}
ThreadLocal
存储共享变量
方法
Ø initValue 初始化方法 开发者可以实现
Ø set
Ø get
Exchanger
多线程之间的数据交互,这个使用不多
第五章 线程池
优点
1、 降低我们的资源消耗;
2、 提高我们的响应速度,T1:线程创建时间;T2:任务运行的时间;T3:销毁线程
3、 提高了线程的可管理性
原理
提交一个任务到线程池中,线程池的处理流程如下:
1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
详细阐述
- 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
- 如果此时线程池中的数量大于等于corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。
- 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理添加的任务。
- 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。也就是处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。
- 当线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。
UML类图
| ------------------------------------------------------------ |
| |
Ø Executor
一个接口,其定义了一个接收Runnable对象的方法executor,其方法签名为executor(Runnable command)
Ø ExecutorService
是一个比Executor使用更广泛的子类接口,其提供了生命周期管理的方法,以及可跟踪一个或多个异步任务执行状况返回Future的方法
Ø AbstractExecutorService
ExecutorService执行方法的默认实现
Ø ThreadPoolExecutor
线程池,可以通过调用Executors以下静态工厂方法来创建线程池并返回一个ExecutorService对象
Ø ScheduledThreadPoolExecutor
ScheduledExecutorService****的实现,一个可定时调度任务的线程池
Ø ScheduledExecutorService
一个可定时调度任务的接口
Ø Executors
封装各类线程池的实现,比如
FixedThreadPool
SingleThreadPool
CachedThreadPool
WorkStealingPool
…
内部实现
尽快开发当中我们经常使用的Executors为我们封装我的常用线程池,但是如果要更好的使用好线程池,需要从了解实现细节,就是
ThreadPoolExecutor
corePoolSize
核心线程数,
maximumPoolSize
允许的最大线程数,
keepAliveTime
空闲的线程存活的时间
TimeUnit
keepAliveTime时间单位
workQueue
阻塞队列
threadFactory
线程工厂,缺省的线程的命名规则: pool-数字+thead-数字
RejectedExecutionHandler 饱和策略
(1)AbortPolicy:;直接抛出异常,默认
(2)CallerRunsPolicy:;
(3)DiscardOldestPolicy:;丢弃最老的任务
(4)DiscardPolicy:直接丢弃的任务
拒绝策略代码演示
当用户请求的线程超过了最大线程数就会把没有处理的线程存放到队列中,
如果队列的容量也满了,这个时候就会调用对应的拒绝处理handler。
FixedThreadPool
使用固定线程数的线程池,适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的服务器
链表结构且是有界的
SingleThreadExecutor
创建使用单个线程的,适用于需要保证顺序地执行各个任务;并且在任意时间点,不会有多个线程是活动的应用场景。
CachedThreadPool
创建一个会根据需要创建新线程的大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器
不存储元素的队列
WorkStealingPool
利用所有运行的处理器数目来创建一个工作窃取的线程池,使用forkjoin实现
利用forkjoin机制实现了线程之间的’窃取’
ScheduledThreadPoolExecutor
Ø schedule 只调度一次
Ø scheduleAtFixedRate 固定时间间隔循环的执行
Ø scheduleWithFixedDelay 固定延时时间间隔的循环的执行
线程池扩展
如果我们想在一个线程池执行之前,执行之后,关闭的时候做一些事情,这个时候需要自定义线程池的扩展,主要是继承ThreadPoolExecutor,常用的有三个方法
Ø beforeExecute 线程执行之前调用,如果它抛出异常,那么after方法也不会被调用
Ø afterExecute 线程执行之后调用,如果抛出的不是error,该方法都会被调用
Ø terminated 线程池退出的时候调用
注意
1、 无论任务是从run中正常返回,还是抛出一个异常而返回,afterExecute都会被调用。如果任务在完成后带有一个Error,那么就不会盗用afterExecute。
2、 如果beforeExecute抛出一个RuntimeException,那么任务将不被执行,并且afterExecute也不会被调用。
3 在线程完成关闭操作时调用terminated,也就是在所有任务都已经完成并且所有工作者线程也已经关闭后。terminated可以用来释放Executor在其生命周期里分配的各种资源,此外还可以执行发送通知、记录日志或者手机finalize统计信息等操作。
代码实例
//定义线程池
public class CustomThreadPool extends ThreadPoolExecutor {
public CustomThreadPool() {
super(1, 1, 0L, TimeUnit.SECONDS, null);
}
private final ThreadLocal<Long> startTime = new ThreadLocal<Long>();
private final Logger log = Logger.getLogger("CustomThreadPool");
private final AtomicLong numTasks = new AtomicLong();
private final AtomicLong totalTime = new AtomicLong();
//线程执行之前调用的方法
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
log.fine(String.format("Thread %s: start %s", t, r));
startTime.set(System.nanoTime());
}
//线程执行之后调用的方法
@Override
protected void afterExecute(Runnable r, Throwable t) {
try {
long endTime = System.nanoTime();
long taskTime = endTime - startTime.get();
numTasks.incrementAndGet();
totalTime.addAndGet(taskTime);
log.fine(String.format("Thread %s: end %s, time=%dns",
t, r, taskTime));
} finally {
super.afterExecute(r, t);
}
}
//线程推出时候调用的方法
@Override
protected void terminated() {
try {
log.info(String.format("Terminated: avg time=%dns",
totalTime.get() / numTasks.get()));
} finally {
super.terminated();
}
}
}
线程最佳数量
CPU密集型:建议是n+1
尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,只能增加上下文切换的次数,因此会带来额外的开销
IO密集型:建议是2n
IO密集型任务 可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间
备注:其中n, 线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程
第六章 高级特性
CompletionService
异步批量提交任务接口,且任务是带返回结果的,就是线程实现的接口是callable,这个时候用它比较合适,因为它对future做了一个打包装,把多个future的结果进行了合并.它的创建依赖于一个已经创建好的线程池
创建
ExecutorService exec = Executors.newCachedThreadPool();
CompletionService<Integer> execcomp = new ExecutorCompletionService<Integer>(exec);
实例分析
比如开启10个线程,计算线程求和
public class CompeletionServiceDemo {
//获取到一个callable
private Callable<Integer> getCallableTask() {
final Random rand = new Random();
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int i = rand.nextInt(10);
int j = rand.nextInt(10);
int sum = i * j;
System.out.print(sum + "\t");
return sum;
}
};
return callable;
}
//用队列来进行求和
public int summaryQueue() throws Exception {
BlockingQueue<Future<Integer>> queue = new LinkedBlockingQueue();
int sum = 0, res = 0;
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
Future<Integer> future = executorService.submit(getCallableTask());
queue.add(future);
}
//开始遍历所有future
int qs = queue.size();
for (int i = 0; i < qs; i++) {
res = queue.take().get();
sum = sum + res;
}
executorService.shutdown();
return sum;
}
//使用CS来合并future计算
public int summaryCompletionService() throws Exception {
int sum = 0, res = 0;
ExecutorService executorService = Executors.newCachedThreadPool();
CompletionService completionService = new ExecutorCompletionService(executorService);
for (int i = 0; i < 5; i++) {
completionService.submit(getCallableTask());
}
//开始遍历所有future
for (int i = 0; i < 5; i++) {
Future<Integer> future = completionService.take();
res = future.get();
sum = sum + res;
}
executorService.shutdown();
return sum;
}
public static void main(String[] args) throws Exception {
CompeletionServiceDemo cs = new CompeletionServiceDemo();
int sum = cs.summaryCompletionService();
System.out.println("sum result:" + sum);
}
}
ForkJoin
实现多线程的并行计算,采取了分而治之的思想,同时实现了线程之间的’窃取’(一个线程把自己的事情做完了,又会主动去偷偷获取还在忙碌的线程的任务,做完之后再返回结果-做好事不留名)
本质
把一个大的任务进行拆分成小任务,一直拆到不能拆为主,然后再把每个小的任务的计算结果进行汇总
类型
Ø RecursiveTask 有返回结果
Ø RecursiveAction 无返回结果
方法
Ø invokeAll 执行所有方法
Ø join 结果集进行累加
执行
ForkJoinPool.invoke
代码实例
统计某个磁盘目录下面所有的目录个数和文件个数
public class SumDirsFiles extends RecursiveTask<Integer> {
private File path;
public SumDirsFiles(File path) {
this.path = path;
}
@Override
protected Integer compute() {
int count = 0;
int dirCount = 0;
List<SumDirsFiles> subTasks = new ArrayList<>();
File[] files = path.listFiles();
if (files!=null){
for (File file : files) {
if (file.isDirectory()) {
// 对每个子目录都新建一个子任务。
subTasks.add(new SumDirsFiles(file));
dirCount++;//统计目录个数
} else {
count++;// 遇到文件,文件个数加1。
}
}
System.out.println("目录:" + path.getAbsolutePath()
+"包含目录个数:"+dirCount+",文件个数:"+count);
if (!subTasks.isEmpty()) {
// 在当前的 ForkJoinPool 上调度所有的子任务。
for (SumDirsFiles subTask : invokeAll(subTasks)) {
count = count+subTask.join();
}
}
}
return count;
}
public static void main(String [] args){
try {
// 用一个 ForkJoinPool 实例调度“总任务”
ForkJoinPool pool = new ForkJoinPool();
//new一个ForkJoinTask的实例
SumDirsFiles task = new SumDirsFiles(new File("F:/"));
pool.invoke(task);//提交给ForkJoinPool执行Task
System.out.println("Task is Running......");
System.out.println("File counts ="+task.join());
System.out.println("Task end");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
CompletableFuture
基于JDK1.8之后才有的特性,它提供了异步编排功能,性能更加优异.
本质
基于事件的监听机制,不需要主动等待结果,而是一旦有结果,事件会主动通知的
优点
对于多个服务异步并发调用,再合并结果,不再阻塞主线程
多个服务并发调用,然后消费结果
对于多个服务,服务1执行完,再执行服务2,服务3,然后消费相关服务结果
方法
如果方法是以async结尾的,代表的是异步的调用,如果指定了线程池,会在线程中执行,如果没有的话会在默认ForkJoinPool.commonPool中执行.
变换功能
Ø thenApply
Ø thenApplyAsync
public static void thenApply() { //hello world
String result = CompletableFuture.supplyAsync(() -> "hello").thenApply(s -> s + " world").join();
System.out.println(result);
}
结果消耗
Ø thenAccept
Ø thenAcceptAsync
public static void thenAccept(){ //hello world
CompletableFuture.supplyAsync(() -> "hello").thenAccept(s -> System.out.println(s+" world"));
}
执行下一个操作
Ø thenRun
Ø thenRunAsync
public static void thenRun(){//hello world
CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hello";
}).thenRun(() -> System.out.println("hello world"));
while (true){}
}
合并操作
Ø thenCompoise
Ø thenCompoiseAsync
public static void thenCombine() { //hello world
String result = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hello";
}).thenCombine(CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "world";
}), (s1, s2) -> s1 + " " + s2).join();
System.out.println(result);
}
计算谁快用谁
Ø applyToEither
Ø applyToEitherAsync
public static void applyToEither() {//返回hello world
String result = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "s1";
}).applyToEither(CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hello world";
}), s -> s).join();
System.out.println(result);
}
返回一个completablefuture对象
Ø supplyAsync
Ø supply
结算结果完成是处理
Ø whenComplete
Ø whenCompleteAsync
public static void whenComplete() {
String result = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (1 == 1) {
throw new RuntimeException("测试一下异常情况");
}
return "s1";
}).whenComplete((s, t) -> {
System.out.println(s);
System.out.println(t.getMessage());
}).exceptionally(e -> {
System.out.println(e.getMessage());
return "hello world";
}).join();
System.out.println(result);
}
执行完所有方法
Ø allof
执行所有方法,才返回
private void method() throws Exception {
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "f1";
});
f1.whenCompleteAsync(new BiConsumer<String, Throwable>() {
@Override
public void accept(String s, Throwable throwable) {
System.out.println(System.currentTimeMillis() + ":" + s);
}
});
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "f2";
});
f2.whenCompleteAsync(new BiConsumer<String, Throwable>() {
@Override
public void accept(String s, Throwable throwable) {
System.out.println(System.currentTimeMillis() + ":" + s);
}
});
CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2);
//阻塞,直到所有任务结束。
System.out.println(System.currentTimeMillis() + ":阻塞");
all.join();
System.out.println(System.currentTimeMillis() + ":阻塞结束");
//一个需要耗时2秒,一个需要耗时3秒,只有当最长的耗时3秒的完成后,才会结束。
System.out.println("任务均已完成。");
}
Ø anyof
执行方法中的任何一个即可返回
static void anyOfExample() {
StringBuilder result = new StringBuilder();
List messages = Arrays.asList("a", "b", "c");
List<CompletableFuture> futures = messages.stream()
.map(msg -> CompletableFuture.completedFuture(msg).thenApply(s -> delayedUpperCase(s)))
.collect(Collectors.toList());
CompletableFuture.anyOf(futures.toArray(new CompletableFuture[futures.size()])).whenComplete((res, th) -> {
if(th == null) {
result.append(res);
}
});
}
AsyncLoad
随着业务越来越复杂,对应的代码也越来越复杂,耗时也越来越多,因此急需一套并行框架,通过搜索发现阿里提供了一个并行框架asyncLoad(https://github.com/alibaba/asyncload.git),但是此框架不支持注解的方式,使的对应的代码侵入性很大,所以对asyncLoad框架进行扩展,使其能够支持对应的注解配置的方式来进行异步并行。
https://blog.youkuaiyun.com/yujiak/article/details/80044428 基于扩展为注解方式
第七章 并发容器
ConcurrentHashMap
数据结构
Sengment
HashEntry
锁分段
常用方法
Ø put
Ø get
Ø putIfAbsent
ConcurrentSkipListMap
数据结构
跳跃链表
使用场景
一般开发者用到的比较少,只要在多线程排序的情况下可以使用,单线程针对key排序的可以使用treemap
常用方法
Ø subMap返回此映射的部分视图,其键的范围从 fromKey 到 toKey
Ø tailMap返回此映射的部分视图,其键大于等于 fromKey。
Ø putIfAbsent 不存在在存储
ConcurrentSkipListSet
TreeSet 单线程,而它是多线程,底层是基于ConcurrentSkipListMap
CopyOnWriteArrayList/Set
Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到
什么是CopyOnWrite容器
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
典型的用空间换时间,且如果是并发读,有可能或读取到旧的数据
使用场景,适合读多写少,比如白名单,黑名单,商品类目访问
第八章 阻塞队列
BlockingQueue
提供了线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空
方法
插入
方法名 | 说明 |
---|---|
add | 抛出异常 |
offer | 返回特殊值 |
put | 方法阻塞 |
移除
方法名 | 说明 |
---|---|
remove | 抛出异常 |
take | 方法阻塞 |
poll | 返回特殊值 |
四组不同的行为方式解释:
抛异常:如果试图的操作无法立即执行,抛一个异常。
特定值:如果试图的操作无法立即执行,返回一个特定的值(常常是 true / false)。
阻塞:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
超时:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功(典型的是true / false。
子类
ArrayBlockingQueue
数组结构,有界的,先进先出
LinkedBlockingQueue
链表结构,有界的,先进先出
PriorityBlockingQueue
支持优先级排序的,无界队列
DelayQueue
支持延时获取元素,比如 缓存订单,延时支付,订单到期
SynchronousQueue
不存储元素的队列,只有1个容量的大小
LinkedTransferQueue
链表结构,无界的
LinkedBlockingDeque
链表结构,双向队列
ConcurrentLinkedQueue
链表结构,无界的,线程安全队列
代码实例
队列最典型的应用就是生产者和消费者的机制….
public class ThreadQueue03 {
public static void main(String[] args)throws Exception {
// 声明一个容量为10的缓存队列
BlockingQueue<String> queue = new LinkedBlockingQueue<String>(10);
ProducerRunnable producer1 = new ProducerRunnable(queue);
ProducerRunnable producer2 = new ProducerRunnable(queue);
ProducerRunnable producer3 = new ProducerRunnable(queue);
ConsumerRunnable consumer = new ConsumerRunnable(queue);
// 借助Executors
ExecutorService service = Executors.newCachedThreadPool();
// 启动线程
service.execute(producer1);
service.execute(producer2);
service.execute(producer3);
service.execute(consumer);
// 执行10s
Thread.sleep(10 * 1000);
producer1.stop();
producer2.stop();
producer3.stop();
Thread.sleep(2000);
// 退出Executor
service.shutdown();
}
}
/**
* 生产线程
*/
class ProducerRunnable implements Runnable{
private static final int DEFAULT_RANGE_FOR_SLEEP = 1000;
private BlockingQueue blockingQueue;
private volatile boolean isRunning = true;
private static AtomicInteger count = new AtomicInteger();
public ProducerRunnable(BlockingQueue blockingQueue){
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
String data = null;
Random r = new Random();
System.out.println("启动生产者线程");
try {
while (isRunning){
System.out.println("正在生产数据");
Thread.sleep(r.nextInt(DEFAULT_RANGE_FOR_SLEEP));
data = "data:" + count.incrementAndGet();
System.out.println("将数据:" + data + "放入队列...");
if (!blockingQueue.offer(data, 2, TimeUnit.SECONDS)) {
System.out.println("放入数据失败:" + data);
}
}
}
catch (Exception e){
Thread.currentThread().interrupt();
}
finally {
System.out.println("退出生产者线程!");
}
}
public void stop() {
isRunning = false;
}
}
/**
* 消费线程
*/
class ConsumerRunnable implements Runnable{
private static final int DEFAULT_RANGE_FOR_SLEEP = 1000;
private BlockingQueue blockingQueue;
volatile boolean isRunning = true;
public ConsumerRunnable(BlockingQueue blockingQueue){
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
System.out.println("启动消费者线程!");
Random r = new Random();
try {
while (isRunning) {
System.out.println("正从队列获取数据...");
String data = blockingQueue.poll(2, TimeUnit.SECONDS).toString();
if (null != data) {
System.out.println("拿到数据:" + data);
System.out.println("正在消费数据:" + data);
Thread.sleep(r.nextInt(DEFAULT_RANGE_FOR_SLEEP));
} else {
// 超过2s还没数据,认为所有生产线程都已经退出,自动退出消费线程。
isRunning = false;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
} finally {
System.out.println("退出消费者线程!");
}
}
}
第九章 多线程性能调优
锁优化
Ø 减少锁的开销
Ø 使用带参数的锁
Ø 减小锁的粒度
Ø 减小临界区的长度
Ø 无锁化
上下文切换
Ø 控制线程数量
Ø 不要在临界区执行阻塞IO操作
Ø 不要在临界区执行比较耗时的操作
Ø 减少GC
写多读少且是缓存
在【超高并发】,【写多读少】,【定长value】的【业务缓存】场景下:
1) 可以通过水平拆分来降低锁冲突 : 锁分段
2) /**
* 给map进行分段加锁
* @param <K>
* @param <V>
*/
public class SegmentLockMap<K,V> {
private final int LOCK_COUNT = 16;
private final Map<K,V> map;
private final Object[] locks ;
public SegmentLockMap() {
this.map = new HashMap<K,V>();
locks = new Object[LOCK_COUNT];
for (int i=0;i<LOCK_COUNT;i++){
locks[i] = new Object();
}
}
private int keyHashCode(K k){
return Math.abs(k.hashCode() % LOCK_COUNT);
}
public V get(K k){
int keyHashCode = keyHashCode(k);
//锁定部分资源
synchronized (locks[keyHashCode % LOCK_COUNT]){
return map.get(k);
}
}
}
3)
2)可以通过Map转Array的方式来最小化锁冲突,一条记录一个锁 : 行级索
MAP变Array+最细锁粒度优化
3)可以把锁去掉,最大化并发,但带来的数据完整性的破坏
4)可以通过签名的方式保证数据的完整性,实现无锁缓存 :无锁
多线程基础源码 AQS
数据结构
双向链表,它有区别于双端链表(如果从尾部删除的话,需要从头到尾遍历一遍,耗时间也耗空间),主要优势就是可以解决从尾部删除元素.