多线程/JUC (2
11. 线程池(重点)
线程池必会: 三大方法、七大参数、四种拒绝策略
池化技术:事先准备好一些资源,有人要用就来这里拿,用完再还回去。
好处
- 降低资源消耗
- 提高响应速度(效率) (ps:创建、销毁浪费资源)
- 方便管理
线程复用、可以控制最大并发数、管理线程
三大方法
// ExecutorService threadPool = Executors.newSingleThreadExecutor();
// ExecutorService threadPool = Executors.newFixedThreadPool(5);
// ExecutorService threadPool = Executors.newCachedThreadPool();
自定义创建线程池
最大承载: 5+3 = 8
调优:maximumPoolSize 池的最大线程数怎么定义?
- CPU 密集型
几核就是几,可以保持CPU效率最高
Runtime.getRuntime().availableProcessors()
- IO 密集型
判断程序中十分耗IO的线程有多少个,大于它(2倍)
七大参数
分析上面三个方法的源码,可以看出他们的本质就是ThreadPoolExecutor()
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
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;
}
类型 | 参数 | 含义 |
---|---|---|
int | corePoolSize | 核心线程池大小 |
int | maximumPoolSize | 最大核心线程池大小 |
long | keepAliveTime | 空闲线程存活时间(超时) |
TimeUnit | unit | 超时单位 |
BlockingQueue | workQueue | 阻塞队列 |
ThreadFactory | threadFactory | 线程工厂:创建线程,一般不用懂 |
RejectedExecutionHandler | handler | 拒绝策略 |
private static final RejectedExecutionHandler defaultHandler =
new AbortPolicy();
可以看出默认的拒绝策略是AbortPolicy()
四种拒绝策略
- AbortPolicy
(ps:对拒绝任务抛弃处理,并且抛出异常。)
超出最大承载报异常:java.util.concurrent.RejectedExecutionException
-
CallerRunsPolicy
(ps:哪来的,去哪里 :main
这个策略重试添加当前的任务,他会自动重复调用 execute() 方法,直到成功。)
-
DiscardPolicy
(ps:队列满了,丢掉任务,不会抛出异常) -
DiscardOldestPolicy
(ps:队列满了,尝试丢掉最早的任务,不会抛出异常
当任务被拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务添加进去。
对拒绝任务不抛弃,而是抛弃队列里面等待最久的一个线程,然后把拒绝任务加到队列。)
12. 四大函数式接口
函数式接口 Function
有参数,有返回值
public static void main(String[] args) {
Function<String, String> function =
/*new Function<String, String>() {
@Override
public String apply(String o) {
return o;
}
};*/
(str)->{return str;};
System.out.println(function.apply("asd"));
//输出: “asd”
}
断定型接口 Predicate
有参数,有返回值,但是返回值类型只能是boolean
public static void testPredicate(){
Predicate<String> predicate =
// new Predicate<String>() {
// @Override
// //断定输入是否为空
// public boolean test(String o) {
// return o.isEmpty();
// }
// };
(str)->{return str.isEmpty();};
System.out.println(predicate.test(""));
}
消费型接口 Consumer
只有参数,没有返回值
public static void testConsumer(){
Consumer<String> consumer =
// new Consumer<String>() {
// @Override
// public void accept(String o) {
// System.out.println(o);
// }
// };
(str)->{System.out.println(str);};
consumer.accept("hi");
}
供给型接口 Supplier
没有参数,只有返回值
13. Stream流式计算
什么是流式计算? : 存储 + 计算
计算交给流来做
public static void main(String[] args) {
User u1 = new User(1,"a",21);
User u2 = new User(2,"b",22);
User u3 = new User(3,"c",23);
User u4 = new User(4,"d",24);
User u5 = new User(6,"e",25);
List<User> users = Arrays.asList(u1, u2, u3, u4, u5);
users.stream()
.filter((uu)->{return uu.getId()%2 == 0;}) //id偶数
.filter((uu)->{return uu.getAge()>23;}) //年龄>23
.map((uu)->{ //map:转换
uu.setName(uu.getName().toUpperCase());
return uu;
}) //用户名转大写
.sorted((uu1,uu2)->{
return uu2.getName().compareTo(uu1.getName());
}) //用户名倒序
.limit(1) //只输出一个用户
.forEach(System.out::println);
}
结果
14. ForkJoin
ForkJoin:并行执行任务,提高效率,大数据量。
特点: 工作窃取
A还在执行,B已经执行完了,于是B就去“窃取”A的任务来执行
被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行
通常会使用双端队列
优点:
~ 充分利用线程进行并行计算,并减少了线程间的竞争。
缺点:
~ 在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且该算法会消耗更多的系统资源,比如创建多个线程和多个双端队列。
操作
如何使用ForkJoin?
1 forkJoinPool 通过它来执行
2 计算任务 execute(ForkJoinTask<?> task)
3 计算类要继承ForkJoinTask
/**
* 如何使用ForkJoin?
* 1 forkJoinPool 通过它来执行
* 2 计算任务 execute(ForkJoinTask<?> task)
* 3 计算类要继承ForkJoinTask
*/
public class MyForkJoinTask extends RecursiveTask<Long> {
private long start;
private long end;
//临界
private long temp = 10000L;
public MyForkJoinTask(long start, long end) {
this.start = start;
this.end = end;
}
//计算方法
@Override
protected Long compute() {
//当小于临界值之后 就不用ForkJoin了
if((end - start) < temp){
long sum = 0L;
for (long i = start; i<= end; i++){
sum += i;
}
return sum;
}else { //forkJoin 递归
long middle = (start + end) / 2 ; //中间值
//把一个任务拆成两个任务
MyForkJoinTask myForkJoinTask1 = new MyForkJoinTask(start, middle);
MyForkJoinTask myForkJoinTask2 = new MyForkJoinTask(middle+1, end);
myForkJoinTask1.fork(); //拆分任务,把任务压入线程队列
myForkJoinTask2.fork();
return myForkJoinTask1.join() + myForkJoinTask2.join();
}
}
}
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
test1(); //2821
test2(); //1163
test3(); //826
}
//傻瓜式
public static void test1(){
long beginTime = System.currentTimeMillis();
long start = 0L;
long end = 2000000000L;
long sum = 0L;
//1
for (long i = start; i<= end; i++){
sum += i;
}
long endTime = System.currentTimeMillis();
System.out.println("sum="+sum+" 时间"+(endTime-beginTime));
}
//ForkJoin
public static void test2() throws ExecutionException, InterruptedException {
long beginTime = System.currentTimeMillis();
long start = 0L;
long end = 2000000000L;
// long sum = 0L;
ForkJoinPool forkJoinPool = new ForkJoinPool();
MyForkJoinTask myForkJoinTask = new MyForkJoinTask(start,end);
ForkJoinTask<Long> submit = forkJoinPool.submit(myForkJoinTask);//提交,有结果
Long sum = submit.get();
long endTime = System.currentTimeMillis();
System.out.println("sum="+sum+" 时间"+(endTime-beginTime));
}
//流式
public static void test3(){
long beginTime = System.currentTimeMillis();
long start = 0L;
long end = 2000000000L;
long sum = LongStream.rangeClosed(start, end)
.parallel() //并行流
// .reduce(0,(l1,l2)->{return Long.sum(l1,l2);});
.reduce(0,Long::sum);
long endTime = System.currentTimeMillis();
System.out.println("sum="+sum+" 时间"+(endTime-beginTime));
}
}
15. 异步回调
Future 设计初衷: 对将来某个事件的结果进行建模
一般用的增强实现类:CompletableFuture
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName()+"----Integer");
return 1024;
});
System.out.println(future.whenComplete((t, u) -> { //当成功了
System.out.println(t + " 正常返回值");
System.out.println(u + " 失败信息");
}).exceptionally((e) -> { //当失败了
System.out.println(e.getMessage());
return 404;
}).get());
16. JMM
什么是JMM?
java内存模型,不存在的东西,是一种概念,约定
8种操作
内存交互操作
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
JMM对这八种指令的使用,制定了如下规则:
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
关于JMM的一些同步约定
- 线程解锁前,必须把共享变量立刻刷回主存
- 线程解锁后,必须读取主存中的最新值到工作内存中
- 加锁和解锁必须是同一把锁
17. Volatile
谈谈对Volatile的理解?
- Volatile是java虚拟机提供轻量级的同步机制
- 保证可见性
- 不保证原子性
- 禁止指令重排
1、保证可见性
2、不保证原子性
除了加lock锁或synchronized,还能怎样保证原子性?
使用原子类,来解决原子性问题
3、禁止指令重排
什么是指令重排?
你写的程序,计算机并不是按照你写的那样去执行的
源代码 --> 编译器优化的重排 --> 指令并行也可能会重排 --> 内存系统也会重排 --> 执行
处理器在进行进行指令重排的时候,会考虑:数据之间的依赖性
volatile可以避免指令重排:
内存屏障 ≈ CPU指令
作用:
- 保证特定的操作的执行顺序
- 可以保证某些变量的内存可见性
(利用这些特性,volatile实现了可见性)
volatile在哪里用的最多?
单例模式↓
18. 单例模式
懒汉式(双重检验锁 DCL)
public class LazyMan {
private LazyMan(){}
//加原子操作
private volatile static LazyMan instance; //懒汉: 要用的时候再去创建
public static LazyMan getInstance(){ //安全:synchronized
// //判断是否为空 空了再创建
// if (instance == null){
// instance = new LazyMan();
// }
//双重检测锁模式 DCL懒汉式
if (instance == null){
synchronized (LazyMan.class){
if (instance == null) {
instance = new LazyMan(); //还是不安全 不是一个原子性操作
/** 经过:
* 1. 分配内存空间
* 2. 执行构造方法,初始化对象
* 3. 把这个对象指向这个空间
*
* !!指令重排!!
* 可能123 也可能132(完蛋)
* 此时还没完成构造(但是其他线程不知道
*
* 所以要加 volatile
*/
}
}
}
return instance;
}
public void show(){
System.out.println("wulihihi");
}
public static void main(String[] args) {
Hungry instance = Hungry.getInstance();
instance.show();
}
}
经过:
- 1、分配内存空间
- 2、执行构造方法,初始化对象
- 3、把这个对象指向这个空间
- !!指令重排!!
- 可能123 也可能132(完蛋)
- 此时还没完成构造(但是其他线程不知道
- 所以要加 volatile
枚举式
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
public static void main(String[] args) throws Exception {
EnumSingle instance1 = EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstructor =
//注意!异常有问题 报的是NoSuchMethodException: com.zzt.single.EnumSingle.<init>()
// 空参问题 idea告诉我们这里面有个空参构造
//探究源码发现 原来根本就不是无参 其实是有参构造String,int
// EnumSingle.class.getDeclaredConstructor(null);
EnumSingle.class.getDeclaredConstructor(String.class,int.class);
//现在报 IllegalArgumentException: Cannot reflectively create enum objects
//才是符合我们想要的enum自带的:不要用反射来创建枚举对象
declaredConstructor.setAccessible(true); //破除私有权限
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
相关教程
https://www.runoob.com/design-pattern/singleton-pattern.html
19. CAS
: CompareAndSwap
比较当前工作内存中的值和主内存中的值,如果是期望的,就执行操作,如果不是就一直循环(自旋锁)
Unsafe类
缺点
1、循环会耗时 (CPU开销大)
2、一次性只能保证一个共享变量的原子性
3、ABA问题
20. 原子引用
CAS会问: ABA问题
ABA:狸猫换太子
A喝了水,然后又灌满放回去了。
对B而言,水始终都是满的,但是他不知道被人喝过换过了,这杯水不是之前的那杯水了
解决ABA问题:原子引用 : 乐观锁
带版本号的原子引用
AtomicStampedReference atomicStampedReference = new AtomicStampedReference<>(initialRef,initialStamp);
//两个参数: 第一个是初始对象V默认值,第二个是初始印记值
atomicStampedReference.compareAndSet(1,2,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
//4个参数:预期值;变化后的值;预期印记;印记变化后的值
21. 锁
1. 公平锁、不公平锁
公平锁:不能插队,线程进来必须先来后到
不公平锁:可以插队,允许超锁现象 (CPU说了算) (默认非公平锁)
synchronized默认非公平
2. 可重入锁(递归锁)
拿到外面的锁就可以自动获得里面的锁
可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。 (套娃)
3. 自旋锁
spinLock
4. 死锁
死锁4个条件:
- 互斥 : 一个资源只能被一个线程使用
- 请求与保持 : 一个进程因为一个资源而阻塞了,不能再抱着不放
- 不剥夺条件 : 一个进程已经获得了这个资源,在没有使用完之前不能强行剥夺
- 循环等待 : 环状
解开:破坏4个其中一个
怎么排查问题?
- 看日志
- 看堆栈信息 :jps