JUC概述
JUC是java.util.concurrent工具包的简称,是一个处理线程的工具包。
进程和线程
进程:系统中正在运行的一个应用程序,程序一但运行就是进程,进程是资源分配的最小单位。
线程:系统分配处理器时间资源的基本单元,进程之间独立执行的一个单元执行流。线程是程序执行的最小单位。
线程状态
NEW 新建
RUNNABLE 就绪
BLOCKED 阻塞
WAITING 等待
TIME_WAITING 过时不候
TERMINATED 终止
wait/sleep的区别
sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能使用
sleep不会释放锁,也不需要占用锁。wait会释放锁,调用的前提是当前线程占有锁(代码在synchronized中)
都可以被interrupted方法中断
并发和并行
并发:同一时刻多个线程在访问同一资源
并行:多项工作同时进行,然后再汇总
管程 (Monitor 监视器)
是同步机制,保证同一时间,只有一个线程访问代码、数据。
jvm同步是基于进入和退出,使用管理对象实现的Monitor来对临界区进行加锁和解锁。
用户线程和守护线程
用户线程:自定义线程。主线程结束,用户线程还在运行的话,jvm存活。
守护线程:运行在后台,例如垃圾回收。没有用户线程,主线程结束的话,jvm结束。
Lock接口
synchronized关键字
synchronized是java中的关键字,是一种同步锁,修饰的对象有一下几种。
1、修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是{},作用的对象是调用这个代码块的对象。
(对于同步方法块,锁是synchronized括号里配置的对象)
2、修饰一个方法,被修饰的方法称为同步方法,其作用是整个方法,作用的对象是调用整个方法的对象。
(对于普通同步方法,锁是当前实例对象)
3、修饰一个静态方法,作用的范围是整个静态方法,作用的对象是类的所有对象。
(对于静态同步方法,锁是当前类的Class对象)
4、修饰一个类,作用范围是synchronized后面括号括起来的部分,作用对象是类的所有对象。
创建线程
1、继承Thread类(少用)
2、实现Runnable接口
// 方法一:
new Thread(new Runnable() {
@Override
public void run() {
// 执行代码
}
},"AA").start();
// 方法二:
new Thread(() -> {
//执行代码
},"BB").start();
3、使用Callable接口
如果使用runnable创建的线程,当run完成后,无法获得线程返回的结果。因此提供了Callable接口。
Class MyThread1 implements Runnable{
@Override
public void run() {
}
}
Class MyThread2 implements Callable{
@Override
public Object call() throw Exception{
return null;
}
}
public class Demo1
public static void main(String[] args){
new Thread(new MyThread1(),"AA").start();
// Runnable接口有实现类FutureTask,FutureTask构造可以传递Callable
FutureTask<Object> futureTask1 = new FutureTask<>(new MyThread2());
// 通过lam表达式简化
FutureTask<Object> futureTask2 = new FutureTask<>( () -> {
return null;
});
// 创建线程
new Thread(futureTask2,"nana").start();
while (!futureTask2.isDone()){
sout("waiting....")
}
// 调用FutureTask的get方法
sout(futureTask2.get());
}
}
4、使用线程池
Lock与Synchronized区别
1、Lock不是java语言内置的,synchronized是java语言的关键字,是内置的。Lock是一个类,通过这个类可以实现同步访问(private final ReentrantLock lock = new ReentrantLock();lock.lock(); lock.unlock(); );
2、Lock和synchronized的不同,采用synchronized不需要用户去释放锁,系统会自动让线程释放对锁的占用,而Lock必须用户去手动释放锁,如果没有主动释放锁,就有可能导致死锁现象。
3、Lock可以让等待锁的线程响应中断,而synchronized不行,使用synchronized时,等待线程会一直等待下去,不能够中断响应
4、通过Lock可以知道有没有成功获取锁,而synchronized无法办到
5、Lock可以提高多个线程进行读操作的效率
如果竞争资源不激烈,Lock和synchronized性能是差不多的。当竞争资源非常激烈时,Lock的性能远远优于synchronized。
多线程编程步骤
第一步:创建资源类、在资源类创建属性和操作方法(锁也在这加)
(第一步里干了三件事:1、判断(需要放到while中,否则会出现虚假唤醒) 2、干活 3、通知)
第二步:创建多个线程,调用资源类的操作方法
解决ArrayList线程不安全问题
方案一:Vector。将List<String> list = new ArrayList<>();改成List<String> list = new Vector<>();
方案二:Collections。List<String> list = Collections.synchronizedList(new ArrayList<>());
方案三:CopyOnWriteArrayList。(常用)写时复制技术。读的时候支持并发读,写的时候独立写。先复制一个和原来相同的内容,然后写入新的内容,内容写完后,与原来内容做一个合并(覆盖),后续读的时候直接读新的。好处:兼顾了并发读,还能写,避免并发修改的异常。List<String> list = new CopyOnWriteArrayList<>();
Hashset也会遇到线程不安全的问题,可以使用CopyOnWriteArraySet<>();
Hashmap也会遇到这个问题,可以使用ConcurrentHashMap<>();
多线程锁
公平锁和非公平锁
非公平锁:线程饿死,效率高
公平锁:效率相对低,但每个线程都有机会。
private final ReentrantLock lock = new ReentrantLock(true);
可重入锁
synchronized(隐式)和Lock(显示)都是可重入锁。
又称为:递归锁
最外层的锁放开后,内层的所有锁都放开。相当于回家开了大门后,卧室也可以随时去。
synchronized实现可重入锁
Object o = new Object();
new Thread(() -> {
synchronized(o){
// sout(外层)
synchronized(o){
// sout(中层)
synchronized(o){
// sout(内层)
}
}
}
},"AA").start();
Lock实现可重入锁
Lock lock = new ReentrantLock();
new Thread( () -> {
try{
lock.lock();
// sout(外层)
try{
lock.lock();
// sout(内层)
}finally{
lock.unlock();
}
}finally{
lock.unlock();
}
},"t1").start();
死锁
两个或两个以上的进程在执行过程中,因为争夺资源而造成一种互相等待的现象。如果没有外力干涉,他们无法再继续执行下去。
产生的原因
1、资源不足
2、进程推进顺序不合适
3、资源分配不当
JUC的辅助类
减少计数CountDownLatch
CountDownLatch类可以设置一个计数器,然后通过CountDown方法来进行减1操作,使用await方法等待计数器不大于0,然后继续执行await方法之后的语句。
CountDownLatch countDownLatch = new CountDownLatch(6);
countDownLatch.countDown();
countDownLatch.await();
循环栅栏CyclicBarrier
一个同步辅助类,允许一组线程互相等待,直到到达某个公共屏障点。神龙等待7颗龙珠集齐才能出现许愿,然后龙珠又重新分散。
private static final int NUMBER = 7;
CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER,()->{
sout("7颗集齐召唤神龙");
});
for (int i=1;i<=7;i++){
new Thread( () -> {
sout(Thread.currentThread().getName() + "星龙珠找到了");
cyclicBarrier.await(); //try
},String.ValueOf(i)).start();
}
信号灯Semaphore
计数信号量。信号量维护了一个许可集,在许可可用前会阻塞每一个acquire,然后再获取该许可,每个release添加一个许可,从而可能释放一个正在阻塞的获取者。
6辆车,停3个车位
Semaphore semaphore = new Semaphore(3);
for(int i=1;i<=6;i++){
new Thread( () -> {
semaphore.acquire(); //try
sout(Thread.currentThread().getName());
// 设置随机停车时间
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
semaphore.release(); //finally
},String.ValueOf(i)).start();
}
读写锁 ReadWriteLock
一个资源可以被多个读线程访问,或者可以被一个写线程访问,但是不能同时存在读和写线程,读写互斥,读读共享。
缺点:
1、有可能造成锁饥饿,一直读,就没办法写,写操作进行不了
2、读的时候不能写,只有读,完成之后才能写。写操作可以读
private ReadWriteLock rwLock = new ReentrantReadWriteLock();
// 写操作前加 写锁
rwLock.writeLock().lock();
//执行写操作
rwLock.writeLock().unlock();
// 读操作前加 读锁
rwLock.readLock().lock();
// 读操作
rwLock.readLock().unlock();
演变过程
本质的过程是锁降级。将写入锁降级为读锁
jdk8中降级步骤:
获取写锁 --> 获取读锁 --> 释放写锁 --> 释放读锁
阻塞队列 BlockingQueue
队列为空,从队列中获取元素的操作会被阻塞,直到其他线程往队列中插入新的元素。
队列为满,从队列中添加元素的操作会被阻塞,直到其他线程从队列中移除一个或多个元素。
ArrayBlockingQueue(常用)
由数组结构组成的有界阻塞队列。内部维护了一个定长数组,以便缓存队列中的数据元素。内部还保存两个整型变量,分别标识着队列的头部和尾部在数组中的位置。
LinkedBlockingQueue(常用)
由链表组成的有界(大小默认为integer.MAX_VALUE)阻塞队列
DelayQueue
使用优先级队列实现的延迟无界阻塞队列。只有当其指定的延迟时间到了,才能从队列中获取到该元素。
PriorityBlockingQueue
支持优先级排序的无界阻塞队列。锁采用公平锁
SynchronousQueue
不存储元素的阻塞队列,里面只有单个元素
LinkedTransferQueue
链表组成的无界阻塞队列
LinkedBlockingDeque
链表组成的双向阻塞队列
ThreadPool线程池
线程池维护着多个线程,避免了处理短时间任务时创建与销毁线程的代价。保证了内核的充分利用,和防止过分调度。线程池的工作是控制运行的线程数量,处理过程将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了线程池有的最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
特点:
1、降低资源消耗
2、提高响应速度
3、提高线程的可管理性
线程池使用方法
创建线程的时候,一般不通过Executors来创建线程,而是通过ThreadPoolExecutor的方法来创建。为了简单才这么写。用Executor返回线程池对象可能会堆积大量的请求,导致OOM异常。
一池N线程 Executors.newFixedThreadPool(int)
ExecutorService threadPool = Executors.newFixedThreadPool(5);
try{
for (int i=1;i<=10;i++){
threadPool.execute( () -> {
// 办理业务
});
}catch(Exception e) {
e.printStackTrace();
}finally{
threadPool.shutdown();
}
一池一线程 Executors.newSingleThreadExecutor()
ExecutorService threadPool = Executors.newSingleThreadExecutor();
try{
for (int i=1;i<=10;i++){
threadPool.execute( () -> {
// 办理业务
});
}catch(Exception e) {
e.printStackTrace();
}finally{
threadPool.shutdown();
}
根据需求创建线程 Executors.newCachedThreadPool()
ExecutorService threadPool = Executors.newCachedThreadPool();
try{
for (int i=1;i<=10;i++){
threadPool.execute( () -> {
// 办理业务
});
}catch(Exception e) {
e.printStackTrace();
}finally{
threadPool.shutdown();
}
线程池的7个参数
int corePoolSize; 常驻线程数量(核心),例如银行固定开发窗口
int maximumPoolSize; 最大线程数量;(最多能开多少窗口)
long keepAliveTime ;线程存活时间,(临时开发的窗口多久不用就关)
TimeUnit unit,线程存活时间单位
BlockingQueue<Runnable> workQueue;阻塞队列
ThreadFactory threadFactory;线程工厂
RejectedExecutionHandler hanler;拒绝策略,(人太多了,拒绝接纳)
ExecutorService threadPoolExecutor = new ThreadPoolExecutor{
2,
5,
2L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executor.defaultThreadFactory,
new ThreadPoolExecutor.AbortPolicy()
};
threadPoolExecutor.execute( () -> {
// 办理业务
});
线程池工作原理
只有当线程execute时,线程才会创建。当前两个线程使用完常驻线程数时,第3,4,5个线程到了会先在阻塞队列中等待,当第6,7,8个线程到时,会使用最大线程数中的其他线程。如果此时还有第9个线程到,就会使用拒绝策略。
拒绝策略
AbortPolicy(默认):直接抛出RejectExecutionException异常阻止系统正常运行
CallerRunsPolicy:"调用者运行",一种调节机制,该策略不抛弃任务,也不抛出异常。例如银行负责人说,谁让你来的,你就找谁去。
DiscardOldestPolicy:抛弃等待最久的任务,然后把当前队列中尝试再次提交任务。例如你去海底捞排队等待,已经等了一早上了,此时有新客户来,刚好空了个位,位置会让给新来的人,不要你了。
DiscardPolicy:默默丢弃无法处理的业务,不给处理也不抛出异常,如果允许任务丢失,这是最好的策略。例如去饭店吃饭,你喊老板我要吃饭,服务员和老板都不理你。
分支合并框架 Fork/Join
将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并输出。
Fork:将一个复杂任务进行拆分,大事化小
Join:把拆分任务的结果进行合并
Fork分类:
ForkJoinPool:分支合并池,类比线程池
ForkJoinTask、RecursiveTask:递归任务,继承后可以实现递归调用的任务
Class MyTask extends RecursiveTask<Integer> {
// 拆分差值不超过10
private static final Integer VALUE = 10;
private int begin;
private int end;
private int result;
//创建有参构造
public MyTask(){
this.begin = begin;
this.end = end;
}
// 拆分和合并过程
@Override
protected Integer compute(){
// 判断相加的两数是否大于10
if (end - begin <= VALUE) {
for (int i=begin;i<=end;i++){
result += i;
}
}else{
// 获取中间值
int mid = (begin + end) /2;
MyTask task1 = new MyTask(begin,mid);
MyTask task2 = new MyTask(mid+1,end);
task1.fork();
task2.fork();
result = task1.join() + task2.join();
}
return result;
}
}
public class ForkJoinDemo {
public static void main (String[] args) throws ExecutionException,InterruptedException{
MyTask myTask = new MyTask(0,100);
ForkJoinPool forkJoinPool -= new ForkJoinPool();
ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(myTask);
Integer result = forkJoinTask.get();
// sout(result);
forkJoinPool.shutdown();
}
}
CompletableFuture异步回调
异步调用 没用返回值
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync( () -> {
//sout(Thread.currentThread().getName());
});
completableFuture.get();
异步调用 有返回值
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync ( () -> {
//sout(Thread.currentThread().getName())
return 1024;
});
completableFuture.whenComplete((t,u) -> {
// sout(t); // 1024 return的信息
// sout(u); //null 异常信息
}).get();