线程池有哪些好处?
1.降低资源的消耗
2.提供响应速度
3.统一的管理(方便管理)
总结:控制最大并发数,可以控制最大并发数,管理线程
//Executors 工具类 3大方法
public class test {
public static void main(String[] args) {
// Executors.newSingleThreadExecutor();//单个线程
ExecutorService executor = Executors.newFixedThreadPool(5);//创建一个固定的线程
// Executors.newCachedThreadPool();//可伸缩的 遇强则强
//使用线程池 不再用原来的方式创建线程 而是用executor.execute()
try {
for (int i = 0; i < 10; i++) {
executor.execute(() -> {
System.out.println("执行");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
executor.shutdown();//使用完一定要关闭线程池
}
}
}
//单个线程
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
//创建一个固定的线程
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
//可伸缩的 遇强则强
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
查看源码可以发现,创建线程都是调用了一个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.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
此时我们回过头再去看
//单个线程
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
//最大线程数 和 核心线程数都是1 所以它是单个线程
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
//可伸缩的 遇强则强
public static ExecutorService newCachedThreadPool() {
//核心线程数0 最大是21亿 如果出现OOM 内存溢出
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
/**
* 自定义线程池
* @author Tu_Yooo
* @Date 2021/7/1 14:22
*/
public class ThreadPool {
public ThreadPoolExecutor getThreadPoolExecutor(){
//核心线程数
int corePoolSize = 3;
//最大线程数
int maximumPoolSize = 6;
//超过核心线程数的最大空闲时间
long keepAliveTime = 2;
//以秒为时间单位
TimeUnit time = TimeUnit.SECONDS;
//创建线程池
return new ThreadPoolExecutor(corePoolSize,
maximumPoolSize,
keepAliveTime,
time,
//创建阻塞队列 超过最大线程数后 启用队列 存放等待执行任务
new ArrayBlockingQueue<>(2),
//线程工厂 一般不用改变
Executors.defaultThreadFactory(),
//拒绝策略 当最大线程数满了 并且阻塞队列也满了时 启用拒绝策略
new ThreadPoolExecutor.AbortPolicy());;
}
线程池的最大承载数:阻塞队列数+最大线程数
超过最大承载数 启动拒绝策略:
1.AbortPolicy()
: 不处理新的任务,直接抛出异常
2.CallerRunsPolicy()
:哪来的回哪里去,如果是main线程传递过来的,让它回main线程处理去
3.DiscardPolicy()
:队列满了,不会抛出异常,丢掉任务
4.DiscardOldestPolicy()
:不会抛出异常,尝试跟最早的竞争,竞争失败也会丢掉任务
有两种解决方案:
CPU密集型:有几核CPU就定义几,可以保证CPU效率最高
//获取CPU核心数 确保再不同的电脑上运行
Runtime.getRuntime().availableProcessors();
IO密集型:判断程序中,非常耗费IO的线程数 大于这个IO数
函数式接口用于简化编程模型
//工具类
Function function =new Function<String,String>() {
@Override
public String apply(String o) {
return o;
}
};
//Lambda表达式
Function functio = (str) -> {return str;};
Predicate predicate = (s) -> {return true;};
//有输入参数 无返回值
Consumer consumer = (s) -> {
System.out.println(s);
};
//无输入参数 有返回值
Supplier supplier = () -> {
return 1;
};
public static void main(String[] args) {
User a = new User(1, "a", 13);
User b = new User(2, "b", 23);
User c = new User(3, "c", 10);
User d = new User(4, "c", 5);
User e = new User(5, "c", 9);
List<User> list = new ArrayList<>();
list.add(a);
list.add(b);
list.add(c);
list.add(d);
list.add(e);
list.stream()
//filter过滤
.filter(user -> {return user.getId()%2!=0;})
.filter(user -> {return user.getAge()>10;})
//Function接口 传入一个user参数 返回一个user参数
.map({user -> return user.getName().toUpperCase();})
//比较 排序
.sorted()
//分页 只获取一个参数
.limit(1)
//消费性接口Consumer 无返回值
.forEach((user) -> {
System.out.println(user);
});
}
分支合并,并行处理任务,将比较大的任务拆成小任务
适用场景:适合处理大型数据
ForkJoin特点:工作窃取
此时有两个线程,A线程执行了一半,B线程已经执行完了,此时B线程会去窃取A线程的任务,来帮助它完成,这就叫工作窃取
ForkJoin 使用步骤
1.集成RecursiveTask 递归任务 重写compute方法
2.创建ForkJoinPool类 submit() 提交任务–有返回值 /execute()执行任务–无返回值
public class ForkJoinDemo extends RecursiveTask<Long> {
private Long start;
private Long end;
//临界值
private Long temp = 10000L;
public ForkJoinDemo(Long start, Long end) {
this.start = start;
this.end = end;
}
//计算方法
@Override
protected Long compute() {
long middle = (start+end)/2;//中间值
ForkJoinDemo fork1 = new ForkJoinDemo(start, middle);
fork1.fork();//拆分任务
ForkJoinDemo fork2 = new ForkJoinDemo(middle, end);
fork2.fork();
return fork1.join()+fork2.join();//结果
}
}
class TestFork{
public static void main(String[] args) {
test2();
}
//ForkJoin 计算long
public static void test1() throws ExecutionException, InterruptedException {
ForkJoinPool joinPool = new ForkJoinPool();
ForkJoinTask<Long> submit = joinPool.submit(new ForkJoinDemo(0L, 10_0000_0000L));//提交任务
submit.get();//获取结果
}
//使用stream流式计算
public static void test2(){
long sum = LongStream.rangeClosed(0L,10_0000_0000L).parallel().reduce(0, Long::sum);
}
}
CompletableFuture类似与ajax,用于异步执行任务
public class Demo01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Future<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("没有返回值的异步回调");
});
//有返回值的supplyAsync异步回调
//与ajax一样 成功和失败都可以获取响应信息
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
System.out.println("有返回值的异步回调");
return 1024;
});
future2.whenComplete((t,u) -> {
//成功时 的回调方法
System.out.println(t);//1024 t正常的返回结果
System.out.println(u); // 错误的信息
}).exceptionally(e -> {
//失败时的回调
e.printStackTrace();
return 244;
});
future2.get();//阻塞等待执行结果
}
}
什么是Volatile
Volatile是Java虚拟机提供的轻量级的同步机制
什么是可见性,说这个东西之前要先说一下JMM
JMM:Java内存模型,不存在的东西,概念!约定
1.线程解锁前,必须把共享变量立刻刷会主存
2.线程加锁前,必须读取主存中的最新值到工作内存
3.加锁和解锁是用一把锁
线程 工作内存, 主内存
8种操作
JMM的八种交互操作(每个操作都为原子操作)
1.lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
2.unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
3.read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
4.load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
5.use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
6.assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
7.store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
8.write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
对八种操作的规则
1.不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
2.不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
3.不允许一个线程将没有assign的数据从工作内存同步回主内存
4.一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
5.一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
6.如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
7.如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
8.对一个变量进行unlock操作之前,必须把此变量同步回主内存
//多个线程使用同一个变量时 需要加入volatile保证可见性
//main线程修改了变量 要及时通知其他线程 否则会造成死循环
private volatile static int sum =0;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (sum==0){ //sum等于0时 程序死循环
}
}).start();
Thread.sleep(2);//确保线程启动
sum=1;//主线程修改sum的值
System.out.println(sum);
}
//volatile不保证原子性
private volatile static int sum =0;
public static void add(){
sum++;
}
public static void main(String[] args) {
//理论上结果为两万 实际上volatile并不保证原子性 结果肯定不为两万
for (int i = 1; i <= 20; i++) {
new Thread(()-> {
add();
}).start();
}
while (Thread.activeCount() > 2){//线程存活数
//main gc
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+""+sum);
}
在不使用synchronized和lock的情况下,如何保证原子性
首先我们要了解到sum++
不是一个原子性操作
所以要解决这个问题使用原子类
//AtomicInteger int类型的原子类
private volatile static AtomicInteger sum =new AtomicInteger();
public synchronized static void add(){
sum.getAndIncrement();// +1 操作 底层使用CAS 非常高效
}
public static void main(String[] args) {
//理论上结果为两万 实际上volatile并不保证原子性 结果肯定不为两万
for (int i = 1; i <= 20; i++) {
new Thread(()-> {
add();
}).start();
}
while (Thread.activeCount() > 2){//线程存活数
//main gc
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+""+sum);
}
这些类的底层都直接和操作系统挂钩,直接操作内存 unsafe
什么是指令重排?
你写的程序,计算机并不是按照你写的顺序执行
源代码 -> 编译器优化的重排 -> 指令并行也可能会重排 ->内存系统也会重排 ->执行
处理器在进行指令重排的时候,会考虑数据的依赖问题
Volatile如何避免指令重排
内存屏障,作用:
1.保证特定的操作执行顺序
//一上来就创建对象
public class Demo02 {
//饿汉式 构造器私有 避免别人创建对象
private Demo02(){
}
// 饿汉式可能会造成浪费空间
private final static Demo02 DEMO = new Demo02();
public static Demo02 getInstance(){
return DEMO;
}
}
public class Demo02 {
//构造器私有 避免别人创建对象
private Demo02(){
}
// 懒汉式可能会造成浪费空间 volatile确保不会被指令重排
private volatile static Demo02 DEMO;
public static Demo02 getInstance(){
if (DEMO == null){
//多线程获取 需要使用synchronized 双重检测锁模式
synchronized (Demo02.class){
if (DEMO == null){
DEMO = new Demo02();
}
}
}
return DEMO;
}
}
DCL懒汉式双重检测机制,在极端情况下还是会出现问题
这几行代码 在正常情况下的执行流程应该是:
1.分配内存空间
2.执行构造方法,初始化对象
3.把这个对象指向这个空间
如果出现了指令重排123 变成了132 就会出现问题
所以我们应该加上volatile
确保不会发生指令重排
使用反射创建对象
public class Demo02 {
//构造器私有 避免别人创建对象
private Demo02(){
}
// 懒汉式可能会造成浪费空间 volatile确保不会被指令重排
private volatile static Demo02 DEMO;
public static Demo02 getInstance(){
if (DEMO == null){
//多线程获取 需要使用synchronized 双重检测锁模式
synchronized (Demo02.class){
if (DEMO == null){
DEMO = new Demo02();
}
}
}
return DEMO;
}
//使用反射创建对象 破坏单例模式
public static void main(String[] args) throws Exception {
Constructor<Demo02> declaredConstructor = Demo02.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);//关闭安全检测
Demo02 demo02 = declaredConstructor.newInstance();//通过反射创建对象
}
}
使用反射就可以绕过私有变量创建新的对象
什么是枚举类?
枚举是JDK1.5的时候出现的,本质上是一个class,它默认就是单例模式
public enum EnumSingTest {
INSTANCE;
public EnumSingTest getInstance(){
return INSTANCE;
}
}
枚举类本身没有无参构造
什么是CAS
compareAndSet :比较并交换
如果期望的值达到了,那么就会更新,否则就会不更新
public static void main(String[] args) {
//CAS 如果期望的值达到了 就更新
//CAS 是CPU的并发原语
AtomicInteger atomicInteger = new AtomicInteger(2020);
atomicInteger.compareAndSet(2020,2021);
}
点进源码可以看到一个unsafe
的东西,它是什么?
我们知道Java是无法操作内存的
但是可以通过unsafe这个类来操作内存
查看原子类AtomicInteger
的加1操作,再次理解CAS
CAS:比较当前工作内存中的值,和主内存中的值,如果这个值是期望的,则进行交换,否则会一直循环(自旋锁)
缺点:
1.循环会耗时
2.一次性只能保证一个共享变量的原子性
3.ABA问题
CAS:什么是ABA问题?(狸猫换太子)
比如是有两个线程A,B ,一个变量:苹果
A线程期望拿到一个苹果
B线程一进来把苹果改成了梨子,但是在最后结束的时候又把梨子换成了苹果
A线程在此期间是不知情的,以为自己拿到的苹果还是原来的那一个,其实已经被换过了
如何解决ABA问题
原子引用
AtomicStampedReference
类似于乐观锁,比较时会去对比版本号,确认变量是否被换过了
public static void main(String[] args) {
//AtomicStampedReference 构造方法需要传入变量 和 版本号
AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(1, 1);
new Thread(() -> {
int stamp = reference.getStamp();//获取版本号
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//期望2020 换成2021 期望的版本号是reference.getStamp() 结束后版本号+1
reference.compareAndSet(1,2,reference.getStamp(),reference.getStamp()+1);
reference.compareAndSet(2,1,reference.getStamp(),reference.getStamp()+1);
}).start();
new Thread(() -> {
int stamp = reference.getStamp();//获取版本号
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//如果版本号不是原来的那一个 那么修改会不成功
reference.compareAndSet(1,6,stamp,stamp+1);
}).start();
}
class Phone{
//可重入锁 拿到外面的锁 也就获得了里面的锁
public synchronized void sms(){
System.out.println("1111");
call();
}
public synchronized void call(){
System.out.println(2222);
}
}
lock锁
class Phone{
Lock lock =new ReentrantLock();
public void sms(){
lock.lock();
lock.lock();//lock锁细节 一把锁 对应一把钥匙 如果多把锁 只解了一次 那么一定会产生死锁
try {
System.out.println("1111");
call();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
lock.unlock();
}
}
public void call(){
lock.lock();
try {
System.out.println(2222);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
自旋锁就是不断尝试,直到成功为止
死锁是什么
怎么排查死锁