JUC - 并发编程进阶

一、CompletableFuture

1. Future 接口理论知识复习

Future接口(FutureTask实现类)定义了操作异步任务执行一些方法,如:获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等。

比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就可以去做其他事情了,忙其它事情或者先执行完,过了一会才去获取子任务的执行结果或变更的任务状态。

打开 Future 类的源码,使用快捷键 ALT+7 查看类中全部的方法

image-20221221143115446

为什么会出现这个类?解决什么痛点?

当主线程忙着其他业务的时候,如果需要执行另外一个耗时的任务时,则可以使用该类,新开一个子线程去办耗时的任务,主线程依旧忙自己的业务,当主线程业务忙完之后,可以调用改类的方法去检查子线程是否已经完成,以及完成的结果是怎样的,如果子线程已经完成且任务的结果是成功,那么流程完成;如果结果是失败,那么可以进行相应的重试操作。

image-20221221144547731

**总结:**Future接口可以为主线程开一个分支任务,专门为主线程处理耗时和费力的复杂业务。且在主线程空闲后,可以调用子线程的方法来检查耗时的任务的执行情况,以此来判断是否需要进行重试操作。

2. Future 接口常用实现类 FutureTask 异步任务

接口一般都是通过使用它的实现类来进行业务操作的,而FutureTask 就是最常用的实现类。

Future是Java5新加的一个接口,它提供了一种异步并行计算的功能。如果主线程需要执行一个很耗时的计算任务,我们就可以通过future把这个任务放到异步线程中执行。主线程继续处理其他任务或者先行结束,再通过Future获取计算结果。

目的:异步多线程任务执行且返回有结果,三个特点:多线程/有返回/异步任务 (班长为老师去买水作为新启动的异步多线程任务且买到水有结果返回)

代码举例

子线程类


/**
 * 线程类 2 实现 Callable<String>
 * 返回 string类型的结果
 *
 * @author byChen
 * @date 2022/12/21
 */
public class MyThread2 implements Callable<String> {

    public String call() throws Exception {
        System.out.println("子线程:开启子线程,执行耗时任务。。。");
        Thread.sleep(7000);
        System.out.println("子线程:-子线程执行完成-");
        boolean flag = true;
        if (flag) {
            //执行成功,返回200
            return "200";
        } else {
            return "500";
        }
    }
}

主类


    @GetMapping("/test1")
    public void test() throws ExecutionException, InterruptedException {
        /**
         * 1.创建子线程
         */
        //使用 FutureTask类包装 资源类
        FutureTask futureTask=new FutureTask(new MyThread2());

        /**
         * 2.开启子线程,执行耗时的业务
         */
        //放入,分别交由两个线程执行
        new Thread(futureTask,"A线程").start();

        /**
         * 3.主线程继续执行主业务
         */
        System.out.println("主线程:主线程继续执行。。。");
        Thread.sleep(3000);
        System.out.println("主线程:主线程执行完成。。。");
        System.out.println("主线程:开始检查子线程情况。。。");

        /**
         * 4.主线程任务完成,检查子线程执行的返回值
         */
        //想要获取子线程的返回值,就需要等待子线程完成,完成前,主线程被阻塞
        System.out.println("主线程:子线程执行状态:"+futureTask.get());

        /**
         * 5.根据返回值,决定是否重试
         */
        if ("200".equals(futureTask.get())){
            System.out.println("子线程执行成功,流程结束");
        }else {
            System.out.println("子线程执行失败,重试");
        }

    }

执行结果:

主线程:主线程继续执行。。。                     ## 主线程不受影响,照常执行
子线程:开启子线程,执行耗时任务。。。            ## 子线程同步进行处理
主线程:主线程继续执行。。。               
主线程:主线程执行完成,开始检查子线程情况。。。   ## 主线程执行完成,检查子线程情况

## 主线程去查看子线程的状态返回时,如果子线程一直没有执行完成,主线程也会一直阻塞等待。。

子线程:-子线程执行完成-            
主线程:子线程执行状态:200                      ## 根据子线程返回值,判断是否重试子任务
子线程执行成功,流程结束

这样不管子线程是否执行完成都去获取返回值的方式,会导致线程阻塞,且不符合逻辑。因此需要进行改进,使用while循环+isDone 的方式,去不断的检查是否完成,完成才去获取返回值

代码改进:

    @GetMapping("/test2")
    public void test2() throws ExecutionException, InterruptedException {
        /**
         * 1.创建子线程
         */
        //使用 FutureTask类包装 资源类
        FutureTask futureTask=new FutureTask(new MyThread2());

        /**
         * 2.开启子线程,执行耗时的业务
         */
        //放入,分别交由两个线程执行
        new Thread(futureTask,"A线程").start();

        /**
         * 3.主线程继续执行主业务
         */
        System.out.println("主线程:主线程继续执行。。。");
        Thread.sleep(3000);
        System.out.println("主线程:主线程执行完成。。。");
        System.out.println("主线程:开始检查子线程情况。。。");

        /**
         * 4.主线程任务完成,检查子线程执行的返回值
         */
        //想要获取子线程的返回值,就需要等待子线程完成,完成前,主线程被阻塞
        //这里使用 while+idDone 的方法,间隔去轮询
        while (true){
            if (futureTask.isDone()){
                System.out.println("主线程:子线程执行状态:"+futureTask.get());
                break;
            }else {
                TimeUnit.SECONDS.sleep(1);
                System.out.println("子线程未完成,每隔一秒去检查状态");
            }
        }
        /**
         * 5.根据返回值,决定是否重试
         */
        if ("200".equals(futureTask.get())){
            System.out.println("子线程执行成功,流程结束");
        }else {
            System.out.println("子线程执行失败,重试");
        }

    }

执行结果

主线程:主线程继续执行。。。
子线程:开启子线程,执行耗时任务。。。
主线程:主线程执行完成。。。
主线程:开始检查子线程情况。。。

## 开始 while+isDone 去循环检查子线程状态。。。

子线程未完成,每隔一秒去检查状态
子线程未完成,每隔一秒去检查状态
子线程未完成,每隔一秒去检查状态   ## 不断检查
子线程:-子线程执行完成-
## 当 子线程执行完成 ,isDone 返回true,则证明子线程完成,可以去获取状态
主线程:子线程执行状态:200
子线程执行成功,流程结束

这样虽然一定程度上解决了阻塞问题,但是因为不断的轮询,又会耗费cpu资源

3. CompletableFuture对Future的改进

3.1 为什么要改进?FutureTask有什么缺点?

①.主线程阻塞:主线程去查看子线程的返回结果时,如果子线程还没有执行结束,那么主线程就会被阻塞,因此我们通常在主线程任务的末尾来去检查子线程状态,避免主线程被阻塞;这是一种弊端,而进一步的解决办法,就是使用 CompletableFuture 来对其进行优化。

②.轮询耗费cpu资源:如果不想阻塞进程,那么通常就采用轮询的方式去获取结果。但是这样会耗费无谓的cpu资源,而且也不能保证及时的获取到计算结果。

**缺点总结:**Future 对于结果的获取不是很友好,只能通过阻塞或者轮询的方式得到任务的结果。

3.2 优化改进内容

首先,Future能干的,Completable 都能干。并且提供了一些新的操作,比如回调通知、创建异步任务、选取计算最快的结果、多任务前后依赖等内容。

① 异步处理,回调通知

对于正在的异步处理,我们希望是可以通过传入回调函数,在异步任务完成得时候自动调用该回调函数,这样就不用一直轮询去获取结果。Completable 提供了一种观察者模式类似的机制,可以让异步任务完成后,通知监听的一方。

② 处理结果,选取结果

③ 计算结果合并

3.3 具体使用

① 获取

首先,需要先获取到。获取 CompletableFuture 有四种静态方法,分别是

runAsync 无返回值,不指定线程池

    @GetMapping("/createFuture")
    public void createFuture() throws ExecutionException, InterruptedException {
        /**
         * 不指定线程池的方法,进行无返回值的异步 CompletableFuture 创建
         */
        CompletableFuture<Void> voidCompletableFuture = CompletableFuture.runAsync(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                //因为没有指定线程池,因此会使用默认的 ForkJoinPool 线程池中的线程
                System.out.println(Thread.currentThread().getName());
                TimeUnit.SECONDS.sleep(1);
            }
        });
        //因为本方法是没有返回值的,因此这里输出应该是null
        System.out.println(voidCompletableFuture.get());
    }

执行结果

ForkJoinPool.commonPool-worker-1  # 不指定线程池,就使用默认的线程池 ForkJoinPool
null

runAsync 无返回值,指定线程池

  @GetMapping("/createFuture2")
    public void createFuture2() throws ExecutionException, InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        /**
         * 指定线程池的方法,进行无返回值的异步 CompletableFuture 创建
         */
        CompletableFuture<Void> voidCompletableFuture = CompletableFuture.runAsync(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                //因为指定了线程池,因此会使用指定的 线程池中的线程
                System.out.println(Thread.currentThread().getName());
                TimeUnit.SECONDS.sleep(1);
            }
        },threadPool);
        //因为本方法是没有返回值的,因此这里输出应该是null
        System.out.println(voidCompletableFuture.get());

        //释放线程池资源
        threadPool.shutdown();
    }

执行结果

pool-1-thread-1 # 指定线程池,就直接使用指定的
null

supplyAsync 有返回值,不指定线程池

    @GetMapping("/createFuture3")
    public void createFuture3() throws ExecutionException, InterruptedException {
        /**
         * 不指定线程池的方法,进行有返回值的异步 CompletableFuture 创建
         */
        CompletableFuture<String> objectCompletableFuture = CompletableFuture.supplyAsync(()->{
            //因为没有指定线程池,因此会使用默认的 ForkJoinPool 线程池中的线程
            System.out.println(Thread.currentThread().getName());
            //业务逻辑处理
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //需要有返回值
            return "success";
        });

        //因为本方法是有返回值的,因此这里输出应该是 success
        System.out.println(objectCompletableFuture.get());
    }

执行结果

ForkJoinPool.commonPool-worker-1 # 默认线程池
success  # 打印出返回值

supplyAsync 有返回值,指定线程池

    @GetMapping("/createFuture5")
    public void createFuture5() throws ExecutionException, InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        /**
         * 指定线程池的方法,进行有返回值的异步 CompletableFuture 创建
         */
        CompletableFuture<String> objectCompletableFuture = CompletableFuture.supplyAsync(()->{
            //因为指定线程池,因此会使用指定的线程池中的线程
            System.out.println(Thread.currentThread().getName());
            //业务逻辑处理
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //需要有返回值
            return "success";
        },threadPool);

        //因为本方法是有返回值的,因此这里输出应该是 success
        System.out.println(objectCompletableFuture.get());

        //释放线程池资源
        threadPool.shutdown();
    }

执行结果

pool-1-thread-1 # 使用指定的线程池
success  # 打印返回值
② 通用演示
    /**
     *
     */
    @GetMapping("/userTestOne")
    public void userTestOne() throws InterruptedException {

        //开启子线程
        CompletableFuture<Integer> integerCompletableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + " 子线程开启");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int i = ThreadLocalRandom.current().nextInt(10);
            System.out.println("子线程执行完成,结果:" + i);
            return i;
        });
        //设定回调通知,(包含正常通知+异常通知)
        //子线程执行后,没有异常,进入 whenComplete方法;有异常,进入 exceptionally方法;
        integerCompletableFuture
                .whenComplete((v, e) -> { //v 子线程返回的结果;e 子线程执行的异常
                    if (e == null) {
                        System.out.println("子线程执行完毕且成功,回调通知结果为:" + v);
                    }
                }).exceptionally(e -> {
            System.out.println("子线程执行出现异常:" + e.getCause() + " " + e.getMessage());
            return null;
        });

        //主线程继续
        System.out.println(Thread.currentThread().getName() + " 主线程继续其他的业务");

        //这里主线程不要立即结束,因为主线程结束后,会立即关闭线程,会导致回调失效
        TimeUnit.SECONDS.sleep(3);
    }

执行结果

http-nio-8023-exec-2 主线程继续其他的业务
ForkJoinPool.commonPool-worker-1 子线程开启
子线程执行完成,结果:7
子线程执行完毕且成功,回调通知结果为:7

如果想要避免因为主线程结束导致回调失效的问题,可以使用指定线程池的方式来优化

    /**
     *
     */
    @GetMapping("/userTestOne")
    public void userTestOne() throws InterruptedException {
        // 使用线程池的方式创建
        ExecutorService threadPool = Executors.newFixedThreadPool(3);

        try {
            //开启子线程
            CompletableFuture<Integer> integerCompletableFuture = CompletableFuture.supplyAsync(() -> {
                System.out.println(Thread.currentThread().getName() + " 子线程开启");
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int i = ThreadLocalRandom.current().nextInt(10);
                System.out.println("子线程执行完成,结果:" + i);
                return i;
            }, threadPool);
            //设定回调通知,(包含正常通知+异常通知)
            //子线程执行后,没有异常,进入 whenComplete方法;有异常,进入 exceptionally方法;
            integerCompletableFuture
                    .whenComplete((v, e) -> {
                        if (e == null) {
                            System.out.println("子线程执行完毕且成功,回调通知结果为:" + v);
                        }
                    }).exceptionally(e -> {
                System.out.println("子线程执行出现异常:" + e.getCause() + " " + e.getMessage());
                return null;
            });

            //主线程继续
            System.out.println(Thread.currentThread().getName() + " 主线程继续其他的业务");
        } catch (Exception e) {
            System.out.println(e);
        } finally {
            //无论如何,释放线程池
            threadPool.shutdown();
        }

    }

执行结果

pool-1-thread-1 子线程开启 ## 采用了指定的线程池
http-nio-8023-exec-1 主线程继续其他的业务
子线程执行完成,结果:0 
子线程执行完毕且成功,回调通知结果为:0   ## 就算没有给主线程做特殊操作,也会等待子线程回调

5.案例精讲

1.案例需求

image-20221222140725168

2.代码编写

先创建一个类,用于模拟数据库数据

/**
 * 用于模拟数据库数据的实体
 * @author byChen
 * @date 2022/12/22
 */
public class NetMall {
    /**
     * 电商名称
     */
    @Getter
    private String netMallName;

    public NetMall(String netMallName) {
        this.netMallName = netMallName;
    }

    /**
     * 模拟查询该产品价格
     *
     * @param productName 产品名
     * @return
     */
    public double calcPrice(String productName) {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //charAt 搭配数字使用,会变为数字相加
        return ThreadLocalRandom.current().nextDouble() * 2 + productName.charAt(0);
    }
}
单线程版
//模拟出来值  
static List<NetMall> netMall = Arrays.asList(
            new NetMall("jd"),
            new NetMall("dangdang"),
            new NetMall("taobao"),
            new NetMall("pdd")
    );

    /**
     * step by step  一步步的查
     * 接口参数传 mysql
     */
    @GetMapping("/comparePrice")
    public void comparePrice(String produceName) {
        long startTime = System.currentTimeMillis();

        //使用stream流进行模拟数据查询、处理
        List<String> collect = netMall.stream()
                .map(netMall1 ->
                        String.format(produceName + " in %s price is %.2f", netMall1.getNetMallName(),          						netMall1.calcPrice(produceName)))
                .collect(Collectors.toList());
        // 遍历打印
        collect.forEach(System.out::println);

        long endTime = System.currentTimeMillis();
        //计算耗时
        System.out.println("查询耗时:"+(endTime-startTime)+"毫秒");
    }

执行结果

mysql in jd price is 110.06
mysql in dangdang price is 110.43
mysql in taobao price is 109.67
mysql in pdd price is 110.47
查询耗时:4003毫秒
多线程版
   /**
     * all in  同步多线程的去查
     * 多线程查
     */
    @GetMapping("/comparePrice2")
    public void comparePrice2(String produceName) {
        long startTime = System.currentTimeMillis();

        /**
         * 1.使用stream流,给集合中每一个元素都开启一个子线程,因为开启子线程返回的是 CompletableFuture
         * 因此,最后收集到的集合内元素也是 CompletableFuture
         */
        List<CompletableFuture<String>> futureList = netMall
                .stream()
                .map(netMall1 -> CompletableFuture.supplyAsync(() ->
                        String.format(produceName + " in %s price is %.2f", netMall1.getNetMallName(), netMall1.calcPrice(produceName))
                )).collect(Collectors.toList());
        
        /**
         * 2.这里获取子线程返回值,使用主动获取的方式,调用 join 方法,并收集返回值
         * join方法跟get方式类似,只不过 join方法没有抛出异常
         */
        List<String> stringList = futureList.stream().map(stringCompletableFuture -> stringCompletableFuture.join()).collect(Collectors.toList());

        /**
         * 3.打印
         */
        stringList.forEach(System.out::println);

        long endTime = System.currentTimeMillis();
        System.out.println("查询耗时:" + (endTime - startTime) + "毫秒");
    }

执行结果

mysql in jd price is 110.30
mysql in dangdang price is 110.90
mysql in taobao price is 109.32
mysql in pdd price is 109.29
查询耗时:1005毫秒  # 耗时由 4003毫秒 降低到 1005毫秒

6.ComplatableFuture 常用方法api

ComplatableFuture 是对Future的强化版,而强化的地方,就在于它另外实现了CompletionStage 接口。而额外的功能,大致可以分为五大类,分别为:

① 获得结果和触发计算

获得结果

get() 正常的获取子线程结果,子线程没有执行完就一直阻塞等待

get(long timeout,TimeUnit unit) 限定时间,超过指定时间不返回结果,就直接抛出异常

join 跟get基本一样,只不过不会主动抛出异常

getNow(T valueIfAbsent) 获取执行结果,如果执行完正常返回结果;如果没有执行完,就返回 valueIfAbsent 的值

    /**
     *获得结果和触发计算
     */
    @GetMapping("/userOne")
    public void userOne() throws ExecutionException, InterruptedException, TimeoutException {
        CompletableFuture<String> stringCompletableFuture = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "success";
        });

//        // 正常的获取子线程结果,子线程没有执行完就一直阻塞等待
//        System.out.println(stringCompletableFuture.get());
//        // 限定时间,超过1秒不返回结果,就直接抛出异常
//        stringCompletableFuture.get(1,TimeUnit.SECONDS);
//        // 跟get基本一样,只不过不会主动抛出异常
//        stringCompletableFuture.join();
        //获取执行结果,如果执行完正常返回结果;如果没有执行完,就返回 valueIfAbsent 的值
        System.out.println(stringCompletableFuture.getNow("未计算完成"));
    }

执行结果

未计算完成

触发计算

boolen complete(T value) 运行到本行代码时,会去检查子线程是否已经完成,如果没有完成,就立即中断子线程执行,并返回接口中value的值;如果子线程已经完成,就不会中断子线程并返回false;

    /**
     *获得结果和触发计算
     */
    @GetMapping("/userOne")
    public void userOne() throws ExecutionException, InterruptedException, TimeoutException {
        CompletableFuture<String> stringCompletableFuture = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "success";
        });
        
        //子线程还没有结束,则打断子线程的执行,并返回 true
        //这个时候,去get的话,只会获取到 complete中传入的参数
        System.out.println(stringCompletableFuture.complete("打断子线程执行"));    
        System.out.println(stringCompletableFuture.get());
        ---------------------------------
        //子线程已经结束,则不会再打断子线程的执行,并返回 fales
	   //这个时候,去get的话,会正常获取到子线程的计算结果
        TimeUnit.SECONDS.sleep(3);
        System.out.println(stringCompletableFuture.complete("打断子线程执行"));
        System.out.println(stringCompletableFuture.get());
        
        
    }

执行结果

## 子线程还没有结束,则打断子线程的执行,并返回 true
## 这个时候,去get的话,只会获取到 complete中传入的参数
true
打断子线程执行

------------------------------------
## 子线程已经结束,则不会再打断子线程的执行,并返回 fales
## 这个时候,去get的话,会正常获取到子线程的计算结果
false
success

② 对计算结果进行处理

thenApply 计算结果存在前后依赖关系,两个线程串行化;同时因为存在前后关系,因此当前步骤错误,就会停止不再往下运行。

    /**
     * 对计算结果进行处理
     */
    @GetMapping("/userTwo")
    public void userTwo() {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        CompletableFuture<String> stringCompletableFuture = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "第一步";
        },threadPool);

        //进行后续步骤处理
        stringCompletableFuture.thenApply(f -> {
            System.out.println("第二步");
            return f + "|第二步";
        }).thenApply(f -> {
            System.out.println("第三步");
            return f + "|第三步";
        }).whenComplete((v, e) -> {
            if (e == null) {
                System.out.println("最终结果:" + v);
            }
        }).exceptionally(e -> {
            System.out.println("有异常:" + e.getMessage());
            return null;
        });

        System.out.println(Thread.currentThread().getName()+" 主线程先去忙其他的");
        threadPool.shutdown();

    }

执行结果

http-nio-8023-exec-1 主线程先去忙其他的
第二步
第三步
最终结果:第一步|第二步|第三步

异常处理:第二步手动创建一个异常

 //进行后续步骤处理
        stringCompletableFuture.thenApply(f -> {
            System.out.println("第二步");
            int a=10/0;
            return f + "|第二步";
        }).thenApply(f -> {
            System.out.println("第三步");
            return f + "|第三步";
        }).whenComplete((v, e) -> {
            if (e == null) {
                System.out.println("最终结果:" + v);
            }
        }).exceptionally(e -> {
            System.out.println("有异常:" + e.getMessage());
            return null;
        });

异常执行结果

http-nio-8023-exec-1 主线程先去忙其他的
第二步
有异常:java.lang.ArithmeticException: / by zero

handle 跟thenApply一样,差别在于:1.传参需要传两个;2.其中一步出现异常,不会中断,可以继续往下走

    /**
     * 对计算结果进行处理
     */
    @GetMapping("/userTwo")
    public void userTwo() {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        CompletableFuture<String> stringCompletableFuture = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "第一步";
        }, threadPool);

        //进行后续步骤处理
        stringCompletableFuture.handle((f, e) -> {
            System.out.println("第二步");
//            int a = 10 / 0;
            return f + "|第二步";
        }).handle((f, e) -> {
            System.out.println("第三步");
            return f + "|第三步";
        }).whenComplete((v, e) -> {
            if (e == null) {
                System.out.println("最终结果:" + v);
            }
        }).exceptionally(e -> {
            System.out.println("有异常:" + e.getMessage());
            return null;
        });

        System.out.println(Thread.currentThread().getName() + " 主线程先去忙其他的");
        threadPool.shutdown();
    }

执行结果

http-nio-8023-exec-1 主线程先去忙其他的
第二步
第三步
最终结果:第一步|第二步|第三步

异常处理:第二步手动创建一个异常

 //进行后续步骤处理
        stringCompletableFuture.handle((f, e) -> {
            //这一步出现异常,则跳过这一步,返回值返回的是null
            System.out.println("第二步");
            int a = 10 / 0;
            return f + "|第二步";
        }).handle((f, e) -> {
            System.out.println("第三步");
            return f + "|第三步";
        }).whenComplete((v, e) -> {
            if (e == null) {
                System.out.println("最终结果:" + v);
            }
        }).exceptionally(e -> {
            System.out.println("有异常:" + e.getMessage());
            return null;
        });

异常执行结果

http-nio-8023-exec-1 主线程先去忙其他的
第二步
第三步
最终结果:null|第三步

**总结:**一般常用的是 thenApply

③ 对计算结果进行消费

thenAccept 接收任务的处理结果,并消费处理,没有返回值

    /**
     * 对计算结果进行消费
     */
    @GetMapping("/userThree")
    public void userThree() {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        CompletableFuture<String> stringCompletableFuture = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "第一步";
        }, threadPool);

        stringCompletableFuture.thenApply(f->{
            return f+"|第二步";
        }).thenApply(f->{
            return f+"|第三步";
        }).thenAccept(f->{
            System.out.println("单纯消费");
            System.out.println(f);
        });

        System.out.println(Thread.currentThread().getName() + " 主线程先去忙其他的");
        threadPool.shutdown();
    }

执行结果

http-nio-8023-exec-1 主线程先去忙其他的
单纯消费
第一步|第二步|第三步

对比补充

image-20221222172643160

代码举例

   /**
     * 对计算结果进行消费
     */
    @GetMapping("/userThree")
    public void userThree() throws ExecutionException, InterruptedException {
		//只是单纯的前后关系,两个线程之间没有参数关系
        System.out.println(CompletableFuture.supplyAsync(()->"resultA").thenRun(()->{}).get());
        System.out.println("-------------------");
        //有前后关系,且后一个线程需要前一个线程的结果来进行消费
        System.out.println(CompletableFuture.supplyAsync(()->"resultA").thenAccept(r->{ System.out.println(r); }).get());
        System.out.println("-------------------");
        //有前后关系,且后一个线程需要前一个线程的结果来进行消费,且得有返回值
        System.out.println(CompletableFuture.supplyAsync(()->"resultA").thenApply(r-> r +"123").get());
    }

执行结果

null
-------------------
resultA
null
-------------------
resultA123

④ 对计算速度进行选用

applyToEither 谁快用谁

    /**
     *  对计算速度进行选用
     */
    @GetMapping("/userFour")
    public void userFour() throws ExecutionException, InterruptedException {
        //开启子线程A
        CompletableFuture<String> playA = CompletableFuture.supplyAsync(() -> {
            System.out.println("playA come in");
            try {
                //2秒
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "playA";
        });

        //开启子线程B
        CompletableFuture<String> playB = CompletableFuture.supplyAsync(() -> {
            System.out.println("playB come in");
            try {
                //3秒
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "playB";
        });

        //使用 applyToEither 进行速度比较 
        // 子线程A 调用 applyToEither()参数里面传需要跟他进行对比的线程
        CompletableFuture<String> result = playA.applyToEither(playB, f -> {
            return f + " is winner";
        });
        System.out.println("最快的是:"+result.get());
    }

执行结果

playA come in
playB come in
最快的是:playA is winner

⑤ 对计算结果进行合并

thenCombine 两个CompletionStage任务都完成后,最终能把两个任务的结果一起交给thenCombine来处理,先完成的先等着,等待其它分支任务

 /**
     *  对计算结果进行合并
     */
    @GetMapping("/userFive")
    public void userFive() throws ExecutionException, InterruptedException {
        //开启子线程A
        CompletableFuture<String> playA = CompletableFuture.supplyAsync(() -> {
            System.out.println("playA come in");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "playA";
        });

        //开启子线程B
        CompletableFuture<String> playB = CompletableFuture.supplyAsync(() -> {
            System.out.println("playB come in");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "playB";
        });

        //使用
        CompletableFuture<String> result = playA.thenCombine(playB, (x, y) -> {
            System.out.println("开始合并结果");
            return x + y;
        });
        System.out.println("合并后的结果的是:"+result.get());

    }

执行结果

playA come in
playB come in
开始合并结果
合并后的结果的是:playAplayB

补充 线程池的选择

image-20221222174347398

二、锁

1.八锁情况分析

首先,看一下阿里编程手册中关于锁的建议:

【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体能用对象锁,就不要用类锁

说明︰尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用RPC方法。

接下来具体结合八种锁的情况,解释理解上面的约束含义。

① 标准访问,给两个方法都加 synchronized

资源类

/**
 * 资源类
 *
 * @author byChen
 * @date 2022/12/23
 */
public class Phone {
    public synchronized void sendMall() {
        System.out.println(Thread.currentThread().getName() + "--发送邮件");
    }

    public synchronized void callPhone() {
        System.out.println(Thread.currentThread().getName() + "--打电话");
    }
}

操作

    /**
     * 标准访问,给两个方法都加 synchronized 
     */
    @GetMapping("/one")
    public void one() {
        Phone phone = new Phone();

        new Thread(()->{
            phone.sendMall();
        },"A线程").start();

        new Thread(()->{
            phone.callPhone();
        },"B线程").start();
    }

执行结果

A线程--发送邮件
B线程--打电话
A线程--发送邮件
B线程--打电话
A线程--发送邮件
B线程--打电话
B线程--打电话
A线程--发送邮件  ## 发起4次请求,每次线程执行顺序都不固定,具体看CPU对线程的调度。

② 手动让其中一个 synchronized 方法 sleep,看执行顺序

资源类

/**
 * 资源类
 *
 * @author byChen
 * @date 2022/12/23
 */
public class Phone {
    public synchronized void sendMall(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "--发送邮件");
    }

    public synchronized void callPhone() {
        System.out.println(Thread.currentThread().getName() + "--打电话");
    }
}

操作

    /**
     *  手动让其中一个 synchronized 方法 sleep,看执行顺序
     */
    @GetMapping("/two")
    public void two() {
        Phone phone = new Phone();

        new Thread(()->{
            phone.sendMall();
        },"A线程").start();

        new Thread(()->{
            phone.callPhone();
        },"B线程").start();
    }

执行结果


A线程--发送邮件 ## 运行到加了sleep方法,会睡眠
B线程--打电话  ## 另外一个 方法会等待A线程执行完才会执行

因为在同一个对象里如果有多个synchronized方法或synchronized代码块,某一个时刻内,只要有一个线程去调用了其中一个synchronized方法,其它调用该资源类中其他锁方法的线程只能等待。
换句话说,某一个时刻内,只能有一个线程去访问这些synchronized方法,在本例中即使是线程A休眠了2秒,因为是它先调用资源类,所以线程B会等待线程A执行完才会执行

③ 增加一个普通方法,看执行顺序

资源类


/**
 * 资源类
 *
 * @author byChen
 * @date 2022/12/23
 */
public class Phone {
    public synchronized void sendMall(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "--发送邮件");
    }

    public synchronized void callPhone() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "--打电话");
    }

    public void sayHello(){
        System.out.println(Thread.currentThread().getName()+"--say hello!");
    }

操作

    /**
     * 增加一个普通方法,看执行顺序
     */
    @GetMapping("/three")
    public void three() {
        Phone phone = new Phone();

        new Thread(() -> {
            phone.sendMall();
        }, "A线程").start();

        new Thread(() -> {
            phone.callPhone();
        }, "B线程").start();

        new Thread(() -> {
            phone.sayHello();
        }, "C线程").start();

    }

执行结果

C线程--say hello!  ## 普通方法虽然最后才被调用,但是因为他不是锁方法,因此它不会等待其余的锁方法执行完,而是直接执行
A线程--发送邮件  ## 另外两个锁方法,会按照先后顺序去等待
B线程--打电话

普通方法不加锁,会在主线程执行到的时候直接执行。

④ 调用两个不同资源类实例的两个锁方法,看执行顺序

资源类

/**
 * 资源类
 *
 * @author byChen
 * @date 2022/12/23
 */
public class Phone {
    /**
     * 锁方法
     */
    public synchronized void sendMall(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "--发送邮件");
    }

    /**
     * 锁方法
     */
    public synchronized void callPhone() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "--打电话");
    }
}

操作

   /**
     * 调用两个不同资源类实例的两个锁方法,看执行顺序
     */
    @GetMapping("/four")
    public void four() {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();


        new Thread(() -> {
            //会睡眠3秒
            phone1.sendMall();
        }, "A线程").start();

        new Thread(() -> {
            //会睡眠1秒
            phone2.callPhone();
        }, "B线程").start();


    }

执行结果

B线程--打电话  ## 因为是两个不同的实例对象,因此 B线程不会等待A线程执行完后才执行
A线程--发送邮件

同一时刻只能执行其中一个锁方法,是在同一个资源类内部的限定前提下生效的;本例是创建两个不同的资源类分别交给两个线程调用。
因此当线程调用资源类①发邮件方法睡眠时,阻塞的只是资源类①的发短信方法,而不会阻塞资源类②的发短信方法,因此发短信执行在邮件前面

⑤ 将锁方法变为静态方法,使用相同的资源类实例,看执行顺序

资源类


/**
 * 资源类
 *
 * @author byChen
 * @date 2022/12/23
 */
public class Phone {
    /**
     * 锁方法变为静态
     */
    public static synchronized void sendMall(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "--发送邮件");
    }

    /**
     * 锁方法变为静态
     */
    public static synchronized void callPhone() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "--打电话");
    }
}

操作

    /**
     * 将锁方法变为静态方法,使用相同的资源类实例,看执行顺序
     */
    @GetMapping("/five")
    public void five() {
        Phone phone1 = new Phone();
        new Thread(() -> {
            phone1.sendMall();
        }, "A线程").start();

        new Thread(() -> {
            phone1.callPhone();
        }, "B线程").start();

    }

执行结果

A线程--发送邮件
B线程--打电话   # B线程还是会等待A线程执行完成之后才执行

⑤类与⑥类一起做总结

⑥ 将锁方法变为静态方法,使用不同的资源类实例,看执行顺序

资源类


/**
 * 资源类
 *
 * @author byChen
 * @date 2022/12/23
 */
public class Phone {
    /**
     * 锁方法变为静态
     */
    public static synchronized void sendMall(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "--发送邮件");
    }

    /**
     * 锁方法变为静态
     */
    public static synchronized void callPhone() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "--打电话");
    }
}

操作

    /**
     *  将锁方法变为静态方法,使用不同的资源类实例,看执行顺序
     */
    @GetMapping("/six")
    public void six() {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();

        new Thread(() -> {
            phone1.sendMall();
        }, "A线程").start();

        new Thread(() -> {
            phone2.callPhone();
        }, "B线程").start();

    }

执行结果

A线程--发送邮件
B线程--打电话    # 就算是使用不同的实例对象,B线程还是会等待A线程执行完成之后才执行

被static修饰的静态方法在JVM中只会有一份,所以不管实例多少个对象,调用的都是同一个方法,自然不管创建多少资源类实例,不管多少线程,都是相当于在一个资源类中;当然也会遵循 “同一时刻只能执行一个资源类中的一个锁方法,其余普通方法正常执行,锁方法等待” 的原则。

⑦ 一个锁方法,一个静态锁方法,一个实例,看执行顺序

资源类

/**
 * 资源类
 *
 * @author byChen
 * @date 2022/12/23
 */
public class Phone {
    /**
     * 锁方法变为静态
     */
    public static synchronized void sendMall(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "--发送邮件");
    }

    /**
     * 锁方法
     */
    public  synchronized void callPhone() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "--打电话");
    }
}

操作

    /**
     *   一个锁方法,一个静态锁方法,一个实例,看执行顺序
     */
    @GetMapping("/seven")
    public void seven() {
        Phone phone1 = new Phone();

        new Thread(() -> {
            phone1.sendMall();
        }, "A线程").start();

        new Thread(() -> {
            phone1.callPhone();
        }, "B线程").start();

    }

执行结果

B线程--打电话   ## B线程没有等待A线程完成,直接就执行了
A线程--发送邮件 

原理同⑧,放一块总结

⑧ 一个锁方法,一个静态锁方法,两个不同实例

资源类

/**
 * 资源类
 *
 * @author byChen
 * @date 2022/12/23
 */
public class Phone {
    /**
     * 锁方法变为静态
     */
    public static synchronized void sendMall(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "--发送邮件");
    }

    /**
     * 锁方法
     */
    public  synchronized void callPhone() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "--打电话");
    }
}

操作

    /**
     * 一个锁方法,一个静态锁方法,两个不同实例,看执行顺序
     */
    @GetMapping("/eight")
    public void eight() {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(() -> {
            phone1.sendMall();
        }, "A线程").start();

        new Thread(() -> {
            phone2.callPhone();
        }, "B线程").start();

    }

执行结果

B线程--打电话  ## B线程依然没有等待A线程完成,直接就执行了
A线程--发送邮件  

因为 静态锁方法,锁的是当前存在于JVM中的的整个类中的方法
而 普通锁方法,锁的只是当前的实例对象类中的方法
可以理解为两个方法锁的不是同一个类,自然不会遵循“同一时刻只能执行一个资源类中的一个锁方法,其余普通方法正常执行,锁方法等待” 原则

总结

1.普通方法不参与锁
2.同一时刻只能执行一个资源类中的一个锁方法,其余普通方法正常执行,锁方法等待
3.静态锁方法锁的是整个类,普通锁方法锁的是当前实例对象,他们相当于两个不同的类,不遵循 2

2.synchronized 分析

3.公平锁和非公平锁

案例:卖票

资源类


/**
 * 卖票资源类
 * 模拟3个售票员卖50张票
 *
 * @author byChen
 * @date 2022/12/23
 */
public class Ticket {
    /**
     * 票数量
     */
    private int number = 15;
    /**
     * 可重入锁,因为没有参数,因此是非公平锁
     */
    ReentrantLock lock = new ReentrantLock();

    /**
     * 模拟卖票方法
     */
    public void sale() {
        lock.lock();
        try {
            //如果还有余票,就打印模拟卖票
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出弟:" + (number--) + " 张票,还剩下:" + number + " 张");
            }
        } finally {
            lock.unlock();
        }
    }

}

操作

      /**
     * 卖票模拟
     */
    @GetMapping("/sale")
    public void sale() {
        Ticket ticket = new Ticket();

        new Thread(()->{
            for (int i = 0; i < 20; i++) {
                try {
                    //为了效果,这里手动睡眠
                    TimeUnit.MILLISECONDS.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket.sale();
            }
        },"a售票员").start();

        new Thread(()->{
            for (int i = 0; i < 20; i++) {
                try {
                    //为了效果,这里手动睡眠
                    TimeUnit.MILLISECONDS.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket.sale();
            }
        },"b售票员").start();

        new Thread(()->{
            for (int i = 0; i < 20; i++) {
                //为了效果,这里手动睡眠
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket.sale();
            }
        },"c售票员").start();


    }

执行结果

c售票员卖出弟:15 张票,还剩下:14 张
c售票员卖出弟:14 张票,还剩下:13 张
c售票员卖出弟:13 张票,还剩下:12 张
c售票员卖出弟:12 张票,还剩下:11 张
c售票员卖出弟:11 张票,还剩下:10 张
a售票员卖出弟:10 张票,还剩下:9 张
c售票员卖出弟:9 张票,还剩下:8 张
b售票员卖出弟:8 张票,还剩下:7 张
c售票员卖出弟:7 张票,还剩下:6 张
c售票员卖出弟:6 张票,还剩下:5 张
c售票员卖出弟:5 张票,还剩下:4 张
b售票员卖出弟:4 张票,还剩下:3 张
c售票员卖出弟:3 张票,还剩下:2 张
a售票员卖出弟:2 张票,还剩下:1 张
c售票员卖出弟:1 张票,还剩下:0 张    ## 可以看到,线程的执行完全没有规律,完全靠cpu调度,因此是不公平的

如果想要变为公平锁,就需要在资源类中,将锁的参数赋予 true

资源类修改

/**
 * 卖票资源类
 * 模拟3个售票员卖50张票
 *
 * @author byChen
 * @date 2022/12/23
 */
public class Ticket {
    /**
     * 票数量
     */
    private int number = 50;
    /**
     * 可重入锁,因为传入的参数为 true表明公平,因此是公平锁
     */
    ReentrantLock lock = new ReentrantLock(true);

    /**
     * 模拟卖票方法
     */
    public void sale() {
        lock.lock();
        try {
            //如果还有余票,就打印模拟卖票
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出弟:" + (number--) + " 张票,还剩下:" + number + " 张");
            }
        } finally {
            lock.unlock();
        }
    }

}

操作

    /**
     * 卖票模拟
     */
    @GetMapping("/sale")
    public void sale() {
        Ticket ticket = new Ticket();

        new Thread(()->{
            for (int i = 0; i < 55; i++) {
                ticket.sale();
            }
        },"a售票员").start();

        new Thread(()->{
            for (int i = 0; i < 55; i++) {
                ticket.sale();
            }
        },"b售票员").start();

        new Thread(()->{
            for (int i = 0; i < 55; i++) {
                ticket.sale();
            }
        },"c售票员").start();


    }

再次执行

a售票员卖出弟:50 张票,还剩下:49 张
a售票员卖出弟:49 张票,还剩下:48 张
a售票员卖出弟:48 张票,还剩下:47 张
a售票员卖出弟:47 张票,还剩下:46 张
a售票员卖出弟:46 张票,还剩下:45 张
a售票员卖出弟:45 张票,还剩下:44 张
a售票员卖出弟:44 张票,还剩下:43 张
a售票员卖出弟:43 张票,还剩下:42 张
b售票员卖出弟:42 张票,还剩下:41 张
a售票员卖出弟:41 张票,还剩下:40 张
b售票员卖出弟:40 张票,还剩下:39 张  ## 可以看出,刚开始还是线程不公平

a售票员卖出弟:39 张票,还剩下:38 张  ## 但是逐渐的就会趋于公平
b售票员卖出弟:38 张票,还剩下:37 张
c售票员卖出弟:37 张票,还剩下:36 张

a售票员卖出弟:36 张票,还剩下:35 张
b售票员卖出弟:35 张票,还剩下:34 张
c售票员卖出弟:34 张票,还剩下:33 张

a售票员卖出弟:33 张票,还剩下:32 张
b售票员卖出弟:32 张票,还剩下:31 张
c售票员卖出弟:31 张票,还剩下:30 张

a售票员卖出弟:30 张票,还剩下:29 张
b售票员卖出弟:29 张票,还剩下:28 张
c售票员卖出弟:28 张票,还剩下:27 张

a售票员卖出弟:27 张票,还剩下:26 张
b售票员卖出弟:26 张票,还剩下:25 张
c售票员卖出弟:25 张票,还剩下:24 张

a售票员卖出弟:24 张票,还剩下:23 张
b售票员卖出弟:23 张票,还剩下:22 张
c售票员卖出弟:22 张票,还剩下:21 张

a售票员卖出弟:7 张票,还剩下:6 张
c售票员卖出弟:6 张票,还剩下:5 张
b售票员卖出弟:5 张票,还剩下:4 张

a售票员卖出弟:4 张票,还剩下:3 张 ## 但是逐渐的就会趋于公平
c售票员卖出弟:3 张票,还剩下:2 张
b售票员卖出弟:2 张票,还剩下:1 张

a售票员卖出弟:1 张票,还剩下:0 张

概念

image-20221223144609642

为什么会有公平锁\非公平锁的设计?为什么默认非公平?

1.恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。而非公平锁中,线程切换是交给cpu来决定的,所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间。

2.使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。I

什么时候用公平?什么时候用非公平?什么时候用公平?什么时候用非公平?

如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;否则那就用公平锁,大家公平使用。

4.可重入锁(又名递归锁)

概念

可重入锁又名递归锁

是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

举例:如果是1个有 synchronized修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

image-20221223150428233

代码举例

① 同步举例 - 代码块

    /**
     * 可重入锁
     */
    @GetMapping("/reEnter")
    public void reEnter() {
        //指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
        new Thread(() -> {
            //第一层
            synchronized (lock){
                System.out.println(Thread.currentThread().getName()+"---外层调用");
                //第二层
                synchronized (lock){
                    System.out.println(Thread.currentThread().getName()+"---中层调用");
                    //第三层
                    synchronized (lock){
                        System.out.println(Thread.currentThread().getName()+"---内层调用");
                    }
                }
            }
        }).start();
    }

执行结果

Thread-9---外层调用
Thread-9---中层调用
Thread-9---内层调用  ## 可以看到,没有释放锁的动作,但是每次都正常的获取到了对象锁,没有发生死锁的现象,证明可重入

② 同步举例 - 方法

    public synchronized void m1(){
        System.out.println(Thread.currentThread().getName()+" --m1方法");
        m2();
    }
    public synchronized void m2(){
        System.out.println(Thread.currentThread().getName()+" --m2方法");
        m3();
    }
    public synchronized void m3(){
        System.out.println(Thread.currentThread().getName()+" --m3方法");
    }


    /**
     * 可重入锁 程序入口
     */
    @GetMapping("/reEnter2")
    public void reEnter2() {
        //指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
        System.out.println(Thread.currentThread().getName()+" --come in");
        m1();
        System.out.println(Thread.currentThread().getName()+" --end");
    }

执行结果

http-nio-8023-exec-1 --come in
http-nio-8023-exec-1 --m1方法
http-nio-8023-exec-1 --m2方法
http-nio-8023-exec-1 --m3方法
http-nio-8023-exec-1 --end

原理分析

可重入锁分为隐式的跟显式的,隐式就是 synchronized 显式就是其他的那些,显式的需要手动去关闭;

代码举例


    /**
     * 可重入锁
     */
    @GetMapping("/reEnter3")
    public void reEnter3() {
        ReentrantLock lock=new ReentrantLock();
        //加锁,计数器加一
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " --进入第一层");
            //再次判断,线程还是一样的当前线程,再次加一
            lock.lock();
            try{
                System.out.println(Thread.currentThread().getName() + " --进入第二层");
            }finally {
                //显示的关闭掉,计数器减一
                lock.unlock();
            }
        }finally {
            //显示的关闭掉,计数器减一
            lock.unlock();
        }
    }

执行结果

http-nio-8023-exec-1 --进入第一层
http-nio-8023-exec-1 --进入第二层  ## 同样是可重入

机制分析

image-20221223162638329

可重入的时候,如果没有显示的手动关闭,就会使得线程一直不释放该资源,其他线程也无法访问资源,形成死锁

 /**
     * 可重入锁
     */
    @GetMapping("/reEnter3")
    public void reEnter3() {
        ReentrantLock lock=new ReentrantLock();
        new Thread(()->{
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " --进入第一层");
                lock.lock();
                try{
                    System.out.println(Thread.currentThread().getName() + " --进入第二层");
                }finally {
                    //这里不进行显示的关闭,会导致B线程一直获取不到资源
                    //由于加锁次数和释放次数不一样,B线程始终无法获取到锁,导致一直在等待。
//                    lock.unlock();
                }
            }finally {
                //显示的关闭掉
                lock.unlock();
            }
        },"A线程").start();

        new Thread(()->{
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " --进入第一层");
            }finally {
                //显示的关闭掉
                lock.unlock();
            }
        },"B线程").start();
    }

执行结果

A线程 --进入第一层
A线程 --进入第二层
## B线程一直在等待,形成死锁

5.死锁案例及排查

概念

image-20221223164516930

案例代码

 /**
     * 死锁
     */
    @GetMapping("/test")
    public void test() {
        //定义两个对象,也就是两个锁
        Object objectA=new Object();
        Object objectB=new Object();

        //A线程拥有A锁,并在不释放的前提下尝试去获取B锁
        new Thread(()->{
            synchronized (objectA){
                System.out.println(Thread.currentThread().getName()+" 持有A锁,尝试去获取B锁。。。");
                try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                synchronized (objectB){
                    System.out.println(Thread.currentThread().getName()+" 成功获取B锁");
                }
            }
        },"A线程").start();

        //B线程拥有B锁,并在不释放的前提下尝试去获取A锁
        new Thread(()->{
            synchronized (objectB){
                System.out.println(Thread.currentThread().getName()+" 持有B锁,尝试去获取A锁。。。");
                synchronized (objectA){
                    System.out.println(Thread.currentThread().getName()+" 成功获取A锁");
                }
            }
        },"B线程").start();

    }

执行结果

A线程 持有A锁,尝试去获取B锁。。。
B线程 持有B锁,尝试去获取A锁。。。
## 线程一直处于等待状态,不会返回,从而形成死锁

如何排查死锁

①原生命令排查

首先进入idea的命令行

image-20221223165136726

然后依次执行命令

F:\spring-Cloud学习\源码\JUC并发\juc>jps -l   ## 检查运行中的全部Java程序
14040 com.spring.JucApplication 
16268 sun.tools.jps.Jps

F:\spring-Cloud学习\源码\JUC并发\juc>jstack 14040  ## 检查指定的进程的内存情况
2022-12-23 16:50:43
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.181-b13 mixed mode):

### 中间省略
### 。。。


## 得出结果
Found one Java-level deadlock: #发现一个死锁
=============================
"B线程":
  waiting to lock monitor 0x000000001f9deef8 (object 0x000000076b759e48, a java.lang.Object),
  which is held by "A线程"
"A线程":
  waiting to lock monitor 0x000000001ca11368 (object 0x000000076b759e58, a java.lang.Object),
  which is held by "B线程"

Java stack information for the threads listed above:
===================================================
"B线???":
        at com.spring.controller.DeadLockDemo.lambda$test$49(DeadLockDemo.java:46)
        - waiting to lock <0x000000076b759e48> (a java.lang.Object) ## 等待去获取 0x000000076b759e48 的锁
        - locked <0x000000076b759e58> (a java.lang.Object) ## 当前占用 0x000000076b759e58 的锁
        at com.spring.controller.DeadLockDemo$$Lambda$468/689332761.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
"A线程":
        at com.spring.controller.DeadLockDemo.lambda$test$48(DeadLockDemo.java:36)
        - waiting to lock <0x000000076b759e58> (a java.lang.Object) ## 等待去获取 0x000000076b759e58 的锁
        - locked <0x000000076b759e48> (a java.lang.Object) ## 当前占用 0x000000076b759e48 的锁
        at com.spring.controller.DeadLockDemo$$Lambda$467/360246495.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock. ## 发现死锁


②可视化

电脑控制台输入命令

jconsole

image-20221223165815253

image-20221223165838831

image-20221223165913060

同样能够显示出来死锁的情况。

6.总结

image-20221223170721191

image-20221223170800068

三、LockSupport 与线程中断

1.线程中断

是什么?

**首先,**一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止,自己来决定自己的命运。所以,Thread.stop,Thread.suspend, Thread.resume都已经被废弃了。

其次,在Java中没有办法立即停止一条线程,然而停止线程却是一个很重要的操作,如取消一个耗时操作。因此,Java提供了一种用于停止线程的协商机制——中断,也即中断标识协商机制。

中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现

若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true;接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程请求这条线程中断,此时究竟该做什么需要你自己写代码实现。

每个线程对象中都有一个中断标识位,用于表示线程是否被其他线程请求中断;该标识位为true表示有线程请求中断它,为false表示未被申请中断;通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。

原生中断方法

① volatile值 方法中断线程

public class InterruptDemo {
    /**
     * 是否被请求中断标志
     * <p>false 未被请求中断</p>
     * <p>true  已被请求中断</p>
     */
    static volatile boolean isInterrupt = false;

    /**
     * volatile方法中断线程
     */
    @GetMapping("/one")
    public void one() throws InterruptedException {
        new Thread(() -> {
            while (true) {
                if (isInterrupt) {
                    System.out.println(Thread.currentThread().getName() + " 线程中断状态为{申请协商中断},开始协商进行中断。。");
                    System.out.println(Thread.currentThread().getName() + " 已自行中断。");
                    break;
                } else {
                    System.out.println(Thread.currentThread().getName() + "线程中断状态为{未中断}正常运行。。");
                }
            }
        }, "A线程").start();
        //睡眠,保证A线程正常运行一段时间
        TimeUnit.MILLISECONDS.sleep(1);

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " --去申请中断线程");
            isInterrupt = true;
            System.out.println(Thread.currentThread().getName() + " --成功修改A线程状态为协商中断。");
        }, "B线程").start();
    }
}

执行结果

A线程线程中断状态为{未中断}正常运行。。
A线程线程中断状态为{未中断}正常运行。。
A线程线程中断状态为{未中断}正常运行。。
A线程线程中断状态为{未中断}正常运行。。
A线程线程中断状态为{未中断}正常运行。。
A线程线程中断状态为{未中断}正常运行。。  ## 未被其他线程申请协商中断,一直正常运行

B线程 --去申请中断线程  ## 有另外线程进行协商申请中断
B线程 --成功修改A线程状态为协商中断。## 有另外线程进行协商申请中断

A线程线程中断状态为{未中断}正常运行。。
A线程 线程中断状态为{申请协商中断},开始协商进行中断。。 ## A线程收到线程中断的协商申请,开始主动进行停止
A线程 已自行中断。

② AtomicBoolen 方法实现线程中断

AtomicBoolen 原子布尔类型,天生自带原子性,不用额外进行锁处理。用来做是否中断标志位很合适。

public class InterruptDemo {
    /**
     * 是否被请求中断标志
     * <p>false 未被请求中断</p>
     * <p>true  已被请求中断</p>
     */
    static AtomicBoolean atomicBoolean=new AtomicBoolean(false);

    /**
     * AtomicBoolen 方法中断线程
     */
    @GetMapping("/two")
    public void two() throws InterruptedException {
        new Thread(() -> {
            while (true) {
                if (atomicBoolean.get()) {
                    System.out.println(Thread.currentThread().getName() + " 线程中断状态为{申请协商中断},开始协商进行中断。。");
                    System.out.println(Thread.currentThread().getName() + " 已自行中断。");
                    break;
                } else {
                    System.out.println(Thread.currentThread().getName() + "线程中断状态为{未中断}正常运行。。");
                }
            }
        }, "A线程").start();
        //睡眠,保证A线程正常运行一段时间
        TimeUnit.MILLISECONDS.sleep(1);

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " --去申请中断线程");
            atomicBoolean.set(true);
            System.out.println(Thread.currentThread().getName() + " --成功修改A线程状态为协商中断。");
        }, "B线程").start();
    }
}

执行结果

跟上面的一样

A线程线程中断状态为{未中断}正常运行。。
A线程线程中断状态为{未中断}正常运行。。
A线程线程中断状态为{未中断}正常运行。。
A线程线程中断状态为{未中断}正常运行。。
A线程线程中断状态为{未中断}正常运行。。
A线程线程中断状态为{未中断}正常运行。。  ## 未被其他线程申请协商中断,一直正常运行

B线程 --去申请中断线程  ## 有另外线程进行协商申请中断
B线程 --成功修改A线程状态为协商中断。## 有另外线程进行协商申请中断

A线程线程中断状态为{未中断}正常运行。。
A线程 线程中断状态为{申请协商中断},开始协商进行中断。。 ## A线程收到线程中断的协商申请,开始主动进行停止
A线程 已自行中断。

上面都是使用原生Java代码的方式进行线程中断协商,接下来使用JUC中的中断机制的api来进行操作

中断机制两大api实例方法

  1. public void interrupt()

实例方法,Just to set the interrupt fLag。实例方法interrupt()仅仅是设置线程的中断状态为true,发起一个协商而不会立刻停止线程

  1. public boolean isInterrupted()

实例方法,判断当前线程是否被中新(通过检查中断标志位)

怎么做?

在需要中断的线程中不断监听中断状态,一旦发生了申请中断,就执行相应的中断处理业务逻辑stop线程。

整体思想跟上面原生的一致。

代码示例

   /**
     * JUC中断机制api 方法中断线程
     */
    @GetMapping("/three")
    public void three() throws InterruptedException {
        Thread threadA = new Thread(() -> {
            while (true) {
                //实例方法,判断当前线程是否被中新(通过检查中断标志位)
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(Thread.currentThread().getName() + " 线程中断状态为{申请协商中断},开始协商进行中断。。");
                    System.out.println(Thread.currentThread().getName() + " 已自行中断。");
                    break;
                } else {
                    System.out.println(Thread.currentThread().getName() + "线程中断状态为{未中断}正常运行。。");
                }
            }
        }, "A线程");
        threadA.start();
        //睡眠,保证A线程正常运行一段时间
        TimeUnit.MILLISECONDS.sleep(1);

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " --去申请中断线程");
            //实例方法,Just to set the interrupt fLag。实例方法interrupt()仅仅是设置线程的中断状态为true,发起一个协商而不会立刻停止线程
            threadA.interrupt();
            System.out.println(Thread.currentThread().getName() + " --成功修改A线程状态为协商中断。");
        }, "B线程").start();
    }

执行结果,同上

A线程线程中断状态为{未中断}正常运行。。
A线程线程中断状态为{未中断}正常运行。。
A线程线程中断状态为{未中断}正常运行。。
A线程线程中断状态为{未中断}正常运行。。
A线程线程中断状态为{未中断}正常运行。。
A线程线程中断状态为{未中断}正常运行。。  ## 未被其他线程申请协商中断,一直正常运行

B线程 --去申请中断线程  ## 有另外线程进行协商申请中断
B线程 --成功修改A线程状态为协商中断。## 有另外线程进行协商申请中断

A线程线程中断状态为{未中断}正常运行。。
A线程 线程中断状态为{申请协商中断},开始协商进行中断。。 ## A线程收到线程中断的协商申请,开始主动进行停止
A线程 已自行中断。
案例验证规则

案例一 验证线程正常活动状态下的情况

   /**
     * 中断线程 效果检验
     * 验证:实例方法interrupt()仅仅是设置线程的中断状态位设置为true,不会停
     */
    @GetMapping("/check")
    public void check() throws InterruptedException {
        Thread threadA = new Thread(() -> {
            for (int i = 0; i < 300; i++) {
                System.out.println("----:" + i);
            }
            System.out.println(Thread.currentThread().getName() + " 运行结束,结束时中断协商状态为:" + Thread.currentThread().isInterrupted());
        }, "A线程");
        threadA.start();
        //检查默认标志位
        System.out.println(threadA.getName()+" 默认的中断协商标志为:"+threadA.isInterrupted());
        //睡眠,保证A线程正常运行一段时间
        TimeUnit.MILLISECONDS.sleep(1);

        threadA.interrupt();
        System.out.println(Thread.currentThread().getName() + " 对A线程进行中断协商申请,后的中断状态为:" + threadA.isInterrupted());
        
        //再次睡眠,保证A线程运行结束
        TimeUnit.MILLISECONDS.sleep(200);
        System.out.println("A线程运行结束后的中断状态为:" + threadA.isInterrupted());
        
        //再次发送协商中断申请
        threadA.interrupt();
        System.out.println("再次向A线程发送协商中断申请后的中断状态为:" + threadA.isInterrupted());
    }

执行结果

A线程 默认的中断协商标志为:false
----:0
##  省略
----:224
----:225
----:226
----:227
----:228
http-nio-8023-exec-1 对A线程进行中断协商申请,后的中断状态为:true  ## 别的线程对A线程发起中断申请,但是A线程并没有进行处理,因此依旧正常运行
----:229
----:230
----:231
----:232
----:298
----:299
A线程 打印结束,此刻中断协商状态为:true ## 直到结束也还是中断协商中 状态

A线程运行结束后的中断状态为:false  ##因为线程任务已经执行完了,所以他就自己停下来了,中断标志位回归默认状态

再次向A线程发送协商中断申请后的中断状态为:false  ## 向已经中断的线程发送中断协商申请,将不会产生任何效果

案例二 验证线程被阻塞状态下的情况

  /**
     * 中断线程 效果检验
     * 验证:阻塞状态下,尝试中断协商
     */
    @GetMapping("/check2")
    public void check2() throws InterruptedException {
        Thread threadA = new Thread(() -> {
            while (true){
                //实例方法,判断当前线程是否被中新(通过检查中断标志位)
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(Thread.currentThread().getName() + " 线程中断状态为{申请协商中断},开始协商进行中断。。");
                    System.out.println(Thread.currentThread().getName() + " 已自行中断。");
                    break;
                } else {
                    //线程睡眠,模拟线程阻塞
                    try {
                        TimeUnit.MILLISECONDS.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "线程中断状态为{未中断}正常运行。。");
                }
            }
        }, "A线程");
        threadA.start();

        //暂停几秒钟线程
        TimeUnit.SECONDS.sleep(1);

        //B线程发送中断申请
        new Thread(()->{
            threadA.interrupt();
        },"B线程").start();
    }

执行结果

A线程线程中断状态为{未中断}正常运行。。
A线程线程中断状态为{未中断}正常运行。。
A线程线程中断状态为{未中断}正常运行。。
A线程线程中断状态为{未中断}正常运行。。

java.lang.InterruptedException: sleep interrupted  ## 报错异常 InterruptedException 
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at com.spring.controller.InterruptDemo.lambda$check2$7(InterruptDemo.java:152)
	at java.lang.Thread.run(Thread.java:748)
A线程线程中断状态为{未中断}正常运行。。 ## 但还是正常运行,且一直运行
A线程线程中断状态为{未中断}正常运行。。
A线程线程中断状态为{未中断}正常运行。。
A线程线程中断状态为{未中断}正常运行。。
A线程线程中断状态为{未中断}正常运行。。

## 一直循环下去了。。。

如何解决呢?

现在问题的原因就是,B线程发送中断协商申请的时候,A线程因为阻塞的调用了**mait(),, wait(1ong) , 或wait(1ong,int)的方法,或者join() , join(1ong) , join(long,int) , sleep(long) ,或sleeo(1long , int)**从而被阻塞;那么它的中断状态将被清除为默认false,并且将收到InterruptedException 。

此时B线程发送中断申请,A线程会抛出 InterruptedException 异常。然后因为B线程只请求一次,因此A线程就不会再被修改中断标志位状态了。因此,解决方法就是,在A线程第一次抛出异常的时候,就捕获异常并立即再次修改中断标志位为true.

代码修改:

    /**
     * 中断线程 效果检验
     * 验证:阻塞状态下,尝试中断协商
     */
    @GetMapping("/check2")
    public void check2() throws InterruptedException {
        Thread threadA = new Thread(() -> {
            while (true){
                //实例方法,判断当前线程是否被中新(通过检查中断标志位)
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(Thread.currentThread().getName() + " 线程中断状态为{申请协商中断},开始协商进行中断。。");
                    System.out.println(Thread.currentThread().getName() + " 已自行中断。");
                    break;
                } else {
                    try {
                        TimeUnit.MILLISECONDS.sleep(200);
                    } catch (InterruptedException e) {
                        //如果发生阻塞中断异常,就立即修改中断标志位状态。
                        Thread.currentThread().interrupt();
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "线程中断状态为{未中断}正常运行。。");
                }
            }
        }, "A线程");
        threadA.start();

        //暂停几秒钟线程
        TimeUnit.SECONDS.sleep(1);

        //B线程发送中断申请
        new Thread(()->{
            threadA.interrupt();
        },"B线程").start();
    }

执行结果

A线程线程中断状态为{未中断}正常运行。。
A线程线程中断状态为{未中断}正常运行。。
A线程线程中断状态为{未中断}正常运行。。
A线程线程中断状态为{未中断}正常运行。。
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at com.spring.controller.InterruptDemo.lambda$check2$7(InterruptDemo.java:153)
	at java.lang.Thread.run(Thread.java:748)
A线程线程中断状态为{未中断}正常运行。。
A线程 线程中断状态为{申请协商中断},开始协商进行中断。。  ## 捕获到异常,修改中断标志位为 true 
A线程 已自行中断。
总结

具体来说,当对一个线程,调用interrupt()时:

如果线程处于正常活动状态,那么会将该线程的协商中断标志设置为true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。所以,interrupt()并不能真正的中断线程,需要被调用的线程自己进行配合才行。

如果线程处于被阻塞状态(例如处于sleep, wait, join等状态),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态,中断标志位也会清除为初始化false,并抛出一个InterruptedException异常。

中断机制 api 静态方法

public static boolean interrupted()

静态方法,Thread.interrupted();判断线程是否被中断并清除当前中断状态。这个方法做了两件事:

1返回当前线程的中断状态,测试当前线程是否已被申请中断

2将当前线程的中断状态清零并重新设为false,清除线程的中断状态

此方法有点不好理解,如果连续两次调用此方法,则第二次调用将返回false,因为连续调用两次的结果可能不一样

    /**
     * 中断线程 效果检验
     * 静态api方法测试
     */
    @GetMapping("/staticTest")
    public void staticTest() throws InterruptedException {
        System.out.println(Thread.currentThread().getName()+" 中断状态标志位:"+Thread.interrupted());
        System.out.println(Thread.currentThread().getName()+" 中断状态标志位:"+Thread.interrupted());
        System.out.println("修改中断状态标志位。。。");
        Thread.currentThread().interrupt();
        System.out.println("修改成功。");
        System.out.println(Thread.currentThread().getName()+" 中断状态标志位:"+Thread.interrupted());
        System.out.println(Thread.currentThread().getName()+" 中断状态标志位:"+Thread.interrupted());
    }

执行结果

http-nio-8023-exec-1 中断状态标志位:false
http-nio-8023-exec-1 中断状态标志位:false
修改中断状态标志位。。。
修改成功。
http-nio-8023-exec-1 中断状态标志位:true
http-nio-8023-exec-1 中断状态标志位:false

总结

image-20221224191751602

2.LockSupport

用来优化线程等待+唤醒的类

原生唤醒等待方法复习

① Object类中的wait notify 方法唤醒等待
    /**
     * Object类中的wait  notify 方法唤醒等待
     */
    @GetMapping("/one")
    public void one() throws InterruptedException {
        //锁资源
        Object objectLock = new Object();
        
        new Thread(() -> {
            synchronized (objectLock) {
                //获得锁资源
                System.out.println(Thread.currentThread().getName() + " --come in");
                try {
                    //直接进行交出资源控制权,等待唤醒
                    objectLock.wait();
//                    System.out.println(Thread.currentThread().getName() + " --等待线程唤醒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //成功被唤醒
                System.out.println(Thread.currentThread().getName() + " --被唤醒");
            }
        },"A线程").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            synchronized (objectLock){
                //获得锁资源
                System.out.println(Thread.currentThread().getName() + " --come in");
                objectLock.notify();
                System.out.println(Thread.currentThread().getName() + " --发出通知,唤醒其他线程");
            }
        },"B线程").start();

    }

执行结果

A线程 --come in
B线程 --come in
B线程 --发出通知,唤醒其他线程
A线程 --被唤醒

存在的问题

  1. 如果去掉同步代码块标志 synchronized 程序会报错
  2. 如果线程B先唤醒线程,然后A线程再执行,执行等待后,会永远无法被唤醒而陷入一直等待状态
  3. 也就是说:wait和notify方法必须要在同步块或者方法里面,且成对出现使用先wait后notify才oK
② Condition接口中的await signal 方法唤醒等待

    /**
     * Condition接口中的await  signal 方法唤醒等待
     */
    @GetMapping("/two")
    public void two() throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        new Thread(() -> {
            //该种方式的锁
            lock.lock();
            try {
                //获得锁资源
                System.out.println(Thread.currentThread().getName() + " --come in");
                try {
                    //直接进行交出资源控制权,等待唤醒
                    condition.await();
//                    System.out.println(Thread.currentThread().getName() + " --等待线程唤醒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //成功被唤醒
                System.out.println(Thread.currentThread().getName() + " --被唤醒");
            } catch (InternalError error) {
                error.getMessage();
            } finally {
                lock.unlock();
            }
        }, "A线程").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            lock.lock();
            try {
                //获得锁资源
                System.out.println(Thread.currentThread().getName() + " --come in");
                condition.signal();
                System.out.println(Thread.currentThread().getName() + " --发出通知,唤醒其他线程");
            } catch (InternalError error) {
                error.getMessage();
            } finally {
                lock.unlock();
            }
        }, "B线程").start();
    }

执行结果

A线程 --come in
B线程 --come in
B线程 --发出通知,唤醒其他线程
A线程 --被唤醒

存在的问题,跟Object的方法问题一致

  1. 如果去掉同步代码块标志 synchronized 程序会报错
  2. 如果线程B先唤醒线程,然后A线程再执行,执行等待后,会永远无法被唤醒而陷入一直等待状态
  3. 也就是说:wait和notify方法必须要在同步块或者方法里面,且成对出现使用先wait后notify才oK
总结

原来的方法使用存在限制

1.线程先要获得并持有锁,必须在锁块(synchronized或lock)中

2.必须要先等待后唤醒,线程才能够被唤醒

LockSupport 方法使用

是什么?

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。

LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),

但与 Semaphore 不同的是,许可的累加上限是1。

tips所谓原语,一般是指由若干条指令组成的程序段,用来实现某个特定功能,在执行过程中不可被中断

怎么使用?

该类与使用它的每个线程关联一个许可证(在Semaphore类的意义上)。如果许可证可用,将立即返回park ,并在此过程中消费掉许可证,否则可能会阻止。如果尚未提供许可,则致电unpark获得许可。(与semaphores不同,许可证不会累积。最多只有一个。)

api使用

park() / park(Object blocker) 阻塞当前线程/阻塞传入的具体线程,相当于 wait await 方法

permit许可证默认没有,不能放行,所以一开始调park()方法当前线程就会阻塞,直到别的线程给当前线程的发放permit,park方法才会被唤醒。

unpark(Thread thread) 唤醒处于阻塞状态的指定线程(发放permit许可证) ,相当于notufy() signal() 方法

调用unpark(thread)方法后,就会将thread线程的许可证permit发放,会自动唤醒park线程,即之前阻塞中的LockSupport.park()方法会立即返回。

代码举例

     /**
     * LockSupport 类中的 park等待  unpark 方法唤醒
     */
    @GetMapping("/three")
    public void three() throws InterruptedException {
        Thread threadA = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " --come in");
            //直接将本线程进行阻塞,等待唤醒
            LockSupport.park();
            //成功被唤醒
            System.out.println(Thread.currentThread().getName() + " --被唤醒");
        }, "A线程");
        threadA.start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " --come in");
            //利用B线程,给A线程发放通行证
            LockSupport.unpark(threadA);
            //给线程A 发放通行证
            System.out.println(Thread.currentThread().getName() + " --给A线程发放通行证");
        }, "B线程").start();
    }

执行结果

A线程 --come in
B线程 --come in
B线程 --给A线程发放通行证
A线程 --被唤醒   ## 正常效果
异常测试
    /**
     * LockSupport 类中的 park等待  unpark 方法唤醒
     */
    @GetMapping("/three")
    public void three() throws InterruptedException {
        Thread threadA = new Thread(() -> {
            //A线程进来就睡眠,先让B线程发放通行证,唤醒A线程
            睡眠,保证B线程先发放通行证
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " --come in");
            //直接将本线程进行阻塞
            LockSupport.park();
            //成功被唤醒
            System.out.println(Thread.currentThread().getName() + " --被唤醒");
        }, "A线程");
        threadA.start();

		//B线程进来就给A线程发放通行证,然后再让A线程进行阻塞
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " --come in");
            //利用B线程,给A线程发放通行证
            LockSupport.unpark(threadA);
            //给线程A 发放通行证
            System.out.println(Thread.currentThread().getName() + " --给A线程发放通行证");
        }, "B线程").start();
    }

执行结果

B线程 --come in
B线程 --给A线程发放通行证  ##B线程进来,直接给A发放通行证

A线程 --come in 
A线程 --被唤醒 ## A线程并没有因为在阻塞前被发放通行证唤醒而一直循环,解决了原生方法的缺点2

补充

通行证的发放数量。上限就是1,因此,如果提前给线程发放多个通行证,也是没有用的,只能放行一个阻塞

    /**
     * LockSupport 类中的 park等待  unpark 方法唤醒
     * 发放多个通行证
     */
    @GetMapping("/five")
    public void five() throws InterruptedException {
        Thread threadA = new Thread(() -> {
            //睡眠,保证B线程先发放通行证
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " --come in");
            //直接将本线程进行阻塞
            System.out.println(Thread.currentThread().getName() +"第一次阻塞");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() +"第一次放行");

            System.out.println(Thread.currentThread().getName() +"第二次阻塞");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() +"第二次放行");
            //成功被唤醒
            System.out.println(Thread.currentThread().getName() + " --被唤醒");
        }, "A线程");
        threadA.start();


        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " --come in");
            //利用B线程,给A线程多次发放通行证
            LockSupport.unpark(threadA);
            LockSupport.unpark(threadA);
            LockSupport.unpark(threadA);
            //给线程A 发放通行证
            System.out.println(Thread.currentThread().getName() + " --给A线程发放通行证");
        }, "B线程").start();
    }

执行结果

B线程 --come in
B线程 --给A线程发放通行证  ## 提前给A线程发放多个通行证
A线程 --come in
A线程第一次阻塞
A线程第一次放行  ## 第一次同行了

A线程第二次阻塞
## 一直阻塞等待,因为发放多次通行证,实际只保存了一个。因此第二个阻塞无法通行。
总结

解决了原生的 Object Condition 的两个弊端

1.它原生就是 正常+无锁块要求

2.之前错误的先唤醒后等待,Locksupport照样支持。因为是否阻塞只看是否拥有通行证,其他线程提前唤醒其实就是提前发放了通行证,然后拥有通行证的线程,调用park()方法进行阻塞,因为本身就拥有通行证,因此park()方法相当于直接被放行了。

但是它同样要求,**要牢记成双成对的出现 park() unpark() **。并且,不能够提前发放多个通行证

image-20221225210328042

四、JMM - JAVA内存模型

1入门知识

image-20221226092435634

|计算机存储结构,从本地磁盘到主存到CPU缓存,也就是从硬盘到内存,到CPU。一般对应的程序的操作就是从数据库查数据到内存然后到CPU进行计算

image-20221226092928068

学术定义

JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式,并决定一个线程对共享变量的写入,何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。

能干嘛?

1通过JMM来实现线程和主内存之间的抽象关系。

2屏蔽各个硬件平台和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致的内存访问效果。

2.三大特性

① 可见性

image-20221226094012952

运行过程

系统主内存共享变量数据修改被写入的时机是不确定的,多线程并发下很可能出现"“脏读”",所以每个线程都有自己的工作内存(本地内存)、线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等)都必须在线程自己的工作内存中进行,而不能够直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

多线程下,可能会导致脏读;也就是A线程获取到值并计算后,还没有来得及将修改后的值刷新回主内存,这个时候B线程也去主内存获取值,然后计算,并刷新回主内存;然后A线程又刷新回主内存,这就会导致值的错乱;

因此,引入可见性,一个线程在处理内存值之后,立马通知其他线程,该值已经被修改,请重新获取内存值。

同样的,如果一个线程处理内存共享变量的时候,不允许其他线程对该变量操作,能够达到相同的效果,这就能够引出原子性

② 原子性

指一个操作是不可打断的,即多线程环境下,操作不能被其他线程干扰

③ 有序性 - 指令重排

image-20221226100733825

3.先行发生原则 - happens-before

image-20221226102720510

总原则

如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。

八条细则

  1. 次序规则一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作;前一个操作的结果可以被后一个操作获取到。
  2. 锁定规则:一个unLock操作先行发生于后面((这里的“后面”是指时间上的先后))对同一个锁的lock操作;也就是前一个线程释放锁后,下一个线程才能够获取锁。
  3. volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的“后面”同样是指时间上的先后。
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作c,则可以得出操作A先行发生于操作C;
  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作;线程先启动,才会执行线程中的每一个方法。
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;可以通过Thread.interrupted()检测到是否发生中断;也就是说你要先调用interrupt()方法设置过中断标志位,我才能检测到中断发送。
  7. 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过isAlive()等手段检测线程是否已经终止执行。
  8. 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法(JVM垃圾回收)的开始。对象没有完成初始化之前,是不能调用finalized()方法的。
总结

image-20221226111224110

五、volatile 与 JMM

1.两大特性

volatile 只拥有 可见性+有序性,有序性方面体现在,它可以禁用指令重排。

内存语义

1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。

2.当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量,

所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。

至于为什么volatile能够保证可见性+有序性,靠的就是 内存屏障

2.四大内存屏障

是什么?

内存屏障(也称内存栅栏,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序(但是内存屏障点之前的语句不能避免重排序)。

内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性(禁重排),但volatile无法保证原子性

内存屏障之前的所有写操作都要回写到主内存,

内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。

写屏障(Store Memory Barrier):告诉处理器在写屏障之前将所有存储在缓存(store bufferes)中的数据同步到主内存。也就是说当看到Store屏障指令,就必须把该指令之前所有写入指令执行完毕才能继续往下执行。

读屏障(Load Memory Barrier):处理器在读屏障之后的读操作,都在读屏障之后执行。也就是说在Load屏障指令之后就能够保证后面的读取数据指令一定能够读取到最新的数据。

**总结:**重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。一句话:对一个volatile变量的写,先行发生于后续任意对这个volatile变量的读,也叫写后读。

分类

分类的话,粗分两种:读屏障+写屏障 细分四种:读读、读写、写读、写写

读屏障+写屏障

读屏障:在读指令之前插入读屏障,让工作内存或CPU高速缓存当中的缓存数据失效,重新回到主内存中获取最新数据

写屏障:在写指令之后插入写屏障,强制把写缓冲区的数据刷回到主内存中

读读、读写、写读、写写

image-20221226163123548

volatile 禁重排 规则查询

image-20221226165140932

特性案例

特性1.可见性

① 不使用volatile关键字

    static boolean flag = true;
    /**
    *  保证不同线程对某个变量完成操作后结果及时可见,即该共享变量一旦改变所有线程立即可见
    *  不使用 volatile 关键字,则修改变量不会立即可见
    */
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " --进入线程");
            while (flag) {
                //默认flag是true,如果别的线程将flag的值修改了,如果是即时可见的,就应该能够跳出循环
            }
            System.out.println(Thread.currentThread().getName() + " --flag更改为false,跳出循环");
        }, "A线程").start();

        TimeUnit.SECONDS.sleep(2);
        //使用主线程去更改状态,测试可见性
        flag = false;
        System.out.println(Thread.currentThread().getName() + " --线程将flag更改为false");
    }

执行结果

A线程 --进入线程
main --线程将flag更改为false

## 程序一直循环,并不会停止,也一直没有跳出循环,证明变量修改不可见

问题:不知道为什么,使用接口测试就会直接跳出,好像是强制刷主存一样。只能使用psvm主线程来测试

② 使用volatile 关键字

    /**
     * 使用 volatile 关键字,有立即可见性
     */
    static volatile boolean volatileFlag = true;   
/**
    *  使用 volatile 关键字.保证不同线程对某个变量完成操作后结果及时可见,
    *  即该共享变量一旦改变所有线程立即可见
    */   
public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " --进入线程");
            // **工作内存中每次读取共享变量时,都去主内存中重新读取,然后拷贝到工作内存。**
            while (volatileFlag) {
                //默认flag是true,如果别的线程将flag的值修改了,如果是即时可见的,就应该能够跳出循环
            }
            System.out.println(Thread.currentThread().getName() + " --flag更改为false,跳出循环");
        }, "A线程").start();

        TimeUnit.SECONDS.sleep(2);
        //使用主线程去更改状态,测试可见性
        volatileFlag = false;
        System.out.println(Thread.currentThread().getName() + " --线程将flag更改为false");
    }

执行结果

A线程 --进入线程
main --线程将flag更改为false  ## 变量值被修改,立即可见。跳出循环
A线程 --flag更改为false,跳出循环

总结

使用volatile修饰共享变量,就可以达到立即可见的效果,被volatile修改的变量有以下特点:

  1. 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存。

  2. 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存。

可见性操作 - 主内存工作内存交互流程

image-20221226175109187

read:作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存.

load:作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载.

use:作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作.

assign:作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作store:作用于工作内存,将赋值完毕的工作变量的值写回给主内存.

write:作用于主内存,将store传输过来的变量值赋值给主内存中的变量.

由于上述6条只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令

​lock:作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程。

unlock:作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用

特性2.有序性(禁重排)

**重排序的含义:**重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序。

什么情况可以运行重排序?什么情况应该禁止重排序?

不存在数据依赖关系,可以重排序;

存在数据依赖关系,禁止重排序;

tips:数据依赖性:若两个操作访问同一变量,且这两个操作中有一个为写操作,此时两操作间就存在数据依赖性。

举例

image-20221226214959396

**第一前提:**重排后的指令绝对不能改变原有的串行语义! 如果是单线程,无论编译器如何重排序,都不会改变串行语义;但在多线程并发的情况下,这点必须要重点设计考虑!

特性3.无原子性

首先,使用 synchronized 锁方法,进行原子性的举例

/**
 * 数量累加 - 资源类
 *
 * @author byChen
 * @date 2022/12/26
 */
@Data
public class MyNumber {
    /**
     * 数量
     */
    int number;

    /**
     * 同步方法,累加
     */
    public synchronized void addPlusPlus() {
        number++;
    }
}

操作

    /**
     * volatile变量的复合操作不具有原子性,比如number++
     */
    @GetMapping("/two")
    public void two() throws InterruptedException {
        MyNumber myNumber = new MyNumber();
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int i1 = 0; i1 < 1000; i1++) {
                    myNumber.addPlusPlus();
                }
            },String.valueOf(i)).start();
        }

        TimeUnit.SECONDS.sleep(2);

        System.out.println(myNumber.getNumber());
    }

执行结果

10000  ## 证明线程之间对变量是原子性的操作

然后,将方法取消同步标志 synchronized 并给变量加上 volatile标志

资源类

/**
 * 数量累加 - 资源类
 *
 * @author byChen
 * @date 2022/12/26
 */
@Data
public class MyNumber {
    /**
     * 数量,加 volatile 标志
     */
    volatile int number;

    /**
     * ,累加
     */
    public void addPlusPlus() {
        number++;
    }
}

操作

    /**
     * volatile变量的复合操作不具有原子性,比如number++
     */
    @GetMapping("/two")
    public void two() throws InterruptedException {
        MyNumber myNumber = new MyNumber();
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int i1 = 0; i1 < 1000; i1++) {
                    myNumber.addPlusPlus();
                }
            },String.valueOf(i)).start();
        }

        TimeUnit.SECONDS.sleep(2);

        System.out.println(myNumber.getNumber());
    }

执行结果

9305
9305
8814  ## 请求了三次,值都不一样,证明使用volatile并不能保证原子性

总结:对于volatle变量具备可见性,JVM只是保证从主内存加载到线程工作内存的值是最新的,也仅是数据加载时是最新的。但是多线程环境下,"数据计算"和"数据赋值"操作可能多次出现,若数据在加载之后,若主内存volatile修饰变量发生修改之后,线程工作内存中的操作将会作废,然后去读主内存最新值,操作出现写丢失问题。即各线程私有内存和主内存公共内存中变量不同步,进而导致数据不一致。由此可见volatle解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改主内存共享变量的场景必须使用加锁同步。

因为原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。但是现在这种情况,线程开始之后,会因为主内存变量修改而导致本线程值作废,也就是会中断线程因此volatile不保证原子性

六、CAS

1.是什么?

CAS 锁(Compare and Swap) ,即乐观锁,按照英文直接翻译是 “比较并交换”,

​ 它包含三个操作数 V,A,B

​ v表示要更新的变量在内存位置当前的值

​ a表示旧的预期值

​ b表示要修改为的新值

当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。(因此称为比较并交换)

​ CAS是一种乐观锁,它抱着乐观的态度认为自己一定可以成功。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。

失败的线程1也不会被挂起,仅是被告知失败返回false,并且允许再次尝试(再次尝试需要手动实现,CAS本身并不会主动重试),当然也允许失败的线程直接放弃操作。

​ 实际开发中,因为如果CAS操作失败就放弃操作的话,会导致无法更新,也就无法将业务进行下去,因此一般不会选择直接失败放弃操作,而是在CAS外面加一层自旋锁的操作,内层依旧是CAS,也就是每次失败依旧会直接返回false,但是因为外层的自旋锁,它会再次进行乐观锁的 “取值-比较-更改”这一系列流程,一直尝试到在它的操作时间区间内,预期值没有被其他线程打扰,进而完成更新操作。

​ 而为什么选用自旋锁而不是其他的锁,这是因为自旋锁可以避免轻量级锁直接升级为重量级锁带来更多资源消耗,而自旋的过程每次都是一个CAS操作,务必不要混淆。

当然,乐观锁CAS也会有缺点

1.只能保证对一个变量的原子性操作,(但有取巧的方法,就是将多个值相乘得出一个值,对这个值进行原子性规约即可);

2.长时间自旋会给CPU带来压力

3.ABA问题

2.使用

单线程,不使用自旋

    /**
     * 单线程使用
     */
    @GetMapping("/one")
    public void one() {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        //先获取到操作时,预期旧值
        int i = atomicInteger.get();
        System.out.println("拿到的预期旧值:" + i);

        //cas 更新,程序会后台获取到当前内存位置的值,然后与预期旧值去比较
        // 预期旧值跟当前内存位置的值相同,就更新为新值 6,并返回 true
        // 否则就放弃操作,并返回false
        boolean b = atomicInteger.compareAndSet(i, 6);
        System.out.println("第一次预期修改值为:" + 6 + ",修改结果为:" + b);

        boolean c = atomicInteger.compareAndSet(i, 7);
        System.out.println("第二次预期修改值为:" + 7 + ",修改结果为:" + c);

        System.out.println("两次cas操作后,最终的值:" + atomicInteger.get());
    }

执行结果

拿到的预期旧值:5
第一次预期修改值为:6,修改结果为:true
第二次预期修改值为:7,修改结果为:false
两次cas操作后,最终的值:6

多线程,不使用自旋

    /**
     * 多线程使用
     */
    @GetMapping("/two")
    public void two() throws InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger(5);

        new Thread(() -> {
            //先获取到操作时,预期旧值
            int i = atomicInteger.get();
            //A线程睡眠一秒,保证让B线程先执行,把值修改掉
            System.out.println(Thread.currentThread().getName() + " --拿到的预期旧值:" + i);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //CAS操作,查看与预期值不同的情况下,修改结果
            boolean b = atomicInteger.compareAndSet(i, 6);
            System.out.println(Thread.currentThread().getName() + " 预期修改为:" + 6 + ",实际修改结果:" + b);
        }, "A线程").start();

        new Thread(() -> {
            //先获取到操作时,预期旧值
            int i = atomicInteger.get();
            System.out.println(Thread.currentThread().getName() + " --拿到的预期旧值:" + i);
            //CAS操作
            boolean b = atomicInteger.compareAndSet(i, 7);
            System.out.println(Thread.currentThread().getName() + " 预期修改为:" + 7 + ",实际修改结果:" + b);
        }, "B线程").start();

        //睡眠3秒,保证A B线程执行完成
        TimeUnit.SECONDS.sleep(3);
        System.out.println("最终cas操作后的值:" + atomicInteger.get());
    }

执行结果

A线程 --拿到的预期旧值:5
B线程 --拿到的预期旧值:5  ## 两个线程同时获取到变量

B线程 预期修改为:7,实际修改结果:true  ## A线程睡眠,B线程成功把值修改掉
A线程 预期修改为:6,实际修改结果:false   ## 因为与预期旧值不同,因此A线程修改失败

最终cas操作后的值:7 ##最终结果为B线程修改为的 7

多线程,使用自旋

   /**
     * 多线程,使用自旋
     */
    @GetMapping("/three")
    public void three() throws InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger(5);

        new Thread(() -> {
            //先获取到操作时,预期旧值
            int i = atomicInteger.get();
            //A线程睡眠一秒,保证让B线程先执行,把值修改掉
            System.out.println(Thread.currentThread().getName() + " --拿到的预期旧值:" + i);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //CAS操作,查看与预期值不同的情况下,修改结果
            boolean b = atomicInteger.compareAndSet(i, 6);
            System.out.println(Thread.currentThread().getName() + " 预期修改为:" + 6 + ",实际修改结果:" + b);

            while (!b) {
                System.out.println(Thread.currentThread().getName() +" --cas失败,手动自旋");
                int newI = atomicInteger.get();
                b = atomicInteger.compareAndSet(newI, 6);
                System.out.println(Thread.currentThread().getName() +" --自旋再次获取值并CAS," + " 预期修改为:" + 6 + ",实际修改结果:" + b);
            }
            System.out.println(Thread.currentThread().getName() +" --修改成功,自旋结束");

        }, "A线程").start();

        new Thread(() -> {
            //先获取到操作时,预期旧值
            int i = atomicInteger.get();
            System.out.println(Thread.currentThread().getName() + " --拿到的预期旧值:" + i);
            //CAS操作
            boolean b = atomicInteger.compareAndSet(i, 7);
            System.out.println(Thread.currentThread().getName() + " 预期修改为:" + 7 + ",实际修改结果:" + b);
        }, "B线程").start();

        //睡眠3秒,保证A B线程执行完成
        TimeUnit.SECONDS.sleep(3);
        System.out.println("最终cas操作后的值:" + atomicInteger.get());
    }

执行结果

A线程 --拿到的预期旧值:5
B线程 --拿到的预期旧值:5  ## 两个线程同时获取到变量

B线程 预期修改为:7,实际修改结果:true  ## A线程睡眠,B线程成功把值修改掉
A线程 预期修改为:6,实际修改结果:false   ## 因为与预期旧值不同,因此A线程修改失败

## A线程因为更新失败,手动自旋
A线程 --cas失败,手动自旋

A线程 --自旋再次获取值并CAS, 预期修改为:6,实际修改结果:true ## 每一次自旋,都重复CAS操作

## A线程因为更新成功,停止自旋
A线程 --修改成功,自旋结束


最终cas操作后的值:6  ##最终结果为A线程修改为的 7

3.原子引用

是什么?

上一节使用到的 AtomicInteger 是原子整型,除开原子整型,还有原子长整型AtomicLong原子布尔AtomicBoolean,除了这些基本数据类型的原子类,还提供一种对象类型的原子类,这就被称为 原子引用类AtomicReferece

原子引用,可以用来对一些对象类型数据,比如 Sring自建对象 等类型来操作。

代码使用

    /**
     * 原子引用类型
     */
    @GetMapping("/four")
    public void four() throws InterruptedException {
        AtomicReference<UserInfo> atomicReference = new AtomicReference<>();
        UserInfo z3 = new UserInfo("z3", 23);
        UserInfo w5 = new UserInfo("w5", 28);
        UserInfo t7 = new UserInfo("t7", 30);
        //赋值
        atomicReference.set(z3);
        //获取旧值
        UserInfo userInfo = atomicReference.get();
        //CAS 操作
        boolean b = atomicReference.compareAndSet(userInfo, w5);
        System.out.println("预期修改为w5,修改结果为:" + b + " 修改后的值为:" + atomicReference.get());

        System.out.println("------------------------");

        //e二次操作,测验更改属性值后,会不会视为不一个对象
        UserInfo userInfo1 = atomicReference.get();
        UserInfo userInfo2 = atomicReference.get();
        userInfo2.setAge(1000);
        boolean b1 = atomicReference.compareAndSet(userInfo1, t7);
        System.out.println("预期修改为t7,修改结果为:" + b1 + " 修改后的值为:" + atomicReference.get());

        System.out.println("------------------------");
        AtomicReference<String> stringAtomicReference = new AtomicReference<>();
        stringAtomicReference.set("111");

        String s = stringAtomicReference.get();
        boolean b2 = stringAtomicReference.compareAndSet(s, "222");
        System.out.println("预期修改为222,修改结果为:" + b2 + " 修改后的值为:" + stringAtomicReference.get());
        boolean b3 = stringAtomicReference.compareAndSet(s, "333");
        System.out.println("二次预期修改为333,修改结果为:" + b3 + " 修改后的值为:" + stringAtomicReference.get());
    }

执行结果

## 自建对象 cas
预期修改为w5,修改结果为:true 修改后的值为:UserInfo(userName=w5, age=28)
------------------------
预期修改为t7,修改结果为:true 修改后的值为:UserInfo(userName=t7, age=30) ## 自建对象的属性值就算变化,也不会产生影响。


## java对象 cas
------------------------
预期修改为222,修改结果为:true 修改后的值为:222
二次预期修改为333,修改结果为:false 修改后的值为:222 ## Java基本对象,跟基本数据类型一样。

4.自旋锁

是什么?

CAS是实现自旋锁的基础,CAS利用CPU指令保证了操作的原子性,以达到锁的效果,至于自旋呢,看字面意思也很明白,自己旋转。是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁(也就是循环),当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。

这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU.

线程自旋锁
--- 先定义两个方法:加锁、释放锁    
/**
     * 加锁方法
     */
    public void lock(AtomicReference<Thread> threadAtomicReference) {
        //获取当前调用该方法的线程
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + " --尝试获取锁");
        //使用cas,只有预期值,也就是原先的线程是 null 的时候,才会把当前线程放入。
        //也就是,没有其他线程的时候,才会将当前线程放入
        //如果获取不到锁,就自旋等待,将cas语句,写在while参数中,才会每次都执行
        while (!threadAtomicReference.compareAndSet(null, thread)) {

        }
        System.out.println(Thread.currentThread().getName() + " --成功获取锁!");
    }

    /**
     * 释放锁方法
     */
    public void unlock(AtomicReference<Thread> threadAtomicReference) {
        //获取当前调用该方法的线程
        Thread thread = Thread.currentThread();
        //使用cas,只有预期值,也就是原先的线程是 当前线程 的时候,才会把当前线程重置为null。
        boolean b = threadAtomicReference.compareAndSet(thread, null);
        System.out.println(Thread.currentThread().getName() + " --成功释放锁");
    }


----  发起请求  ----
    /**
     * 手写 自旋锁
     * *题目:实现一个自旋锁,复习CAS思想
     * 自旋锁好处:循环比较获取没有类似于 wait 的阻塞。
     * </n>
     * 通过CAS操作完成自旋锁, A线程先进来调用myLock方法自己持有锁5秒钟,
     * B随后进来后发现当前有线程持有锁,所以只能通过自旋等待,直到A释放锁后郈B随后抢到。
     */
    @GetMapping("/spinLock")
    public void spinLock() {
        //原子线程类
        AtomicReference<Thread> threadAtomicReference = new AtomicReference<>();

    //A线程先进入,持有5秒钟锁
        new Thread(() -> {
            lock(threadAtomicReference);
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            unlock(threadAtomicReference);
        }, "A线程").start();

    TimeUnit.MILLISECONDS.sleep(500);//线程睡眠,保证A线程获取到锁
    
    	//B线程进入,尝试获取锁,获取不到就自旋等待
        new Thread(() -> {
            lock(threadAtomicReference);
            unlock(threadAtomicReference);
        }, "B线程").start();
    }

执行结果

## 两个线程同时争抢锁
A线程 --尝试获取锁
B线程 --尝试获取锁

A线程 --成功获取锁! ## A线程成功获取锁

## A线程持有锁 5秒钟    
## B线程自旋等待。。

A线程 --成功释放锁 ## 释放锁


B线程 --成功获取锁!## B线程才能获取到锁
B线程 --成功释放锁

5.两大缺点

  1. cas 需要搭配自旋使用,而长时间自旋,导致CPU耗费。

  2. cas 存在着ABA问题

① 版本号原子类 AtomicStampedReference,解决ABA问题

示例代码① - 自建对象

    /**
     *
     */
    @GetMapping("/aba")
    public void aba() {
        Book book = new Book(1, "java入门");
        AtomicStampedReference<Book> atomicStampedReference = new AtomicStampedReference<>(book, 1);
        Book reference = atomicStampedReference.getReference();
        int stamp = atomicStampedReference.getStamp();
        System.out.println("赋值后的值:" + reference);
        System.out.println("当前版本号:" + stamp);

        System.out.println("---------------------");

        Book book2 = new Book(2, "MySql性能调优");
        boolean b = atomicStampedReference.compareAndSet(book, book2, stamp, stamp + 1);
        Book newReference = atomicStampedReference.getReference();
        int newStamp = atomicStampedReference.getStamp();
        System.out.println("预期修改为:" + book2 + " 修改结果:" + b);
        
        System.out.println("---------------------");
        
        System.out.println("修改后的值:" + newReference);
        System.out.println("修改后的版本号:" + newStamp);
    }

执行结果

赋值后的值:Book(id=1, bookName=java入门)
当前版本号:1
---------------------
预期修改为:Book(id=2, bookName=MySql性能调优) 修改结果:true
---------------------
修改后的值:Book(id=2, bookName=MySql性能调优)
修改后的版本号:2

示例代码② - Java对象String

顺带测试ABA问题

    /**
     *
     */
    @GetMapping("/aba2")
    public void aba2() {
        AtomicStampedReference<String> atomicStampedReference = new AtomicStampedReference<>("111", 1);
        String reference = atomicStampedReference.getReference();
        int stamp = atomicStampedReference.getStamp();
        System.out.println("初始值:" + reference);
        System.out.println("初始版本号:" + stamp);

        System.out.println("---------------------");
        //第一次修改
        boolean b = atomicStampedReference.compareAndSet(reference, "222", stamp, stamp + 1);
        String newReference = atomicStampedReference.getReference();
        int newStamp = atomicStampedReference.getStamp();
        System.out.println("第一次预期修改为:" + "222" + " 修改结果:" + b + " 修改后的值:" + newReference + " 修改后的版本号:" + newStamp);

        System.out.println("---------------------");
        //第二次修改
        boolean c = atomicStampedReference.compareAndSet(newReference, "111", newStamp, newStamp + 1);
        String newReference2 = atomicStampedReference.getReference();
        int newStamp2 = atomicStampedReference.getStamp();
        System.out.println("第二次预期修改为:" + "111" + " 修改结果:" + c + " 修改后的值:" + newReference2 + " 修改后的版本号:" + newStamp2);

        System.out.println("---------------------");
        //第二次修改
        boolean d = atomicStampedReference.compareAndSet(newReference, "111", newStamp, newStamp + 1);
        String newReference3 = atomicStampedReference.getReference();
        int newStamp3 = atomicStampedReference.getStamp();
        System.out.println("第三次预期修改为:" + "333" + " 修改结果:" + d + " 修改后的值:" + newReference3 + " 修改后的版本号:" + newStamp3);

    }

执行结果

初始值:111
初始版本号:1  ## 初始情况  A
---------------------
第一次预期修改为:222 修改结果:true 修改后的值:222 修改后的版本号:2  ## 第一次修改  B
---------------------
第二次预期修改为:111 修改结果:true 修改后的值:111 修改后的版本号:3  ## 第二次修改  A
---------------------
第三次预期修改为:333 修改结果:false 修改后的值:111 修改后的版本号:3  ## 第三次修改失败,因此没有发生 ABA 问题。

② ABA 问题案例解决

1.首先不用版本号

  /**
     * 多线程下的 ABA 问题
     */
    @GetMapping("/abaThread")
    public void abaThread() throws InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger(100);

        new Thread(() -> {
            atomicInteger.compareAndSet(100, 101);
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicInteger.compareAndSet(101, 100);
        }, "A线程").start();

        new Thread(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicInteger.compareAndSet(100, 101);
            System.out.println("B线程修改后的值为:" + atomicInteger.get());
        }, "A线程").start();

    }

执行结果

最终的值为:101  # 发生了ABA问题

2.使用版本号

    /**
     * 多线程下的 ABA 问题
     * 使用版本号
     */
    @GetMapping("/abaThread2")
    public void abaThread2() {
        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);

        new Thread(() -> {
            Integer reference = atomicStampedReference.getReference();
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+" 初始版本号:"+stamp+" 初始值:"+reference);

            //暂停500毫秒,保证后面的t4线程初始化拿到的版本号和我一样
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            atomicStampedReference.compareAndSet(100, 101, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName() + " 一次修改后的值:" + atomicStampedReference.getReference() + " 版本号:" + atomicStampedReference.getStamp());

            atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + " 二次修改后的值:" + atomicStampedReference.getReference() + " 版本号:" + atomicStampedReference.getStamp());
        }, "T3").start();

        new Thread(() -> {
            Integer reference = atomicStampedReference.getReference();
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+" 初始版本号:"+stamp+" 初始值:"+reference);

            //睡眠一秒,保证让T3线程发生过ABA问题
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            boolean b = atomicStampedReference.compareAndSet(100, 2022, stamp, stamp + 1);

            System.out.println(Thread.currentThread().getName() + " 修改结果为:" + b + " 值为:" + atomicStampedReference.getReference());


        }, "T4").start();
    }

执行结果

## 两个线程都拿到一样的初始值、版本号
T3 初始版本号:1 初始值:100
T4 初始版本号:1 初始值:100

T3 一次修改后的值:101 版本号:2 
T3 二次修改后的值:100 版本号:3 ## T3线程首先进行操作,发生了ABA问题


T4 修改结果为:false 值为:100 ## 因为使用了版本号,因此不同版本号更新失败,避免了ABA错误。

七、原子操作类

1.基本类型原子类

AtomicInteger 原子整型

AtomicBoolean 原子布尔类型

AtomicLong 原子长整型

常见的通用api

public final int get() 获取当前的值

public final int getAndSet(int newValue) 获取当前的值,并设置新的值

public final int getAndlncrement() 获取当前的值,并自增

public final int getAndDecrement() 获取当前的值,并自减

public final int getAndAdd(int delta) 获取当前的值,并加上预期的值

boolean compareAndSet(int expect, int update) 如果输入的数值等于预期值,则以原子方式将该值设置为输入值((update)

案例

       /**
     * 基本类型原子类
     */
    @GetMapping("/one")
    public void one() throws InterruptedException {
        int size=50;
        MyNumber myNumber = new MyNumber();
        //这里引入计数器,50个线程计算完成之后,就打印结果。可以替换掉等待睡眠操作
        CountDownLatch countDownLatch = new CountDownLatch(size);
        for (int i = 0; i < size; i++) {
            new Thread(() -> {
                try {
                    for (int i1 = 0; i1 < 1000; i1++) {
                        myNumber.atomicAddPlusPlus();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    //计算完,计数器减一
                    countDownLatch.countDown();
                }
            }, String.valueOf(i)).start();
        }
//        TimeUnit.SECONDS.sleep(2);
        //线程遭遇计数器等待,计数器归零才会继续执行
        countDownLatch.await();
        System.out.println(myNumber.getAtomicInteger().get());
    }

执行结果

50000  ## 

**tips:**重点关注计数器,实际工作中,不会使用睡眠去等待线程执行结束,都是用计数器来阻塞等待。

2.数组a类型原子类

AtomiclntegerArray

AtomicLongArray

AtomicReferenceArray

不是重点,了解即可

案例

    /**
     * 数组类型原子类
     */
    @GetMapping("/two")
    public void two() throws InterruptedException {
        AtomicIntegerArray atomicIntegerArray=new AtomicIntegerArray(new int[5]);
        AtomicIntegerArray atomicIntegerArray2=new AtomicIntegerArray(5);
        AtomicIntegerArray atomicIntegerArray3=new AtomicIntegerArray(new int[]{1,2,3,4,5});

        //遍历获取
        for (int i = 0; i < atomicIntegerArray.length(); i++) {
            System.out.println(atomicIntegerArray.get(i));
        }
        System.out.println("----------------------------");
        //获取并更新值
        atomicIntegerArray.getAndSet(0,2022);
        for (int i = 0; i < atomicIntegerArray.length(); i++) {
            System.out.println(atomicIntegerArray.get(i));
        }
        System.out.println("----------------------------");
        //指定值自增
        atomicIntegerArray.getAndIncrement(0);
        for (int i = 0; i < atomicIntegerArray.length(); i++) {
            System.out.println(atomicIntegerArray.get(i));
        }

    }

执行结果

0
0
0
0
0
----------------------------
2022
0
0
0
0
----------------------------
2023
0
0
0
0

3.引用类型原子类

AtomicReference 最基本的原子引用类

AtomicStampedReference 带版本号的原子引用类,使用版本号解决ABA问题,上一节了解过

AtomicMarkableReference 带标识标记的原子引用,原子更新带有标记位的引用类型对象。它的定义就是将状态戳简化为true/false

案例

    /**
     * 引用类型原子类
     */
    @GetMapping("/three")
    public void three() throws InterruptedException {
        AtomicMarkableReference<String> atomicMarkableReference=new AtomicMarkableReference<>("初始值",false);
        new Thread(()->{
            String reference = atomicMarkableReference.getReference();
            boolean marked = atomicMarkableReference.isMarked();
            System.out.println(Thread.currentThread().getName()+" 初始值:"+reference+"初始标志位:"+marked);
            //睡眠保证B线程也能同时拿到
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean a = atomicMarkableReference.compareAndSet(reference, "A线程成功修改", marked, !marked);
            System.out.println(Thread.currentThread().getName()+" 修改结果:"+a+" 修改后的值:"+atomicMarkableReference.getReference()+" 修改后的标记位:"+atomicMarkableReference.isMarked());
        },"A线程").start();

        new Thread(()->{
            String reference = atomicMarkableReference.getReference();
            boolean marked = atomicMarkableReference.isMarked();
            System.out.println(Thread.currentThread().getName()+" 初始值:"+reference+"初始标志位:"+marked);
            //睡眠保证A线程能够执行完
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean a = atomicMarkableReference.compareAndSet(reference, "B线程成功修改", marked, !marked);
            System.out.println(Thread.currentThread().getName()+" 修改结果:"+a+" 修改后的值:"+atomicMarkableReference.getReference()+" 修改后的标记位:"+atomicMarkableReference.isMarked());
        },"B线程").start();
    }

执行结果

A线程 初始值:初始值初始标志位:false
B线程 初始值:初始值初始标志位:false
A线程 修改结果:true 修改后的值:A线程成功修改 修改后的标记位:true
B线程 修改结果:false 修改后的值:A线程成功修改 修改后的标记位:true

标志原子引用类,适用于判断是否单次修改的情况。不能解决ABA问题

4.对象属性修改原子类

AtomiclntegerFieldUpdater 原子更新对象中int类型字段的值,基于反射的实用程序,可对指定类的指定volatile int字段进行原子更新。

AtomicLongFieldUpdater 原子更新对象中Long类型字段的值,基于反射的实用程序,可对指定类的指定volatile Long字段进行原子更新。

AtomicReferenceFieldUpdater 原子更新引用类型字段的值,基于反射的实用程序,可对指定类的指定volatile 引用字段字段进行原子更新。

**使用目的:**以一种线程安全的方式操作非线程安全对象内的某些字段。

使用要求:

  1. 更新的对象属性必须使用public volatile 修饰符。

  2. 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。

编码使用

① 案例一 基本数据类型字段

实体类 - 资源类


/**
 * 原子类操作 - 银行账户
 *
 * @author byChen
 * @date 2022/12/28
 */
@Data
public class BankAccount {
    /**
     * 银行名称,不经常变的字段
     */
    String bankName;
    /**
     * 账户余额,经常变化的字段
     * <P>>更新的对象属性必须使用public volatile 修饰符。</P>
     */
    public volatile int money = 0;

    /**
     * 转账方法 - 线程不安全的方法
     */
    public void add() {
        money = money + 1000;
    }

    /**
     * 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器
     * 并且需要设置想要更新的类和属性。
     */
    AtomicIntegerFieldUpdater<BankAccount> fieldUpdater
            = AtomicIntegerFieldUpdater.newUpdater(BankAccount.class, "money");

    public void tranMoney(BankAccount bankAccount) {
        //字段自增方法
        //因为构造的时候指定了字段,因此这里直接传入对象即可
        fieldUpdater.getAndIncrement(bankAccount);
    }
}

操作

    /**
     * 对象属性修改原子类 - 案例01
     * 以一种线程安全的方式操作非线程安全对象的某些字段。
     *
     * <p>需求</p>
     * <p>10个线程,每个线程转账1000,不使用synchronized,尝试使用AtomicIntegerFieldupdater来实现。</p>
     */
    @GetMapping("/five")
    public void five() throws InterruptedException {
        BankAccount bankAccount = new BankAccount();
        CountDownLatch countDownLatch = new CountDownLatch(10);

        for (int i = 0; i < 10; i++) {

            new Thread(() -> {
                try {
                    for (int i1 = 0; i1 < 1000; i1++) {
                        //原子对象属性操作方法
                        bankAccount.tranMoney(bankAccount);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    //线程计数器
                    countDownLatch.countDown();
                }

            }, String.valueOf(i)).start();
        }
        //线程完成之前,进行阻塞
        countDownLatch.await();
        System.out.println("最终值:" + bankAccount.getMoney());
    }

执行结果

最终值:10000
最终值:10000
最终值:10000
最终值:10000  ## 请求4次,结果都一致没问题

② 案例二 引用数据类型字段

资源类

/**
 * 原子类操作 - 银行账户
 *
 * @author byChen
 * @date 2022/12/28
 */
@Data
public class MyVar {
    /**
     * 引用类型字段 - 标志位
     */
    public volatile Boolean isInit = Boolean.FALSE;

    /**
     * 因为字段的类型是引用类型,因此这里会多出一个参数,用来指定参数的类型
     */
    AtomicReferenceFieldUpdater<MyVar, Boolean> fieldUpdater
            = AtomicReferenceFieldUpdater.newUpdater(MyVar.class, Boolean.class, "isInit");

    public void init(MyVar myVar) {
        boolean b = fieldUpdater.compareAndSet(myVar, Boolean.FALSE, Boolean.TRUE);

        if (b) {
            System.out.println(Thread.currentThread().getName() + " 开始初始化,持续两秒钟。。");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 初始化完成!");
        } else {
            System.out.println(Thread.currentThread().getName() + " --已经有其他线程初始化过,放弃初始化");
        }
    }
}

操作

    /**
     * 对象属性修改原子类 - 案例02
     * <p>需求</p>
     * <p>多线程并发调用一个类的初始化方法,如果未被初始化过,将执行初始化工作,要求只能被初始化一次,只有一个线程操作成功I</p>
     */
    @GetMapping("/six")
    public void six() {
        MyVar myVar = new MyVar();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                myVar.init(myVar);
            }, String.valueOf(i)).start();
        }
    }

执行结果

0 开始初始化,持续两秒钟。。
3 --已经有其他线程初始化过,放弃初始化
2 --已经有其他线程初始化过,放弃初始化
1 --已经有其他线程初始化过,放弃初始化
4 --已经有其他线程初始化过,放弃初始化
0 初始化完成!

5.原子操作增强类 - 原理深度解析

DoubleAccumulator 一个或多个变量共同维护使用提供的函数更新的运行double值。

DoubleAdder 一个或多个变量共同维持最初的零和double总和。

LongAccumulator 一个或多个变量共同维护使用提供的函数更新的运行long值。

LongAdder 一个或多个变量共同维持最初为零的总和为long 。

常用api

因为两组是不同的数据类型,因此这里使用LongAdder + LongAccumulator 来举例

LongAdder 常用api

void add(long x) 将当前的value力加x。

void increment() 将当前的value加1。

void decrement() 将当前的value减1。

long sum() 返回当前值。特别注意,在没有并发更新value的情况下,sum会返回一个精确值,在存在并发的情况下,sum不保证返回精确值。

void reset() 将value重置为0,可用于替代重新new一个LongAdder,但此方法只可以在没有并发更新的情况下使用。

long sumThenReset() 获取当前value,并将value重置为0。

简单使用

    /**
     * 原子操作增强类
     */
    @GetMapping("/superOne")
    public void superOne() {
        /**
         * LongAdder  api
         */
        //LongAdder:创建一个初始总和为零的新加法器。
        //LongAdder只能用来计算加法,且从零开始计算
        LongAdder longAdder = new LongAdder();
        //连续三次自增
        longAdder.increment();
        longAdder.increment();
        longAdder.increment();
        System.out.println("LongAdder三次自增后的值:" + longAdder.sum());
        System.out.println("-------------------");

        /**
         *LongAccumulator  api
         */
        //LongAccumulator 提供了自定义的函数操作
        //使用给定的累加器函数和初始值元素创建新实例。
        //累加器函数由自己定义(identity:初始值  x:需要加上的值)
         LongAccumulator longAccumulator = new LongAccumulator((identity, x) -> identity + x, 0);
         
        //使用给定的 x 值来带入定义的函数去跟初始值相计算
        longAccumulator.accumulate(2); //0+2
        longAccumulator.accumulate(3); //2+3

        System.out.println("LongAccumulator 计算后的值:" + longAccumulator.get());
    }

执行结果

LongAdder三次自增后的值:3
-------------------
LongAccumulator 计算后的值:5

tips:如果LongAccumulator的够造方法中,lamba表达式不好理解,可以还原为匿名内部类,改成这样写

        /**
         *LongAccumulator  api
         */
        //LongAccumulator 提供了自定义的函数操作
        //使用给定的累加器函数和初始值元素创建新实例。
        //累加器函数由自己定义(identity:初始值  x:需要加上的值)

        LongAccumulator longAccumulator = new LongAccumulator((identity, x) -> identity + x, 0);

        LongAccumulator longAccumulator = new LongAccumulator(new LongBinaryOperator() {
            /**
             * 初始值为1 计算乘法
             * @param left 初始化值
             * @param right 计算的另一个值
             * @return
             */
            @Override
            public long applyAsLong(long left, long right) {
                return left * right;
            }
        }, 1);
        //使用给定的 x 值来带入定义的函数去跟初始值相计算
        longAccumulator.accumulate(2); //1X2
        longAccumulator.accumulate(3); //2X3

        System.out.println("LongAccumulator 计算后的值:" + longAccumulator.get());

使用乘法,也是能够正常计算出计算结果的

LongAdder三次自增后的值:3
-------------------
LongAccumulator 计算后的值:6

案例 - 高性能热点商品点赞计数器

进行使用原子操作类不使用原子操作类的性能对比

① 不使用原子操作类

资源类

/**
 * 热门商品点赞计数器 - 资源类
 *  四种加加方法,进行性能比较
 *
 * @author byChen
 * @date 2022/12/28
 */
@Data
public class ClickNumber {
    /**
     * synchronized方法 进行加加
     */
    int number = 0;

    public synchronized void clickBySynchronized() {
        number++;
    }

    /**
     * AtomicLong 进行加加
     */
    AtomicLong atomicLong = new AtomicLong(0);

    public void clickByAtomicLong() {
        atomicLong.getAndIncrement();
    }

    /**
     * LongAdder 进行加加
     */
    LongAdder longAdder = new LongAdder();

    public void clickByLongAdder() {
        longAdder.increment();
    }

    /**
     * LongAccumulator 进行加加
     */
    LongAccumulator longAccumulator = new LongAccumulator((identity, x) -> identity + x, 0);

    public void clickByLongAccumulator() {
        longAccumulator.accumulate(1);
    }
}

操作

    /**
     * 原子操作增强类 - 案例
     * <p>需求</p>
     * <p>50个线程,每个线程1w次,计算总点赞数</p>
     *
     * 四种方法,进行性能比较
     */
    @GetMapping("/hotCount")
    public void hotCount() throws InterruptedException {
        //执行次数
        final int workNum = 10000;
        //线程数量
        final int threadNum = 50;
        //执行开始时间、结束时间
        long startTime;
        long endTime;
        //资源类
        ClickNumber clickNumber = new ClickNumber();
        //线程计数器
        CountDownLatch downLatch1 = new CountDownLatch(threadNum);
        CountDownLatch downLatch2 = new CountDownLatch(threadNum);
        CountDownLatch downLatch3 = new CountDownLatch(threadNum);
        CountDownLatch downLatch4 = new CountDownLatch(threadNum);
        /*=-=-=-=-=-=-=-=-=synchronized 方法-=-=-=-=-=-=--=-=-*/
        startTime = System.currentTimeMillis();
        for (int i = 0; i < threadNum; i++) {
            new Thread(() -> {

                try {
                    for (int i1 = 0; i1 < workNum; i1++) {
                        clickNumber.clickBySynchronized();
                    }
                } finally {
                    downLatch1.countDown();
                }
            }, "方法1线程").start();
        }
        downLatch1.await();
        endTime = System.currentTimeMillis();
        System.out.println("synchronized --costTime:" + (endTime - startTime) + " 毫秒,最后的结果:" + clickNumber.getNumber());

        /*=-=-=-=-=-=-=-=-=AtomicLong 方法-=-=-=-=-=-=--=-=-*/
        startTime = System.currentTimeMillis();
        for (int i = 0; i < threadNum; i++) {
            new Thread(() -> {

                try {
                    for (int i1 = 0; i1 < workNum; i1++) {
                        clickNumber.clickByAtomicLong();
                    }
                } finally {
                    downLatch2.countDown();
                }
            }, "方法2线程").start();
        }
        downLatch2.await();
        endTime = System.currentTimeMillis();
        System.out.println("AtomicLong --costTime:" + (endTime - startTime) + " 毫秒,最后的结果:" + clickNumber.getAtomicLong().get());

        /*=-=-=-=-=-=-=-=-=LongAdder 方法-=-=-=-=-=-=--=-=-*/
        startTime = System.currentTimeMillis();
        for (int i = 0; i < threadNum; i++) {
            new Thread(() -> {

                try {
                    for (int i1 = 0; i1 < workNum; i1++) {
                        clickNumber.clickByLongAdder();
                    }
                } finally {
                    downLatch3.countDown();
                }
            }, "方法3线程").start();
        }
        downLatch3.await();
        endTime = System.currentTimeMillis();
        System.out.println("LongAdder --costTime:" + (endTime - startTime) + " 毫秒,最后的结果:" + clickNumber.getLongAdder().sum());

        /*=-=-=-=-=-=-=-=-=LongAccumulator 方法-=-=-=-=-=-=--=-=-*/
        startTime = System.currentTimeMillis();
        for (int i = 0; i < threadNum; i++) {
            new Thread(() -> {

                try {
                    for (int i1 = 0; i1 < workNum; i1++) {
                        clickNumber.clickByLongAccumulator();
                    }
                } finally {
                    downLatch4.countDown();
                }
            }, "方法4线程").start();
        }
        downLatch4.await();
        endTime = System.currentTimeMillis();
        System.out.println("LongAccumulator --costTime:" + (endTime - startTime) + " 毫秒,最后的结果:" + clickNumber.getLongAccumulator().get());
        
    }

执行结果

synchronized --costTime:89 毫秒,最后的结果:500000
AtomicLong --costTime:22 毫秒,最后的结果:500000
LongAdder --costTime:5 毫秒,最后的结果:500000
LongAccumulator --costTime:4 毫秒,最后的结果:500000  ## 可以看出来,原子增强类,性能最好

LongAdder 源码分析

LongAdder 为什么速度这么快?

image-20221228163533828

image-20221228163807771

LongAdder的基本思路就是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。

sum()会将所有Cell数组中的value和base累加作为返回值,核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点

**LongAdder 与 AtomicLong 对比 **

AtomicLong 线程安全,可允许一些性能损耗,要求高精度时可使用;保证精度,性能代价。AtomicLong是多个线程针对单个热点值value进行原子操作

LongAdder 当需要在高并发下有较好的性能表现,且对值的精确度要求不高时,可以使用;保证性能,精度代价。LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行cAS操作。

八、ThreadLocal 线程局部变量全链路跟踪

1.是什么?能干嘛?

是什么?

ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其getlset方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID))与线程关联起来。

线程内独立有效

能干嘛?

实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份)。

主要解决了让每个线程绑定自己的值,通过使用get)和iset()方法,获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题,比如我们之前讲解的8锁案例,资源类是使用同一部手机,多个线程抢夺同一部手机使用,假如人手一份是不是天下太平? ?

2.api

get( ) 返回当前线程的此线程局部变量副本中的值。

initialvalue() 返回此线程局部变量的当前线程的“初始值”。方法不推荐再使用了

remove( ) 删除此线程局部变量的当前线程值。

set(T value) 将此线程局部变量的当前线程副本设置为指定值。

withInitial(Supplier<? extends S> supplier) 创建一个线程局部变量。

3.案例分析

不使用 ThreadLocal

资源类

/**
 * 房子销售数量实体类 - 资源类
 *
 * @author byChen
 * @date 2022/12/29
 */
@Data
public class House {
    /**
     * 销售数量
     */
    int saleCount = 0;

    /**
     * 锁同步 - 销售方法
     */
    public synchronized void saleHouse() {
        saleCount++;
    }
}

操作

  /**
     * <p>需求1</p>
     * <p>5个销售卖房子,集团高层只关心销售总量的准确统计数。</p>
     */
    @GetMapping("/one")
    public void one() throws InterruptedException {
        int threadNum = 5;
        House house = new House();
        CountDownLatch countDownLatch = new CountDownLatch(threadNum);

        for (int i = 0; i < threadNum; i++) {
            new Thread(() -> {
                int size = new Random().nextInt(5) + 1;
                System.out.println(Thread.currentThread().getName() + " --卖出 " + size + " 套");
                for (int i1 = 0; i1 < size; i1++) {
                    house.saleHouse();
                }
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }
        //计数器,阻塞等待
        countDownLatch.await();
        System.out.println("最终销售数量:" + house.getSaleCount());
        System.out.println("-------------------------");
    }

执行结果

0 --卖出 11 --卖出 12 --卖出 53 --卖出 14 --卖出 1 套
最终销售数量:9   ## 并发线程竞争下,数量正确。
-------------------------

使用ThreadLocal

资源类


/**
 * 房子销售数量实体类 - 资源类
 *
 * @author byChen
 * @date 2022/12/29
 */
@Data
public class House {
    /**
     * 销售数量
     */
    int saleCount = 0;

    /**
     * 锁同步 - 销售方法
     */
    public synchronized void saleHouse() {
        saleCount++;
    }

//       ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
//        /**
//         * 初始化,不推荐再使用,仅作演示。
//         * @return
//         */
//        @Override
//        protected Integer initialValue() {
//            return 0;
//        }
//    };
    //推荐使用该方法进行初始化
    ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    /**
     * 使用线程局部变量来统计各自的销售数量
     */
    public void saleVolumeByThreadLocal() {
        threadLocal.set(threadLocal.get() + 1);
    }
}

操作

    /**
     * <p>需求2</p>
     * <p>5个销售卖完随机数房子,各自独立销售额度,自己业绩按提成走,分灶吃饭,各个销售自己动有手,丰衣足食</p>
     */
    @GetMapping("/two")
    public void two() throws InterruptedException {
        int threadNum = 5;
        House house = new House();
        CountDownLatch countDownLatch = new CountDownLatch(threadNum);
        for (int i = 0; i < threadNum; i++) {
            new Thread(() -> {
                int size = new Random().nextInt(5) + 1;
                try {
                    for (int i1 = 0; i1 < size; i1++) {
                        //普通卖出
                        house.saleHouse();
                        //ThreadLocal 线程局部变量卖出
                        house.saleVolumeByThreadLocal();
                    }
                    System.out.println(Thread.currentThread().getName() + " --卖出 " + house.getThreadLocal().get() + " 套");
                } finally {
                    countDownLatch.countDown();
                    // **注意:阿里规约规定,为避免内存泄漏,需要再用完之后,释放掉。
                    house.getThreadLocal().remove();
                }
            }, "线程" + String.valueOf(i)).start();
        }
        countDownLatch.await();
        //因为主线程并没有调用卖出方法,因此主线程的局部变量为0
        System.out.println("主线程:" + Thread.currentThread().getName() + "卖出:" + house.getThreadLocal().get());
        System.out.println("最终销售数量:" + house.getSaleCount());

    }

执行结果

线程0 --卖出 3 套
线程2 --卖出 1 套
线程3 --卖出 3 套
线程4 --卖出 2 套
线程1 --卖出 4## 每个线程各种卖出的数量

主线程:http-nio-8023-exec-1卖出:0 ## 主线程并没有调用卖出方法,因此主线程的局部变量为0

最终销售数量:13  ## 最终销量保持正确

补充案例:阿里编码手册规约

规约内容

【强制】必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用try-finally块进行回收。

资源类


/**
 * ThreadLocal 数据
 * 验证阿里规约中,ThreadLocal不回收导致内存泄漏问题
 *
 * @author byChen
 * @date 2022/12/29
 */
@Data
public class MyData {
    ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    /**
     * 线程局部变量 +1 方法
     */
    public void add() {
        threadLocal.set(threadLocal.get() + 1);
    }
}

操作

    /**
     * <p>需求3</p>
     * <p>验证阿里规约</p>
     * <p></p>
     * .【强制】必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal 变量,
     * 可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用try-finally块进行回收。
     */
    @GetMapping("/three")
    public void three() throws InterruptedException {
        MyData myData = new MyData();
        ExecutorService threadPool = Executors.newFixedThreadPool(3);

        try {
            for (int i = 0; i < 10; i++) {
                threadPool.submit(() -> {
                    Integer beforeNum = myData.getThreadLocal().get();
                    myData.add();
                    Integer afterNum = myData.getThreadLocal().get();
                    System.out.println(Thread.currentThread().getName()+" beforeNum:"+beforeNum+" afterNum:"+afterNum);
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }

执行结果

pool-1-thread-1 beforeNum:0 afterNum1
pool-1-thread-3 beforeNum:0 afterNum1
pool-1-thread-2 beforeNum:0 afterNum1
pool-1-thread-3 beforeNum:1 afterNum2
pool-1-thread-1 beforeNum:1 afterNum2
pool-1-thread-3 beforeNum:2 afterNum3
pool-1-thread-2 beforeNum:1 afterNum2
pool-1-thread-3 beforeNum:3 afterNum4
pool-1-thread-1 beforeNum:2 afterNum3
pool-1-thread-2 beforeNum:2 afterNum3   ## 应该每个线程都是做一个从 0 加到 1 的操作,但现在明显线程复用的时候,把值累加了

修改办法:每次线程对局部变量使用完不再使用了之后,必须手动清理释放

操作

    /**
     * <p>需求3</p>
     * <p>验证阿里规约</p>
     * <p></p>
     * .【强制】必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal 变量,
     * 可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用try-finally块进行回收。
     */
    @GetMapping("/three")
    public void three() throws InterruptedException {
        MyData myData = new MyData();
        ExecutorService threadPool = Executors.newFixedThreadPool(3);

        try {
            for (int i = 0; i < 10; i++) {
                threadPool.submit(() -> {
                    try {
                        Integer beforeNum = myData.getThreadLocal().get();
                        myData.add();
                        Integer afterNum = myData.getThreadLocal().get();
                        System.out.println(Thread.currentThread().getName() + " beforeNum:" + beforeNum + " afterNum:" + afterNum);
                    } finally {
                        //不在使用了,就手动清理释放掉
                        myData.getThreadLocal().remove();
                    }
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }

执行结果

pool-1-thread-1 beforeNum:0 afterNum:1
pool-1-thread-2 beforeNum:0 afterNum:1
pool-1-thread-1 beforeNum:0 afterNum:1
pool-1-thread-2 beforeNum:0 afterNum:1
pool-1-thread-3 beforeNum:0 afterNum:1
pool-1-thread-2 beforeNum:0 afterNum:1
pool-1-thread-1 beforeNum:0 afterNum:1
pool-1-thread-2 beforeNum:0 afterNum:1
pool-1-thread-3 beforeNum:0 afterNum:1
pool-1-thread-1 beforeNum:0 afterNum:1  ## 结果正常了,不发生内存泄漏

4.总结

因为每个Thread内有自己的实例副本且该副本只由当前线程自己使用。

既然其它Thread不可访问,那就不存在多线程间共享的问题。

统一设置初始值,但是每个线程对这个值的修改都是各自线程互相独立的

也就是说,如果才能不争抢?

  1. 加入synchronized或者Lock控制资源的访问顺序。锁同步思想
  2. 人手一份,大家各自安好,没必要抢夺。线程局部变量思想

Thread 、ThreadLocal、ThreadGroup 三者关系

image-20221229113715087

image-20221229114043116

5.内存泄漏问题

是什么?

不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。

四大引用

强引用

当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收。

强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。

当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到,JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。

对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域(接口走完)或者显式地将相应强引用赋值为null,一般认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾收集策略)。

资源类

/**
 * 测试释放被回收
 * 不管怎样,只要对象还在被引用,都不会被回收
 *
 * @author byChen
 * @date 2022/12/29
 */
public class MyObject {
    /**
     * 案例演示,正常情况下不要重写
     * finalize 的通常目的是在对象被不可撤销地丢弃之前执行清理操作。
     * 可以理解为,被回收前会执行本方法
     *
     * @throws Throwable
     */
    @Override
    protected void finalize() throws Throwable {
//        super.finalize();
        System.out.println("-----执行 finalize 方法");
    }
}

操作

    /**
     * 强引用
     *
     * @throws InterruptedException
     */
    @GetMapping("/strongReference")
    public void strongReference() {
        MyObject myObject = new MyObject();
        System.out.println("gc回收 之前:" + myObject);

        System.gc();
        //对象还被指向,还在被引用,因此不应该被回收,是强引用
        System.out.println("第一次,直接手动gc回收 之后:" + myObject);

        //将对象赋值为 null ,表示不再有引用指向它,它才可以被回收
        myObject = null;
        System.gc();
        System.out.println("赋值为null,并手动gc回收 之后:" + myObject);
    }

执行结果

gc回收 之前:com.spring.domain.MyObject@53e00130

第一次,直接手动gc回收 之后:com.spring.domain.MyObject@53e00130 ## 还在被引用,无法被回收,强引用

赋值为null,并手动gc回收 之后:null ## 被赋值为null,也就是没有被引用了,就被回收掉了
-----执行 finalize 方法 ##回收前正常打出语句
软引用

软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。

对于只有软引用的对象来说,

  1. 当系统内存充足时它不会被回收.
  2. 当系统内存不足时它会被回收。

软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!

操作

    /**
     * 软引用
     * - 内存够,就等于是强引用
     * - 内存不够,就会被回收
     *
     * @throws InterruptedException
     */
    @GetMapping("/softReference")
    public void softReference() {
        //软引用需要包装类
        SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());
        System.out.println("gc回收 之前:" + softReference.get());

        //内存够,不回收
        System.gc();
        System.out.println("--内存够的情况下:"+softReference.get());

        //模拟内存不够
        byte[] bytes = new byte[20 * 1024 * 1024]; //模拟将内存占用完
        //不用手动回收,程序自己就会调用回收方法
        System.out.println("--内存够的情况下:"+softReference.get());

    }

执行结果

gc回收 之前:com.spring.domain.MyObject@53e00130

--内存够的情况下:com.spring.domain.MyObject@53e00130  ## 内存够,就不回收

--内存够的情况下:null ## 内存不够,就被回收掉了
-----执行 finalize 方法 ##回收前正常打出语句
弱引用

弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短,

对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够Ⅰ都会回收该对象占用的内存。

操作

    /**
     * 弱引用
     * - 不管内存够不够,只要发生gc回收,它都会被回收掉
     *
     * @throws InterruptedException
     */
    @GetMapping("/weakReference")
    public void weakReference() {
        //弱引用需要使用包装类
        WeakReference<MyObject> weakReference = new WeakReference<>(new MyObject());
        System.out.println("gc回收 之前:" + weakReference.get());

        System.gc();
        System.out.println("gc回收 之后:" + weakReference.get());
    }

执行结果

gc回收 之前:com.spring.domain.MyObject@6df88147
gc回收 之后:null
-----执行 finalize 方法
虚引用

image-20221229145352399

操作

    /**
     * 虚引用
     *
     * @throws InterruptedException
     */
    @GetMapping("/phantomReference")
    public void phantomReference() {
        //定义一个 引用队列,虚引用对象被回收前,会放入该队列中
        ReferenceQueue<MyObject> objectReferenceQueue = new ReferenceQueue<>();
        //虚引用需要包装类+引用队列
        PhantomReference<MyObject> phantomReference = new PhantomReference<>(new MyObject(), objectReferenceQueue);

        //虚引用仅仅是一种通知机制,无法进行业务逻辑,因为get()方法总是返回 null。
        System.out.println("原生无法访问:" + phantomReference.get());

        List<byte[]> list = new ArrayList<>();

        //模拟个线程,一直向内存里添加
        new Thread(() -> {
            while (true) {
                list.add(new byte[1 * 1024 * 1024]);
                System.out.println(phantomReference.get() + " list add ok");
            }
        }, "t1").start();

        //另外一个线程,一直检查虚对象队列中是否添加了虚对象,有就通知
        new Thread(() -> {
            while (true) {
                Reference<? extends MyObject> poll = objectReferenceQueue.poll();
                if (poll != null) {
                    System.out.println("---有虚对象进入队列了,通知一次");
                    break;
                }
            }
        }, "t2").start();
    }

执行结果

null  list add ok
null  list add ok
null  list add ok
null  list add ok
null  list add ok

## 发生内存溢出异常
## 虚对象被回收,进入了虚对象队列

---有虚对象进入队列了,通知一次 ##加入队列,通知
引用总结

image-20221229151407105

为什么ThreadLocal使用弱引用?

弱引用:每次gc必定被回收。

案例代码

    /**
     * test
     *
     * @throws InterruptedException
     */
    @GetMapping("/test")
    public void test() {
        ThreadLocal<String> tl=new ThreadLocal<>();
        tl.set("111111");
        tl.get();
    }

//当方法执行完成之后,对象 threadLocal 也会被弹栈,然后被回收销毁
//虽然是强引用,但是作用域已经超过了,依旧要被回收

image-20221229152940535

总结:

  1. 因为ThreadLocal中存储数据的ThreadLocalMap在另外一个作用域,它不会随着接口的走完而被弹栈回收;
  2. 又因为这个ThreadLocalMap的键指向着ThreadLocal中的线程,因此这个线程会被引用而导致不会被回收;
  3. 如果是使用强引用的话,就会一直无法被回收而造成内存泄漏,反之,如果使用弱引用,这个指向会在下一次gc的时候被回收掉,也就不会造成内存泄漏了。

**tips:**弱引用会允许ThreadLocal的线程在作用域走完之后被回收,解决了ThreadLocal无法被回收的问题;但是回收之后,ThreadLocal的线程就会变为null,用来存值的 map 中的entiry就会产生键为null,但是有value的情况。这些脏entry依旧会造成内存泄漏。因此,阿里手册规约才会规定:在不使用某个ThreadLocal的时候,要手动调用 remove方法来清理释放内存

6.总结

使用建议

使用时,需要初始化 ThreadLocal threadLocal = ThreadLocal.withInitial(() -> 0); 如果不初始化的话,调用的时候可能会报空指针

建议使用static来修饰 因为ThreadLocal能实现了线程的数据隔离,不在于它自己本身,而在于Thread的ThreadLocalMap,所以,ThreadLocal可以只初始化一次,只分配一块存储空间就足以了,没必要作为成员变量多次被初始化。

用完记得remove 强制使用,必须remove;否制会有内存泄漏的风险。

九、对象内存布局和对象头

1.对象在堆内存中的存储布局

image-20221230094842834

分为三部分:对象头实例数据对齐填充

对象头

对象头又分为三部分:对象标记MarkWord类元信息ClassPointer(又叫类型指针)、数组对象还会有一个长度length

对象标记MarkWord

它保存什么?

默认存储对象的HashCode、分代年龄和锁标志位等信息。这些信息都是与对象自身定义无关的数据,所以MarkWord被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间MarkWNord里存储的数据会随着锁标志位的变化而变化。

image-20221230113000996

image-20221230141733477

类型指针ClassPointer

对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

对象头有多大?在64位系统中,Mark Word占了8个字节,类型指针占了8个字节,一共是16个字节。

实例数据

存放类的属性(Field)数据信息,包括父类的属性信息

对齐填充

虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐这部分内存按8字节补充对齐。

三者关系:当创建一个类的时候,如果改类没有属性,那么new出来的对象就只有对象头(占16个字节);如果该类有属性,那么new出来的对象就有对象头+实例数据;如果该对象的内存起始位置不是8字节的整数倍,那么就会出现对齐填充,用来将内存位置填充到最近的8字节的整数倍的位置。

2.案例验证

准备工作

引入分析工具依赖

        <!--对象在JVM的大小和分布分析工具-->
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.9</version>
        </dependency>

代码测试① - 虚拟机详情

    /**
     * 获取当前虚拟机详情
     */
    @GetMapping("/one")
    public void one() {
        //获取当前虚拟机详情
        System.out.println(VM.current().details());
        
        //所有对象分配的字节都是8的整数倍
        System.out.println(VM.current().objectAlignment());
    }

执行结果

# Running 64-bit HotSpot VM.                                 --虚拟机类型
# Using compressed oop with 3-bit shift.  
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.                               --对象是按照8字节对齐的
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]     --文件的大小
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]     --数组的大小

8

代码测试② - 对象详情

    /**
     * 对象内部信息查看
     */
    @GetMapping("/two")
    public void two() {
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }

执行结果

java.lang.Object object internals:
#内存偏移 大小   类型 描述                                值
 OFFSET  SIZE   TYPE DESCRIPTION                        VALUE
      0     4        (object header)                    05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                    00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                    e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
      
     12     4        (loss due to the next object alignment)  # 对齐填充
     
Instance size: 16 bytes  ## 对象一般的默认大小
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

对象信息字段解析

OFFSET 偏移量,也就是到这个字段位置所占用的byte数

SIZE 后面类型的字节大小

TYPE 是Class中定义的类型

DESCRIPTION DESCRIPTION是类型的描述

VALUE VALUE是TYPE在内存中的值

代码实例

① 对象内部没有属性

对象类

/**
 * 对象内存模型分析类
 *   属性都给注释掉
 * @author byChen
 * @date 2022/12/30
 */
@Data
public class Customer {
//    int id; 
//    boolean flag = false;
}

操作

    /**
     * 对象内部信息查看
     */
    @GetMapping("/two")
    public void two() {
        Customer customer = new Customer();
        System.out.println(ClassLayout.parseInstance(customer).toPrintable());
    }

执行结果

com.spring.domain.Customer object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                           VALUE
      0     4        (object header)                       05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                       00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                       a2 d2 08 f8 (10100010 11010010 00001000 11111000) (-133639518)
      
     12     4        (loss due to the next object alignment)
     
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
## 同准备案例一样的结果,(只有对象头,没有实例数据)

② 对象内部有属性

对象类

/**
 * 对象内存模型分析类
 *
 * @author byChen
 * @date 2022/12/30
 */
@Data
public class Customer {
    int id;
    boolean flag = false;
}

操作

    /**
     * 对象内部信息查看
     */
    @GetMapping("/two")
    public void two() {
        Customer customer = new Customer();
        System.out.println(ClassLayout.parseInstance(customer).toPrintable());
    }

执行结果

com.spring.domain.Customer object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                     VALUE
 															     ## --1.对象头
      0     4           (object header)                 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4           (object header)                 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                 a2 d2 08 f8 (10100010 11010010 00001000 11111000) (-133639518)
      
     12     4       int Customer.id                               0    ## --2.出现了实例数据的内容
     16     1   boolean Customer.flag                             false
     
     17     7           (loss due to the next object alignment)        ## --3.对齐填充
     
Instance size: 24 bytes ##(实际大小=4+4+4 + 4+1 +7=24)对象头+属性+对齐填充
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
## 对象内部有属性,就出现了实例数据的内容

3.压缩指针

上面的案例可以看出来,命名应该是8字节的类型指针,现在却只有4字节;这是因为JVM默认开启了压缩指针命令以节约空间;

如果手动关闭之后,就是正常的8字节了,但是一般不会去调它,默认就好。

十、synchronized 与 锁升级

1.synchronized 的性能变化

image-20221230162805443

2.升级流程

synchronized用的锁是存在Java对象头里的Mark Word中。

锁升级功能主要依赖MarkWord中锁标志位释放偏向锁标志位

image-20221230165532736

偏向锁: MarkWord存储的是偏向的线程ID;

轻量锁:MarkWord存储的是指向线程栈中Lock Record的指针;

重量锁: MarkWord存储的是指向堆中的monitor对象的指针

①无锁

无锁:初始状态,一个对象被实例化后,如果还没有被任何线程竞争锁,那么它就为无锁状态(001)

image-20221230174626089

代码示例

    @GetMapping("/one")
    public void one() {
        Object o = new Object();
        //只有对象调用了.hashCode() 方法,对象头MarkWord中才会显示出来
        System.out.println("10进制的hashCode:" + o.hashCode());
        System.out.println("16进制的hashCode:" + Integer.toHexString(o.hashCode()));
        System.out.println("2进制的hashCode:" + Integer.toBinaryString(o.hashCode()));

        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }

执行结果 - 只截取了对象头的部分

10进制的hashCode:2028697957
16进制的hashCode:78eb7965
2进制的hashCode:1111000111010110111100101100101

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION              VALUE
      0     4        (object header)          01 65 79 eb (00000001 01100101 01111001 11101011) (-344365823)
      4     4        (object header)          78 00 00 00 (01111000 00000000 00000000 00000000) (120)
      
## 上面是仅截取的对象头中的对象标记

这64位2进制数字,整体上是从右下角往左上角数的,但是每个8个数字的组,是从左往右的。也就是

    // 1111000 11101011 01111001 01100101  ## 二进制的hash值
    //01111000 11101011 01111001 01100101  ## 对象头中的2进制信息
    
## 因为hash值占31位,因此还需要把第一个数字排除掉,于是就对应上了

具体的位数看法为:👇

image-20221230174334092

后面读位跟这个一致,不在赘述

②偏向锁

image-20221230174638788

是什么

偏向锁:单线程竞争

当线程A第一次竞争到锁时,通过操作修改Mark Word中的偏向线程ID、偏向模式。如果不存在其他线程竞争,那么持有偏向锁的线程将永远不需要进行同步。

作用

当一段同步代码一直被同一个线程多次访问,由于只有一个线程那么该线程在后续访问时便会自动获得锁。

避免频繁的由用户态切换为内核态,避免消耗内存性能。

代码案例

资源类

/**
 * 卖票资源类
 * 模拟3个售票员卖50张票
 *
 * @author byChen
 * @date 2022/12/23
 */
public class Ticket {
    /**
     * 票数量
     */
    private int number = 50;
    /**
     * 可重入锁,因为没有参数,因此是非公平锁
     */
    ReentrantLock lock = new ReentrantLock(true);

    /**
     * 模拟卖票方法
     */
    public void sale() {
        Object o = new Object();
        synchronized (o){
            if (number>0){
                System.out.println(Thread.currentThread().getName()+" --卖出去第 "+(number--)+" 还剩下:"+number);
            }
        }
    }

}

操作

   /**
     * 偏向锁
     */
    @GetMapping("/two")
    public void two() {
        Ticket ticket = new Ticket();
        new Thread(()->{
            for (int i = 0; i < 55; i++) {
                ticket.sale2();
            }
        },"a").start();
        new Thread(()->{
            for (int i = 0; i < 55; i++) {
                ticket.sale2();
            }
        },"b").start();
        new Thread(()->{
            for (int i = 0; i < 55; i++) {
                ticket.sale2();
            }
        },"c").start();

        //查看对象内存模型
        System.out.println(ClassLayout.parseInstance(ticket).toPrintable());
    }

执行结果

a --卖出去第 50 还剩下:49
a --卖出去第 17 还剩下:16
## a出现39次

b --卖出去第 34 还剩下:33
## b出现六次

c --卖出去第 26 还剩下:25
## c出现5次


## 根据对象头中的信息,可以看出,锁标志位确实是 101 
com.spring.domain.Ticket object internals:
 OFFSET  SIZE               TYPE DESCRIPTION               VALUE
      0     4               (object header)                05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4               (object header)                00 00 00 00 (00000000 00000000 00000000 00000000) (0)

偏向锁,其实可以理解为第二章3小节中的非公平锁

偏向锁的持有

理论:

在实际应用运行过程中发现,“锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程。

那么只需要在锁第一次被拥有的时候,记录下偏向线程ID(也就是当前拥有锁的线程的id)。这样偏向线程就一直持有着锁(后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接会去检查锁的MarkWord里面是不是放的自己的线程ID)。

  1. 如果相等,表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步。无需每次加锁解锁都去CAS更新对象头。如果自始至终使用锁的线程只有一个(也就是没有其他线程来竞争),很明显偏向锁几乎没有额外开销,性能极高。

  2. 如果不等,表示发生了竞争,锁已经不是总是偏向于同一个线程了,这个时候会尝试使用CAS来替换MarkWord里面的线程ID为新线程的ID

    ​ a.竞争成功:表示之前的线程不存在了,MarkWord里面的线程ID为新线程的ID,锁不会升级,仍然为偏向锁;

    ​ b.竞争失败:这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。

注意,偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。

技术实现:

image-20230103151526756

查看当前线程id
  /**
     * 偏向锁 - 查看当前线程id
     */
    @GetMapping("/three")
    public void three() throws InterruptedException {
        Object o = new Object();
        //不发生线程争抢的时候,没有存入线程id
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        
        System.out.println("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-");

        new Thread(() -> {
            //多线程发生竞争的时候,才会有线程id
            synchronized (o) {
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        }).start();
    }

执行结果:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION               VALUE
      0     4        (object header)           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION               VALUE
      0     4        (object header)           05 20 13 21 (00000101 00100000 00010011 00100001) (554901509)
      4     4        (object header)           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

## 前54位开始出现了线程id
偏向锁的撤销

当有另外线程逐步来竞争锁的时候,就不能再使用偏向锁了,要撤销偏向锁转而升级为轻量级锁。

竞争线程尝试CAS更新对象头失败,会等待到**全局安全点(此时不会执行任何代码)**撤销偏向锁。

撤销:

偏向锁使用一种等到竞争出现才释放锁的机制,只有当出现其他线程来竞争锁时,持有偏向锁的原来线程才会被撤销。撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行:

  1. 第一个线程正在执行synchronized方法(处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现锁升级。此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。
  2. 第一个线程执行完成synchronized方法(退出同步块),则将对象头设置成无锁状态并撤销偏向锁(撤销上一个线程的偏向锁),重新偏向。

竞争撤销流程

image-20230103155713790

–偏向锁在Java15版本之后,就逐步被废弃了(因为开销跟成本比较大)–

③轻量级锁

image-20221230174731823

是什么

轻量级锁:多线程竞争,但是任意时刻最多只有一个线程竞争,即不存在锁竞争太过激烈的情况,也就没有线程阻塞。

主要作用:

有线程来参与锁的竞争,但是获取锁的冲突时间极短(竞争等待锁的时间极短)

本质就是CAS+自旋

轻量级锁的获取

轻量级锁是为了在线程近乎交替执行同步块时提高性能。

主要目的:在没有多线程竞争的前提下,通过CAS+自旋以减少重量级锁使用操作系统互斥量产生的性能消耗,说白了先cas+自旋,不行才升级阻塞。

升级时机:1.当关闭偏向锁功能,无锁直接升级为轻量级锁 2.多线程竞争偏向锁会导致偏向锁升级为轻量级锁。

竞争过程:

1.假如线程A已经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到,当前该锁已是偏向锁了(偏向A线程)。

2.而线程B在争抢时发现对象头Mak Word中的线程ID不是线程B自己的线程ID(而是线程A),那线程B就会进行CAS+自旋操作希望能获得锁。此时线程B操作中有两种情况:

​ ①. 如果锁获取成功,直接替换Mark Word中的线程ID为B自己的ID(A→B),重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程"被"释放了锁),该锁会保持偏向锁状态,A线程Over,B线程上位; (这时还是偏向锁,只是进行了一次撤销竞争)

​ ②. 如果锁获取失败,则偏向锁升级为轻量级锁(设置偏向锁标识为0并设置锁标志位为OO),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程B会进入自旋等待获得该轻量级锁。(这时才升级为轻量级锁)

升级锁的条件

轻量级锁说白了就是CAS+自旋,但是如果自旋达到一定次数,但是还是没有解决,就会触发锁升级。

自适应自旋锁的大致原理

线程如果自旋成功了,那下次自旋的最大次数会增加,因为JVM认为既然上次成功了,那么这一次也很大概率会成功。

反之

如果很少会自旋成功,那么下次会减少自旋的次数甚至不自旋,避免CPU空转。

④重量级锁

image-20221230174851814

阻塞锁,涉及到用户态到内核态的转变。当一个线程获取到资源后,其他线程只会阻塞等待。

3.总结

①升级流程图解

②锁升级与hashCode的关系

对象在无锁状态下,是可以计算出hashCode值得,但锁升级之后,原本在对象头里存储hashCode的地方被占了,那hashCode要怎么办?

image-20230103165737059

image-20230103170118132

③各种锁对比

image-20230103170849814

偏向锁:适用于单线程使用的情况,在不存在锁竞争的时候进入同步方法/代码块则使用偏向锁。

轻量级锁:适用于竞争较不激烈的情况(这和乐观锁的使用范围类似),存在竞争时升级为轻量级锁,轻量级锁采用的是自旋锁,如果同步方法/代码块执行时间很短的话,采用轻量级锁虽然会占用cpu资源但是相对比使用重量级锁还是更高效。

重量级锁:适用于竞争激烈的情况,如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁。

④补充:JIT编译器对锁的优化

JIT:Just In Time Compiler,一般翻译为即时编译器

锁消除

锁消除问题

某些操作,导致线程之间根本不会产生竞争,锁不存在,JIT编译器会无视它,即为锁清除。

锁清除代码示例

资源类

/**
 * 锁清除
 *
 * @author byChen
 * @date 2023/1/3
 */
@Data
public class LockClean {
    static Object localObject = new Object();

    /**
     * 正常的锁方法
     */
    public void m1() {
        synchronized (localObject) {
            System.out.println(" ---hello " + localObject.hashCode() + " ");
        }
    }

    /**
     * 锁清除的方法
     */
    public void m2() {
        Object o = new Object();
        synchronized (o) {
            System.out.println(" ---hello " + o.hashCode() + " " + localObject.hashCode());
        }
    }
}

操作

    /**
     * 某些操作,导致线程之间根本不会产生竞争,锁不存在,即为锁清除
     * <p>
     * JIT编译器会无视它,synchronized(o),每次new出来的,不存在了,非正常的。
     * 这个锁对象并没有被共用扩散到其它线程使用,
     * 极端的说就是根本没有加这个锁对象的底层机器码,消除了锁的使用</p>
     *
     * @throws InterruptedException
     */
    @GetMapping("/lockClean")
    public void lockClean() throws InterruptedException {
        LockClean lockClean = new LockClean();

        //m1方法,每次进入线程,锁的都是同一个对象,存在着锁竞争;锁存在
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                lockClean.m1();
            }).start();
        }

        //m2方法,每次进入线程,都是新new一个对象,然后各自锁各自的对象,不存在锁竞争;锁消除了
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                lockClean.m2();
            }).start();
        }
    }
锁粗化

锁粗化问题

编译器将方法中,首尾相接、前后相邻的同一个锁对象合并成一个大锁块。

锁粗化代码示例

资源类

/**
 * 锁粗化
 *
 * @author byChen
 * @date 2023/1/3
 */
@Data
public class LockBig {
    static Object lockObject = new Object();
}

操作

    /**
     * 锁粗化问题
     * <p>
     * 假如方法中首尾相接,前后相邻的都是同一个锁对象,
     * 那IT编译器就会把这几个synchronized块合并成一个大块,加粗加大范围,
     * 一次申请锁使用即可,避免次次的申请和释放锁,提升了性能</p>
     *
     * @throws InterruptedException
     */
    @GetMapping("/lockBig")
    public void lockBig() throws InterruptedException {
        LockBig lockBig = new LockBig();

        //在一个线程中,针对同一个资源类反复加锁,没有必要
        new Thread(() -> {
            synchronized (lockBig) {
                System.out.println("111");
            }
            synchronized (lockBig) {
                System.out.println("222");
            }
            synchronized (lockBig) {
                System.out.println("333");
            }
        }).start();

        //JIT 底层编译会自动更改优化为一个锁;锁粗化
        new Thread(() -> {
            synchronized (lockBig) {
                System.out.println("111");
                System.out.println("222");
                System.out.println("333");
            }
        }).start();
    }

十一、AQS

1.是什么

抽象的队列同步器

是用来实现锁或者其它同步器组件的公共基础部分的抽象实现,是重量级基础框架及整个JUC体系的基石,主要用于解决锁分配给"谁"的问题

整体就是一个抽象的FIFO队列(先进先出队列)来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态;

image-20230105162255365

2.如何运作的

image-20230105163456845

image-20230105164341895

AQS结构解析

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    //Node 节点,下一节详解
    static final class Node{...};
    //头节点信息
    private transient volatile Node head;
    //尾节点信息
    private transient volatile Node tail;
    
    //同步状态state成员变量
    private volatile int state;
    
    //CAS 方法
    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
}

node 节点解析

//node 节点中重要的几个参数   
static final class Node {
  
        static final Node SHARED = new Node();

        static final Node EXCLUSIVE = null;

        //线程被取消了
        static final int CANCELLED =  1;
	   //后继线程需要被唤醒
        static final int SIGNAL    = -1;
       //等待condition唤醒
        static final int CONDITION = -2;
		//共享式同步状态获取将会无条件的传播下去
        static final int PROPAGATE = -3;
       
       //节点的状态;初始为0,会被修改状态,状态为上面的几种
       volatile int waitStatus;
       
       //本节点的前置节点
       volatile Node prev;
       //本节点的后置节点
       volatile Node next;
       
       //当前线程
       volatile Thread thread;
  
           }

tips:注意AQS的同步状态state,跟Node节点的状态waitStatus是不一样的概念,注意区分开

3.源码分析

本节 以ReentrantLock 且选择非公平锁,来做示例

    /**
     *
     */
    @GetMapping("/one")
    public void one() {
        //使用非公平锁
        ReentrantLock lock = new ReentrantLock();
        lock.lock();
        try {


        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

01 - 非公平锁与公平锁的关键区别

image-20230105173008830

对比公平锁和非公平锁的 tryAcquire()方法 的实现代码,其实差别就在于非公平锁获取锁时比公平锁中少了一个判断 IhasQueuedPredecessors() 判断前边是否有其他节点

hasQueuedPredecessors()中判断了是否需要排队,导致公平锁和非公平锁的差异如下:

公平锁:公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;

非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一个排队线程苏醒后,不一定就是排头的这个线程获得锁,它还是需要参加竞争锁(存在线程竞争的情况下,正巧有个其他线程来竞争且还没有失败进入队列),后来的线程可能不讲武德插队夺锁了。

image-20230105173554169

acquire() 方法解析

/**
* 以独占模式获取,忽略中断。通过至少调用一次 **tryAcquire()**  ,
*    成功就返回。
*    否则就失败,线程将进入队列,可能会重复阻塞和解除阻塞,调用{@link tryAcquire}直到成功。
* 这个方法可以用来实现方法{@link Locklock}。
*/
public final void acquire(int arg) {
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        { selfInterrupt();}
    }

02 - 加锁 ,关键的 acquire() 方法

lock.lock();加锁方法的底层,是一个acquire() 方法 , 而acquire() 方法中主要有三个方法

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

image-20230106151859051

tryAcquire - 抢锁

本次举例使用非公平锁方法

//tryAcquire 方法在非公平锁的底层代码
//  -- 尝试去占用锁 --
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    //获取锁标志位 state 的状态
    int c = getState();
    //如果等于0,表示未被占用
    if (c == 0) { 
        //尝试去抢占锁
        if (compareAndSetState(0, acquires)) {
            //抢到锁,将占用线程设置为自己
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果不等于0,表示被占用
    else if (current == getExclusiveOwnerThread()) {//getExclusiveOwnerThread 查看占用的线程是哪个,然后判断是否是当前线程
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
addWaiter - 抢锁失败后,封装为节点入队列
// addWaiter 方法的底层代码   
//  --  加入队列  --
private Node addWaiter(Node mode) {
       //将当前线程封装为Node节点
        Node node = new Node(Thread.currentThread(), mode);
        // 获取队列的尾结点
        Node pred = tail;
        // 如果尾结点不为空,证明队列中已经有线程等待了
        if (pred != null) {
            //  将当前线程封装的节点的前指针指向之前的尾结点
            node.prev = pred;
            //  然后以CAS的方式将最后的尾结点的下一节点指向当前节点
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                //成功了,就正常入队列了,返回即可
                return node;
            }
        }
    
        //将节点插入队列,必要时(队列为空时)进行初始化
        enq(node);
        return node;
    }
acquireQueued - 入队阻塞等待,并设置前边节点要唤醒自己
//    
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //获得封装节点的前置节点
                final Node p = node.predecessor();
                //如果前置节点是头结点,且抢到锁了
                if (p == head && tryAcquire(arg)) {
                    setHead(node);//封装节点变为头结点
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //根据前置节点的 waitState 来改变前置节点的waitState
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
流程总结
  1. 线程进来,先去尝试获取锁(因为有可能进来之后正巧上一个线程释放锁) - tryAcquire() 方法

  2. 获取成功就去执行;失败就进行入队列; - addWaiter() 方法

  3. 进入队列需要进行阻塞等待,并设置前置的节点要唤醒自己。

03 - 解锁唤醒出队,关键的release() 方法

T 底层编译会自动更改优化为一个锁;锁粗化
new Thread(() -> {
synchronized (lockBig) {
System.out.println(“111”);
System.out.println(“222”);
System.out.println(“333”);
}
}).start();
}




# 十一、AQS

## 1.是什么

> **抽象的队列同步器**
>
> 是用来**实现锁或者其它同步器组件的公共基础部分的抽象实现**,是**重量级基础框架及整个JUC体系的基石**,主要用于解决锁分配给"谁"的问题
>
> 整体就是一个抽象的FIFO队列(先进先出队列)来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态;

[外链图片转存中...(img-Fhr7GDyZ-1688143071531)]





## 2.如何运作的

[外链图片转存中...(img-VNBhBbLV-1688143071532)]

[外链图片转存中...(img-Y8ZzraQu-1688143071532)]

### AQS结构解析

```java
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    //Node 节点,下一节详解
    static final class Node{...};
    //头节点信息
    private transient volatile Node head;
    //尾节点信息
    private transient volatile Node tail;
    
    //同步状态state成员变量
    private volatile int state;
    
    //CAS 方法
    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
}

node 节点解析

//node 节点中重要的几个参数   
static final class Node {
  
        static final Node SHARED = new Node();

        static final Node EXCLUSIVE = null;

        //线程被取消了
        static final int CANCELLED =  1;
	   //后继线程需要被唤醒
        static final int SIGNAL    = -1;
       //等待condition唤醒
        static final int CONDITION = -2;
		//共享式同步状态获取将会无条件的传播下去
        static final int PROPAGATE = -3;
       
       //节点的状态;初始为0,会被修改状态,状态为上面的几种
       volatile int waitStatus;
       
       //本节点的前置节点
       volatile Node prev;
       //本节点的后置节点
       volatile Node next;
       
       //当前线程
       volatile Thread thread;
  
           }

tips:注意AQS的同步状态state,跟Node节点的状态waitStatus是不一样的概念,注意区分开

3.源码分析

本节 以ReentrantLock 且选择非公平锁,来做示例

    /**
     *
     */
    @GetMapping("/one")
    public void one() {
        //使用非公平锁
        ReentrantLock lock = new ReentrantLock();
        lock.lock();
        try {


        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

01 - 非公平锁与公平锁的关键区别

[外链图片转存中…(img-NbacmeHp-1688143071533)]

对比公平锁和非公平锁的 tryAcquire()方法 的实现代码,其实差别就在于非公平锁获取锁时比公平锁中少了一个判断 IhasQueuedPredecessors() 判断前边是否有其他节点

hasQueuedPredecessors()中判断了是否需要排队,导致公平锁和非公平锁的差异如下:

公平锁:公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;

非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一个排队线程苏醒后,不一定就是排头的这个线程获得锁,它还是需要参加竞争锁(存在线程竞争的情况下,正巧有个其他线程来竞争且还没有失败进入队列),后来的线程可能不讲武德插队夺锁了。

[外链图片转存中…(img-ygVg05bA-1688143071534)]

acquire() 方法解析

/**
* 以独占模式获取,忽略中断。通过至少调用一次 **tryAcquire()**  ,
*    成功就返回。
*    否则就失败,线程将进入队列,可能会重复阻塞和解除阻塞,调用{@link tryAcquire}直到成功。
* 这个方法可以用来实现方法{@link Locklock}。
*/
public final void acquire(int arg) {
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        { selfInterrupt();}
    }

02 - 加锁 ,关键的 acquire() 方法

lock.lock();加锁方法的底层,是一个acquire() 方法 , 而acquire() 方法中主要有三个方法

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

[外链图片转存中…(img-f9Z5hNH7-1688143071535)]

tryAcquire - 抢锁

本次举例使用非公平锁方法

//tryAcquire 方法在非公平锁的底层代码
//  -- 尝试去占用锁 --
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    //获取锁标志位 state 的状态
    int c = getState();
    //如果等于0,表示未被占用
    if (c == 0) { 
        //尝试去抢占锁
        if (compareAndSetState(0, acquires)) {
            //抢到锁,将占用线程设置为自己
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果不等于0,表示被占用
    else if (current == getExclusiveOwnerThread()) {//getExclusiveOwnerThread 查看占用的线程是哪个,然后判断是否是当前线程
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
addWaiter - 抢锁失败后,封装为节点入队列
// addWaiter 方法的底层代码   
//  --  加入队列  --
private Node addWaiter(Node mode) {
       //将当前线程封装为Node节点
        Node node = new Node(Thread.currentThread(), mode);
        // 获取队列的尾结点
        Node pred = tail;
        // 如果尾结点不为空,证明队列中已经有线程等待了
        if (pred != null) {
            //  将当前线程封装的节点的前指针指向之前的尾结点
            node.prev = pred;
            //  然后以CAS的方式将最后的尾结点的下一节点指向当前节点
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                //成功了,就正常入队列了,返回即可
                return node;
            }
        }
    
        //将节点插入队列,必要时(队列为空时)进行初始化
        enq(node);
        return node;
    }
acquireQueued - 入队阻塞等待,并设置前边节点要唤醒自己
//    
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //获得封装节点的前置节点
                final Node p = node.predecessor();
                //如果前置节点是头结点,且抢到锁了
                if (p == head && tryAcquire(arg)) {
                    setHead(node);//封装节点变为头结点
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //根据前置节点的 waitState 来改变前置节点的waitState
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
流程总结
  1. 线程进来,先去尝试获取锁(因为有可能进来之后正巧上一个线程释放锁) - tryAcquire() 方法

  2. 获取成功就去执行;失败就进行入队列; - addWaiter() 方法

  3. 进入队列需要进行阻塞等待,并设置前置的节点要唤醒自己。

03 - 解锁唤醒出队,关键的release() 方法

未完待更新。。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值