1、什么是JUC
JUC即Java.util.concurrent包,这是一个处理线程的工具包,JDK 1.5开始出现的。
2、进程与线程
一个进程可以包含多个线程,至少包含一个
java默认有两个线程(main、 gc)
对于Java而言,创建线程的方式:进程Thread、实现Runnable接口、实现Callback接口
java并不能直接开启线程,它使调了底层c++的方法。
并发:多个线程操作一个资源,一个CUP
并行:多个线程同时执行,多个CPU
3、线程的状态
由源码
NEW :新建
RUNNABLE, 就绪
BLOCKED, 阻塞
WAITING, 等待
TIMED_WAITING, 超时等待
TERMINATED; 终止
4、wait与sleep
wait来自Object
sleep 来自Thread
wait会释放锁,sleep不会释放锁
5、Lock锁
线程就是一个单独的资源,没有其他附属操作。
传统的synchronized
package cn.butcher;
public class Test02 {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
for (int i = 0; i < 50; i++) {
ticket.sale();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 50; i++) {
ticket.sale();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 50; i++) {
ticket.sale();
}
}).start();
}
}
class Ticket{
int num = 50;
public synchronized void sale(){
if (num>0){
System.out.println(Thread.currentThread().getName()+"卖出了:1张票,剩余:"+(num--));
}
}
}
使用Lock锁
package cn.butcher;
import java.util.concurrent.locks.ReentrantLock;
public class Test03 {
public static void main(String[] args) {
Ticket1 ticket = new Ticket1();
new Thread(() -> {
for (int i = 0; i < 50; i++) {
ticket.sale();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 50; i++) {
ticket.sale();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 50; i++) {
ticket.sale();
}
}).start();
}
}
class Ticket1{
ReentrantLock lock = new ReentrantLock(true);
int num = 50;
public void sale(){
lock.lock();
try {
if (num>0){
System.out.println(Thread.currentThread().getName()+"卖出了:1张票,剩余:"+(num--));
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
区别:
- synchronized是关键字lock是java类
- synchronized不可以获取锁的状态,lock可以
- synchronized会自动释放锁,lock必须手动释放锁,不然会死锁
- synchronized 锁定的资源,线程会阻塞,直到上一个线程使用完,lock不一定
- synchronized不可以中断,是公平的,lock可以公平可以非公平(根据构造方法决定)
- 适合锁少量同步代码,lock锁大量同步
6、生产者与消费者
线程之间的通信。等待,通知
6.1 synchronized 版
package cn.butcher;
public class ProductAndSale {
public static void main(String[] args) {
Data data = new Data();
new Thread(() ->{
try {
for (int i = 0; i < 100; i++) {
data.add();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程1").start();
new Thread(() ->{
try {
for (int i = 0; i < 100; i++) {
data.reduce();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程2").start();
}
}
class Data{
int num;
synchronized void add() throws InterruptedException {
if (num<=0){
num++;
System.out.println(Thread.currentThread().getName()+"生产了"+num+"个产品");
this.notifyAll();
}
this.wait();
}
synchronized void reduce() throws InterruptedException {
if (num>0){
num--;
System.out.println(Thread.currentThread().getName()+"消费了"+num+"个产品");
this.notifyAll();
}
this.wait();
}
}
但是如果线程数量多了,也会出现多产的现象。虚假唤醒
因为我们用了if,将它换成while就能解决这个问题,官方文档。
6.2 Lock版:
package cn.butcher;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ProductAndSaleLock {
public static void main(String[] args) {
Data2 data = new Data2();
new Thread(() -> {
try {
for (int i = 0; i < 100; i++) {
data.add();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "线程1").start();
new Thread(() -> {
try {
for (int i = 0; i < 100; i++) {
data.reduce();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "线程2").start();
}
}
class Data2 {
int num;
Lock lock = new ReentrantLock(true);
Condition condition = null;
public Data2() {
this.condition = lock.newCondition();
}
void add() throws InterruptedException {
lock.lock();
try {
while (num <= 0) {
num++;
System.out.println(Thread.currentThread().getName() + "生产了" + num + "个产品");
condition.signalAll();
}
condition.await();
}finally {
lock.unlock();
}
}
void reduce() throws InterruptedException {
lock.lock();
try {
while (num > 0) {
num--;
System.out.println(Thread.currentThread().getName() + "消费了" + num + "个产品");
condition.signalAll();
}
condition.await();
}finally {
lock.unlock();
}
}
}
7、线程顺序执行
在之前,我们控制线程的顺序执行是一件非常困难的事情,我们不知道如何去唤醒特定的线程,但是使用了Lock以后,这一切变得简单起来。
Lock中有Condition 条件,其中有两个方法await()和single()对应我们之前wait()和notify()
Condition在一个Lock里面可以有多个实例!
我们可以给不同的页面放置不同Condition管理,当线程使用到这个资源的时候,如果当前条件符合,那么就让它执行,如果不符合,就让它等待
package cn.butcher;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SequentialExecution {
public static void main(String[] args) {
Printer printer = new Printer();
new Thread(() ->{
for (int i = 0; i < 10; i++) {
printer.printA();
}
}).start();
new Thread(() ->{
for (int i = 0; i < 10; i++) {
printer.printB();
}
}).start();
new Thread(() ->{
for (int i = 0; i < 10; i++) {
printer.printC();
}
}).start();
}
}
class Printer{
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
String msg = "A";
void printA(){
lock.lock();
try {
while (!msg.equals("A")){
condition1.await();
}
System.out.println(Thread.currentThread().getName()+"=>AAAAAA");
msg = "B";
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
void printB(){
lock.lock();
try {
while (!msg.equals("B")){
condition2.await();
}
System.out.println(Thread.currentThread().getName()+"=>BBBBBB");
msg = "C";
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
void printC(){
lock.lock();
try {
while (!msg.equals("C")){
condition3.await();
}
System.out.println(Thread.currentThread().getName()+"=>CCCCCC");
msg = "A";
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
8、关于锁
- 一个对象一把锁
- 一个类一把锁,如果是静态方法,锁的是类,对应的实例也会被锁住
9、集合不安全
我们都知道集合有线程安全的,有非线程安全的,例如Vector是线程安全的,ArrayList是非线程安全的,HashTable是线程安全的,HashMap是非线程安全的等,这些线程安全的集合底层基本上都是有synchronized关键字修饰的,而这种方式的同步,就必然会影响效率。
比较有趣的是:
是的,java很早就想到了线程安全的重要性,但为了提高效率后面还是出现了ArrayList。
既然说它不安全,那就测试一下。
9.1 测试ArrayList与Vector
public class CollectionNotThreadSafe {
public static void main(String[] args) {
List<String> arrayList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(() ->{
arrayList.add(String.valueOf(UUID.randomUUID()));
//ConcurrentModificationException
// 并发修改异常
System.out.println(arrayList);
}).start();
}
}
}
果然:
出现了并发修改异常。
如果使用Vector呢?
public class CollectionNotThreadSafe {
public static void main(String[] args) {
List<String> vector = new Vector<String>();
for (int i = 0; i < 10; i++) {
new Thread(() ->{
vector.add(String.valueOf(UUID.randomUUID()));
System.out.println(vector);
}).start();
}
}
}
一切正常。
9.2 将ArrayList转为线程安全
如果我们非要使用ArrayList如何将它转成线程安全的呢?java想得非常周到,它给我们提供的集合工具有这么一个方法Collections.synchronizedList()
,也就是下面的代码:
public class CollectionNotThreadSafe {
public static void main(String[] args) {
List<String> arrayList = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < 10; i++) {
new Thread(() ->{
arrayList.add(String.valueOf(UUID.randomUUID()));
System.out.println(arrayList);
}).start();
}
}
}
9.3 JUC中的集合之CopyOnWriteArrayList
JUC中也提供了很多我们常用的集合类,这些集合都是线程安全的。
如:
Concurrent:并发的
- ConcurrentHashMap<K,V>
- ConcurrentLinkedDeque
- CopyOnWriteArraySet
- …
对于CopyOnWriteArrayList,CopyOnWrite直译为写入时复制,这是计算机程序设计领域的一种优化策略,某个调用者试图修改资源的内容时,系统会复制一份专用副本给该调用者,而其他调用者所见到的最初的资源仍然保持不变。
package cn.butcher;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
public class CollectionNotThreadSafe {
public static void main(String[] args) {
List<String> copy = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(() ->{
copy.add(String.valueOf(UUID.randomUUID()));
System.out.println(copy);
}).start();
}
}
}
这里与Collections.synchronizedList()
有什么区别?
通过查看源代码我们发现:
Collections.synchronizedList()传入一个List返回了一个由synchronized修饰的List包装类,这个包装类时使用了大量的synchronized关键字。
而CopyOnWriteArrayList
却不是,查看源码:
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
看到了熟悉的ReentrantLock锁。
transient
关键字的作用是需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化。
volatile
是Java提供的一种轻量级的同步机制。相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
读的多用CopyOnWriteArrayList,写多用synchronized。为啥?
其实看源码时候我们发现了,CopyOnWriteArrayList是每次修改复制了一次数组,这是很耗资源的,而synchronized就没那么复杂了。但是synchronized会将资源锁,当一点线程拥有该资源的时候,其他线程只能观望。。自然读的效率就没有那么高了。
10、Callable
Callable接口类似于Runnable ,因为它们都是为其实例可能由另一个线程执行的类设计的。 然而,Runnable不返回结果,也不能抛出被检查的异常。
但是问题是创建线程要不继承Thread,要么实现Runnable接口,并没有Callable,如何实现呢?
Runnable所有已知实现类:
AsyncBoxView.ChildState , ForkJoinWorkerThread , FutureTask , RenderableImageProducer , SwingWorker , Thread , TimerTask
其中的FutureTask 可以传入Callable接口
如此,我们就可以通过FutureTask将Callable与Thread给勾搭上了。
package cn.butcher;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableTest {
public static void main(String[] args) {
MyCallable myCallable = new MyCallable();
//通过FutureTask类关联Thread和Callable
FutureTask<String> futureTask = new FutureTask<>(myCallable);
new Thread(futureTask).start();
try {
System.out.println(futureTask.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
class MyCallable implements Callable<String>{
@Override
public String call() throws Exception {
System.out.println("MyCallable中的call()执行");
return "hello";
}
}
11、辅助类
11.1 CountDownLatch减法计数器
允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。
比如现在模拟一个场景,有10个线程去上厕所,我们要求所有线程都上完厕所才能关门。
我们如果这样实现:
public class CountDownLatchTest {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() ->{
System.out.println(Thread.currentThread().getName()+"出来了");
},String.valueOf(i)).start();
}
System.out.println("WC关门了~");
}
}
这显然不符合我们的需求,现在使用CountDownLatch
改造:
public class CountDownLatchTest {
public static void main(String[] args) {
// 通过构造传入10作为基数
CountDownLatch count = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
new Thread(() ->{
System.out.println(Thread.currentThread().getName()+"出来了");
// 每执行一次,计数器减1
count.countDown();
},String.valueOf(i)).start();
}
try {
// 计数器没有归零,就不往下执行
count.await();
System.out.println("WC关门了~");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
11.2 CyclicBarrier加法计数器
允许一组线程全部等待彼此达到共同屏障点的同步辅助。 循环阻塞在涉及固定大小的线程方的程序中很有用,这些线程必须偶尔等待彼此。 屏障被称为循环 ,因为它可以在等待的线程被释放之后重新使用。
加法计数器就是,设定一个初始值,当到达一定的数量时执行某段代码。
例如拼车,我们需要满10个人才能发车:
package cn.butcher;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierTest {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(10,()->{
System.out.println("人数已满拼车成功");
});
for (int i = 0; i < 10; i++) {
new Thread(() ->{
System.out.println(Thread.currentThread().getName()+"上车");
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
11.3 Semaphore 信号量(停车位)
一个计数信号量。 在概念上,信号量维持一组许可证。 如果有必要,每个acquire()都会阻塞,直到许可证可用,然后才能使用它。 每个release()添加许可证,潜在地释放阻塞获取方。 但是,没有使用实际的许可证对象; Semaphore只保留可用数量的计数,并相应地执行。
package cn.butcher;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SemaphoreTest {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
// 有许可证才能进入执行代码,否则只能阻塞等待
for (int i = 0; i < 10; i++) {
// 有10辆车要停车但是停车场一次只能停3辆车
new Thread(() ->{
try {
semaphore.acquire();// 获取许可证,如果满了,会等待知道有空闲的许可证
System.out.println(Thread.currentThread().getName()+"进入停车");
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+"离开了");
semaphore.release(); // 释放许可证
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
12、读写锁 ReadWriteLock
ReadWriteLock维护一对关联的locks ,一个用于只读操作,一个用于写入。读锁可以由多个线程同时进行,写锁只能由一个线程访问。
也就是我们说的读锁(共享锁),写锁(独占锁)
所有已知实现类: ReentrantReadWriteLock
我们使用Map模拟我们的数据库,然后分别各自启动5个线程对数据库进行读写操作。
看看我们的实现:
public class ReadWriteLockTest {
public static void main(String[] args) {
MyReadWriteLock db = new MyReadWriteLock();
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(() ->{
db.write(temp+"",temp+":value");
}).start();
}
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(() ->{
System.out.println(db.read(Thread.currentThread().getName()+"读取成功:"+temp));
}).start();
}
}
}
class MyReadWriteLock{
Map<String, Object> db = new HashMap<>();
void write(String key,Object value){
System.out.println(Thread.currentThread().getName()+":开始写入"+key);
this.db.put(key,value);
System.out.println(Thread.currentThread().getName()+":"+key+"写入成功");
}
Object read(String key){
System.out.println(Thread.currentThread().getName()+":开始读取"+key);
return this.db.get(key);
}
}
结果:
使用ReadWriteLock
改良后:
public class ReadWriteLockTest {
public static void main(String[] args) {
MyReadWriteLock db = new MyReadWriteLock();
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(() ->{
db.write(temp+"",temp+":value");
}).start();
}
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(() ->{
System.out.println(db.read(Thread.currentThread().getName()+"读取成功:"+temp));
}).start();
}
}
}
class MyReadWriteLock{
Map<String, Object> db = new HashMap<>();
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
void write(String key,Object value){
try {
readWriteLock.writeLock().lock();
System.out.println(Thread.currentThread().getName()+":开始写入"+key);
this.db.put(key,value);
System.out.println(Thread.currentThread().getName()+":"+key+"写入成功");
}catch (Exception e){
e.printStackTrace();
}finally {
readWriteLock.writeLock().unlock();
}
}
Object read(String key){
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+":开始读取"+key);
return this.db.get(key);
}catch (Exception e){
e.printStackTrace();
}finally {
readWriteLock.readLock().unlock();
}
return null;
}
}
13、阻塞队列
BlockingQueue方法有四种形式,具有不同的操作方式:
- 一个抛出异常
- 第二个返回一个特殊值( null或false ,具体取决于操作)
- 第三个程序将无限期地阻止当前线程,直到操作成功为止
- 而第四个程序块在放弃之前只有给定的最大时限。
13.1 抛出异常
static void test01(){
BlockingQueue blockingQueue = new LinkedBlockingQueue(3);
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
// System.out.println(blockingQueue.add("d"));
// java.lang.IllegalStateException: Queue full
System.out.println("==========================");
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
// System.out.println(blockingQueue.remove());
// java.util.NoSuchElementException
}
13.2 返回boolean
static void test02(){
BlockingQueue blockingQueue = new LinkedBlockingQueue(3);
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
// System.out.println(blockingQueue.offer("c"));
// false
System.out.println("===========================");
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
//System.out.println(blockingQueue.poll());
// null
}
13.3 一直阻塞
static void test03() throws InterruptedException {
BlockingQueue blockingQueue = new LinkedBlockingQueue(3);
blockingQueue.put("a");
blockingQueue.put("b");
blockingQueue.put("c");
//blockingQueue.put("c");
// 一直阻塞
System.out.println("======================");
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
// System.out.println(blockingQueue.take());
// 一直阻塞
}
13.4 超时阻塞
static void test04() throws InterruptedException {
BlockingQueue blockingQueue = new LinkedBlockingQueue(3);
System.out.println(blockingQueue.offer("a", 2, TimeUnit.SECONDS));
System.out.println(blockingQueue.offer("b", 2, TimeUnit.SECONDS));
System.out.println(blockingQueue.offer("c", 2, TimeUnit.SECONDS));
//System.out.println(blockingQueue.offer("d", 2, TimeUnit.SECONDS));
// 两秒后退出
System.out.println("==============================");
System.out.println(blockingQueue.poll(2, TimeUnit.SECONDS));
System.out.println(blockingQueue.poll(2, TimeUnit.SECONDS));
System.out.println(blockingQueue.poll(2, TimeUnit.SECONDS));
//System.out.println(blockingQueue.poll(2, TimeUnit.SECONDS));
//两秒后退出
}
13.5 同步队列 SynchronousQueue
同步队列不存放数据,一次只有一个,只有里面有值的时候才能take()取出,否则就阻塞直到有值,同样的要存入必须先确保队列里面是空的。
static void test05(){
BlockingQueue blockingQueue = new SynchronousQueue();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"放入a");
blockingQueue.put("a");
// 先打印再放,线程的速度比打印快,先放的话下面取的形成就能拿到了,会很诡异
System.out.println(Thread.currentThread().getName()+"放入b");
blockingQueue.put("b");
System.out.println(Thread.currentThread().getName()+"放入c");
blockingQueue.put("c");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+"取出"+blockingQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+"取出"+blockingQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+"取出"+blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
14、线程池
池的作用:节省系统开销,创建和消费资源开销非常大
如:常量池、数据库连接池、内存池。。。
14.1 创建线程三个方法
通过Executors
工具类创建线程池
- 创建单个线程,这个池子里只有一个线程
ExecutorService executor = Executors.newSingleThreadExecutor();
- 创建固定线程池大小,如5个线程
ExecutorService executor = Executors.newFixedThreadPool(5);
- 创建可伸缩的线程池,根据需要确定池的大小
ExecutorService executor = Executors.newCachedThreadPool();
都使用下面的循环来测试
for (int i = 0; i < 10; i++) {
final int j = i;
// 使用线程池创建线程执行代码
executor.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行"+j);
});
}
14.2 自定义线程池(7大参数)
在上面,我们使用工具类去创建线程,进入源码我们发现,它内部也是使用的ThreadPoolExecutor
创建的线程池。但是,这样做是有弊端的,阿里巴巴开发手册上面说明了:
OOM为out of memory的简称,称之为内存溢出。所以我们需要自定义线程池。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
通过源码我们可以看到,自定义线程有7大参数,分别是:
- corePoolSize 核心线程数量
- maximumPoolSize最大线程数量
- keepAliveTime 存活时间
- unit时间单位
- BlockingQueue阻塞队列
- threadFactory 线程创建工厂(可以从
Executors
工具类中获取) - handler 拒绝策略,也就是超过最大线程数量后的线程该如何处理(四大策略)
ExecutorService executor = new ThreadPoolExecutor(
2,
5,
3,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
for (int i = 1; i <=8; i++) {
final int j = i;
executor.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行"+j);
});
}
以上参数的意思是,默认创建两个核心线程,不管有没有人用,都创建。最大5个线程全开,阻塞队列里可以放5个人,如果处理的人数超过7个人(核心两个,阻塞5个),就增加线程去处理,如果超过了最大的处理能力10个,拒接策略就生效。
14.3 四大拒绝策略
拒绝策略是实现了RejectedExecutionHandler
接口的类,目前有四个实现类
- AbortPolicy 会抛出异常(RejectedExecutionException)
- CallerRunsPolicy 抛回原来线程执行
- DiscardPolicy 放弃当前任务
- DiscardOldestPolicy 去和最早的任务竞争,竞争竞争谁赢谁执行
14.4 最大线程如何定义
- CPU密集型
开头的时候说了,线程有并行和并发,并行的效率是最高的,我们将线程池的最大数量设置为服务器支持的最大线程数量,可以保证线程的效率最高!线程数量超过了CPU的核心数,就没那么快了。
// 获取当前电脑或服务器的可并行线程数,作为自定义线程池的参数
Runtime.getRuntime().availableProcessors();
- IO密集型
IO操作是非常耗时的,如果有10个大型任务,我们最好分配10个线程去执行,为了不阻塞,我们一般会在这个基础上在加一倍,保证在执行着10个大型任务的时候,其他任务不阻塞,当然具体多分配几个,依然视情况而定。
总之线程不是越多越好,当超过CPU支持的并行数量,线程的执行效率就会下降,线程越多效率越低。
15、Fork/Join 分支合并,将一个任务分解让多个线程执行
Java 7开始引入了一种新的Fork/Join线程池,它可以执行一种特殊的任务:把一个大任务拆成多个小任务并行执行。
如何使用?
使用ForkJoinPool
线程池创建
ForkJoinPool forkJoinPool = new ForkJoinPool();
submit有返回值,其中可以传入四种类型的参数:
- ForkJoinTask task
- Runnable task
- Callable task
- Runnable task, T result
这里我们需要ForkJoinTask
所以我们需要创建一个类去继承ForkJoinTask的子类
package cn.butcher;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
import java.util.stream.LongStream;
public class ForkJoinTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
test01();
// 结果:2205000001050000000 耗时:833(稳定在800s左右)
test02();
// 结果:2205000001050000000 耗时:455(不稳定,时快时慢300多,700多)
test03();
// 结果:2205000001050000000 耗时:400(稳定400左右)
}
static void test01() {
long start = System.currentTimeMillis();
long sum = 0;
for (long i = 1; i <= 21_0000_0000; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println("暴力结果:" + sum + "\t耗时:" + (end - start));
}
static void test02() throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> joinTask = forkJoinPool.submit(new AddPlus(1L, 21_0000_0000L));
Long res = joinTask.get();
long end = System.currentTimeMillis();
System.out.println("ForkJoin结果:" + res + "\t耗时:" + (end - start));
}
static void test03() {
long start = System.currentTimeMillis();
long reduce = LongStream.rangeClosed(1, 21_0000_0000L).parallel().reduce(0, Long::sum);
long end = System.currentTimeMillis();
System.out.println("Stream结果:" + reduce + "\t耗时:" + (end - start));
}
}
class AddPlus extends RecursiveTask<Long> {
Long start;
Long end;
public AddPlus(Long start, Long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
long sum = 0L;
if ((end - start) < 10_0000L) {
for (long i = start; i <= end; i++) {
sum += i;
}
return sum;
} else {
long splice = (end + start) / 2;
// 拆分任务
AddPlus addPlus1 = new AddPlus(start, splice);
addPlus1.fork(); // 将这个addPlus1任务压入线程队列
AddPlus addPlus2 = new AddPlus(splice + 1, end);
addPlus2.fork(); // 将这个addPlus2任务压入线程队列
return addPlus1.join() + addPlus2.join();
}
}
}
9次运行结果比较
暴力结果:2205000001050000000 耗时:788
ForkJoin结果:2205000001050000000 耗时:814
Stream结果:2205000001050000000 耗时:382
暴力结果:2205000001050000000 耗时:794
ForkJoin结果:2205000001050000000 耗时:906
Stream结果:2205000001050000000 耗时:374
暴力结果:2205000001050000000 耗时:849
ForkJoin结果:2205000001050000000 耗时:704
Stream结果:2205000001050000000 耗时:369
暴力结果:2205000001050000000 耗时:825
ForkJoin结果:2205000001050000000 耗时:470
Stream结果:2205000001050000000 耗时:441
暴力结果:2205000001050000000 耗时:832
ForkJoin结果:2205000001050000000 耗时:845
Stream结果:2205000001050000000 耗时:390
暴力结果:2205000001050000000 耗时:786
ForkJoin结果:2205000001050000000 耗时:523
Stream结果:2205000001050000000 耗时:409
暴力结果:2205000001050000000 耗时:913
ForkJoin结果:2205000001050000000 耗时:505
Stream结果:2205000001050000000 耗时:392
暴力结果:2205000001050000000 耗时:834
ForkJoin结果:2205000001050000000 耗时:670
Stream结果:2205000001050000000 耗时:388
暴力结果:2205000001050000000 耗时:894
ForkJoin结果:2205000001050000000 耗时:962
Stream结果:2205000001050000000 耗时:358
ForkJoin确实不稳定哈,但是如果任务拆分更细一些呢?
class AddPlus extends RecursiveTask<Long> {
Long start;
Long end;
public AddPlus(Long start, Long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
long sum = 0L;
if ((end - start) < 10_0000L) {
for (long i = start; i <= end; i++) {
sum += i;
}
return sum;
} else {
long splice = (end + start) / 4;
// 拆分任务
AddPlus addPlus1 = new AddPlus(start, splice);
addPlus1.fork(); // 将这个addPlus1任务压入线程队列
AddPlus addPlus2 = new AddPlus(splice + 1, splice*2);
addPlus2.fork(); // 将这个addPlus2任务压入线程队列
AddPlus addPlus3 = new AddPlus(splice*2+1, splice*3);
addPlus3.fork(); // 将这个addPlus3任务压入线程队列
AddPlus addPlus4 = new AddPlus(splice*3+1, end);
addPlus4.fork(); // 将这个addPlus4任务压入线程队列
return addPlus1.join() + addPlus2.join() + addPlus3.join()+ addPlus4.join();
}
}
}
奇迹发生了:
Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.StackOverflowError
at java.util.concurrent.ForkJoinTask.get(ForkJoinTask.java:1006)
at cn.butcher.ForkJoinTest.test02(ForkJoinTest.java:85)
at cn.butcher.ForkJoinTest.main(ForkJoinTest.java:15)
Caused by: java.lang.StackOverflowError
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at java.util.concurrent.ForkJoinTask.getThrowableException(ForkJoinTask.java:598)
at java.util.concurrent.ForkJoinTask.get(ForkJoinTask.java:1005)
这个问题还没得解决掉,有人知道吗?