JUC并发编程学习笔记(二)

线程池

线程池有哪些好处?

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>());
    }

在阿里云开发手册中有这样一句话:
JUC并发编程学习笔记(二)_第1张图片

自定义线程池

/**
 * 自定义线程池
 * @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函数型接口

函数型接口
JUC并发编程学习笔记(二)_第2张图片
传入参数T 返回参数R

       //工具类
        Function function =new Function<String,String>() {
            @Override
            public String apply(String o) {
                return o;
            }
        };

        //Lambda表达式
        Function functio = (str) -> {return str;};

Predicate断定型接口

断定型接口
JUC并发编程学习笔记(二)_第3张图片
只有一个输入参数 返回值只能是布尔值

 Predicate predicate = (s) -> {return true;};

Consumer消费性接口

消费性接口
JUC并发编程学习笔记(二)_第4张图片
只有输入参数 没有返回值

 //有输入参数 无返回值
 Consumer consumer = (s) -> {
            System.out.println(s);
        };

Supplier供给型接口

供给型接口
JUC并发编程学习笔记(二)_第5张图片
没有输入 只有返回值

      //无输入参数 有返回值
        Supplier supplier = () -> {
          return 1;  
        };

Stream流式计算

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

什么是ForkJoin

分支合并,并行处理任务,将比较大的任务拆成小任务
适用场景:适合处理大型数据
JUC并发编程学习笔记(二)_第6张图片
ForkJoin特点:工作窃取

此时有两个线程,A线程执行了一半,B线程已经执行完了,此时B线程会去窃取A线程的任务,来帮助它完成,这就叫工作窃取

这里面维护的是双端队列
JUC并发编程学习笔记(二)_第7张图片

ForkJoin使用

JUC并发编程学习笔记(二)_第8张图片

  • 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

Volatile是Java虚拟机提供的轻量级的同步机制

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

什么是可见性,说这个东西之前要先说一下JMM

JMM:Java内存模型,不存在的东西,概念!约定

关于JMM的一些同步约定

1.线程解锁前,必须把共享变量立刻刷会主存
2.线程加锁前,必须读取主存中的最新值到工作内存
3.加锁和解锁是用一把锁

线程 工作内存, 主内存
8种操作
JUC并发编程学习笔记(二)_第9张图片
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操作之前,必须把此变量同步回主内存

JUC并发编程学习笔记(二)_第10张图片

保证可见性

    //多个线程使用同一个变量时 需要加入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++不是一个原子性操作
JUC并发编程学习笔记(二)_第11张图片
所以要解决这个问题使用原子类

    //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
JUC并发编程学习笔记(二)_第12张图片

禁止指令重排

什么是指令重排?

你写的程序,计算机并不是按照你写的顺序执行

源代码 -> 编译器优化的重排 -> 指令并行也可能会重排 ->内存系统也会重排 ->执行

处理器在进行指令重排的时候,会考虑数据的依赖问题

Volatile如何避免指令重排

内存屏障,作用:
1.保证特定的操作执行顺序

2.可以保证某些变量的内存可见性
JUC并发编程学习笔记(二)_第13张图片

彻底玩转单例模式

饿汉式

//一上来就创建对象
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();//通过反射创建对象
    }

}

使用反射就可以绕过私有变量创建新的对象

我们查看反射的源码可以发现
反射机制不能操作枚举类
JUC并发编程学习笔记(二)_第14张图片

什么是枚举类?

枚举是JDK1.5的时候出现的,本质上是一个class,它默认就是单例模式

public enum  EnumSingTest {
    
    INSTANCE;
    
    public EnumSingTest getInstance(){
        return INSTANCE;
    }
}

枚举类本身没有无参构造

深入理解CAS

什么是CAS

compareAndSet :比较并交换

如果期望的值达到了,那么就会更新,否则就会不更新

public static void main(String[] args) {
        //CAS 如果期望的值达到了 就更新
        //CAS 是CPU的并发原语
        AtomicInteger atomicInteger = new AtomicInteger(2020);
        atomicInteger.compareAndSet(2020,2021);
    }

点进源码可以看到一个unsafe的东西,它是什么?

我们知道Java是无法操作内存的
但是可以通过unsafe这个类来操作内存
JUC并发编程学习笔记(二)_第15张图片
查看原子类AtomicInteger的加1操作,再次理解CAS
JUC并发编程学习笔记(二)_第16张图片
JUC并发编程学习笔记(二)_第17张图片
CAS:比较当前工作内存中的值,和主内存中的值,如果这个值是期望的,则进行交换,否则会一直循环(自旋锁)
缺点:
1.循环会耗时
2.一次性只能保证一个共享变量的原子性
3.ABA问题

CAS:什么是ABA问题?(狸猫换太子)

比如是有两个线程A,B ,一个变量:苹果
A线程期望拿到一个苹果
B线程一进来把苹果改成了梨子,但是在最后结束的时候又把梨子换成了苹果

A线程在此期间是不知情的,以为自己拿到的苹果还是原来的那一个,其实已经被换过了

如何解决ABA问题

原子引用

原子引用-解决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();
    }

各种锁的理解

可重入锁

又叫递归锁
JUC并发编程学习笔记(二)_第18张图片

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(); 
        }
    }
}

自旋锁

自旋锁就是不断尝试,直到成功为止

JUC并发编程学习笔记(二)_第19张图片

死锁

死锁是什么

多个线程同时争夺对方的资源
JUC并发编程学习笔记(二)_第20张图片

怎么排查死锁

1.使用JPS定位进程号,命令:jsp -l
2.使用jstack加进程号找到死锁问题
JUC并发编程学习笔记(二)_第21张图片

你可能感兴趣的:(狂神说学习笔记,java,多线程,并发编程)