JUC就是java.util.concurrent包,俗称java并发包
简单来说,这个学习
方法就三个点,是什么?为什么?怎么办?
JUC下面有两个子包,分别是atomic和locks,atomic即原子类,
经常用到的类AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference、AtomicStampedReference等,
打开源码包我们看到其实这些方法是调用Unsafe类提供compareAndSwapObject、compareAndSwapInt、compareAndSwapLong等方法
我们使用的volatile只保证了可见性,和禁止重排序,没有保证原子性,
原子变量:atomic包下提供了常用的原子变量 ,
1.里面的值使用了volatile修饰
2.使用了CAS算法保证了数据的原子性
cas算法是硬件对于并发操作共享数据的支持,是一种特殊指令,是无锁的非阻塞算法的实现
包含三个操作
1.内存值
2.预估值
3.更新值
只有当内存值等于预估值是,也就是没有被其他线程修改时,将新值赋值给内存值,否则不做任何操作
补充:消费者和生成者模式的虚假唤醒问题,使用while解决,就是多个消费者线程或生产者线程在wait处停留,使用while循环判断
Unsafe类又是什么呢
他的全称是sun.misc.Unsafe,这个类是属于JNI的类,Unsafe里面的native方法直接操作内存
compareAndSwap
其实就是调用了CPU的系统原语,系统原语简单来说就是CPU的一个不能再分割指令,
所以比较和执行是在CPU原子操作的,而不是像之前一样,从工作内存考呗到执行引擎,
阻塞队列
BlockingQueue
在多线程邻域,在某些情况下会会挂起线程,阻塞,一旦条件满足,线程会被唤醒
其实这些我们可以使用wait,await,notify等可以实现
那么为什么需要BlockingQueue
好处是我们不需要关心什么时候阻塞,什么时候唤醒
继承图
ArrayBlockingQueue:右数组结构组成的有界阻塞队列 ****
LinkedBlockQueue:由链表结构组成的有界(默认大小是Integer.MAX_VALUE)阻塞队列 ****
priorityBlockQueue:支持优先级排序的无界阻塞队列
DelayQueue:使用优先级队列实现的延迟无界阻塞队列
SynchronousQueue:不存储元素的阻塞队列,单个元素的队列 ***
LinkedTransferQueue:由链表组成的无界阻塞队列
LinkedBlockingDeueue 由链表组成的双向阻塞队列
重点这两个方法
ArrayBlockingQueue:右数组结构组成的有界阻塞队列
LinkedBlockQueue:由链表结构组成的有界(默认大小是Integer.MAX_VALUE)阻塞队列
BlockingQueue接口方法
当队列满时,在插入会有异常 add()
当队列为空时,remove会由异常 remove()
插入方法是,返回布尔值 offer()
移除方法,成功则返回元素,否则返回null poll()
当阻塞队列满时,生产继续往队列put元素时,队列会阻塞生产线程添加, put()
当取阻塞队列空时,消费则从队列take()元素,队列会一直阻塞消费线程知道队列可用 take()
当阻塞队列满时,队列会阻塞生产线程一定时间,超过限时后生产线程退出 。 就是在方法里面添加时间的参数
方法分类
补充线程池:
Executors.newCachedThreadPool() 缓存,自动扩张,最大线程数是Integer.Max_value,核心线程数是0,所有线程空闲会回收
Executors.newFixedThreadPool(4)知道个数 ,核心线程数和最大线程是传入的固定值,而且一直存活,存活时间为0,表示一直都不会回收
Executors.newSingleThreadExecutor() 单个线程的线程池,核心和最大线程数都为1,而阻塞队列是Integer.Max_value,并且不会回收,存活时间为0
Executors.ScheduledThreadpool() 做定时任务的
线程池的底层实现是使用了ThreadPoolExecutor类
上面三个阻塞队列都使用了不同的实现,
构造方函数时使用到了BlockingQueue<Runnable> workQueue 阻塞队列,
ThreadPoolExecutorx相关的7个参数,其中上面三个方法创键ThreadPoolExecutorx时就携带了5个参数,而当取去创建对象时又调用了自身的7参构造方法
Executors类里的
public static ExecutorService newFixedThreadPool(int nThreads) { 其中一个列子newFixedThreadPool
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()); 使用到的阻塞队列
}
。。。。。。。
ThreadPoolExecutorx类里的
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
调用下面的构造
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
{........}
corePoolSize:线程池的常驻核心线程数
maximumPoolSize:线程池中能容纳同时执行的最大线程数,必须大于1
keepAliveTime:多余的空闲线程的存活时间,当线程中数量超过corePoolSize时,当空闲时间达到keepAliveTime时
多余线程会被销毁知道剩余corePoolSize个线程
unit: keepAliveTime的单位时间
workQueue:任务队列,被提交但尚未被执行的任务
threadFactory:表示生成线程池中工作线程的线程工厂用于创建线程,一般默认即可
handler:拒绝策略,表示当队列满了,并且工作线程大于等于线程的最大线程数maximumPoolSize时
如何来拒绝请求执行的runnable的策略
线程底层工作原理
底层原理:
在创建了线程后,开始等待请求
当调用execute()方法添加任务时,线程会如下判断:
1.判断运行的线程数是否小于corepoolsize,了,没有则创建线程执行任务;
2.如果运行的线程数大于等于corepoolsize,则将任务添加到队列里;
3.如果阻塞队列满了,且运行的线程数小于maximumpoolsizee,那么还是继续创建线程执行任务
4.如果队列满了且正在运行的线程数量大于或等于maximumpoolsizee,那么线程会启动饱和拒绝策略来执行
当一个线程完成任务后,会从队列中取下一个任务执行
当线程无事可做时,超过一定的时间keepAliveTime,线程会判断:
如果当前运行的线程大于corepoolsize,那么这个线程会被停掉
所以线程的所有任务完成后,最终会收缩到corepoolsize的大小
线程池的拒绝策略
拒绝策略是什么
jdk内置的拒绝策略
以上内置拒绝策略都实现了RejectedExecutionHandle接口
在工作中 单一的/固定的/可变的三种创建线程池的方法哪个用的多
答案是都不用 (强制) 使用自定义的
线程池不允许使用Executors去创建,而是通过threadpoolExecutor的方式,规避资源耗尽的风险
固定和单一的请求队列长为Integer.MAX_VALUE,会堆积大量的请求,导致oom
可变的和ScheduledThreadpool运行创建的线程数是Integer.MAX_VALUE,可能会大量的创建线程,导致oom
自定义线程池时,一般我们使用默认的创建线程的工厂,
拒绝策略有四中
AbortPolicy(默认):直接抛出RejectedExecutedException异常阻止系统正常运行
CallerRunsPolicy():”调用者运行”一种机制,不会放弃任务,直接调用(注意:run方法来调用)来同步调用,不会进线程池
DiscardOldestpolicy():抛弃队列中等待最久的任务,然后把当前任务加入到任务队列中,尝试再次提交当前任务
DiscardPolicy():该策略默默的丢弃无法处理的任务,不予任何处理也不抛异常,(丢的是当前这个进来的任务)
这四种策略是threadPoolExcetor的静态内部类
那么这个最大线程数应该如何设置才是最好的
我们知道线程是被cpu调度的,所以,cpu有几核,我们就使用几个,加个1
可用使用Runtime.getRuntime().availableProcessors()获取本机的cpu核数
注意线程队列的大小我们也需要进行设置,
举例使用该 LinkedBlockQueue队列的话,默认是Integer.MAX_VALUE ,那么是非常不好的
当然我们使用其他队列的时候,先看一下源码,在进行一下设置
线程池的最大容量如何设置
1。cpu密集型 就是几核的cpu 获取Random.getrandom.alivablepro...()获取
2.io 密集型 。。。
使用的时候我们使用的是execute()将任务放到里面去 ,没有返回值
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
其实效果都一样,只不过有返回值 ,注意,submit内部会有异常处理,而execute是没有异常处理
当使用的是callable时,由于是可以抛异常的,所以在使用submit是可以被调用的一方捕捉异常的,但是execute是捕捉不到的
补充: 当时有list集合时,需要线程线程安全的,
解决方案 ,使用voctor ,效率低,底层使用的是synchronized
解决方案:使用Collections.synchronizedlisr(new Arraylist()) 使用工具类,原理同上
解决方案:使用copyOnWriteArraylist 底层使用的是Reentrantlock锁
也就是删除,添加,修改使用了这个锁 ,底层add操作时通过复制数组赋值和arraylist一样
set的线程安全
使用collectios.synchronizedSet(new HashSet) 工具类
使用copyOnWriteArraySet()
其实底层就是使用的copyOnWriteArraylist()
只不过一个是可重复,一个不可重复,
补充:线程的四种实现方式
Thread runnable 线程池 callable
callable接口似于runnable 但有返回值,可以抛异常,覆写的方法是call,只有这一个方法,不是run
返回值是类型是泛型
callable使用
首先实现该接口
Runnable接口
子接口RunnableFuture
实现FutureTask 实现了RunnableFuture
构造
public FutureTask(Runnable runnable, V result)
public FutureTask(Callable<V> callable)
所以就可以这样
class mycallable extends Callable<Integer>{ Integer call{....}}
mycallable my=new mycallable()
FutureTask fu=new FutureTask(my);
Thread(fu,"线程名") 和runable一样
通过get可以获取返回值,fu.get()
阻塞就是只有当线程执行完后,才会访问到返回值,如果先执行了返回值,则阻塞调用了get的这个线程
当多线操作同一任务时,就会有缓存的现,也就是返回会只执行一次,因为有缓存
cancel()方法
取消你已提交给执行者的任务,使用Future接口的cancel()方法。
据cancel()方法参数和任务的状态不同,这个方法的行为将不同:
1、如果这个任务已经完成或之前的已经被取消或由于其他原因不能被取消,
那么这个方法将会返回false并且这个任务不会被取消。
2、如果这个任务正在等待执行者获取执行它的线程,那么这个任务将被取消而且不会开始他的执行。
如果这个任务已经正在运行,则视方法的参数情况而定。
cancel()方法接收一个Boolean值参数。
如果参数为true并且任务正在运行,那么这个任务将被取消。
如果参数为false并且任务正在运行,那么这个任务将不会被取消。
因此在于get方法一起使用时要注意了,get是方法任务执行完后才会掉用的
```java
......
ca c = new ca();
FutureTask fuu = new FutureTask(c);
new Thread(fuu, "A").start();
new Thread(fuu, "i").start();
try {
System.out.println(fuu.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
......
class ca implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("f");
TimeUnit.SECONDS.sleep(1);
return 3;
}
}
辅助工具类
CountDownLatch
CyclicBarrier
Semaphore
闭锁: CountDownLatch:在完成某些运算时,只有其他所有线程的运算全部完成,当前运算才继续执行
减法计数器
public class demo {
public static void main(String[] args) {
CountDownLatch countDownLatch=new CountDownLatch(3);设置个数,必须执行完这些线程才会有接下来的操作,
ss s=new ss(countDownLatch);
for (int i=0;i<4;i++)
new Thread(s).start();
try {
countDownLatch.await(); 当count当于0时,就不睡眠
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程");
}
}
class ss implements Runnable{
private CountDownLatch coun;
public ss(CountDownLatch coun){
this.coun=coun;
}
@Override
public void run() {
for (int i=0;i<10;i++){
System.out.println(i);
}
coun.countDown(); 运行完一个线程,则数量减一,减少锁存器的计数
}
}
由于是单线程,所以效率低,所以我们一般是进行一些总量的计算的时候用到
底层是CAS的方式
new CountDownLatch(4)。其实也就相当于new Sync(4),相当于setState(4)。
tryAcquireShared方法其实就是判断一下当前计数器的值,是否为0了,如果为0的话返回1(返回1的时候,就表明当前线程可以继续往下走了
tryReleaseShared方法就是利用CAS的方式,我们实际上每次调用countDownLatch.countDown()方法的时候,最终都会调到这个方法
调用countDownLatch.await()的时候,做了些什么。是怎么阻塞线程的,调用了sync.acquireSharedInterruptibly(1);
判断当前线程是否中断状态,如果不是返回false,是则返回true,并且设置是否重置中断状态
CountDownLatch内部维护一个计数器(父类的int state),主线程先执行await方法,如果此时计数器大于0,则阻塞等待。当一个线程完成任务后,计数器值减1。直到计数器为0时,表示所有的线程已经完成任务,等待的主线程被唤醒继续执行。
CountDownLatch实现主要基于java同步器AQS
主线程执行await方法,先执行tryAcquireShared方法尝试获取锁,如果此时计数器state != 0,返回-1,会创建一个节点,构建阻塞队列的双向链表,,加入到AQS阻塞队列,并同时把当前线程挂起。,
判断计数器是计数完毕,未完毕则把当前线程加入阻塞队列
当我们调用countDownLatch.coundown()方法的时候,会对计数器进行减1操作,AQS内部是通过释放锁的方式,对state进行减1操作,当state=0的时候证明计数器已经递减完毕,此时会将AQS阻塞队列里的节点线程全部唤醒。
该类主要通过countDown()和await()两个方法实现功能的,首先通过建立CountDownLatch对象,并且传入参数即为count初始值。如果一个线程调用了await()方法,那么这个线程便进入阻塞状态,并进入阻塞队列。如果一个线程调用了countDown()方法,则会使count-1;当count的值为0时,这时候阻塞队列中调用await()方法的线程便会逐个被唤醒,从而进入后续的操作。
CyclicBarrier
加法计数器
两构造
public CyclicBarrier(int parties, Runnable barrierAction)
public CyclicBarrier(int parties)
举例 当数加到parties就执行任务,无参则就不执行
public static void main(String[] args) {
CyclicBarrier cyclicBarrier=new CyclicBarrier(6,()->{
System.out.println("我成功了,500万大奖是我的");
});
for (int i=0;i<6;i++){
final int j=i;
new Thread(()->{
System.out.println("第"+j+"个线程");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
只有当有线程启动,数量达到6时,才会唤醒线程
底层使用ReentrantLock锁的方式
栅栏(Barrier)类似闭锁,他能阻塞一组线程直到某个事件发生.栅栏与闭锁的关键区别在于,所有线程必须同时到达栅栏位置,才能继续执行.
. 闭锁用于等待事件,而栅栏用于等待其他线程
当所有线程都到达了栅栏位置,那么栅栏将打开,此时所有线程都会被释放,栅栏可以reset以便于下一次复用 。
CyclicBarrier 的构造函数public CyclicBarrier(int parties,Runnable barrierAction)可以传递一个Runnable对象,当成功通过栅栏时会被
执行,且由最后一个进入 barrier 的线程执行。
如果await的调用超时,或者await阻塞的线程被打断,那么栅栏就被认为是打破了,所有阻塞的await调用都将终止并抛出BrokenBarrierException.如果成
功地通过栅栏,那么await将为每个线程返回一个唯一的到达索引号.
Semaphore
计数信号量
Semaphore是一种计数信号量,用于管理一组资源,内部是基于AQS的共享模式。它相当于给线程规定一个量从而控制允许活动的线程数。
用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量.计数信号量还可以用来实现某种资源池,或者对容器施加边界。
Semaphore中管理着一组虚拟的许可(Permit),许可的初始数量可通过构造函数来指定.在执行操作时可以首先获得许可(只要还有剩余的许可),并在使用以后
释放许可.如果没有许可,那么acquire将阻塞直到有许可(或者直到被中断或者操作超时).release方法将返回一个许可信号量
计数信号量的一种简化形式是二值信号量,即初始值为1的Semaphore,二值信号量可以用做互斥体(mutex),并具备不可重入的加锁语义:谁拥有这个唯一许可,
谁就拥有了互斥锁
可以使用Semaphore将任何一种容变成有界阻塞容器
public static void main(String[] args) {
Semaphore s=new Semaphore(3);
for (int i=0;i<8;i++){
final int t=i;
new Thread(()->{
try {
s.acquire(); //得到
System.out.println("获取车位"+t);
TimeUnit.SECONDS.sleep(3);
System.out.println("离开车位"+t);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
s.release(); 释放
}
}).start();
}
}
s.acquire();获得,假设满了,等待,等到释放为止
s.release():释放,会将当前的信号量释放+1,然后唤醒等待的线程
作用:用于资源共享的互斥,并发限流
异步回调
CompletableFuture
没有返回值
CompletableFuture<Void> co=new CompletableFuture().runAsync(()->{
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
});
System.out.println(Thread.currentThread().getName()); //此时依然可以执行
co.get();
此时main会先输出
//有返回值
static void test2() throws Exception {
CompletableFuture fu=CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName());
int i=9/0;
return 10;
});
//回调
fu.whenComplete((t,g)->{
System.out.println("t:"+t); //正常返回的值
System.out.println("g:"+g);//错误信息,没错误,返回null
}).exceptionally((e)->{
System.out.println(e); //异常的信息
return 33;
}).get();
死锁
如
public class 死锁 {
public static void main(String[] args) {
new Thread(new sisuo("a","b")).start();
new Thread(new sisuo("b","a")).start();
}
}
class sisuo implements Runnable{
public String s1;
public String s2;
public sisuo(String s1, String s2) {
this.s1 = s1;
this.s2 = s2;
}
public void run(){
synchronized (s1){
try {
System.out.println(Thread.currentThread().getName()+s1);
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2){
System.out.println(Thread.currentThread().getName()+s2);
}
}
}
}
死锁检查
idea的Terminal的命令终端使用jps -l 查看程序文件的端口号
然后使用 jstack 端口号 来查看信息
ForkJoinPool
也是线程池 ForkJoinPool 不是为了替代 ExecutorService,而是它的补充,在某些应用场景下性能比 ExecutorService 更好。
主要用于实现“分而治之”的算法
适合的是计算密集型的任务,如果存在 I/O,线程间同步,sleep() 等会造成线程长时间阻塞的情况时,最好配合使用 ManagedBlocker。
public class Forkjoindemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
myy m=new myy(0,100);
ForkJoinPool f=new ForkJoinPool();
ForkJoinTask<Integer> submit = f.submit(m);
System.out.println(submit.get());
f.shutdown();
}
}
class myy extends RecursiveTask<Integer> {
private final int H = 10;
private int sum = 0;
private int beg=0, end=0;
public myy(int beg, int end) {
this.beg = beg;
this.end = end;
}
@Override
protected Integer compute() {
if (end - beg <= H) {
for (int i = beg; i <= end; i++)
sum =sum+ i;
return sum;
} else {
System.out.println(Thread.currentThread().getName());
int g = (beg + end) / 2;
myy myy1 = new myy(beg, g);
myy myy2 = new myy(g+1, end);
myy1.fork();
myy2.fork();
return sum=myy1.join()+myy2.join();
}
}
}
ForkJoinPool间接的实现了ExectorService接口
ForkJoinTask实现了Future接口
ForkJoinTask一共有3个实现:
* RecursiveTask:有返回值
* RecursiveAction:无返回值
* CountedCompleter:无返回值任务,完成任务后可以触发回调
ForkJoinPool
最大的特点就是分叉(fork)合并(join),将一个大任务拆分成多个小任务,并行执行,再结合工作窃取模式(worksteal)提高整体的执行效率,充分利用CPU资源。
和ThreadpoolExector的区别
ForkJoinPool每个线程都有自己的队列
ThreadPoolExecutor共用一个队列
使用ForkJoinPool可以在有限的线程数下来完成非常多的具有父子关系的任务,比如使用4个线程来完成超过2000万个任务。但是使用ThreadPoolExecutor是不可能的
因为ThreadPoolExecutor中的线程无法选择优先执行子任务,要完成2000万个具有父子关系的任务时,就需要2000万个线程,这样会导致ThreadPoolExecutor的任务队列撑满或创建的最大线程数把内存撑爆直接gg。
ForkJoinPool最适合计算密集型任务,而且最好是非阻塞任务,ForkJoinPool是ThreadPoolExecutor线程池的一种补充,是对计算密集型场景的加强
工作窃取的实现原理
窃取数跟拆分层级和计算复杂度有关
ForkJoinPool类中的WorkQueue正是实现工作窃取的队列
Deque是双端队列(double ended queue缩写),头部和尾部任何一端都可以进行插入,删除,获取的操作,即支持FIFO(队列)也支持LIFO(栈)顺序
deque接口的实现最常见的是LinkedList
工作窃取模式主要分以下几个步骤:
.每个线程都有自己的双端队列
.当调用fork方法时,将任务放进队列头部,线程以LIFO顺序,使用push/pop方式处理队列中的任务
.如果自己队列里的任务处理完后,会从其他线程维护的队列尾部使用poll的方式窃取最老的任务,以达到充分利用CPU资源的目的
.从尾部窃取可以减少同原线程的竞争
.当队列中剩最后一个任务时,通过cas解决原线程和窃取线程的竞争
工作窃取便是ForkJoinPool线程池的优势所在,在一般的线程池比如ThreadPoolExecutor中,
如果一个线程正在执行的任务由于某种原因无法继续运行,那么该线程会处于等待状态,
包括singleThreadPool,fixedThreadPool,cachedThreadPool这几种线程池。
而在ForkJoinPool中,那么线程会主动寻找其他尚未被执行的任务然后窃取过来执行,减少线程等待时间。
上面我们讲的CompletableFuture异步回调future,内部使用的线程池也是ForkJoinPool
新任务通过Fork操作被添加到线程的队列中,每个线程总是处理“最后添加到队列中的任务
空闲的线程将从其它线程的队列中“窃取”任务来执行;一个线程总是从其他线程中窃取“最老”的任务。
双端队列的操作:push()/pop()仅在其所有者工作线程中调用,poll()是由其它线程窃取任务时调用的;
内部基于“工作窃取”算法实现
自己从队列头存取任务,其它线程从尾部窃取任务;
ForkJoinPool要先执行完子任务才能执行上一层任务,所以ForkJoinPool适合在有限的线程数下完成有父子关系的任务场景
比如:快速排序,二分查找,矩阵乘法,线性时间选择等场景,以及数组和集合的运算。
ForkJoinPool构造方法可以设置线程数量,默认不写,空参,则会获取当前计算机cpu的内核数作为线程数量
当计算的数字非常大的时候,优势才能体现出来。也就是说,如果你的计算比较小,或者不是CPU密集型的任务,不太建议使用并行处理
fork()方法类似于线程的Thread.start()方法,但是它不是真的启动一个线程,而是将任务放入到工作队列中。
join():类似于线程的Thread.join()方法,但是它不是简单地阻塞线程,而是利用工作线程运行其它任务。当一个工作线程中调用了join()方法,
它将处理其它任务,直到注意到目标子任务已经完成了。获得返回值。
ForkJoinPool 的每个工作线程都维护着一个工作队列(WorkQueue)
这是一个双端队列(Deque),里面存放的对象是任务(ForkJoinTask)。