线程
概念
什么是叫一个进程? 什么叫一个线程?
Program app ->QQ.exe
进程:做一个简单的解释,你的硬盘上有一个简单的程序,这个程序叫QQ.exe,这是一个程序,这个程序是一个静态的概念,它被扔在硬盘上也没人理他,但是当你双击它,弹出一个界面输入账号密码登录进去了,OK,这个时候叫做一个进程。进程相对于程序来说它是一个动态的概念
线程:作为一个进程里面最小的执行单元它就叫一个线程,用简单的话讲一个程序里不同的执行路径就叫做一个线程
启动线程的五种方式
1: 继承Thread类 2: 实现Runnable 3: 线程池Executors.newCachedThrad
1 | package com.oi.juc.c_000; |
生命周期
wait(), join(), LockSupport() 进入waiting状态; notify(), notifyAll(), LockSupport
yield() Running –> Ready
等待过得同步代码块的锁, 进入Blocked状态, 获得后, 进入Runnale
常用方法
1 | package com.oi.juc.c_000; |
Synchronized
这道题加Synchronized就没必要加volatile, synchronized既保证同步, 有保证线程可见
1 | /** |
多个线程去访问同一个资源的时候对这个资源上锁。
访问某一段代码或者某临界资源的时候是需要有一把锁的概念在这儿的。
比如:我们对一个数字做递增,两个程序对它一块儿来做递增,递增就是把一个程序往上加1啊,如果两个线程共同访问的时候,第一个线程一读它是0,然后把它加1,在自己线程内部内存里面算还没有写回去的时候而第二个线程读到了它还是0,加1在写回去,本来加了两次,但还是1,那么我们在对这个数字递增的过程当中就上把锁,就是说第一个线程对这个数字访问的时候是独占的,不允许别的线程来访问,不允许别的线程来对它计算,我必须加完1收释放锁,其他线程才能对它继续加。
实质上,这把锁并不是对数字进行锁定的, 你可以任意指定,想锁谁就锁谁。
- 上了把锁之后才能对count进行减减访问,你可以new一个Object,所以这里锁定就是o,当我拿到这把锁的时候才能执行这段代码。是锁定的某一个对象,synchronized有一个锁升级的概念
1 | /** |
- synchronized它的一些特性。如果说你每次都定义个一个锁的对象Object o 把它new出来那加锁的时候太麻烦每次都要new一个新的对象出来,所以呢,有一个简单的方式就是synchronized(this)锁定当前对象就行
1 | /** |
- 如果你要是锁定当前对象呢,你也可以写成如下方法。synchronized方法和synchronized(this)执行这段代码它是等值的
1 | package com.oi.juc.c_003; |
- 静态方法static是没有this对象的,你不需要new出一个对象来就能执行这个方法,但如果这个这个上面加一个synchronized的话就代表synchronized(T.class)。这里这个synchronized(T.class)锁的就是T类的对象
1 | package com.oi.juc.c_004; |
问题:T.class是单例的吗?
一个类 load到内存它是不是单例的,想想看。一般情况下是,如果是在同一个ClassLoader空间那它一定是。不是同一个类加载器就不是了,不同的类加载器互相之间也不能访问。所以说你能访问它,那他一定就是单例
- 下面程序:很有可能读不到别的线程修改过的内容,除了这点之外count减减完了之后下面的count输出和你减完的结果不对,很容易分析:如果有一个线程把它从10减到9了,然后又有一个线程在前面一个线程还没有输出呢进来了把9又减到了8,继续输出的8,而不是9。如果你想修正它,前面第一个是在上面加volatile,改了马上就能得到。
1 | /** |
- 另外这个之外还可以加synchronized,加了synchronized就没有必要在加volatile了,因为
synchronized既保证了原子性,又保证了可见性。
1 | //对比上一个小程序 |
- 如下代码:同步方法和非同步方法是否可以同时调用?就是我有一个synchronized的m1方法,我调用m1的时候能不能调用m2,拿大腿想一想这个是肯定可以的,线程里面访问m1的时候需要加锁,可是访问m2的时候我又不需要加锁,所以允许执行m2。
这些小实验的设计是比较考验功力的,学习线程的时候自己要多动手进行试验,任何一个理论,都可以进行验证。
1 | /** |
- 我们在来看一个synchronized应用的例子
我们定义了一个class账户,有名称、余额。写方法给哪个用户设置它多少余额,读方法通过这个名字得到余额值。如果我们给写方法加锁,给读方法不加锁,你的业务允许产生这种问题吗?业务说我中间读到了一些不太好的数据也没关系,如果不允许客户读到中间不好的数据那这个就有问题。正因为我们加了锁的方法和不加锁的方法可以同时运行。
问题比如说:张三,给他设置100块钱启动了,睡了1毫秒之后呢去读它的值,然后再睡2秒再去读它的值这个时候你会看到读到的值有问题,原因是在设定的过程中this.name你中间睡了一下,这个过程当中我模拟了一个线程来读,这个时候调用的是getBalance方法,而调用这个方法的时候是不用加锁的,所以说我不需要等你整个过程执行完就可以读到你中间结果产生的内存,这个现象就叫做脏读。这问题的产生就是synchronized方法和非synchronized方法是同时运行的。解决就是把getBalance加上synchronized就可以了,如果你的业务允许脏读,就可以不用加锁,加锁之后的效率低下。
1 | /** |
Synchronized 是可重入锁
锁定的是this
再来看synchronized的另外一个属性:可重入,是synchronized必须了解的一个概念。
如果是一个同步方法调用另外一个同步方法,有一个方法加了锁,另外一个方法也需要加锁,加的是同一把锁也是同一个线程,那这个时候申请仍然会得到该对象的锁。比如说是synchronized可重入的,有一个方法m1 是synchronized有一个方法m2也是synchrionzed,m1里能不能调m2。我们m1开始的时候这个线程得到了这把锁,然后在m1里面调用m2,如果说这个时候不允许任何线程再来拿这把锁的时候就死锁了。这个时候调m2它发现是同一个线程,因为你m2也需要申请这把锁,它发现是同一个线程申请的这把锁,允许,可以没问题,这就叫可重入锁。
1 | /** |
模拟一个父类子类的概念,父类synchronized,子类调用super.m的时候必须得可重入,否则就会出问题(调用父类是同一把锁)。所谓的重入锁就是你拿到这把锁之后不停加锁加锁,加好几道,但锁定的还是同一个对象,去一道就减个1,就是这么个概念。
1 | package com.oi.juc.c_010; |
1 | /** |
synchronized的底层实现
1 | synchronized的底层实现 |
Volatile
保证线程可见性
1 | /** |
禁止指令重排序
synchronized+双重检查的单例模式要加volatile,防止指令重排序
why?
new对象过程(四条指令):
1)给instance实例分配内存;
2)初始化instance的构造器;
3)将instance对象指向分配的内存空间(注意到这步时instance就非null了)
如果指令按照顺序执行倒也无妨,但JVM为了优化指令,提高程序运行效率,允许指令重排序。如此,在程序真正运行时以上指令执行顺序可能是这样的:
a)给instance实例分配内存;
b)将instance对象指向分配的内存空间;
c)初始化instance的构造器;
这时候,当线程一执行b)完毕,在执行c)之前,被切换到线程二上,这时候instance判断为非空,此时线程二直接来到return instance语句,拿走instance然后使用,接着就顺理成章地报错(对象尚未初始化)。
volatile保证a初始化之后再赋值给变量
1 | public class Mgr06 { |
1 | ·volatile 引用类型(包括数组)只能保证引用本身的可见性,不能保证内部字段的可见性 |
锁优化
1 | /** |
Synchronized注意事项
1 | /** |
1 | /** |
锁
CAS / Atomic类
1 | /** |
CAS是CPU原语支持,判断之后不会被打断
ABA问题:基本类型不影响,引用类型会产生ABA
AtomicStampReference 类, 加时间戳解决ABA问题
CAS调用的是Unsafe
JDK11 CompareAndSet 1.8 CompareAndSwap
CAS是乐观锁
ABA问题怎么解决
- AtomicStampedReference:带版本戳的原子引用类型,版本戳为int类型。
- AtomicMarkableReference:带版本戳的原子引用类型,版本戳为boolean类型。(只能降低概率, 不能避免)
乐观锁( Optimistic Locking )
乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。
相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。
悲观锁(Pessimistic Lock)
当我们要对一个数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)
悲观锁主要是共享锁或排他锁
- 共享锁又称为读锁,简称S锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
- 排他锁又称为写锁,简称X锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据行读取和修改。
悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。
三种原子操作对比
1000线程结果:
LongAdder: 分段锁: 也是CAS操作
1000个线程, 分四段锁(250), 最后求和
线程数量少,LongAdder不一定有优势
线程数量少, synchronized反而有优势
ReentrantLock : 可重入锁
Synchronized加在同一类的不同方法, 相当于synchrnized this, 是同一把锁
1 | reentrantlock(可重入锁)用于替代synchronized |
lockIntenrrupt打断
1 | /* 使用ReentrantLock还可以调用lockInterruptibly方法,可以对线程interrupt方法做出响应, |
ReentrantLock默认是非公平, 可设置为公平锁
1 | /*ReentrantLock还可以指定为公平锁*/ |
ReentrantLock Vs Synchronized
CountDownLatch : 倒数发车
- 类似 Join : 门闩作用
- CountDownLatch.countDown(); 是原子操作
- latch.await(); 阻塞住 countDown到0的时候, 继续执行
- latch.countDown(); latch数减一
- 对比Join: CountDownLatch更灵活
1 | public class T06_TestCountDownLatch { |
CyclicBarrier : 人满发车
- 自己指定发车的线程数量
1 | public static void main(String[] args) { |
谷歌开源的组件
配合Zuul网关
Phaser: 相位器
婚礼环节举例, 遗传算法, 一个一个的栅栏
1 | static class MarriagePhaser extends Phaser { |
ReadWriteLock 读写锁 [ 共享锁 (读锁) + 排它锁 (写锁) ]
- 读读共享
- 读写共享
- 写写排他
https://www.jianshu.com/p/9cd5212c8841
- Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性
- ReentrantReadWriteLock读写锁的效率明显高于synchronized关键字
- ReentrantReadWriteLock读写锁的实现中,读锁使用共享模式;写锁使用独占模式,换句话说,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的
- ReetrantReadWriteLock读写锁的实现中,需要注意的,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁
1 | public class T10_TestReadWriteLock { |
Semaphore: 限流, 默认非公平, 可改fair参数
AbstractQuenedSynchronizer抽象的队列式同步器
1 | public static void main(String[] args) { |
Exchanger: 执行到交换方法后阻塞, 然后线程交换数据
两个线程之间, 一个线程命令另一个线程阻塞, 交换数据, 继续执行
1 | import java.util.concurrent.Exchanger; |
三道线程通信面试题
到五停止
实现一个容器,提供两个方法,add,size
写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束
wait / notify
notify()不释放锁
wait()释放锁
1 | /* |
CountDownLatch
1 | /* 使用Latch(门闩)替代wait notify来进行通知 |
LockSupport
1 | public class T07_LockSupport_WithoutSleep { |
Semaphore + join
1 | public class T08_Semaphore { |
生产消费
面试题:写一个固定容量同步容器,拥有put和get方法,以及getCount方法,
能够支持2个生产者线程以及10个消费者线程的阻塞调用
wait / notify
1 | public class MyContainer1<T> { |
ReentrantLock + Condition (背过)
精确叫醒生产 / 消费线程
1 | public class MyContainer2<T> { |
交替打印
常用方法1: LockSupport的park/unpark
1 | //Locksupport park 当前线程阻塞(停止) |
常用方法2: synchronized的wait/notify
1 | public class T07_00_sync_wait_notify { |
方法3: ReentrantLock 的 condition 的 signal/await
1 | public class T09_00_lock_condition { |
AQS (所有锁的心)
ReentrantLock源码 lock( )
通过模板方法, (回调函数, 钩子函数)
调用父类方法, 子类去实现
ReentrantLock.lock( ) -> Sync.acquire( ) -> AQS.tryAcquire( ) -> ReentrantLock.nonfairTryAcquire( ) -> AQS.getState( )
node里装的是thread线程
state 是 volatile 修饰的, 设置state的方法
AQS添加节点: 不用给整个链表加锁, 只观测tail节点, 通过CAS, 提高效率
头节点先获得, 第二个一直尝试
AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:
1 | isHeldExclusively() //该线程是否正在独占资源。只有用到condition才需要去实现它。 |
AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。
以 ReentrantLock 为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease
、tryAcquireShared-tryReleaseShared
中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如 ReentrantReadWriteLock。
AQS的数据结构是双向链表 原理CAS + volatile
- 核心是int类型的state
- state 被 volatile 修饰
- AQS 拥有 boolean compareAndSetState(int expect, int update) 方法, 往队尾加节点
1 | ** 源码 ** |
AQS面试题
ThreadLocal
作用
ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
设到当前线程的map
1 | /** |
强软弱虚四种引用
强引用: new 出来的, 不回收
当内存空间不足时,Java
虚拟机宁愿抛出OutOfMemoryError
错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
1 | M m = new M(); |
软引用: 满则回收
- 软引用是用来描述一些还有用但并非必须的对象。
- 对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行第二次回收。
- 如果这次回收还没有足够的内存,才会抛出内存溢出异常。
- 如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
1 | /** |
弱引用:一般用在容器里ThreadLocal<>(), WeakHashMap
作用: 有强引用指向它的时候, 一旦强引用消失, 就不用再管他了
JVM
首先将软引用中的对象引用置为null
,然后通知垃圾回收器进行回收:
WeakReference
对象的生命周期基本由垃圾回收器决定,一旦垃圾回收线程发现了弱引用对象,在下一次GC
过程中就会对其进行回收。
1 | /** |
tl 指向ThreadLocal 是强引用
ThreadLocalMap 的 key 是弱引用
使用ThreadLocal , 不用的对象, 务必remove() 掉, 因为key如果为空了, value永远访问不到, 导致内存泄漏
ThreadLocal的set() 方法 调用了getMap() , key: threadlocal对象, value:
key一个弱引用指向threadlocal
虚引用
垃圾回收时候直接干掉
并发容器
同步容器类
总结:
1:对于map/set的选择使用
HashMap
TreeMap
LinkedHashMap
Hashtable
Collections.sychronizedXXX
ConcurrentHashMap
ConcurrentSkipListMap
2:队列
ArrayList
LinkedList
Collections.synchronizedXXX
CopyOnWriteList
Queue
CocurrentLinkedQueue //concurrentArrayQueue
BlockingQueue
LinkedBQ
ArrayBQ
TransferQueue
SynchronusQueue
DelayQueue执行定时任务
1:Vector Hashtable :早期使用synchronized实现
2:ArrayList HashSet :未考虑多线程安全(未实现同步)
3:HashSet vs Hashtable StringBuilder vs StringBuffer
4:Collections.synchronized***工厂方法使用的也是synchronized
使用早期的同步容器以及Collections.synchronized***方法的不足之处,请阅读:
http://blog.csdn.net/itm_hadf/article/details/7506529
使用新的并发容器
http://xuganggogo.iteye.com/blog/321630
Map的发展历程
hashTable –> hashMap –> synchronizedHashMap –> concurrentHashMap
hashTable 整体加锁, 所有方法加synchronized
hashmap 没有锁, 并发场景数据不一致
synchronizedHashMap 在 hashMap 的基础上使用了细粒度锁
concurrentHashMap 使用了CAS, 插入效率一般, 读取效率特别高
从Vector -> List -> Queue ( 解决超卖问题 )
Vector: 古老的同步容器, 方法都是synchronized, 但是两个同步方法之间的业务逻辑不是原子性的, 还是会发生超卖问题, 因此需要再外层再加synchronized
ConcurrentLinkedQueue: CAS实现, 适合代码执行时间短
1 | /** |
1 | /* 使用ConcurrentLinkedQueue提高并发性 */ |
Synchronized Vs CAS
要看并发量高低, 并发量不高, 代码执行时间长, 用Synchronized
跳表
ConcurrentSkipListMap
跳表比链表查找快, 比treeMap的cas操作容易
写时复制
CopyOnWriteArrayList
CopyOnWriteArraySet
往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
CopyOnWriteArraySet内部维护着一个CopyOnWriteArrayList
写时排他锁, 读时共享锁, 适合读特别多, 写特别少
LinkedBlockingQueue: 天生的生产者/消费者模型
1 | static BlockingQueue<String> strs = new LinkedBlockingQueue<>(); |
ArrayBlockingQueue
1 | static BlockingQueue<String> strs = new ArrayBlockingQueue<>(10); |
Queue和List的区别
Queue多了很多线程友好的方法, 或者阻塞, 或者时间等待
offer()
peek()
poll()
put()
take()
DelayQueue:
按时间进行任务调度, 等待多长时间后执行
本质是PriorityQueue, 需要指定一种排序方式, 例如等待时间短的先执行
1 | public class T07_DelayQueue { |
PriorityQueue
内部是一个红黑树
可自定义compareTo
SynchronousQueue 用处最大, 任务调度
容量为0, 只能用put()阻塞 , 用来手递手给另一个线程数据
类似Exchanger
1 | public static void main(String[] args) throws InterruptedException { |
TransferQueue & LinkedTransferQueue
transfer(), 装完等着, 等其他线程取走
用于订单提交后, 确认有人处理订单, 再返回
收钱, 确认收到, 返回
1 | public static void main(String[] args) throws InterruptedException { |
线程任务
Excutor
ExcutorService 方法
主要是submint和shutdown和shutdownnow
Callable
类似Runnable, 相当于Runnable + return 一个
1 | public class T03_Callable { |
Future
存储执行的任务将来才会产生的结果
ExcutorService.submit(task, result) 返回值是Future
FutureTask
更灵活, 既是一个Runnable又可以存结果
Future + Runnable
FutureTask对比Callable
Callable执行完, 需要另外的Future进行存储
FutureTask执行完, 结果保存在自己这
1 | public class T06_00_Future { |
CompletableFutrue
方便各种任务的管理, 同时管理多个Future
1 | CompletableFuture<Double> futureTM = CompletableFuture.supplyAsync(()->priceOfTM()); |
线程池
概念
线程池 : 线程集合hashset + 任务集合
工作原理
分类
生命周期
Running状态的线程, 调用ShutDown() 进入ShutDown状态; 调用ShutDownNow(), 进入Stop状态
七个参数
拒绝策略: 1.任务队列满 2.线程池满
JDK默认提供四种
阿里规约
submit与execute区别
(1)可以接受的任务类型
submit:
execute:
可以看出:
execute只能接受Runnable类型的任务
submit不管是Runnable还是Callable类型的任务都可以接受,但是Runnable返回值均为void,所以使用Future的get()获得的还是null
(2)返回值
由Callable和Runnable的区别可知:
execute没有返回值
submit有返回值,所以需要返回值的时候必须使用submit
(3)异常
1.execute中抛出异常
execute中的是Runnable接口的实现,所以只能使用try、catch来捕获CheckedException,通过实现UncaughtExceptionHande接口处理UncheckedException
即和普通线程的处理方式完全一致
2.submit中抛出异常
不管提交的是Runnable还是Callable类型的任务,如果不对返回值Future调用get()方法,都会吃掉异常
Executors
线程池的工厂, 工具类
JDK自带线程池
SingleThreadPool
- 意义在于: 任务队列不用自己维护, 线程的生命周期不自己管理
- 核心和最大都是1
- 任务队列是阻塞队列
- 阻塞队列最大Integer.MAX
- 使用单个工作线程来执行一个无边界的队列。(注意,如果单个线程在执行过程中因为某些错误中止,新的线程会替代它执行后续线程)。它可以保证认为是按顺序执行的,任何时候都不会有多于一个的任务处于活动状态。和 newFixedThreadPool(1) 的区别在于,如果线程遇到错误中止,它是无法使用替代线程的。
1 | // 顺序执行 |
CachedThreadPool
- 核心线程数0, 最大线程数Integer.MAX_VALUE (线程数几乎达不到,区别于阻塞队列的MAX)
- 空闲线程存活时间60S
- 任务队列是同步队列
- 特点: 来了就进入任务队列, 任务队列SynchronousQueue是手递手的, 容量为0
来一个任务, 必须马上执行, 没有线程执行, 马上new一个 - 优势: 大量短生命周期的异步任务时, 不会堆积, 不会启动特别多的线程时使用
- 调用 execute 时,重用空闲线程,如果不存在空闲线程,那么会重新创建一个新的线程。如果线程超过 60 秒还未被使用,就会被中止并从缓存中移除。因此,线程池在长时间空闲后不会消耗任何资源。
1 | public static void main(String[] args) throws InterruptedException { |
FixedThreadPool
- 固定线程数量
- 所有线程都是核心线程, 因此没有回收, 空闲回收时间是0
- 任务队列是阻塞队列, 最大Integer.MAX_VALUE
- 复用 固定数量的线程 处理一个 共享的无边界队列 。任何时间点,最多有 nThreads 个线程会处于活动状态执行任务。如果当所有线程都是活动时,有多的任务被提交过来,那么它会一致在队列中等待直到有线程可用。如果任何线程在执行过程中因为错误而中止,新的线程会替代它的位置来执行后续的任务。所有线程都会一致存于线程池中,直到执行 ExecutorService.shutdown() 关闭。
1 | // 4核CPU, 4个线程, 并行执行任务, 计算0-20000的质数 |
并发 & 并行
CachedThreadPool和FixedThreadPool选择
任务量忽大忽小, 用Cached
任务量比较平稳, 用Fixed
ScheduledThreadPool
定时任务线程池
1 | public static void main(String[] args) { |
WorkStealingPool
每个线程维护一个自己的任务队列
空闲的线程从其他线程的队列中偷一个任务执行
new了一个ForkJoinPool
1 | public static void main(String[] args) throws IOException { |
ForkJoinPool
- 父任务分成小任务, 最后结果汇总
1 | public class T12_ForkJoinPool { |
ParallelStreamAPI 并行流式API
并行流式API, 效率高于foreach, 底层是ForkJoinPool, 快四倍
1 | public static void main(String[] args) { |
自定义拒绝策略
MyHandler implements RejectedExecutionHandler
1 | public static void main(String[] args) { |