JUC
JUC是什么?
java.util.concurrent在并发编程中使用的工具类
进程、线程回顾
-
进程、线程是什么?
-
进程、线程例子
-
线程状态?
-
wait、sleep的区别
- wait放开资源睡,sleep抓着资源睡
- wait需要唤醒,sleep自动醒来
-
什么是并发?什么是并行?
- 秒杀
- 并行计算
Lock接口
复习Synchronized
-
多线程编程模板上
- 线程 操作 资源类
- 高内聚低耦合
-
实现步骤
-
例子:卖票程序
Lock
-
是什么?
- synchronized能实现的Lock都能实现
- Lock能使用Condition生成多把钥匙
-
Lock接口的实现ReentrantLock可重入锁
- ReadWriteLock接口的实现ReentrantReadWriteLock
-
创建线程的方式
-
继承Thread类
- 不能这样写
-
new Thread()
-
第三种
-
-
实现 Runnable 接口
-
新建类实现runnable接口
-
匿名内部类
- new Thread(Runnable target, String name).start()
-
lambda表达式
- new Thread(()->{ method() {} , “ThreadName”).start();
-
-
代码
java8特性
lambda表达式
-
什么是lambda?
-
查看例子:LambdaDemo
- Runnable run = ()->{
System.out.println("");
};
- Runnable run = ()->{
-
要求
- 接口只有一个方法
-
写法
- 拷贝小括号(),写死右箭头 ->,落地大括号()
- 参数可省略类型,一个参数可省略括号
-
函数式接口
- @FunctionalInterface,接口中只能有一个未实现的抽象方法,可以写Lambda表达式
- Lambda表达式,必须是函数式接口,必需只有一个方法,如果接口只有一个方法java默认它为函数式接口。
- Consumer、Supplier、Predicate、Function
接口里是否能有实现方法
-
抽象未实现方法
-
default方法
- default int div(int x,int y) { return x/y;}
-
静态方法/类方法
- public static int sub(int x,int y) { return x-y;}
代码
线程间通信
面试题:两个线程打印
-
两个线程,一个线程打印1-51,另一个打印字母A-Z,打印顺序为12A34B…5152Z。要求用线程间通信
-
private int number = 0;
private char c = ‘a’; -
测试 new 两个 Thread()
-
while (c - number != ‘a’) {
this.wait();
}- num++; sout;
-
while (c - number == ‘a’) {
this.wait();
}- sout;c++;
-
例子:NotifyWaitDemo
-
private int number = 0;
-
测试 new 两个 Thread()
-
if(number != 0) {
this.wait();
}- if在线程通信中会造成虚假唤醒
-
if(number != 1) {
this.wait();
} -
线程不安全例子
线程间通信:1、生产者+消费者;2、通知等待唤醒机制
多线程编程模板中
-
判断
- 一定要用while循环判断
-
干活
-
通知
synchronized实现
-
代码
-
换成4个线程
- 换成4个线程会导致错误,虚假唤醒
- if 只判断一次,线程1抓着资源睡,而线程2就不需判断,直接进入下面的代码
- 原因:在 java 多线程判断时,不能用if,程序问题处在了判断上面,突然有一添加的线程进到 if了,突然中断交出控制权,没有进行验证,而是直接走下去了,加了两次,甚至多次
-
解决办法
- if 被替换为 while。因为if 判断一次,while 多次判断
- while (number != 0) {
this.wait();
} - while (number != 1) {
this.wait();
}
多线程编程模板下
- 注意多线程之间的虚假唤醒
- 线程间通信一定要用while,不能用if
java8新版实现
-
对标实现
- synchronized:wait() 和 notifyAll()
-
Condition
- Lock:lock() 和 unlock()
线程间定制化调用通信
ThreadOrderDemo
线程-调用-资源类
判断-干活-通知
- 有顺序通知,需要有标志位
- 有一个锁Lock,3把钥匙Condition
- 判断标志位
- 输出线程名+第几次+第几轮
- 修改标志位,通知下一个
代码
NotSafeDemo
需求
- 请举例说明集合类是不安全的
证明集合不安全
-
例子:NotSafeDemo
-
线程不安全错误
- java.util.ConcurrentModificationException
- ArrayList在迭代的时候如果同时对其进行修改就会抛出并发修改异常。
-
原理
解决方案
-
Vector
-
Collections
- Collections.synchronizedList(new ArrayList<>());
-
写时复制
- java.util.concurrent.CopyOnWriteArrayList
写时复制
-
Redis:RDB持久化数据时,会fork一个子进程,这个过程就使用了写时拷贝技术。
-
不加锁性能提升出错误,加锁数据一致性能下能
-
CopyOnWriteArrayList定义
-
CopyOnWriteArrayList 理论
- 写数据:首先复制一个长度为len+1的数据;给数组赋值;最后指向新的数据
- 读数据:写数据时读取原数组数据。
-
扩展类比
- HashSet 底层 HashMap,value放的是一个Object常量
- CopyOnWriteArraySet
- ConcurrentHashMap
代码
7 多线程锁
例子:Lock_8
锁得到8个问题
- 1.标准问题,先打印短信还是Email. //短信
- 2.在短信方法内停留4s,先打印短信还是Email. //短信
- 3.新增普通hello方法,先打印短信(睡4s)还是hello. //hello
- 4.有两部手机,先打印短信(睡4s)还是Email. //Email
- 5.两个静态同步方法,1部手机,先打印短信(睡4s)还是Email. //短信
- 6.两个静态同步方法,2部手机,先打印短信(睡4s)还是Email. //短信
- 7.一个静态同步方法,一个普通同步方法,1部手机,先打印短信(睡4s)还是Email. //Email
- 8.一个静态同步方法,一个普通同步方法,2部手机,先打印短信(睡4s)还是Email. //Email
8锁分析
- 1.两个线程先抢到资源的先上锁。
- 2.和1类似,上锁的是所有标synchronized的方法
- 3.hello方法是普通方法,非synchronized的,所以不会上锁。
- 4.synchronized锁的是当前对象this,所以两个对象互不影响。
- 5.静态方法也是类方法,synchronized锁的是Class类
- 6.和5.一样,synchronized锁的是Class类
- 7.静态同步方法锁的是Class类,同步方法锁的是this实例对象,不是同一把锁。
- 8.和7一样,不是同一把锁
- 注意:锁的范围?是不是同一把锁?
锁的小节
-
一个对象里面如果有多个synchronized方法,某一时刻内,只要有一个线程去调用任意一个synchronized方法,其他线程只能等待。
- 换句话说,某一时刻内,只能有唯一一个线程去访问这些synchronized方法的其中一个,锁的是当前对象this,被锁定后,其他的线程都不能进入到当前对象的其他的synchronized方法。
- 如果一个实例对象的非静态同步方法获得锁后,那么该实例对象的其他非静态同步方法,必须等待该锁释放后,才能获得锁。
- 其他实例对象的非静态同步方法和上面实例对象的非静态同步方法使用的是不同的锁,故不存在锁竞争。
-
加个普通方法后发现和同步锁无关。
- 想怎么调用怎么调用
-
换成两个对象后,不是同一把锁了,情况立刻变化。
-
synchronized实现同步的基础:Java中的每个对象都可以作为锁。具体表现为以下3种形式:
- 对于普通同步方法,锁是当前实例对象this
- 对于静态同步方法,锁的是当前类Class对象
- 对于同步代码块,锁的是synchronized括号里面配置的对象
-
所有的静态同步方法用的也是一把锁——锁定的是Class类,即类对象本身。如果两把锁是两个不同的对象,那么静态同步方法和非静态同步方法之间不会有锁竞争。
- 一旦一个静态同步方法获得锁后,其他的静态同步方法之间必须等待该锁释放后,才能获得锁。而不管是同一个实例对象的静态同步方法之间,还是不同实例对象的静态方法之间的调用。只要是同一个类的实例。
代码
8 Callable接口
是什么?
-
面试题:获取多线程的方法有几种?
- 要说哪4种,而非4种。
- 继承Thread类
- 实现Runnable接口
- Java 5:实现Callable接口
- Java 5:java的线程池获取
-
函数式接口
与Runnable对比
- Callable 实现 call() 方法,Runnable 实现 run() 方法
- Callable 有返回值,Runnable 无返回值
- Callable 抛异常,Runnable 不抛异常
- 都是函数式接口,可以写Lambda表达式
- public abstract void run();V cal() throws Exception();
怎么用
- 直接替换runnable是否可行?
- 认识不同的人找中间人
FutureTask
-
是什么?
- 未来的任务,用它就干一件事,异步调用。
- 例子:A计算1+…10,B计算11+…20,C计算21*…30,D计算31+…40。把C的复杂计算分散给另一个子线程,并行计算。
-
原理
- main方法串联所有方法,简单方法串行执行,复杂方法分配另一个线程,与主线程并行执行。效率更高
-
代码
- 构造方法:public FutureTask(Callable callable) {}
- future.get()方法一般在最后执行 ,否则会阻塞主线程。
JUC的强大辅助类 9
CountDownLatchDemo 减少计数
-
例子:CountDownLatchDemo,秦灭六国,一统华夏
-
原理
- CountDownLatch主要有两个方法,当一个或多个线程调用 await 方法时,这些线程会阻塞。
- 其他线程调用 countDownLatch 方法会将计数器减1(调用countDown方法的线程不会阻塞)
- 当计数器的值变为0时,因 await 方法阻塞的线程会被唤醒,继续执行
-
代码
- CountDownLatch cd = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()
+ “号同学离开教室…”);
cd.countDown();
}, String.valueOf(i)).start();
}
cd.await(); //当计数器为0,因await方法阻塞的线程会被唤醒
System.out.println(Thread.currentThread().getName() + “班长离开教室…”);
- CountDownLatch cd = new CountDownLatch(6);
CyclicBarrier 循环栅栏
-
例子:CyclicBarrierDemo,集齐七颗龙珠召唤神龙
-
原理
- 让一组线程达到一个屏障时被阻塞
- 直到最后一个线程达到屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活
- 线程进入屏障通过CyclicBarrier的await()方法。
-
代码
-
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, ()->{
System.out.println(“召唤神龙成功…”);
});for (int i = 1; i <= 7; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()
+ " 星龙珠被收集…");
try {
cyclicBarrier.await(); //阻塞
} catch (Exception e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
-
Semaphore信号灯
-
例子:SemaphoreDemo,三个停车位,六辆车
-
原理
- 在信号量上我们定义两种操作,acquire:当一个线程调用acquire时,要么通过成功获取信号量(信号量减1),要么一直等待下去,直到有线程释放信号量,或超时。
- release实际上会将信号量的值加1,然后唤醒等待的线程。
- 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
-
代码
-
Semaphore sp = new Semaphore(3);//三个停车位
//六辆车强三个车位
for (int i = 1; i <= 6; i++) {
new Thread(()->{
try {
sp.acquire();
System.out.println(Thread.currentThread().getName()
+ “号车驶入停车位…”);
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()
+ “号车驶出停车位…”);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
sp.release();
}
}, String.valueOf(i)).start();
}
-
ReentrantReadWriteLock读写锁 10
例子:ReadWriteLockDemo
类似案例
- 红蜘蛛
- 投影仪
- 缓存
问题例子
-
private Map<String, String> map = new HashMap<>();
public void put(String key, String value) {
try {
System.out.println(Thread.currentThread().getName() + “准备写入数据…” + key);
TimeUnit.MILLISECONDS.sleep(200);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + “完成写入数据…” + key);
} catch (Exception e) {
e.printStackTrace();
}
} -
public void get(String key) {
try {
TimeUnit.MILLISECONDS.sleep(100);
String value = map.get(key);
System.out.println(Thread.currentThread().getName() + “读取数据…” + value);
} catch (Exception e) {
e.printStackTrace();
}
}
代码
- ReadWriteLock rwLock = new ReentrantReadWriteLock();
- rwLock.writeLock().lock(); rwLock.readLock().lock();
- rwLock.writeLock().unlock(); rwLock.readLock().unlock();
BlockingQueueDemo阻塞队列 11
例子:BlockingQueueDemo
栈和队列
- 栈:先进后出、后进先出
- 队列:先进先出
阻塞队列
-
必须要阻塞、不得不阻塞
- 当队列空时,从队列中获取元素时线程会阻塞。直到其他线程往队列中添加元素时。
- 当队列满时,从队列中添加元素线程将会阻塞。知道有其他线程从队列中移除一个元素
阻塞队列的用处
- 在多线程领域:所谓阻塞,就是挂起线程(即阻塞),一旦满足条件时,被挂起的线程又会自动被唤醒。
- 使用BlockingQueue原因:好处是我们不需要关心什么时候阻塞线程,不需要关心什么时候需要唤醒线程,这一切由BlockingQueue完成。
架构梳理、种类分析
-
架构介绍
-
种类分析
- ArrayBlockingQueue:由数组组成的 有界阻塞队列,初始化指定长度
- LinkedBlockingQueue:由链表组成的 有界阻塞队列,大小默认为Integer.MAX_VALUE。
- PriorityBlockingQueue:支持优先级排序的 无界阻塞队列
- DelayQueue:使用优先级队列实现的延迟 无界阻塞队列
- SynchronousQueue:不存储元素的阻塞队列,即 单个元素的阻塞队列
- LinkedBlockingDeque:由链表组成的 双向阻塞队列
- LinkedTransferQueue:由链表组成的 无界阻塞队列
BlockingQueue核心方法
-
抛异常:add方法和remove方法会抛异常,正常返回true或false。element方法:获取头元素,否则抛异常
- add(e)
- remove()
- element()
- 异常:NoSuchElementException;Queue full
-
特殊值:offer方法和poll方法返回null值,正常返回true或false。peek方法:获取头元素,否则返回null值。
offer和poll方法可设置超时退出时间。- offer(e)
- poll()
- peek()
- offer(e,time,unit)
- poll(time,unit)
-
一致阻塞:take方法和put方法会一直阻塞
- put(e)
- take()
代码
ThreadPool线程池 12
例子:ThreadPoolDemo,类比数据库连接池
为什么用线程池
- 线程池的优势:线程池的作用主要是控制运行的线程数量,处理过程中将任务放入队列,在线程创建后启动这些任务。如果线程的数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
- 线程池的主要特点:线程复用;控制最大并发数;管理线程。
- 降低资源的消耗
- 提高响应速度
- 提高线程的管理。进行统一的分配、调优和监控
线程池如何使用
-
架构说明
- Java中的线程池是通过 Executor框架 实现的,该框架中使用到了 Executor、Executors、ExecutorService
-
线程池的创建
-
Executors.newFixedThreadPool(int);
- 执行长期任务性能好,创建一个线程池,一池中有N个固定的线程。有固定线程数的线程。
-
Executors.newSingleThreadExecutor();
- 一个任务一个任务的执行,一池一线程
-
Executors.newCachedThreadPool();
- 执行很多短期异步任务,线程池根据需要创建新线程,但在先前构建的线程可用时将重用他们。可扩容,遇强则强。
-
代码
-
-
ThreadPoolExecutor底层原理
- //Executors.newFixedThreadPool(int);
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
//new ThreadPoolExecutor、new LinkedBlockingQueue() - //Executors.newSingleThreadExecutor();
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
//new ThreadPoolExecutor、new LinkedBlockingQueue() - //Executors.newCachedThreadPool();
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
//[0, Integer.MAX_VALUE]:可扩展线程数
- //Executors.newFixedThreadPool(int);
线程池的7个重要参数
- 1、corePoolSize:线程池中的常驻核心线程数
- 2、maximumPoolSize:线程池中能够容纳同时执行的最大线程数,此值必须大于等于1
- 3、keepAliveTime:多余的空闲线程的存活时间,当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余线程会被销毁直到只剩下corePoolSize个线程为止
- 4、unit:keepAliveTime的单位
- 5、workQueue:任务队列,被提交但尚未被执行的任务
- 6、threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认
- 7、handler:拒绝策略。表示当队列满了,并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝请求执行的runnable的策略
线程池底层的工作原理
-
初始化线程池后,线程池中的线程数是0;
-
当调用 execute() 方法添加一个请求任务时,线程池会做出以下策略:
- 1、如果正在运行的线程数量小于corePoolSize,那么马上会创建线程执行这个任务;
- 2、如果正在运行的线程数大于等于corePoolSize,那么会把这个任务放入阻塞队列;
- 3、如果当前队列满了并且正在运行的线程数还小于maximumPoolSize,那么会立马创建这个线程去执行这个新的任务;
- 4、如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动拒绝策略来执行。
-
当一个线程完成任务时,它会自动取出队列中的任务来执行。
-
当一个线程无事可做超过一定的时间
(keepAliveTime)时,线程会判断:- 1、如果当前运行的线程数大于corePoolSize时,那么线程超过keepAliveTime后会自动销毁;
- 所以当线程池中的任务执行完后,线程会自动收缩到corePoolSize个线程数。
线程池用哪个?生产中如何设置合理参数?
-
线程池的拒绝策略
-
是什么?
- 满足两个条件:1、队列中的任务满了,同时,线程池中的最大线程数也达到了。无法为新任务继续服务。
-
JDK的内置拒绝策略
- AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
- CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
- DiscardOldestPolicy:抛弃队列等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
- DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出一次。如果允许任务丢失,这就是好的一种策略。
-
以上内置拒绝策略均实现了RejectedExecutionHandler接口
-
-
工作中单一/固定/可变的三种创建线程池的方法哪个用的多?超级大坑
- 《阿里巴巴Java开发手册》:Executors.newFixedThreadPool(int) 和 Executors.newSingleThreadExecutor():这两个可能会堆积大量的请求,从而导致OOM。
- Executors.newCachedThreadPool():可能会创建大量的线程,从而导致OOM。
- 一个都不用,工作中使用自定义的线程池
- Executors中JDK已经提供了,为什么不用?
-
在工作中如何使用线程池?是否自定义过线程池?
- ExecutorService pool = new ThreadPoolExecutor(
2,
5,
3,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy() //拒绝多余的新线程
// new ThreadPoolExecutor.DiscardOldestPolicy() //拒绝队列中等待时间最长的任务(旧任务),只会执行8个任务
// new ThreadPoolExecutor.CallerRunsPolicy() //从哪来的回哪去,从main线程来,返回给main线程,不会报错
// new ThreadPoolExecutor.AbortPolicy() //默认拒绝策略
);
- ExecutorService pool = new ThreadPoolExecutor(
java8回顾 13
例子:StreamDemo
函数式接口
-
java.util.function
-
java内置核心四大函数式接口
- JDK 1.8:Consumer 消费型接口
- JDK 1.8:Supplier 供给型接口
- JDK 1.8:Function<T,R> 函数型接口
- JDK 1.8:Predicate 断定型接口
-
实例
Stream流
-
WHAT
- 数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。
- 集合讲的是数据,流讲的是计算。类似 Linux OS 中的 | 管道符。
-
WHY
-
特点
- Stream自己不会存储元素
- Stream不会改变源对象。相反,会返回一个持有结果的Stream。
- Stream操作是延迟执行的。意味着需要等到结果的时候才执行。
-
-
HOW
-
阶段
- 创建一个Stream:一个数据源(数据、集合)
- 中间操作:一个中间操作,处理数据源数据
- 终止操作:一个终止结果,执行中间操作链,产生结果
-
源头=>中间流水线=>结果
-
分支合并框架 14
例子:ForkJoinDemo
原理
- Fork:把一个复杂任务拆分,大事化小
- Join:把拆分任务的结果合并
相关类
- ForkJoinPool:分支合并池,类比线程池
- ForkJoinTask:类比FutureTask,二者都实现了Future接口
- RecursiveTask:递归任务,继承后可以实现递归调用的任务。继承ForkJoinTask类,实现了Future接口。
实例
异步回调 15
例子:CompletableFutureDemo
原理
- 同步:掉完方法后等结果
- 异步回调:掉完方法还可以做其他事情
实例
XMind - Trial Version