JUC高级编程
一 多线程基础知识
-
一把锁
synchronized
- 修饰一个同步代码块, 作用的对象是调用这个代码块的对象.
- 修饰一个方法, 该方法称为同步方法
- 修饰一个静态的方法
- 修饰一个类
-
两个并
- 并发(concurrent): 在同一实体上的多个事件 , 在同一台机器上同时处理过个任务 , 同一时刻 , 只有一个事情发生
- 并行(parallel) : 是在不同尸体上的多个事件 , 在多个处理器上同时处理多个任务 , 同一时刻 , 多个机器干多个事
-
三个程
- 进程 : 系统正在运行的一个应用程序 ; 程序一旦运行就是进程 , 每个进程都有它自己的内存空间和系统资源 , 进程是资源分配的最小单位
- 线程 : 系统分配处理器时间资源的最小单位 , 或者说是进程之内独立执行的一个单元执行流 , 线程是程序执行的最小单位,也被称为轻量级进程 .
- 管程 : Monitor(锁) , Monitor是一种同步机制 , 也就是我们平时说的锁, 保证了同一时刻只有一个进程在管程中活动 , 每个对象实例都会一个Monitor对象 , 执行线程首先要持有管程对象, 然后才能执行方法, 当方法完成之后会释放管程, 方法在执行时候会持有管程, 其他线程无法在获取同一个管程
-
线程分类
**用户线程: **平时用到的普通线程,自定义线程
**守护线程: **运行在后台,是一种特殊的线程,eg: 垃圾回收
**注意:**守护线程结束, 主线程不一定结束; 主线程结束, 守护线程一定结束
Thread t1 = new Thread(()->{ System.out.println(Thread.currentThread().getName()+"::"+Thread.currentThread().isDaemon()); },"t1"); //设置守护线程 t1.setDaemon(true);
二 CompletableFuture
2.1 Future接口知识
Future接口(FutureTask实现类) 定义了操作异步任务执行一些方法 , 如获取异步任务的执行结果 , 取消异步任务的执行 , 判断任务是否被取消 , 判断任务执行是否完毕等.
异步任务就是把一个大的任务分成好多小任务 , 需要这些小任务的返回值时, 主线程再获取就可以了
2.2 FutureTask异步任务
2.2.1 Future接口介绍
Future是java5新加的一个接口 , 它提供了一种异步并行计算的功能 , 如果主线程需要执行一些很耗时的计算任务 , 我们就会可以通过Future把这个任务放进异步线程中执行 , 主线程继续处理其他任务或先行结束 , 然后通过Future获取计算结果
2.2.2 Future接口相关构架
-
目的 : 异步多线程任务执行且返回有结果
-
三个特点 : 多线程 , 有返回 , 异步任务
-
代码实现 : Runnable接口+Callable接口+Future接口+FutureTask实现类
FutureTask<String> futureTask = new FutureTask<>(()->{
try {
TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}
return "hello";
});
Thread thread = new Thread(futureTask);
thread.start();
try {
System.out.println(futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
2.2.3 Future的优缺点
- 优点 : Future + 线程池异步任务配合使用 , 能显著提高程序的运行效率
- 缺点 :
- get()阻塞 一旦调用get方法 , 必须要有一个返回值 , 否则会一直阻塞 ; 也可以在get方法里设置参数 , 设置要等待的时间
- isDone()轮询 会一直循环访问 , 知道有结果
- Future只能通过阻塞或轮询来得到任务的结果
/**
* @AUTHOR TingFeng
* @DATE 2023/10/18-9:09
* @DESCRIPTION
*/
public class Demo2 {
public static void main(String[] args) {
FutureTask<String> futureTask = new FutureTask<>(()->{
try {
TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}
return "结果";
});
Thread t = new Thread(futureTask);
t.start();
while (true){
if (futureTask.isDone()){
try {
System.out.println(futureTask.get());
break;
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}else {
try {TimeUnit.MILLISECONDS.sleep(500);
System.out.println("努力获得结果");
} catch (InterruptedException e) {e.printStackTrace();}
}
}
}
}
2.2.4 对于一些复杂任务
- 简单任务完全可行
- 回调通知:
- 当Future完成后通知我 , 这就是回调通知
- 通过轮询一直判断的话会占用cpu
- 创建异步任务 : Future+线程池
- 优化 : 使用CompletableFuture
2.3 CompletableFuture
jdk8设计出CompletableFuture , 它提供了一种类似观察者模式的机制 , 可以让任务执行完成后通过回调函数通知监听的一方
2.3.1 CompletableFuture介绍
类架构
- 提供了强大的Future的扩展功能 , 简化异步编程的复杂性
- 提供了函数式编程的能力 , 通过回调的方式处理结果
- 它可能代表一个明确完成的Future , 也可能代表一个完成阶段
- 支持在计算完成后触发一些函数或执行某些动作
2.3.2 静态方法
-
runAsync是无返回值的异步
-
supplyAsync是带返回值的异步
/**
* @AUTHOR TingFeng
* @DATE 2023/10/18-9:33
* @DESCRIPTION
*/
public class Demo3 {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(3);
CompletableFuture<Void> future = CompletableFuture.runAsync(()->{
System.out.println(Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}
},pool);
try {
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
pool.shutdown();
}
}
/**
* @AUTHOR TingFeng
* @DATE 2023/10/18-9:36
* @DESCRIPTION
*/
public class Demo4 {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(3);
CompletableFuture<String> future = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}
return "hello";
},pool);
try {
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
pool.shutdown();
}
}
CompletableFuture优点
- 异步任务结束时 , 会自动回调某个对象的方法
- 主线程设置好回调后 , 不用关心异步任务的执行 , 异步任务之间可以顺序执行
- 异步任务出错 , 会自动回调某个对象的方法
2.4 电商网站的比价案例
2.4.1 扩展知识
函数式编程
函数式接口名 | 方法名称 | 参数 | 返回值 |
---|---|---|---|
Runnable | run | 无参数 | 无返回值 |
Function | apply | 1个参数 | 有返回值 |
Consume | accept | 1个参数 | 无返回值 |
Supplier | get | 没有参数 | 有返回值 |
BiConsumer | accept | 2个参数 | 无返回值 |
案例 :
电商网站比价需求分析:
需求说明:
- 同一款产品,同时搜索出同款产品在各大电商平台的售价
- 同一款产品,同时搜索出本产品在同一个电商平台下,各个入驻卖家售价是多少
输出返回:
- 出来结果希望是同款产品的在不同地方的价格清单列表,返回一个List
例如:《Mysql》 in jd price is 88.05 《Mysql》 in taobao price is 90.43
解决方案,对比同一个产品在各个平台上的价格,要求获得一个清单列表
- step by step,按部就班,查完淘宝查京东,查完京东查天猫…
- all in,万箭齐发,一口气多线程异步任务同时查询
/**
* @AUTHOR TingFeng
* @DATE 2023/10/18-15:44
* @DESCRIPTION
*/
public class Demo5 {
static List<NetMall> list = Arrays.asList(
new NetMall("jd"),
new NetMall("dangdan"),
new NetMall("meituan")
);
public static void main(String[] args) {
long start = System.currentTimeMillis();
List<String> price = getPrice(list,"mysql");
for (String s : price) {
System.out.println(s);
}
long end = System.currentTimeMillis();
System.out.println(1.0*(end-start)/1000+"秒");
}
private static List<String> getPrice(List<NetMall> list, String name) {
return list.stream().map(s-> CompletableFuture.supplyAsync(()->
String.format(name+" in %s price is %.2f" , s.getNetMallName(),s.calcPrice(name))))
.collect(Collectors.toList())
.stream()
.map(s->s.join())
.collect(Collectors.toList());
}
private static List<String> getPrice1(List<NetMall> list, String name) {
return list.stream().map(s->String.format(name+" in %s price is %.2f" , s.getNetMallName(),s.calcPrice(name)))
.collect(Collectors.toList());
}
}
class NetMall{
@Getter
private String netMallName;
public NetMall(String netMallName) {
this.netMallName = netMallName;
}
public double calcPrice(String productName){
try {
TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
return ThreadLocalRandom.current().nextDouble()*2+productName.charAt(0);
}
}
结论:
- 没用异步之前 , 耗时3秒多
- 用了异步之后 , 耗时一秒多
2.4.2 常用方法
-
获得结果和触发计算
- 获得结果
- public T get()
- public T get(long timeout , TimeUnit unit)
- public T join()
- public T get()
- public T getNow(T valueAbsent) : 如果拿到结果就返回正常值 , 否则返回valueAbsent , 不阻塞
- 触发计算
- public boolean complete(T value) 是否打断get方法立即返回括号值
- 获得结果
-
对计算结果进行处理
- thenApply : 计算结果存在依赖关系 , 这两个线程串行化 , 由于存在依赖关系, 当前步骤有异常直接结束
- handle : 计算结果存在依赖关系 , 这两个线程串行化 , 有异常继续走
/** * @AUTHOR TingFeng * @DATE 2023/10/18-16:12 * @DESCRIPTION */ public class Demo6 { public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(3); CompletableFuture<String> future = CompletableFuture.supplyAsync(()->{ try { TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();} return "1"; },pool).thenApply(v->{ try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();} return v+"2"; }).handle((v,e)->{ //int i = 10/0; return v+"3"; }).whenComplete((v,e)->{ if (e==null){ System.out.println(v); } }).exceptionally(e->{ e.printStackTrace(); System.out.println(e.getMessage()); return null; }); System.out.println(Thread.currentThread().getName()+"有任务"); pool.shutdown(); } }
-
对计算结果进行消费
- 消费结果 , 不进行返回
- thenaccept
补充:
- thenRun(Runnable Runnable) : 任务A执行完执行任务B , 并且不需要A的结果
- thenAccept(Consumer action) : 任务A执行完执行B, B需要A的结果 , 但B没有返回值
- thenApply(Function f) : 任务A执行完执行B, B需要A的结果 , 同时B有返回值
三 java锁
3.1 乐观锁和悲观锁
-
乐观锁
认为自己在使用数据的时候不会有别的线程修改数据,不会添加锁, 只是在更新的时候去判断 , 之前有没有别的线程更新了这个数据 , 如果数据没有更新 , 当前线程将自己修改的数据成功写入 , 如果被其他线程更新 , 则根据不同的实现方式执行不同的操作 ,
最常用的是CAS自选
-
悲观锁
认为自己在修改数据的时候一定有别的线程来修改数据 , 因此在获取数据的时候先加锁 , 确保数据不会被别的线程修改 , synchronized和Lock的实现类都是悲观锁 , 适合写操作多的场景
3.2 synchronized的三种应用方式
- 作用于实例方法 , 当前实例加锁 , 进入同步代码前要获取当前实例的锁
- 作用于代码块 , 锁的是括号里的对象
- 作用于静态方法, 当前类加锁 , 进入同步代码块要获得当前类对象的锁
3.3 公平锁和非公平锁
3.3.1 介绍
-
公平锁
多个线程按照申请锁的顺序来获取锁 , 类似排队, 先到先得 .
Lock lock= new ReentrantLock(true) 就是一个公平锁
-
非公平锁
多个线程获取锁的顺序不是按照申请的顺序 , 谁抢到锁就是谁的 , 在高并发下, 有可能造成线程饥饿的状态
Lock lock= new ReentrantLock() 默认非公平锁
**面试题: **
-
为什么会有公平锁/非公平锁的设计?为什么默认非公平?
-
- 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分地利用CPU的时间片,尽量减少CPU空间状态时间。
- 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得很大,所以就减少了线程的开销。
-
什么时候用公平?什么时候用非公平?
-
- 如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省了很多线程切换的时间,吞吐量自然就上去了;否则就用公平锁,大家公平使用。
3.4 可重入锁(递归锁)
-
介绍
在同一线程在外层方法获取得到锁的时候 , 在进入该线程的内层方法会自动获取锁 , (前提: 锁对象是同一个对象) , 不会因为之前已经获取过还未释放而阻塞-----------------一定程度可避免死锁
-
分类
-
隐式锁 (synchronized使用的锁) . 默认是可重入锁
- 在一个synchronized修饰的方法或者代码块的内部调用本类的其他synchronized修饰的方法或者代码时 , 是永远可以得到锁
public class ReEntryLockDemo { public static void main(String[] args) { final Object o = new Object(); new Thread(() -> { synchronized (o) { System.out.println("外层调用"); synchronized (o) { System.out.println("中层调用"); synchronized (o) { System.out.println("内层调用"); } } } }, "t1").start(); } }
-
显式锁(Lock) 也有ReentrantLock
Lock lock = new ReentrantLock(); new Thread(() -> { lock.lock(); try { System.out.println("外层调用"); lock.lock(); try { System.out.println("中层调用"); lock.lock(); try { System.out.println("内层调用"); } finally { lock.unlock(); } } finally { lock.unlock(); } } finally { lock.unlock(); } }, "t2").start();
-