文章目录
锁的理解与使用
通过Synchronized理解锁
详细理解查看视频:8锁问题【狂神说Java】JUC并发编程最新版通俗易懂_哔哩哔哩_bilibili
-
被synchronized锁的对象是方法的调用者
如phone1.call(),那么锁定的是new出来的这个phone1,如果还有一个实例phone2,是不受phone1锁的影响的。
class Phone{ public synchronized void call(){ TimeUnit.SECONDS.sleep(3); //睡眠3秒 sout("打电话"); //sout是输出简写 } }
-
虽然锁的是调用者,但一个实例被锁上后,实例中非同步方法/代码块,不受锁的影响。
这里的sendSms方法,就是正常调用,和锁没有关系
class Phone{ public synchronized void call(){ TimeUnit.SECONDS.sleep(3); //睡眠3秒 sout("打电话"); //sout是输出简写 } //这里没有锁,不是同步方法,不受锁的影响 public void sendSms(){ sout("发短信") } }
-
锁静态代码块时,锁的的是Class模板
static修饰的方法,在类初次加载时就有了,且无论有多少个Phone实例,都只有一个class,如同一个模板,即只有一个call()。那么此时锁的是Class这个模板,即所有的实例调用call()方法,睡眠三秒被上锁。如phone1.call(),phone2.call(),都会对Phone.class上锁,注意这里锁的不是phone1,phone2。
class Phone{ //synchronized修饰静态代码块时,锁的是class public static synchronized void call(){ TimeUnit.SECONDS.sleep(3); //睡眠3秒 sout("打电话"); //sout是输出简写 } }
-
当静态同步方法和普通同步方法同时存在时,静态锁的是class,普通锁的是this(调用者)
new一个实例phone1,phone1.call()与phone1.sendSms()时,前者锁的是Phone.class,后者锁的是phone1这个实例,他们互不影响,不需要相互等待。
class Phone{ //synchronized修饰静态代码块时,锁的是class public static synchronized void call(){ TimeUnit.SECONDS.sleep(3); //睡眠3秒 sout("打电话"); //sout是输出简写 } //synchronized修饰普通代码块时,锁的是调用者,this public synchronized void sendSms(){ sout("发短信") } }
lock与Synchronized的区别
Lock锁:手动挡 Synchronized:自动挡
- Synchronized 内置的java关键字,Lock是一个java类
- Synchronized 无法判断获取锁的状态,Lock可以判断是否获取到了锁
- Synchronized 会自动释放锁,lock必须手动释放,可能会导致死锁。
- Synchronized 阻塞后可能就一直等待了,lock锁可以自己处理
- Synchronized 可重入锁,不可以中断,非公平;lock锁,可重入锁,可判断锁,非公平(可自己设置)
- Synchronized 适合锁少量的代码同步问题,lock适合锁大量的同步代码。
生产者消费者问题
判断等待->业务执行->状态通知,其中判断等待时,使用while而避免使用if,if是一次性判断,可能会有虚假唤醒的情况
Synchronized版
import java.util.ArrayList;
import java.util.List;
/**
* 生产者消费者Synchronized练习
* @author Wyhao
* @date 2021/6/27
**/
public class ProducersAndcomsumers {
public static void main(String[] args) {
Resources resources = new Resources();
new Thread(()->{
for (int i = 0; i < 6; i++) {
try {
resources.update();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"update").start();
new Thread(()->{
for (int i = 0; i < 6; i++) {
try {
resources.delete();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"delete").start();
}
}
class Resources{
private List<Integer> list = new ArrayList();
private int count = 0;
public synchronized void update() throws InterruptedException {
while (list.size() > 0 ) { //判断等待
this.wait();
}
list.add(count); //业务执行
System.out.println(Thread.currentThread().getName()+":"+list);
count++;
this.notifyAll(); //状态通知
}
public synchronized void delete() throws InterruptedException {
while (list.size() == 0 ) {
this.wait();
}
System.out.println(Thread.currentThread().getName()+":"+list.get(0));
list.remove(0);
count++;
this.notifyAll();
}
}
输出:
update:[0]
delete:0
update:[2]
delete:2
update:[4]
delete:4
update:[6]
delete:6
update:[8]
delete:8
update:[10]
delete:10
思考以上代码,虽然我们最终的执行顺序是update->delete,但这是每次都进行了资源判断,如此才保证了执行顺序,在逻辑上这是不恰当的,且synchronized锁的都是this,wait与notify都是对this的操作,具有很多局限性。例如我们要对资源有三个操作,且这三个操作需要保证按一定顺序执行,这是对this上锁,就很难实现。
dk5开始出现的Lock,就充分考虑了复杂的场景需求。
JUC版
lock.newCondition()为lock锁的同步监视器,await对应wait方法,signal()对应notify方法
还是上面的例子,使用lock实现,且要求A,B,C方法按顺序执行。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author Wyhao
* @date 2021/6/27
**/
public class ProducersAndcomsumers2 {
public static void main(String[] args) {
Resources2 resources = new Resources2();
new Thread(() -> {
for (int i = 0; i < 6; i++) {
resources.A();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 6; i++) {
resources.B();
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 6; i++) {
resources.C();
}
}, "C").start();
}
}
class Resources2 {
//定义锁
Lock lock = new ReentrantLock();
//定义同步监视器
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
private List<String> list = new ArrayList();
private String state = "A"; //状态A,B,C
public void A() {
lock.lock(); //上锁
try {
while (state != "A") {
condition1.await(); //condition1等待,此处被lock锁住的代码块都将等待,直到condition1被唤醒(signal())
}
list.add("A"); //业务执行
System.out.println(Thread.currentThread().getName() + list +":a");
state = "B";
condition2.signal(); //唤醒condition2
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock(); //释放锁
}
}
public void B() {
lock.lock(); //上锁
try {
while (state != "B") {
condition2.await();
}
list.add("B"); //业务执行
System.out.println(Thread.currentThread().getName() + list +":b");
state = "C";
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock(); //释放锁
}
}
public void C() {
lock.lock(); //上锁
try {
while (state != "C") {
condition3.await();
}
list.add("C"); //业务执行
System.out.println(Thread.currentThread().getName() + list +":c");
state = "A";
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock(); //释放锁
}
}
}
输出:
A[A]:a
B[A, B]:b
C[A, B, C]:c
A[A, B, C, A]:a
B[A, B, C, A, B]:b
C[A, B, C, A, B, C]:c
A[A, B, C, A, B, C, A]:a
B[A, B, C, A, B, C, A, B]:b
C[A, B, C, A, B, C, A, B, C]:c
A[A, B, C, A, B, C, A, B, C, A]:a
B[A, B, C, A, B, C, A, B, C, A, B]:b
C[A, B, C, A, B, C, A, B, C, A, B, C]:c
A[A, B, C, A, B, C, A, B, C, A, B, C, A]:a
B[A, B, C, A, B, C, A, B, C, A, B, C, A, B]:b
C[A, B, C, A, B, C, A, B, C, A, B, C, A, B, C]:c
A[A, B, C, A, B, C, A, B, C, A, B, C, A, B, C, A]:a
B[A, B, C, A, B, C, A, B, C, A, B, C, A, B, C, A, B]:b
C[A, B, C, A, B, C, A, B, C, A, B, C, A, B, C, A, B, C]:c
JUC下的API详解
并发下集合类的使用
【狂神说Java】JUC并发编程最新版通俗易懂_哔哩哔哩_bilibili
ArrayList
在多线程中Arraylist.add()时,会抛出并发修改异常!
java.util.ConcurrentModificationException
基础版解决办法:
使用Vector(),或Collections.synchronizedList(new ArrayList())。
底层都有synchronized加锁,效率低下。
升级版解决办法:
使用new CopyOnWriteArrayList(),底层加的Lock锁,效率更高。
CopyOnWrite,写入时复制, 计算机程序设计领域的一种优化方案,COW。读写分离包装线程安全。
Set:与ArrayList一样,同样可以有Collections.synchronizedSet(new Set()),CopyOnWriteArraySet()等实现并发。
这里补充一个基础,HashSet底层就是HashMap
//源码:
public boolean add(E e) {
//PRESENT只是一个常量,new Object()
return map.put(e, PRESENT)==null;
}
HashMap()
与上同,Collections的工具可以加锁,同样JUC下存在并发安全且高效的ConcurrentHashMap();
这里有关HashMap()的底层,加载因子为什么是0.75,初始化容量16等等都可以进一步了解。ConcurrentHashMap()的原理,可以参考源码详细了解。都可以看狂神的博客,有详细讲解。
Calleble的使用
返回结果并可能引发异常的任务。 实现者定义一个没有参数的单一方法,称为
call
@FunctionalInterface
public interface Callable<V>
先看看实现Runnable的线程使用:
/**
* @author Wyhao
* @date 2021/7/1
**/
public class CallableTest {
public static void main(String[] args){
//实现Runnable的线程
for (int i = 0; i < 5; i++) {
new Thread(new RunnableTask2(),"thread"+i).start();
}
}
}
class RunnableTask2 implements Runnable {
@Override
public void run() {
System.out.println("线程名:"+Thread.currentThread().getName());
}
}
思考一个问题,我们要如何开启一个Callable的线程呢?我们知道开启一个线程是new Thread(target,name).start(),查看其源码:
//源码:
/**
* Runnable target 这个参数是Runnable,而Thread是实现了Runnable的,所以可以直接使用,Callable与Runnable并没有继承关系,我们要怎么开启一个线程呢?
**/
public Thread(Runnable target, String name) {
init(null, target, name, 0);
}
Callable与Runnable并没有继承关系,那我们就借助一个和Runnable有继承关系的实例作为参数(找中介),因为Callable有返回结果并可能引发异常的任务,所以我们希望这个中介,可以为我们储存返回结果,并且能够抛出异常。java为我们找的这个中介就是FutureTask
//源码:
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
结合FutureTask这个中介,我们就能开启实现Callable的线程了
package jucTrain;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @author Wyhao
* @date 2021/7/1
**/
public class CallableTest {
public static void main(String[] args){
//实现Callable的线程
FutureTask<Integer> futureTask = new FutureTask<>(new CallableTask());
for (int i = 0; i < 5; i++) {
//这里开启线程,并执行call()方法,但需要注意,这里call()只会返回一次,结果被缓存
new Thread(futureTask,"thread"+i).start();
}
Integer callReturn = null;
try {
callReturn = futureTask.get(); //获取返回结果
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("线程返回:"+callReturn);
}
}
class CallableTask implements Callable{
private int count = 0;
@Override
public Integer call() throws Exception {
System.out.println("线程名:"+Thread.currentThread().getName());
return count++;
}
}
返回:
线程名:thread0
线程返回:0
通过上面的方法我看可以成功的获取到异步返回,但这里只能获取其中一个返回,如何获取所有返回呢?这就需要结合线程池使用了,下文再详细探讨。
Concurrent辅助API
CountDownLatch
可以简单理解成一个线程安全的减法计数器,就像:int count = 10; 每次count–;
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5); //count = 5
for (int i = 0; i < 10; i++) { //我们这里新启了10个线程,而countDownLatch中count = 5,并不会报错
new Thread(() -> {
System.out.println("线程名:" + Thread.currentThread().getName());
countDownLatch.countDown(); //count--,注意这个一定要放线程最后面执行才有效果
}, "thread" + i).start();
}
//注意:如果count一直不为0,就会一直等待
countDownLatch.await(); //只有countDownLatch中count为0后,即最少完成5个任务后,才会执行下面的代码,否则处于等待状态
System.out.println("至少5个线程执行完毕");
}
输出:
线程名:thread0
线程名:thread2
线程名:thread1
线程名:thread3
线程名:thread5
线程名:thread4
线程名:thread7
线程名:thread6
线程名:thread8
至少5个线程执行完毕
线程名:thread9
这里我们能够保证的是,“至少5个线程执行完毕”一定是在五个输出以后。
CyclicBarrier
功能和CountDownLatch类似,不同的是,在使用countDownLatch的主函数中,条件不满足可能会一直等待,而CountDownLatch是另启一个线程执行,不会对主程序有影响,并且不止执行一次,我们可以理解为,当count–到0后,又会为我们重置count=n;
public static void main(String[] args){
//观察源码 public CyclicBarrier(int parties, Runnable barrierAction)
CyclicBarrier cyclicBarrier = new CyclicBarrier(2, () -> { //这里也是一个线程,每次满足条件时都会开启一个线程
System.out.println("完成了2个线程");
});
for (int i = 0; i < 6; i++) { //我们开启了6个线程,await的条件是2,那么cyclicBarrier就会开启3个线程
new Thread(() -> {
System.out.println("线程名:" + Thread.currentThread().getName());
try {
cyclicBarrier.await(); //查看源码,这里面有dowait(),也就是count--的操作,我们每抛一个线程,就是一次count--
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}, "thread" + i).start();
}
System.out.println("主函数执行完了");
}
输出:
线程名:thread1
主函数执行完了
线程名:thread0
完成了2个线程
线程名:thread2
线程名:thread3
完成了2个线程
线程名:thread4
线程名:thread5
完成了2个线程
Semaphore
可以近似理解成一个有固定数量的线程池,其中acquire()可以理解为占用一个位置,release()释放一个位置,下面使用一个停车场来说明
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2); //规定有两个车位,可以发放两张许可证
for (int i = 0; i < 4; i++) {
new Thread(() ->{
try {
semaphore.acquire(); //获取/请求许可证
System.out.println(Thread.currentThread().getName()+"把车开进了停车场");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"把车开出了停车场");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); //释放/归还许可证,必须放在finally中,和锁的释放一个道理
}
},"thread"+i).start();
}
}
输出:
thread1把车开进了停车场
thread0把车开进了停车场
thread0把车开出了停车场
thread1把车开出了停车场
thread2把车开进了停车场
thread3把车开进了停车场
thread3把车开出了停车场
thread2把车开出了停车场
读写锁ReadWriteLock
读写锁是lock的颗粒化,主要使用在读写操作中,read lock
可以由多个阅读器线程同时进行,也就是共享锁。 write lock
是独家的,独占锁。观察以下代码:
package jucTrain;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @author Wyhao
* @date 2021/7/6
**/
public class ReadWriteLockTest {
public static void main(String[] args) {
ReadAndWrite readAndWrite = new ReadAndWrite();
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(()->{
try {
readAndWrite.write("key"+temp,temp);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"写线程"+i).start();
}
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(() ->{
readAndWrite.read("key"+temp);
},"读线程"+i).start();
}
}
}
class ReadAndWrite {
private volatile HashMap<String,Object> map = new HashMap<>();
//增加一个读写锁
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void read(String key) {
readWriteLock.readLock().lock(); //加读锁
try {
TimeUnit.SECONDS.sleep(1);
Date date = new Date(); //获取时间
System.out.println(new SimpleDateFormat("HH:mm:ss").format(date)+"读到数据:"+map.get(key));
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock(); //释放读锁
}
}
public void write(String key, Object o) throws InterruptedException {
readWriteLock.writeLock().lock();
try {
Date date = new Date(); //获取时间
System.out.println(new SimpleDateFormat("HH:mm:ss").format(date)+key+"开始写入");
map.put(key,o);
TimeUnit.SECONDS.sleep(1);
Date date2 = new Date(); //获取时间
System.out.println(new SimpleDateFormat("HH:mm:ss").format(date2)+"写入完毕");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
}
得到输出
16:45:16key0开始写入
16:45:17写入完毕
16:45:17key1开始写入
16:45:18写入完毕
16:45:18key2开始写入
16:45:19写入完毕
16:45:19key3开始写入
16:45:20写入完毕
16:45:20key4开始写入
16:45:21写入完毕
16:45:22读到数据:2
16:45:22读到数据:3
16:45:22读到数据:4
16:45:22读到数据:1
16:45:22读到数据:0
查看输出的时间,写入操作都是串行的,这很好理解,因为我们加了独占锁,同时只能一个线程操作,那就只能排队串行运行;而读的操作都是同一时间输出,即并行的,说明读时多线程同时访问了,是一个共享锁。
在一些常被访问的资源中,引入读写锁,会让性能得到显著的提升。值得说明的是,读写锁其本身比互斥锁更复杂,开销也更大,所以读写锁应当被引用在有大量访问需求的资源中。
阻塞队列BlockingQueue
查看源码,得知继承关系:
**明确概念:**队列,先进先出,开车排队进隧道;阻塞队列,当队列装满了,就不能再装了阻塞状态,当队列空了,就不能再取了进入等待状态。
常用API:
API功能 | 抛出异常 | 返回布尔值 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
添加 | add(o) | offer(o) | put(o) | offer(o,timeout,TimeUnit.s/m/h) |
移除 | remove() | poll() | take() | poll(timeout,TimeUnit.s/m/h) |
检测队首元素 | element | peek() | —— | —— |
SynchronousQueue 同步队列
可以理解为容量为1(实际同步队列没有任何内部容量,甚至没有一个容量),每个插入操作必须等待另一个线程相应的删除操作,反之亦然。API遵循BlockingQueue
线程池的使用
理论了解
**池化技术:**优化资源的使用,由池创建好资源,谁需要谁取用,如线程池,常量池,内存池,对象池,JDBC连接池等等
线程池的优势:
- 降低资源消耗,有时候新开100个线程,其实使用10个线程快速交替完成任务,可以达到一样的性能。
- 提高响应速度,每次启动关闭线程都是一次很大的开销。
- 方便线程管理,控制最大并发数,控制进程对电脑最大性能的占用。
阿里巴巴开发手册中:
创建线程池三大方法
newSingleThreadExecutor
理解:单一线程池,相当于一个可复用线程
定义:创建一个 Executor,它使用单个工作线程在无界队列中运行。 (但是请注意,如果这个单线程在关闭之前由于执行失败而终止,如果需要执行后续任务,一个新线程将取代它。)保证任务按顺序执行,并且不会超过一个任务处于活动状态在任何给定的时间。 与其他等效的newFixedThreadPool(1) ,返回的执行器保证不可重新配置以使用其他线程。
newFixedThreadPool
理解:固定线程池,一个规定线程数量的线程池
定义:创建一个线程池,该线程池重用固定数量的线程在共享的无界队列中运行。 在任何时候,最多nThreads线程将是活动的处理任务。 如果在所有线程都处于活动状态时提交了其他任务,它们将在队列中等待,直到有线程可用。 如果任何线程在关闭前的执行过程中因失败而终止,则在需要执行后续任务时,将有一个新线程取代它。 池中的线程将一直存在,直到它被明确shutdown
newCachedThreadPool
理解:缓存线程池,根据需要创建线程,遇强则强
定义:创建一个线程池,根据需要创建新线程,但在可用时将重用先前构造的线程。 这些池通常会提高执行许多短期异步任务的程序的性能。 如果可用,调用execute将重用先前构造的线程。 如果没有可用的现有线程,则会创建一个新线程并将其添加到池中。 60 秒内未使用的线程将被终止并从缓存中删除。 因此,保持空闲足够长时间的池不会消耗任何资源。 请注意,可以使用ThreadPoolExecutor构造函数创建具有相似属性但细节不同(例如,超时参数)的ThreadPoolExecutor
注意!,这个API调用ThreadPoolExecutor时,maximumPoolSize= Integer.MAX_VALUE(21亿),最好使用ThreadPoolExecutor实现。
ThreadPoolExecutor
上面三个线程池创建方法都是调用的ThreadPoolExecutor,在上文提及的阿里巴巴开法手册中也有提及。
/**
* 使用给定的初始参数和默认线程工厂以及被拒绝的执行处理程序创建一个新的ThreadPoolExecutor 。 使用Executors工厂方法之一而不是这个通用构造函数可能更方便。
* 参数:
* corePoolSize – 要保留在池中的线程数,即使它们处于空闲状态,除非设置了allowCoreThreadTimeOut
* maximumPoolSize – 池中允许的最大线程数
* keepAliveTime – 当线程数大于核心数时,这是多余空闲线程在终止前等待新任务的最长时间。
* unit – keepAliveTime参数的时间单位
* workQueue – 用于在执行任务之前保存任务的队列。 这个队列将只保存execute方法提交的Runnable任务。
* 抛出:
* IllegalArgumentException – 如果以下情况之一成立: corePoolSize < 0 keepAliveTime < 0 maximumPoolSize <= 0 maximumPoolSize < corePoolSize
* NullPointerException – 如果workQueue为空
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
使用示例:
/**
* @author Wyhao
* @date 2021/7/11
**/
public class ExecutorsTest {
public static void main(String[] args) {
Executors.newSingleThreadExecutor(); //单一线程池
Executors.newFixedThreadPool(5); //固定线程池
ExecutorService executorService = Executors.newCachedThreadPool(); //缓存线程池
try {
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName()+":开始执行");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executorService.shutdown(); //一定要关闭线程池
}
}
}
newCachedThreadPool是需要多少就创建多少,输出:
pool-3-thread-2:开始执行
pool-3-thread-4:开始执行
pool-3-thread-5:开始执行
pool-3-thread-1:开始执行
pool-3-thread-3:开始执行
pool-3-thread-6:开始执行
pool-3-thread-7:开始执行
pool-3-thread-8:开始执行
pool-3-thread-1:开始执行
pool-3-thread-8:开始执行
观察输出我们发现创建了8个,可以了解为,当main中的for循环到第8次时,第一个for循环创建的线程已经执行完毕空闲出来了,有空闲线程就使用空闲线程。这里会创建几个线程,取决于电脑当前的执行状态。
七大参数
通过上文的分析,我们知道三大方法都是调用的同一个API,ThreadPoolExecutor()这个方法至多有七个参数,灵活运用这七个参数,就能随心所欲的创建我们需要的线程池。先看源码:
public ThreadPoolExecutor(int corePoolSize, //核心线程数,最少也会存在这个数量的线程
int maximumPoolSize, //最大线程数,最多存在这个数量的线程
long keepAliveTime, //存活时间,如果一个线程(大于核心线程数时)超过存活时间,就销毁
TimeUnit unit, //存活时间单位
BlockingQueue<Runnable> workQueue, //阻塞队列,排队进入线程池处理
ThreadFactory threadFactory, //线程工厂,生成线程的方式
RejectedExecutionHandler handler //拒绝策略,大于maximumPoolSize+workQueue数量的线程,有四种策略来拒绝
) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
参数理解,以银行办理业务为例进行理解
其中threadFactory线程工厂可以理解为业务经理,调节窗口的开关,人员分配等等。
四种拒绝策略
当线程池接受的线程数量超过maximumPoolSize+workQueue时,就应该拒绝,有四种拒绝策略
API | 源码定义 | 理解 |
---|---|---|
AbortPolicy | 抛出RejectedExecutionException拒绝任务的处理程序 | 拒绝并告知上级 |
CallerRunsPolicy | 被拒绝任务的处理程序,它直接在execute方法的调用线程中运行被拒绝的任务,除非执行程序已关闭,在这种情况下任务将被丢弃 | 从哪来,回哪去,如果是main调用的线程池,那么这个任务将被交给main线程执行 |
DiscardOldestPolicy | 被拒绝任务的处理程序,丢弃最早的未处理请求,然后重试execute ,除非执行程序关闭,在这种情况下任务将被丢弃 | 尝试与最早的任务竞争资源,不会抛出异常 |
DiscardPolicy | 被拒绝任务的处理程序,它默默地丢弃被拒绝的任务 | 丢掉任务,不抛异常 |
java自己有四种拒绝策略,还有一些其他的拒绝策略,如apache的RejectHandler等
小结与拓展
线程池优化,maximumPoolSize如何设置更为合理?
-
CPU密集型
根据电脑配置(处理器数量)设置maximumPoolSize,电脑几核就设置几,保持CPU效率更高,多的线程可以在组赛队列中等待。
Runtime.getRuntime().availableProcessors()可以获取当前系统的核心数。
-
IO密集型
判断程序中有多少任务是存在大量IO开销的,那么我们设置的maximumPoolSize要大于这个数。
并发编程必备技能
四大函数式接口
函数式接口:提供了lambda表达式和方法引用的目标类型的接口,能够简化编程模型,在jdk8的框架底层被大量运用,接口使用@FunctionalInterface修饰
四大函数四接口,即函数式接口中的最原生的四个接口,其他接口都可以归纳为组合接口
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FBEgWYSY-1626429213702)(JUC并发编程.assets/image-20210712155439372.png)]
Function
函数型接口,表示接受一个参数并产生结果的函数。
这是一个functional interface的功能方法是apply(Object)
。
//.apply()输入object返回+123的string
Function function = x -> x+"123"; //等同(x) -> {return x+"123";};
System.out.println(function.apply(0)); //输出0123
Predicate
表示一个参数的谓词(布尔值函数)
理解:断定型接口,接受一个参数返回一个布尔值。
这是一个functional interface的功能方法是test(Object)
。
//.test()判断Object是否为空
Predicate predicate = x -> !ObjectUtils.isEmpty(x);
System.out.println(predicate.test("")); //输出false
Supplier
代表结果供应商,没有要求每次调用供应商时都会返回新的或不同的结果。
理解:供给型接口,只有返回,没有输入,调用get()拿到实例
这是一个functional interface的功能方法是get()
。
//.get()返回一个FuctionTest实例
Supplier<SupplierTest> supplier = () -> new SupplierTest();
supplier.get().Print(); //get()后可以使用SupplierTest
class SupplierTest{
public void Print(){
System.out.println("SupplierTest.Print()被调用了");
}
}
Consumer
表示接受单个输入参数并且不返回结果的操作。 与大多数其他功能界面不同, Consumer
预计将通过副作用进行操作。
理解:消费型接口,只有输入,没有返回值
这是一个functional interface的功能方法是accept(Object)
。
Consumer<String> consumer = (str) -> System.out.println(str);
consumer.accept("我是一个消费者"); //输出
仅仅看上面的示例,可能很难看出这几个接口有啥用,但当我们使用jdk 8的一些API时,查看源码,发现底层大量运用了这四个接口,如forEach(Consumer<? super E> action)
适应java8的编程模式
上文提及的函数式接口,似乎没什么用?那再看看Lambda表达式+Stream流式计算+链式编程的代码风格,就有体会了
常规处理都是储存+计算,储存我们使用集合,基本类型,mysql等等,而计算,我们应该交给流计算更为合理。
**Stream流:**支持元素流功能性操作的类,例如集合上的map-reduce转换。
**Lambda表达式:**允许把函数作为一个方法的参数(函数作为参数传递进方法中),上文的四大接口中,均使用了lambda。
**链式编程:**每次返回同一个对象,那么就可以通过不断调用API,一行代码解决所有计算,代码简约而优雅。
查看一个简单的业务需求:
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
/**
* @author Wyhao
* @date 2021/7/13
**/
public class StreamTest {
@Test
public void test(){
User user1 = new User(1,"a",21);
User user2 = new User(2,"b",22);
User user3 = new User(3,"c",23);
User user4 = new User(4,"d",24);
User user5 = new User(6,"e",25);
/**
* 现在按要求取出需要的user
* 1.ID必须是偶数
* 2.年龄必须大于23岁
* 3.用户名转为大写
* 4.用户名倒着排序
* 5.只输出一个用户!
*/
List<User> list = Arrays.asList(user1,user2,user3,user4,user5);
//计算交给流,一行代码解决满足上面五个条件
list.stream().filter(u ->u.getId()%2 ==0) //Steam<User> 留下2,4,6
.filter(u -> u.getAge() >23) //Steam<User> 留下4,6
.map(u -> u.getName().toUpperCase()) //Steam<String> 转大写
.sorted((name1,name2) -> name2.compareTo(name1)) //Steam<String> String倒叙
.limit(1) //Steam<String> 分页
.forEach(System.out::println); //Steam<String> 输出
}
}
class User{
private int id;
private String name;
private int age;
public User() {
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public User(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
}
五个需求一条代码就完成了,是不是很简约呢?现在我们分析一下Stream流常用的几个API
Modifier and Type | Method and Description |
---|---|
boolean | anyMatch(Predicate<? super T> predicate) 返回此流的任何元素是否与提供的谓词匹配。 |
Stream<T> | filter(Predicate<? super T> predicate) 返回由与此给定谓词匹配的此流的元素组成的流。 |
void | forEach(Consumer<? super T> action) 对此流的每个元素执行操作。 |
Stream<T> | limit(long maxSize) 返回由此流的元素组成的流,截短长度不能超过 maxSize 。 |
<R> Stream<R> | map(Function<? super T,? extends R> mapper) 返回由给定函数应用于此流的元素的结果组成的流。 |
Stream<T> | sorted(Comparator<? super T> comparator) 返回由该流的元素组成的流,根据提供的 Comparator 进行排序。 |
Stream<T> | parallel() 返回并行的等效流。 可能返回自身,因为流已经是并行的,或者因为底层流状态被修改为并行。这是一个中间操作 |
可以发现API的参数多是函数式接口,如果不明白函数式接口的使用,这里就看不明白了。需要着重了解一下parallel()
并行流,合适运用能够让代码质量有质的提升。
异步回调
在上文Calleble的使用中,提及到一点线程的异步调用,现在了解线程池,能够使用函数式接口后,我们再来回顾这个问题。
CompletableFuture简单使用
在Calleble使用中,我们使用FutureTask也能实现异步回调,但java1.5的FutureTask,功能不够强大且很难满足辅助的并行场景,其计算完成后才能检索结果, 如果计算尚未完成, get方法将阻塞。 一旦计算完成,就不能重新开始或取消计算等。
java1.8新增的CompletableFuture,与FutureTask一样,是继承Future的实现类,结合了函数式接口,lambda表达式等java1.8的新特性,充分考虑并行开发的需求,相比FutureTask拥有更多强大的api,且使用简单,代码优雅。
现在以小明进餐厅吃饭为例,使用CompletableFuture代码实现:
import org.junit.jupiter.api.Test;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
/**
* @author Wyhao
* @date 2021/7/14
**/
public class FutureTest {
@Test
public void test() {
TestUtils.printTimeAndMessage("小明进入餐厅");
TestUtils.printTimeAndMessage("小明点了番茄炒鸡蛋盖饭");
//需要一个厨师开始做菜
CompletableFuture<String> future = CompletableFuture.supplyAsync(new Cook()::make);
TestUtils.printTimeAndMessage("小明开始打王者");//小明不需要白白等待厨师做菜,可以干自己的事情,即异步调用,不需要等待餐厅的同步返回
TestUtils.printTimeAndMessage(future.join());//future.join(),future完成时返回结果值
}
}
class Cook{
public String make(){
TestUtils.printTimeAndMessage("厨师开始做菜");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
TestUtils.printTimeAndMessage("菜做好了,打饭上菜");
return "请用餐";
}
}
class TestUtils {
public static void printTimeAndMessage(String message) {
Date date = new Date(); //获取时间
System.out.println(new SimpleDateFormat("HH:mm:ss").format(date)+":"+message);
}
}
输出:
12:51:18:小明进入餐厅
12:51:18:小明点了番茄炒鸡蛋盖饭
12:51:18:厨师开始做菜
12:51:18:小明开始打王者 //这里小明打王者,和厨师做菜,可以认为是同时开始的,不需要等待。
12:51:19:菜做好了,打饭上菜
12:51:19:请用餐
CompletableFuture高效使用
上文小明只点了一道菜,就只需要等一个厨师做完就行,如果他点了很多道菜呢?此外我们需要考虑,做菜和打饭上菜,不应该都是厨师完成,还应该有一个服务员的角色,且服务员应该在厨师做完菜再行动,考虑餐饮店实际,只有厨师两个,服务员一个。再分析一下当前需求,多个厨师一个服务员同时行动,但需要保证服务员再厨师完成后才行动,结合我们上文中所学的所有知识,线程池,java8新特性,来实现这个问题:
package jucTrain;
import org.junit.jupiter.api.Test;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.concurrent.*;
import java.util.stream.Collectors;
/**
* @author Wyhao
* @date 2021/7/14
**/
public class FutureTest {
@Test
public void test() {
//店里有厨师两名
ExecutorService cookPool = Executors.newFixedThreadPool(2);
//店里有服务员一名
ExecutorService waiterPool = Executors.newFixedThreadPool(1);
TestUtils.printTimeAndMessage("小明进入餐厅,开始点餐");
List<String> foods = Arrays.asList("番茄炒鸡蛋", "水煮肉片", "油闷大虾", "红烧肉", "小炒时蔬");
TestUtils.printTimeAndMessage("小明点了"+foods);
//厨师服务员开始行动
List<CompletableFuture<String>> futures = new ArrayList<>();
foods.forEach(food -> { //遍历处理每一道菜
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> new Cook().make(food),cookPool) //开启异步,厨师开始做菜
.thenComposeAsync(dish -> CompletableFuture //厨师做完菜,服务员上菜,dish是supplyAsync()中返回的
.supplyAsync(() ->new Waiter().make(dish),waiterPool)
);
futures.add(future); //每道菜的任务放入futures中
});
TestUtils.printTimeAndMessage("小明开始打王者");
//所有做菜任务都完成了,得到反馈
List<String> restaurantFeedback = futures.stream().map(CompletableFuture::join).collect(Collectors.toList());
restaurantFeedback.forEach(TestUtils::printTimeAndMessage);
TestUtils.printTimeAndMessage("小明退游戏开始吃饭了");
}
}
class Cook{
public String make(String food){
TestUtils.printTimeAndMessage(Thread.currentThread().getName()+"厨师开始做"+food);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
TestUtils.printTimeAndMessage(Thread.currentThread().getName()+"厨师"+food+"做好了");
return food;
}
}
class Waiter{
public String make(String cookReturn){
TestUtils.printTimeAndMessage(Thread.currentThread().getName()+"服务员开始上菜:"+cookReturn);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
TestUtils.printTimeAndMessage(Thread.currentThread().getName()+"服务员完成上菜:"+cookReturn);
return "这是"+cookReturn+"请用餐";
}
}
class TestUtils {
public static void printTimeAndMessage(String message) {
Date date = new Date(); //获取时间
System.out.println(new SimpleDateFormat("HH:mm:ss").format(date)+":"+message);
}
}
输出:
16:04:16:小明进入餐厅,开始点餐
16:04:16:小明点了[番茄炒鸡蛋, 水煮肉片, 油闷大虾, 红烧肉, 小炒时蔬]
16:04:16:小明开始打王者
16:04:16:pool-1-thread-2厨师开始做水煮肉片
16:04:16:pool-1-thread-1厨师开始做番茄炒鸡蛋
16:04:18:pool-1-thread-1厨师番茄炒鸡蛋做好了
16:04:18:pool-1-thread-2厨师水煮肉片做好了
16:04:18:pool-1-thread-2厨师开始做油闷大虾
16:04:18:pool-1-thread-1厨师开始做红烧肉
16:04:18:pool-2-thread-1服务员开始上菜:番茄炒鸡蛋
16:04:19:pool-2-thread-1服务员完成上菜:番茄炒鸡蛋
16:04:19:pool-2-thread-1服务员开始上菜:水煮肉片
16:04:20:pool-1-thread-2厨师油闷大虾做好了
16:04:20:pool-1-thread-1厨师红烧肉做好了
16:04:20:pool-1-thread-1厨师开始做小炒时蔬
16:04:20:pool-2-thread-1服务员完成上菜:水煮肉片
16:04:20:pool-2-thread-1服务员开始上菜:油闷大虾
16:04:21:pool-2-thread-1服务员完成上菜:油闷大虾
16:04:21:pool-2-thread-1服务员开始上菜:红烧肉
16:04:22:pool-1-thread-1厨师小炒时蔬做好了
16:04:22:pool-2-thread-1服务员完成上菜:红烧肉
16:04:22:pool-2-thread-1服务员开始上菜:小炒时蔬
16:04:23:pool-2-thread-1服务员完成上菜:小炒时蔬
16:04:23:这是番茄炒鸡蛋请用餐
16:04:23:这是水煮肉片请用餐
16:04:23:这是油闷大虾请用餐
16:04:23:这是红烧肉请用餐
16:04:23:这是小炒时蔬请用餐
16:04:23:小明退游戏开始吃饭了
(这里可以不看)这里有个有趣的问题,总共完成时间7秒,加以分析,来做个简单计算,五道菜,做菜需要2s,上菜需要1s,如果厨师和服务员够多,3秒结束,奈何小本生意,2个厨师,最多一个厨师做三个菜6秒,或者看服务员,五道菜上菜一共五秒,等待厨师2秒,一共7秒。小店做多少菜工作效率最高呢?
CompletableFuture诸多的API,这里就不再一一说明了,可根据需求查看源码
从原理上学习并发
JAVA内存模型JMM
JMM:java内存模型,一种概念,约定,存在八种操作
参考博客: JMM:内存模型以及8种原子操作_starbxx的博客-CSDN博客
从JMM出发理解Volatile
Volatile:jvm提供的轻量级的同步机制
特性:
-
保证可见性 --使用它的原因
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
-
不保证原子性 --使用它需要注意
原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
而volatile无法保证这一点,例如一个自增操作,应该包括读取主存,赋值,写入主存,这应该是一个原子性操作,但volatile修饰的变量,无法保证这个原子性。对于这个缺陷,在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,可以用来防止这一问题。要了解atomic的实现原理,可以学一学CAS(比较并交换)和ABA(交换后又换回)问题
-
禁止指令重排
一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的,这就是指令重排。查看这样一个实例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xOKEV4cw-1626429213704)(JUC并发编程.assets/image-20210715162333888.png)]处理器在执行上面代码时,可能有多种执行顺序,并不一定是1234的顺序。但一定不可能出现 语句2 -> 语句1 -> 语句4 -> 语句3的顺序,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。
指令重排在单线程中不会带来问题,但在并发线程中,处理器无法考虑多线程之间的数据依赖性,当发生指令重排时,就可能导致程序出错
而被volatile修饰的变量,将不会发生指令重排
死锁排查思路
死锁:持有资源的同时试图获取对方的锁
1.使用jps -l
定位获取进程号
2.使用jstack 进程号
找出死锁原因
found 1 deadlock
如果不是在开发中
- 查看日志
- 堆栈信息