juc笔记前篇

JUC

本篇章是边学习边记录尚硅谷周阳老师的juc视频写下来的笔记前篇
后篇

1.前置概念复习

start线程

public static void main(String[] args) throws InterruptedException
{
    Thread t1 = new Thread(() -> {
    }, "t1");
    t1. start();
}

t1.start()里面的方法是

public synchronized void start() {
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */
    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}

而其中的start0()方法

private native void start0();

native调用了第三方C语言接口。而Java底层是用c++实现的。

所以native start0()调用了 thread.c ,jvm.cpp,thread.cpp方法

3个文件具体路径为

juc笔记前篇_第1张图片

  • java线程是通过start的方法启动执行的,主要内容在native方法start0中,
  • openjdk的写JNI一般是一一对 应的,Thread.java对 应的就是Thread.c
  • start0其实就是JVM_StartThread。 此时查看源代码可以看到在jvm.h中找到了声明,jvm.cpp中有实现。

即最后由

os::start_ thread(thread);

这行代码来调用系统分配一个原生的基础线程。

java多线程相关概念

1.一把锁

synchronized

2.两个并

1)并发(concurrent)
  • 是在同一实体上的多个事件
  • 是在一台处理器上”同时“处理多个任务
  • 同一时刻,其实是只有一个事件在发生
2)并行(parallel)
  • 是在不同实体_上的多个事件
  • 是在多台处理器上同时处理多个任务
  • 同一时刻,大家真的都在做事情,你做你的,我做我的,但是我们都在做
3)并发vs并行

juc笔记前篇_第2张图片

3.三个程

1)进程

简单的说,在系统中运行的一一个应用程序就是-一个进程,每一个进程都有它自己的内存空间和系统资源。

2)线程

也被称为轻量级进程,在同一个进程内会有1个或多个线程,是大多数操作系统进行时序调度的基本单元。

3)管程

Monitor(监视器),也就是我们平时所说的锁

Monitor其实是一种同步机制, 他的义务是保证(同- -时间)只有一一个线程可以访问被保护的数据和代码。

JVM中同步是基于进入和退出监视器对象(Monitor,管程对象)来实现的,每个对象实例都会有-.个Monitor对象,

Object o = new Object();
new Thread(()->{
    synchronized (o){

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

Monitor对象会和Java对象-同创建并销毁,它底层是由C++语言来实现的。

JVM第三版

juc笔记前篇_第3张图片

用户线程和守护线程

Java线程分为用户线程和守护线程

  • 一般情况下不做特别说明配置,默认都是用户线程
  • 用户线程(User Thread)

​ 是系统的工作线程,它会完成这个程序需要完成的业务操作。例如:main线程

  • 守护线程(Daemon Thread)

    ​ 是一种特殊的线程为其它线程服务的,在后台默默地完成–些系统性的服务,比如垃圾回收线程就是最典型的例子

    ​ 守护线程作为一个服务线程,没有服务对象就没有必要继续运行了,如果用户线程全部结束了,意味着程序 需要完成的业务操作已经结束了,系统可以退出了。所以假如当系统只剩下守护线程的时候,java虚拟机会自动退出。

线程的daemon属性

  • true表示是守护线程
  • false表示是用户线程

code演示

未开启

public class DaemonDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t 开始执行,"+(Thread.currentThread().isDaemon()?"守护线程":"用户线程"));
            while(true){

            }
        },"t1");
      //  thread.setDaemon(true);
        thread.start();
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(Thread.currentThread().getName() + "\t ---end主线程");
    }
}

执行结果:

juc笔记前篇_第4张图片

开启守护线程(setDaemon要在start方法之前,不然会报错)

public class DaemonDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            System.out.println(Thread.urrentThread().getName()+"\t 开始执行,"+(Thread.currentThread().isDaemon()?"守护线程":"用户线程"));
            while(true){

            }
        },"t1");
        thread.setDaemon(true);
        thread.start();
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(Thread.currentThread().getName() + "\t ---end主线程");
    }
}

juc笔记前篇_第5张图片

小总结

如果用户线程全部结束意味着程序需要完成的业务操作已经结束了,守护线程随着JVM一同结束工作setDaemon(true)方法必须在start()之前设置,否则报llelgalIThreadStateException异常

2.CompletableFuture

Future接口理论知识复习

  • Future接口(FutureTask实现类)定义了操作异步任务执行一些方法,如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等。
  • 比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就去做其他事情了,忙其它事情或者先执行完,过了一会才去获取子任务的执行结果或变更的任务状态。

总结一句话:Future接口可以为主线程开一个分支任务,专门为主线程处理耗时和费力的复杂业务。

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

1.Future接口可以干嘛:

  • Future是Java5新加的一个接口,它提供了一种异步并行计算的功能。

    Runnable接口

  • 如果主线程需要执行一个很耗时的计算任务,我们就可以通过future把这个任务放到异步线程中执行。

  • 主线程继续处理其他任务或者先行结束,再通过Future获取计算结果。

  • 代码说话:

    ​ Callable接口

    ​ Future接口和

    ​ Future Task实现类

  • 目的:异步多线程任务执行且返回有结果,三个特点:多线程/有返回/异步任务

  • (班长为老师去买水作为新启动的异步多线程任务且买到水有结果返回)

案例:

import java.util.concurrent.Callable;

public class CompletableFutureDemo {
    public static void main(String[] args) {

    }
}

/**
 * 没有返回值
 */
class MyThread implements Runnable {
    @Override
    public void run() {

    }
}

/**
 * 有返回值
 */
class MyThread2 implements Callable<String> {
    @Override
    public String call() throws Exception {
        return null;
    }
}

2.本源的Future接口相关架构

juc笔记前篇_第6张图片

案例:

public class CompletableFutureDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<String> futureTask = new FutureTask<>(new MyThread1());

        Thread t1 = new Thread(futureTask,"t1");
        t1.start();
        System.out.println(futureTask.get());
    }
}
class MyThread1 implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("---come in call()");
        return "hello Callable";
    }
}

3.Future编码实战和优缺点分析

优点
  • future+线程池异步多线程任务配合,能显著提高程序的执行效率。
  • 代码案例:
import java.util.concurrent.*;

public class FutureThreadPollDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //设置3个线程池
        ExecutorService threadPool= Executors.newFixedThreadPool(3);
        long l = System.currentTimeMillis();
        FutureTask<String > futureTask1=new FutureTask<>(()->{
            try{TimeUnit.MILLISECONDS.sleep(500);}catch (InterruptedException e){e.printStackTrace();}
            return "task1 over";
        });
        threadPool.submit(futureTask1);
        FutureTask<String > futureTask2=new FutureTask<>(()->{
            try{TimeUnit.MILLISECONDS.sleep(300);}catch (InterruptedException e){e.printStackTrace();}
            return "task2 over";
        });
        threadPool.submit(futureTask2);
        FutureTask<String > futureTask3=new FutureTask<>(()->{
            try{TimeUnit.MILLISECONDS.sleep(300);}catch (InterruptedException e){e.printStackTrace();}
            return "task3 over";
        });
        threadPool.submit(futureTask3);

        System.out.println(futureTask1.get());
        System.out.println(futureTask2.get());
        long l1 = System.currentTimeMillis();
        System.out.println("--------costTime:"+(l1-l)+"毫秒");
        System.out.println(Thread.currentThread().getName()+"\t---end");
        threadPool.shutdown();

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

    private static void m1() {
        long l = System.currentTimeMillis();
        try{TimeUnit.MILLISECONDS.sleep(500);}catch (InterruptedException e){e.printStackTrace();}
        try{TimeUnit.MILLISECONDS.sleep(300);}catch (InterruptedException e){e.printStackTrace();}
        try{TimeUnit.MILLISECONDS.sleep(300);}catch (InterruptedException e){e.printStackTrace();}
        long l1 = System.currentTimeMillis();
        System.out.println("--------costTime:"+(l1-l)+"毫秒");
        System.out.println(Thread.currentThread().getName()+"\t---end");
    }
}

执行结果:

juc笔记前篇_第7张图片

缺点
get()阻塞
  • get()要等到结果才会离开,不管计算是否完成。容易导致堵塞,一般get方法放最后
FutureTask<String> futureTask = new FutureTask<>(() -> {
            System.out.println(Thread.currentThread().getName() + "\t---------come in");
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "task over";
        });
        Thread t1 = new Thread(futureTask, "t1");
        t1.start();
        System.out.println(futureTask.get());
        System.out.println(Thread.currentThread().getName() + "\t------------忙其他任务了");

运行结果

juc笔记前篇_第8张图片

稍微解决一点:

在get后加参数

System.out.println(futureTask.get(3,TimeUnit.SECONDS));

运行结果:

juc笔记前篇_第9张图片

isDone()轮询

轮询的方式会耗费无谓的CPU资源,而且也不见得能及时地得到计算结果.
如果想要异步获取结果,通常都会以轮询的方式去获取结果尽量不要阻塞

FutureTask<String> futureTask = new FutureTask<>(() -> {
    System.out.println(Thread.currentThread().getName() + "\t---------come in");
    try {
        TimeUnit.SECONDS.sleep(5);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "task over";
});
Thread t1 = new Thread(futureTask, "t1");
t1.start();

System.out.println(Thread.currentThread().getName() + "\t------------忙其他任务了");
// System.out.println(futureTask.get(3,TimeUnit.SECONDS));
while (true) {
    if (futureTask.isDone()) {
        System.out.println(futureTask.get());
        break;
    } else {
        //暂停毫秒
        try {
            TimeUnit.MILLISECONDS.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("正在处理中,不要再催了,越催越慢,再催熄火");
    }
}

结果:

juc笔记前篇_第10张图片

结论

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

CompletableFuture对Future的改进

completableFuture为什么出现

  • get()方法在Future计算完成之前会一直处在阻 塞状态下,
  • isDone()方法容易耗费CPU资源,
  • 对于真正的异步处理我们希望是可以通过传入回调函数,在Future结 束时自动调用该回调函数,这样,我们就不用等待结果。
  • 阻塞的方式和异步编程的设计理念相违背,而轮询的方式会耗费无谓的CPU资源。因此,
  • JDK8设计出CompletableFuture。
  • CompletableFuture提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方。

CompletableFuture和CompletionStage源码分别介绍

类架构说明

image-20220725211301358

接口CompletionStage
  • CompletionStage代表异步计算过程中的某一个阶段, 一一个阶段完成以后可能会触发另外-一个阶段
  • 一个阶段的计算执行可以是一个Function, Consumer或 者Runnable。比如: stage.thenApply(x -> square()).thenAccept(x -> System.outprint()).thenRun() -> System.out.println()
  • 一个阶段的执行可能是被单个阶段的完成触发,也可能是由多个阶段一起触发

代表异步计算过程中的某一个阶段,一 个阶段完成以后可能会触发另外-个阶段, 有些类似Linux系统的管道分隔符传参数。

类CompletableFuture
  • 在Java8中,CompletableFuture提供 了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处埋计算结果,也提供了转换和组合CompletableFuture的方法。
  • 它可能代表-个明确完成的Future, 也有可能代表-个完成阶段( CompletionStage ),它支持在计算完成以后触发-些函数或执行某些动作
  • 它实现了Future和CompletionStaqe接口

核心的四个静态方法,来创建一个异步任务

runAsync无返回值
  • public static CompletableFuture runAsync(Runnable runnable)
  • public static CompletableFuture runAsync(Runnable runnable ,Executor executor)
supplyAsync有返回值
  • public static CompletableFuture supplyAsync(Supplier supplier)
  • public static CompletableFuture supplyAsync(Supplier supplier,Executor executor)
上述Executor executor参数说明
  • 没有指定Executor的方法,直接使用默认的ForkJoinPool.commonPool()作为它的线程池执行异步代码。
  • 如果指定线程池,则使用我们自定义的或者特别指定的线程池执行异步代码,
Code

无返回值

有返回值

Code之通用演示,减少阻塞和轮询

从Java8开始引入了CompletableFuture,它是Future的功能增强版,减少阻塞和轮询
可以传入回调对象,当异步任务完成或者发生异常时自动调用回调对象的回调方法

CompletableFuture.supplyAsync(() -> {
    System.out.println(Thread.currentThread().getName() + "------com in");
    int result = ThreadLocalRandom.current().nextInt(10);
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("-------1秒钟后出结果:" + result);
    return result;
}).whenComplete((v,e)->{
     if (e==null) {
         System.out.println("----计算完成,更新系统updateVa:"+v);
     }
 }).exceptionally(e->{
     e.printStackTrace();
     System.out.println("异常情况:"+e.getCause()+"\t"+e.getMessage());
     return null;
 });
System.out.println(Thread.currentThread().getName()+"线程先去忙别的事情");

该方法下默认线程池关闭,自定义线程池记得要关闭

try {
    TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
    e.printStackTrace();
}

或者加入Executor

ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture的优点
  • 异步任务结束时,会自动回调某个对象的方法;
  • 主线程设置好回调后,不再关心异步任务的执行,异步任务之间可以顺序执行
  • 异步任务出错时,会自动回调某个对象的方法;

案例精讲-从电商网站的比价需求说开去

函数式接口例子

函数式接口名称 方法名称 参数 返回值
Runnable run 无参数 无返回值
Function apply 1个参数 无返回值
consume accept 1个参数 无返回值
sippier get 没有参数 有返回值
BiConsumer accept 2个参数 无返回值

join和get的区别

join不用在运行时检查异常。get要

CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
            return "hello 1234";
        });
//        System.out.println(completableFuture.get());
        System.out.println(completableFuture.join());

运行结果:

juc笔记前篇_第11张图片

大厂需求是先功能–>完美

需求分析

1需求说明

1.1同一款产品,同时搜索出同款产品在各大电商平台的售价;
1.2同一-款产品,同时搜索出本产品在同一个电商平台下,各个入驻卖家售价是多少.

2输出返回:

出来结果希望是同款产品的在不同地方的价格清单列表,返回一个List
《mysq|l》in jd price is 88.05
《mysq|》in dangdang price is 86.11
《mysq|》in taobao price is 90.43

3解决方案,比对同一个商品在各个平台上的价格,要求获得-一个清单列表,
  • step by step, 按部就班,查完京东查淘宝,查完淘宝查天猫…
  • all in,万箭齐发,一一口气多线程异步任务同时查询。。。。。
public class CompletableFutureMallDemo {
    static  List<NetMail> list= Arrays.asList(
        new NetMail("jd"),
        new NetMail("dangdang"),
        new NetMail("taobao"),
        new NetMail("pdd"),
        new NetMail("tmall")
    );
    /**
     * 一家家搜
     *
     * */
    public static List<String> getPrice( List<NetMail> list, String productName){
        return list
            .stream()
            .map(netMail -> String.format(productName + " in %s price is %.2f", netMail.getNetMallName(),
                                          netMail.calcPrice(productName)))
            .collect(Collectors.toList());
    }

    /**
     *并发搜索
     * */
    public static  List<String> getPriceByCompletableFuture(List<NetMail> list,String productName){
        return list.stream().map(netMail ->
                                 CompletableFuture.supplyAsync(() -> String.format(productName + "in %s price is %.2f",
                                                                                   netMail.getNetMallName(),
                                                                                   netMail.calcPrice(productName))))
            .collect(Collectors.toList())
            .stream().map(CompletableFuture::join)
            .collect(Collectors.toList());
    }
    public static void main(String[] args)  {
        long startTime = System.currentTimeMillis();
        List<String> list1 = getPrice(list, "mysql");
        for (String element: list1){
            System.out.println(element);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("----costTime:"+(endTime-startTime)+"毫秒");
        System.out.println("------------");
        long startTime2 = System.currentTimeMillis();
        List<String> list2 = getPriceByCompletableFuture(list, "mysql");
        for (String element: list2){
            System.out.println(element);
        }
        long endTime2 = System.currentTimeMillis();
        System.out.println("----costTime:"+(endTime2-startTime2)+"毫秒");
    }
}

class NetMail{
    @Getter
    private String netMallName;

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

    public double calcPrice(String productName){
        try{TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e){e.printStackTrace();}
        return   ThreadLocalRandom.current().nextDouble()*2+productName.charAt(0);
    }
}

运行结果

mysql in jd price is 110.71
mysql in dangdang price is 110.30
mysql in taobao price is 110.73
mysql in pdd price is 109.10
mysql in tmall price is 109.91
----costTime:5100毫秒
------------
mysqlin jd price is 110.15
mysqlin dangdang price is 110.19
mysqlin taobao price is 109.42
mysqlin pdd price is 109.37
mysqlin tmall price is 109.92
----costTime:1017毫秒

CompletableFuture常用方法

1.获得结果和触发计算

获取结果

  • public T get()

    不见不散

  • public T get(long timeout, TimeUnit unit)

    过时不候

  • public T join()

    与get方法类似,但是不会在运行前检查异常

  • public T getNow(T valuelfAbsent)

    没有计算完成的情况下,给我一个替代结果

    立即获取结果不阻塞

    ​ 计算完,返回计算完成后的结果

    ​ 没算完,返回设定的valuelfAbsent值

//            throws ExecutionException, InterruptedException, TimeoutException
{
    CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "abc";
    });
    //        System.out.println(completableFuture.get());
    //        System.out.println(completableFuture.get(2,TimeUnit.SECONDS));
    //        System.out.println(completableFuture.join());
    try{TimeUnit.SECONDS.sleep(3);}catch(InterruptedException e){e.printStackTrace();}
    System.out.println(completableFuture.getNow("xxx"));
}

主动触发计算

  • public boolean complete(T value )
System.out.println(completableFuture.complete("completeValue")+"\t"+completableFuture.join());

2.对计算结果进行处理

thenApply

计算结果存在依赖关系,两个线程串行化

异常相关

  • 由于存在依赖关系(当前步错,不走下一步),当前步骤有异常的话就叫停。
handle

计算结果存在依赖关系,两个线程串行化

异常相关

  • 有异常也可以往下一-步走,根据带的异常参数可以进一-步处理

3.对计算结果进行消费

接收任务的处理结果,并消费处理,无返回结果
thenAccept
CompletableFuture.supplyAsync(()-> 1).thenApply(f-> f+2).thenApply(f-> f+3).thenAccept(System.out::println);
对比补充
code之间的执行顺序

thenRun

  • thenRun(Runnable runnable)

  • 任务A执行完执行B,并且B不需要A的结果

thenAccept

  • thenAccept(Consumer action)
  • 任务A执行完执行B,B需要A的结果,但是任务B无返回值

thenApply

  • thenApply(Function fn)
  • 任务A执行完执行B,B需要A的结果,同时任务B有返回值
CompletableFuture和线程池说明
ExecutorService service = Executors.newFixedThreadPool(5);
try {
    CompletableFuture<Void> completableFuture = CompletableFuture.supplyAsync(() -> {
        try {
            TimeUnit.MILLISECONDS.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("1号任务" + "\t" + Thread.currentThread().getName());
        return "abcd";
    },service).thenRunAsync(() -> {
        try {
            TimeUnit.MILLISECONDS.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("2号任务" + "\t" + Thread.currentThread().getName());
    }).thenRunAsync(() -> {
        try {
            TimeUnit.MILLISECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("3号任务" + "\t" + Thread.currentThread().getName());
    }).thenRunAsync(() -> {
        try {
            TimeUnit.MILLISECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("4号任务" + "\t" + Thread.currentThread().getName());
    });
    System.out.println(completableFuture.get(2L, TimeUnit.SECONDS));
} catch (ExecutionException | InterruptedException | TimeoutException e) {
    e.printStackTrace();
} finally {
    service.shutdown();
}
1没有传入 自定义线程池,都用默认线程池ForkJoinPool;
2传入了一个自定义线程池,

如果你执行第一一个任务的时候,传入了一个自定义线程地:

  • 调用thenRun方法执行第二个任务时,则第二 个任务和第一个任务是共用同一个线程池。
  • 调用thenRunAsync执行第二个任务时,则第-一个任务使用的是你自己传入的线程池,第二个任务使用的是ForkJoin线程池
3备注

有可能处理太快,系统优化切换原则,直接使用main线程处理
其它如: thenAccept 和thenAcceptAsync,thenApply 和thenApplyAsync等,它们之间的区别也是同理

以thenRun和thenRunAsync为例,有什么区别?

4.对计算速度进行选用

谁快用谁
apply ToEither
CompletableFuture<String> playA = CompletableFuture.supplyAsync(() -> {
    System.out.println("A come in");
    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "playA";
});

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

CompletableFuture<String> result = playA.applyToEither(playB, f -> {
    return f + " is winner";
});
System.out.println(Thread.currentThread().getName()+"\t"+"----"+result.join());

5.对计算结果进行合并

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

code标准版

CompletableFuture<Integer> completableFuture1 = CompletableFuture.supplyAsync(() -> {
    System.out.println(Thread.currentThread().getName() + "\t ----启动");
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return 10;
});

CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(() -> {
    System.out.println(Thread.currentThread().getName() + "\t ----启动");
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return 20;
});
CompletableFuture<Integer> completableFuture = completableFuture1.thenCombine(completableFuture2, (x, y) -> {
    System.out.println("---两个值合并");
    return x + y;
});
System.out.println(completableFuture.join());

code表达式(合并版本)

CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "\t ----启动");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 10;
        }).thenCombine(CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "\t ----启动");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 20;
        }),(x,y)->{
            System.out.println("---两个值合并");
            return x + y;
        });

//        CompletableFuture completableFuture2 = CompletableFuture.supplyAsync(() -> {
//            System.out.println(Thread.currentThread().getName() + "\t ----启动");
//            try {
//                TimeUnit.SECONDS.sleep(1);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//            return 20;
//        });
//        CompletableFuture completableFuture = completableFuture1.thenCombine(completableFuture2, (x, y) -> {
//            System.out.println("---两个值合并");
//            return x + y;
//        });
        System.out.println(completableFuture.join());
    }

3.java锁事

从轻松的乐观锁和悲观锁开讲

悲观锁:

  • 认为自己在使用数据的时候一定 有别的线程修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
  • synchronized关键字和Lock的实现类都是悲观锁

乐观锁

  • 认为自己在使用数据时不会有别的线程修改数据或资源,以不会添加锁。
  • 在Java中是通过使用无锁编程来实现,只是在更新数据的时候去判断,之前有没有别的线程更新了这个数据。
  • 如果这个数据没有被更新,当前线程将自己修改的数据成功写入。
  • 如果这个数据已经被其它线程更新,则根据不同的实现方式执行不同的操作,比如放弃修改、重试抢锁等等

通过8种情况演示锁运行案例,看看我们到底锁的是什么

锁相关的8种案例演示code

class Phone{
    public static synchronized void sendEmail(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("----sendEmail");
    }
    public synchronized void sendSMS(){
        System.out.println("----sendSMS");
    }
    public void Hello(){
        System.out.println("---hello");
    }
}
/**
 * 题目:谈谈你对多线程锁的理解, 8锁案例说明
 * 口诀:线程 操作 资源美
 * 8锁案例说明:
 * 1.标准访问有ab两个线程,请问先打印邮件还是短信      邮件
 * 2.sendEmail 方法钟加入暂停3秒钟,请问先打印邮件还是短信        邮件
 * 3.添加一个普通的Hello方法,请问先打印邮件还是Hello         Hello
 * 4.有两部手机,请问先打印邮件还是短信          短信
 * 5.有两个静态同步方法,有1部手机,请问先打印邮件还是短信    邮件
 * 6.有两个静态同步方法,有2部手机,请问先打印邮件还是短信    邮件
 * 7.有1个静态同步方法,有1个普通同步方法,有1部手机,请问先打印邮件还是短信      短信
 * 8.有1个静态同步方法,有1个普通同步方法,有2部手机,请问先打印邮件还是短信      短信
 *
 * 总结:
 * 1-2
 *  一个对象里面如果有多个synchronized方法, 某一 个时刻内,只要一 .个线程去调用其中的一个synchronized方法了,
 * 其它的线程都只能等待,换句话说,某一 -个时刻内,只能有唯的一 个线程去访问这些synchronized方法
 * 锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
 * 3-4
 * 加个普通方法后发现和同步锁无关
 * 换成两个的象后,不是同把锁了,情况立刻变化,
 *
 * 5-6都换成静态同步方法后,情况又变化
 * 三种synchronized锁的内容有一些差别:
 * 对于普通同步方法,锁的是当前实例对象,通常指this, 具体的一部部手机,所有的普通同步方法用的都是同一把锁-> 实例对象本身
 * 对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板
 * 对于同步方法块,锁的是synchronized括号内的对象
 *
 * 7-8
 * 当个线程试图访时同步代码时它 首先必 须得到锁,正常退出或抛出异常时必须释放锁。
 * 所有的普通同步 方法用的都是同一把锁一实例对象 本身,就是new来的具体实例对象本身,本this
 * 也就是说如果一 个实例对象的 普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
 * 所有的静态同步方法用的也是同一把锁一类对象本身, 就是我们说过的唯一模板class
 * 具体实例对象this和唯一模板class, 这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的
 * 但是一旦一-个静态同步方法获取锁后, 其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
 * */
public class Lock8Demo {
    public static void main(String[] args) { //一切程序的入口
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(phone::,"a").start();
        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(phone2::,"b").start();
    }
}

synchronized有三种应用方式

8种锁的案例实际体现在3个地方
  • 作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;

    public synchronized void sendEmail(){
    
  • 作用于代码块,对括号里配置的对象加锁。

    synchronized (this){}
    
  • 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁;

    public static synchronized void sendSMS(){}
    

从字节码角度分析synchronized实现

javap -C **.class文件反编译
-C 对代码进行反汇编
假如你需要更多信息
  • javap -v **.class文件反编译
  • -V -verbose 输出附加信息(包括行号、本地变量表,反汇编等详细信息)+
synchronized同步代码块
javap -C***.class文件反编译
反编译
 javap -c .\LockSyncDemo.class
Compiled from "LockSyncDemo.java"
public class locks.LockSyncDemo {
  java.lang.Object object;

  public locks.LockSyncDemo();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: aload_0
       5: new           #2                  // class java/lang/Object
       8: dup
       9: invokespecial #1                  // Method java/lang/Object."":()V
      12: putfield      #3                  // Field object:Ljava/lang/Object;
      15: return

  public void m1();
    Code:
       0: aload_0
       1: getfield      #3                  // Field object:Ljava/lang/Object;
       4: dup
       5: astore_1
       6: monitorenter
       7: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      10: ldc           #5                  // String ----hello synchronized code block
      12: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      15: aload_1
      16: monitorexit
      17: goto          25
      20: astore_2
      21: aload_1
      22: monitorexit
      23: aload_2
      24: athrow
      25: return
    Exception table:
       from    to  target type
           7    17    20   any
          20    23    20   any

  public static void main(java.lang.String[]);
    Code:
       0: return
}
synchronized同步代码块
  • 实现使用的是monitorenter和monitorexit指令
一定是一个enter两个exit吗?
  • 一般情况就是1个enter对应2个exit

  • 极端

    m1方法添加一个异常

synchronized普通同步方法
javap -C***.class文件反编译
反编译
javap -c .\LockSyncDemo.class
Compiled from "LockSyncDemo.java"
public class locks.LockSyncDemo {
public locks.LockSyncDemo();
Code:
0: aload_0
1: invokespecial #1                  // Method java/lang/Object."":()V
4: return

public synchronized void m2();
Code:
0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc           #3                  // String ----hello synchronized code block
5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return

public static void main(java.lang.String[]);
Code:
0: return
}
synchronized普通同步方法

调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。如果设置了,执行线程会将先持有monitor锁,然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放monitor

synchronized静态同步方法
javap -C***.class文件反编译
反编译
javap -c .\LockSyncDemo.class
Compiled from "LockSyncDemo.java"
public class locks.LockSyncDemo {
public locks.LockSyncDemo();
Code:
0: aload_0
1: invokespecial #1                  // Method java/lang/Object."":()V
4: return

public synchronized void m2();
Code:
0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc           #3                  // String ----hello synchronized code block m2
5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return

public synchronized void m3();
Code:
0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc           #5                  // String ----hello synchronized code block m3
5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return

public static void main(java.lang.String[]);
Code:
0: return
}
synchronized静态同步方法
  • ACC_ STATIC, ACC_ SYNCHRONIZED访问标志区分该方法是否静态同步方法

反编译synchronized锁的是什么

什么是管程monitor
管程

juc笔记前篇_第12张图片通过C底层原语了解

在HotSpot虛拟机中,monitor采 用ObjectMonitor实现
上述C++源码解读
  • ObjectMonitor.java→ObjectMonitor.cpp-→objectMonitor,hpp
  • objectMonitor.hpp
  • 每个对象天生都带着–个对象监视器
  • 每一个被锁住的对象都会和Monitor关联起来

公平锁和非公平锁

从ReentrantLock卖票demo演示公平和非公平现象

class Ticket //资源类,馆拟3个售票员卖50张票
{
    private int number = 50;
    ReentrantLock lock = new ReentrantLock(true);

    public void sale() {
        lock.lock();
        try {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出第: \t" + (number--) + "\t还剩下:" + number);
            }
        } finally {
            lock.unlock();
        }

    }
}

public class SaleTicketDemo {
    public static void main(String[] args) {
        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();
    }
}

何为公平锁/非公平锁?

锁的类型 描述
公平锁 是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的Lock lock = new Reentrantl ock(true);//true表示公平锁,先来先得
非公平锁 是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某个线程一直 得不到锁)Lock lock = new ReentrantL ock(alse);//false表示非公平锁,后来的也可能先获得锁
为什么会有公平锁和非公平锁
  • 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间。
  • 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此
什么时候用公平锁,什么时候用非公平锁
  • 如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;
  • 否则那就用公平锁,大家公平使用

预埋伏AQS

juc笔记前篇_第13张图片

可重入锁(又名递归锁)

说明

可重入锁又名递归锁
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
如果是1个有synchronized修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
所以Java中ReentrantL ock和synchronized都是可重入锁,可重入锁的一一个优点是可一定程度避免死锁。

可重入锁"这四个字分开来解释:

  • 可:可以。
  • 重:再次。
  • 入:进入。
  • 锁:同步锁。

进入什么

  • 进入同步域(即同步代码块/方法或显式锁锁定的代码)

一句话

  • 一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。
  • 自己可以获取自己的内部锁

可重入锁种类

  • 隐式锁( 即synchronized关键字使用的锁)默认是可重入锁

    ​ 同步块

    ​ 同步方法

    public class ReEntryLockDemo {
        public synchronized void m1() {
            //指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
            System.out.println(Thread.currentThread().getName() + "\t-----come in");
            m2();
            System.out.println(Thread.currentThread().getName() + "\t-----end m1");
        }
    
        public synchronized void m2() {
            System.out.println(Thread.currentThread().getName() + "\t-----come in");
            m3();
            //        System.out.println(Thread.currentThread().getName() + "\t-----end m2");
        }
    
        public synchronized void m3() {
            System.out.println(Thread.currentThread().getName() + "\t-----come in");
        }
    
        public static void main(String[] args) {
            ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();
            new Thread(reEntryLockDemo::m1,"t1").start();
        }
    
        private static void reEntryM1() {
            final Object object = new Object();
            new Thread(() -> {
                synchronized (object) {
                    System.out.println(Thread.currentThread().getName() + "\t-----外层调用");
                    synchronized (object) {
                        System.out.println(Thread.currentThread().getName() + "\t-----中层调用");
                        synchronized (object) {
                            System.out.println(Thread.currentThread().getName() + "\t-----内层调用");
                        }
                    }
                }
            }, "t1").start();
        }
    }
    
  • Synchronized的重入的实现机理

    每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
    当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虛拟机会将该锁对象的持有线程设置为当前线程,I并且将其计数器加1
    在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虛拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
    当执行monitorexit时,Java虛拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
    

    ​ 为什么任何一个对象都可以成为一个锁.
    ​ objectMonitor.hpp

  • 显式锁(即Lock)也有ReentrantL ock这样的可重入锁。

    new Thread(()->{
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"\t-----come in外层调用");
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName()+"\t-----come in内层调用");
    
            }finally {
              //  lock.unlock();
            }
        }finally {
            lock.unlock();
        }
    },"t1").start();
    new Thread(()->{
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"\t-----come in外层调用");
            lock.lock();
        }finally {
            lock.unlock();
        }
    },"t2").start();
    

死锁及排查

是什么

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。

juc笔记前篇_第14张图片

请写一个死锁代码case

final Object objectA = new Object();
final Object objectB = new Object();
new Thread(() -> {
    synchronized (objectA) {
        System.out.println(Thread.currentThread().getName() + "\t 自己持有A锁,希望获得B锁");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (objectB) {
            System.out.println(Thread.currentThread().getName() + "\t 成功获得B锁");
        }
    }
},"A").start();
new Thread(() -> {
    synchronized (objectB) {
        System.out.println(Thread.currentThread().getName() + "\t 自己持有B锁,希望获得A锁");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (objectA) {
            System.out.println(Thread.currentThread().getName() + "\t 成功获得A锁");
        }
    }
},"B").start();

运行结果:

juc笔记前篇_第15张图片

如何排查死锁

纯命令
  • jps -|
  • jstack进程编号
图形化
  • jconsole

写锁(独占锁)/读锁(共享锁)

自旋锁SpinLock

无锁→独占锁→读写锁→邮戳锁

无锁→偏向锁→轻量锁→重量锁

4.LockSupport与线程中断

内容简介

LockSupport

juc笔记前篇_第16张图片

线程中断机制

juc笔记前篇_第17张图片

juc笔记前篇_第18张图片

线程中断机制

从阿里蚂蚁金服面试题讲起

如何中断一个运行中的线程? ?
如何停止一个运行中的线程? ?

什么是中断机制?

​ 首先
​ 一个线程不应该由具他线程强制中断或停止,而是应该由线程自己自行停止,自己来决定自己的命运。所以,Thread.stop, Thread. suspend, Thread.resume都已经被废弃了。
​ 其次
​ 在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。因此,Java提供了一种用于停止线程的协商机制一一中断,也即中断标识协商机制。中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true;
接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程请求这条线程中断,此时究竞该做什么需要你自己写代码实现。
​ 每个线程对象中都有一个中断标识位,用于表示线程是否被中断;该标识位为true表示中断,为false表 示未中断;
通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。.

中断的相关API方法之三大方法说明

juc笔记前篇_第19张图片

大厂面试题中断机制考点

如何停止中断运行中的线程?
通过一个volatile变 量实现口
static volatile boolean isStop = false;

public static void main(String[] args) {
    new Thread(()->{
        while(true){
            if(isStop){
                System.out.println(Thread.currentThread().getName()+"\t isStop被修改为true,程序停止");
                break;
            }
            System.out.println("----hello volatile");
        }
    },"t1").start();
    try {
        TimeUnit.MILLISECONDS.sleep(20);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    new Thread(()->{
        isStop=true;
    },"t2").start();

}
通过AtomicBoolean
new Thread(()->{
    while(true){
        if(atomicBoolean.get()){
            System.out.println(Thread.currentThread().getName()+"\t isStop被修改为true,程序停止");
            break;
        }
        System.out.println("----hello volatile");
    }
},"t1").start();
try {
    TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
    e.printStackTrace();
}
new Thread(()->{
    atomicBoolean.set(true);
},"t2").start();
通过Thread类自带的中断api实例方法实现

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

 Thread t1 = new Thread(() -> {
     while(true){
         if(Thread.currentThread().isInterrupted()){
             System.out.println(Thread.currentThread().getName()+"\t isStop被修改为true,程序停止");
             break;
         }
         System.out.println("----hello interrupt api");
     }
 }, "t1");
 t1.start();
 try {
     TimeUnit.MILLISECONDS.sleep(20);
 } catch (InterruptedException e) {
     e.printStackTrace();
 }
t1.isInterrupted();
api
code
实例方法interrupt(),没有返回值

源码分析

juc笔记前篇_第20张图片

实例方法isInterrupted,返回布尔值

源码分析

说明

具体来说,当对一个线程,调用interrupt()时:
①如果线程处于正常活动状态,那么会将该线程的中断标志设置为true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。
所以,interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。
②如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。

当前线程的中断标识为true,是不是线程就立刻停止?
public class InterruptDemo3 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(Thread.currentThread().getName() + "\t" +
                            "中断标志位" + Thread.currentThread().isInterrupted() + "程序停止");
                    break;
                }
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();//为什么要在异常处,再调用一遍interrupt方法
                    e.printStackTrace();
                }
                System.out.println("-----hello InterruptDemo3");
            }
        }, "t1");
        t1.start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(t1::interrupt,"t2").start();
    }

}
/**
 * 1 中断标志位,默认false
 * 2 t2 ----> 发出了终端协商,t2调用t1.interrupt(),中断标志位
 * 3中断标志位true,正常情况,程序停止,^_ ^
 * 4中断标志位true,异常情况,InterruptedException, 将会把中断状态将被清除,并且将收到InterruptedException。中断标志位false
 * 5在catch块中,需要再次给中断标志位设置为true, 2次调用停止程序才ok
 * */
小总结:

中断只是一种协商机制,修改中断标识位仅此而已,不是立刻stop打断

静态方法Thread.interrupted(),谈谈你的理解

静态方法interrupted将会 清除中断状态(传入的参数ClearInterrupted为true),
实例方法isInterrupted则不会(传入的参数Clearlnterrupted为false)

总结

线程中断相关的方法:
public void interrupt() , interrupt()方法是-一个实例方法
它通知目标线程中断,也仅是设置目标线程的中断标志位为true。
public boolean isInterrupted(), isInterrupted()方法也是一个实例方法
它判断当前线程是否被中断(通过检查中断标志位)并获取中断标志,
public static boolean interrupted(), Thread类的静态方法interrupted()
返回当前线程的中断状态真实值(boolean类型)后会将当前线程的中断状态设为false,此方法调用之后会清除当前线程的中断标志位的
状态(将中断标志置为false了),返回当前值并清零置false

LockSupport是什么

用于创建锁和其他同步类的基本线程阻塞原语,不是构造方法

线程等待唤醒机制

3种让线程等待和唤醒的方法

  1. 使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程
  2. 使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
  3. LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

Object类中的wait和notify方法实现线程等待和唤醒

/**
 *
 * 要求:t1线程等待3秒钟,3秒钟后t2线程唤醒t1线程继续工作
 *
 * 1 正常程序演示
 *
 * 以下异常情况:
 * 2 wait方法和notify方法,两个都去掉同步代码块后看运行效果
 *   2.1 异常情况
 *   Exception in thread "t1" java.lang.IllegalMonitorStateException at java.lang.Object.wait(Native Method)
 *   Exception in thread "t2" java.lang.IllegalMonitorStateException at java.lang.Object.notify(Native Method)
 *   2.2 结论
 *   Object类中的wait、notify、notifyAll用于线程等待和唤醒的方法,都必须在synchronized内部执行(必须用到关键字synchronized)。
 *
 * 3 将notify放在wait方法前面
 *   3.1 程序一直无法结束
 *   3.2 结论
 *   先wait后notify、notifyall方法,等待中的线程才会被唤醒,否则无法唤醒
 */
public class LockSupportDemo
{

    public static void main(String[] args)//main方法,主线程一切程序入口
    {
        Object objectLock = new Object(); //同一把锁,类似资源类

        new Thread(() -> {
            synchronized (objectLock) {
                try {
                    objectLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒了");
        },"t1").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            synchronized (objectLock) {
                objectLock.notify();
            }

            //objectLock.notify();

            /*synchronized (objectLock) {
                try {
                    objectLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }*/
        },"t2").start();
    }
}
正常
public class LockSupportDemo
{
    public static void main(String[] args)//main方法,主线程一切程序入口
    {
        Object objectLock = new Object(); //同一把锁,类似资源类

        new Thread(() -> {
            synchronized (objectLock) {
                try {
                    objectLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒了");
        },"t1").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            synchronized (objectLock) {
                objectLock.notify();
            }
        },"t2").start();
    }
}
异常1
/**
 * 要求:t1线程等待3秒钟,3秒钟后t2线程唤醒t1线程继续工作
 * 以下异常情况:
 * 2 wait方法和notify方法,两个都去掉同步代码块后看运行效果
 *   2.1 异常情况
 *   Exception in thread "t1" java.lang.IllegalMonitorStateException at java.lang.Object.wait(Native Method)
 *   Exception in thread "t2" java.lang.IllegalMonitorStateException at java.lang.Object.notify(Native Method)
 *   2.2 结论
 *   Object类中的wait、notify、notifyAll用于线程等待和唤醒的方法,都必须在synchronized内部执行(必须用到关键字synchronized)。
 */
public class LockSupportDemo
{

    public static void main(String[] args)//main方法,主线程一切程序入口
    {
        Object objectLock = new Object(); //同一把锁,类似资源类

        new Thread(() -> {
                try {
                    objectLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒了");
        },"t1").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            objectLock.notify();
        },"t2").start();
    }
}

wait方法和notify方法,两个都去掉同步代码块

异常2
/**
 *
 * 要求:t1线程等待3秒钟,3秒钟后t2线程唤醒t1线程继续工作
 *
 * 3 将notify放在wait方法前先执行,t1先notify了,3秒钟后t2线程再执行wait方法
 *   3.1 程序一直无法结束
 *   3.2 结论
 *   先wait后notify、notifyall方法,等待中的线程才会被唤醒,否则无法唤醒
 */
public class LockSupportDemo
{

    public static void main(String[] args)//main方法,主线程一切程序入口
    {
        Object objectLock = new Object(); //同一把锁,类似资源类

        new Thread(() -> {
            synchronized (objectLock) {
                objectLock.notify();
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"通知了");
        },"t1").start();

        //t1先notify了,3秒钟后t2线程再执行wait方法
        try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            synchronized (objectLock) {
                try {
                    objectLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒了");
        },"t2").start();
    }
}

将notify放在wait方法前面

程序无法执行,无法唤醒

总结

wait和notify方法必须要在同步块或者方法里面,且成对出现使用

先wait后notify才OK

Condition接口中的await后signal方法实现线程的等待和唤醒

正常

public class LockSupportDemo2
{
    public static void main(String[] args)
    {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();

        new Thread(() -> {
            lock.lock();
            try
            {
                System.out.println(Thread.currentThread().getName()+"\t"+"start");
                condition.await();
                System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        },"t1").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            lock.lock();
            try
            {
                condition.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"通知了");
        },"t2").start();

    }
}

异常1

/**
 * 异常:
 * condition.await();和condition.signal();都触发了IllegalMonitorStateException异常
 *
 * 原因:调用condition中线程等待和唤醒的方法的前提是,要在lock和unlock方法中,要有锁才能调用
 */
public class LockSupportDemo2
{
    public static void main(String[] args)
    {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();

        new Thread(() -> {
            try
            {
                System.out.println(Thread.currentThread().getName()+"\t"+"start");
                condition.await();
                System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t1").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            try
            {
                condition.signal();
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"通知了");
        },"t2").start();

    }
}

image-20220802171410773

去掉lock/unlock

condition.await();和 condition.signal();都触发了 IllegalMonitorStateException异常。

结论: lock、unlock对里面才能正确调用调用condition中线程等待和唤醒的方法

异常2

/**
 * 异常:
 * 程序无法运行
 *
 * 原因:先await()后signal才OK,否则线程无法被唤醒
 */
public class LockSupportDemo2
{
    public static void main(String[] args)
    {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();

        new Thread(() -> {
            lock.lock();
            try
            {
                condition.signal();
                System.out.println(Thread.currentThread().getName()+"\t"+"signal");
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        },"t1").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            lock.lock();
            try
            {
                System.out.println(Thread.currentThread().getName()+"\t"+"等待被唤醒");
                condition.await();
                System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒");
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        },"t2").start();

    }
}

先signal后await

总结

Condtion中的线程等待和唤醒方法之前,需要先获取锁

一定要先await后signal,不要反了

LockSupport类中的park等待和unpark唤醒

通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作

image-20220802194530226

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

LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能, 每个线程都有一个许可(permit), permit只有两个值1和零,默认是零。 可以把许可看成是一种(0,1)信号量(Semaphore),但与 Semaphore 不同的是,许可的累加上限是1。

主要方法

juc笔记前篇_第21张图片

阻塞

park() /park(Object blocker)

image-20220802194717836

阻塞当前线程/阻塞传入的具体线程

唤醒

unpark(Thread thread)

juc笔记前篇_第22张图片

唤醒处于阻塞状态的指定线程

代码

正常+无锁块要求

public class LockSupportDemo3
{
    public static void main(String[] args)
    {
        //正常使用+不需要锁块
Thread t1 = new Thread(() -> {
    System.out.println(Thread.currentThread().getName()+" "+"1111111111111");
    LockSupport.park();
    System.out.println(Thread.currentThread().getName()+" "+"2222222222222------end被唤醒");
},"t1");
t1.start();

//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }

LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName()+"   -----LockSupport.unparrk() invoked over");

    }
}

之前错误的先唤醒后等待,LockSupport照样支持

public class T1
{
    public static void main(String[] args)
    {
        Thread t1 = new Thread(() -> {
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t"+System.currentTimeMillis());
            LockSupport.park();
            System.out.println(Thread.currentThread().getName()+"\t"+System.currentTimeMillis()+"---被叫醒");
        },"t1");
        t1.start();

        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

        LockSupport.unpark(t1);
        System.out.println(Thread.currentThread().getName()+"\t"+System.currentTimeMillis()+"---unpark over");
    }
}
为什么可以突破wait/notify的原有调用顺序?

因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。先发放了凭证后续可以畅通无阻。

为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?

因为凭证的数量最多为1,连续调用两次unpark和调用-次unpark效果-样,只会增加一个凭证:而调用两次park却需要消费两个凭证,证不够,不能放行。

5.Java内存模型之JMM

先从大厂面试题开始

你知道什么是Java内存模型JMM吗?

JMM.与volatile它们两个之间的关系? ( 下- - 章详细讲解)

JMM有哪些特性or它的三大特性是什么?

为什么要有JMM,它为什么出现?作用和功能是什么?

happens-before先行发生原则你有了解过吗?

计算机硬件存储体系

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

因为有这么多级的缓存(cpu和物理主内存的速度不一致的),CPU的运行并不是直接操作内存而是先把内存里边的数据读到缓存,而内存的读和写操作的时候就会造成不一致的问题

juc笔记前篇_第23张图片

Java虚拟机规范中试图定义一种Java内存模型(java Memory Model,简称JMM) 来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。推导出我们需要知道JMM

Java内存模型Java Memory Model

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

原则:

JMM的关键技术点都是围绕多线程的原子性、可见性和有序性展开的

能干嘛?

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

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

JMM规范下,三大特性

1.可见性

juc笔记前篇_第24张图片

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

juc笔记前篇_第25张图片

线程脏读:如果没有可见性保证

juc笔记前篇_第26张图片

2.原子性

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

3.有序性

对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行。 但为了提供性能,编译器和处理器通常会对指令序列进行重新排序。 指令重排可以保证串行语义一致,但没有义务保证多线程间的语义也一致,即可能产生"脏读",简单说, 两行以上不相干的代码在执行的时候有可能先执行的不是第一条,不见得是从上到下顺序执行,执行顺序会被优化。

juc笔记前篇_第27张图片

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。 处理器在进行重排序时必须要考虑指令之间的数据依赖性 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测

juc笔记前篇_第28张图片

JMM规范下,多线程对变量的读写过程

读取过程

juc笔记前篇_第29张图片

小总结

  • 我们定义的所有共享变量都储存在物理主内存中
  • 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
  • 线程对共享变量所有的操作都必须先在线程自己的工作内存中进行后写回主内存,不能直接从主内存中读写(不能越级)
  • 不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行(同级不能相互访问)

JMM规范下,多线程先行发生原则之happens-before

在JMM中,如果一个操作执行的结果需要对另一个操作可见性或者 代码重排序,那么这两个操作之间必须存在happens-before(先行发生)原则。逻辑上的先后关系。

先行发生原则说明

如果Java内存模型中所有的有序性都仅靠volatile和synchronized来完成,那么有很多操作都将会变得非常啰嗦, 但是我们在编写Java并发代码的时候并没有察觉到这一点。

我们没有时时、处处、次次,添加volatile和synchronized来完成程序,这是因为Java语言中JMM原则下有一个“先行发生”(Happens-Before)的原则限制和规矩

这个原则非常重要: 它是判断数据是否存在竞争,线程是否安全的非常有用的手段。依赖这个原则,我们可以通过几条简单规则一揽子解决并发环境下两个操 作之间是否可能存在冲突的所有问题,而不需要陷入Java内存模型苦涩难懂的底层编译原理之中。

x、y案例说明

juc笔记前篇_第30张图片

happens-before总原则

  • 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  • 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
    • 1+2+3 = 3+2+1
    • 值日:周一张三周二李四,假如有事情调换班可以的

happens-before之8条

1、次序规则

一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作;

前一个操作的结果可以被后续的操作获取。讲白点就是前面一个操作把变量X赋值为1,那后面一个操作肯定能知道X已经变成了1

2.锁定规则:

一个unLock操作先行发生于后面((这里的“后面”是指时间上的先后))对同一个锁的lock操作;

public class HappenBeforeDemo
{
    static Object objectLock = new Object();

    public static void main(String[] args) throws InterruptedException
    {
        //对于同一把锁objectLock,threadA一定先unlock同一把锁后B才能获得该锁,   A 先行发生于B
        synchronized (objectLock)
        {

        }
    }
}

3.volatile变量规则:

对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的“后面”同样是指时间上的先后。

4.传递规则:

如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;

5.线程启动规则(Thread Start Rule):

Thread对象的start()方法先行发生于此线程的每一个动作

6.线程中断规则(Thread Interruption Rule):

对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;

可以通过Thread.interrupted()检测到是否发生中断

7.线程终止规则(Thread Termination Rule):

线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread::join()方法是否结束、 Thread::isAlive()的返回值等手段检测线程是否已经终止执行。

8.对象终结规则(Finalizer Rule):

一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始

对象没有完成初始化之前,是不能调用finalized()方法的

happens-before-小总结

在Java语言里面,Happens-Before 的语义本质上是- -种可见性
AHappens-BeforeB意味着A发生过的事情对B来说是可见的,无论A事件和B事件是否发生在同一个线程里.
JMM的设计分为两部分:
一部分是面向我们程序员提供的,也就是happens-before规则,它通俗易懂的向我们程序员阐述了一一个强内存模型,我们只要理解happens-before规则,就可以编写并发安全的程序了。
另一部分是针对JVM实现的,为了尽可能少的对编译器和处理器做约束从而提高性能,JMM在不影响程序执行结果的前提下对其不做要求,即允许优化重排序。我们只需要关注前者就好了,也就是理解happens-before规则即可,其它繁杂的内容有JMM规范结合操作系统给我们搞定,我们只写好代码即可。

案例

juc笔记前篇_第31张图片

8.volatile与Java内存模型

1、被volatile修改的变量有2大特点

  • 可见性
  • 有序性

2、volatile的内存语义

  • 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
  • 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
  • 所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。

3、内存屏障(重点)

1、生活case

  • 没有管控,顺序难保
  • 设定规则,禁止乱序

2、内存屏障是什么

内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性,但volatile无法保证原子性。

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

在这里插入图片描述

因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。 一句话:对一个 volatile 域的写, happens-before 于任意后续对这个 volatile 域的读,也叫写后读。

粗分:

读屏障:

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

写屏障:

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

细分:

c++源码分析:

Unsafe.class

juc笔记前篇_第32张图片

orderAccess.hpp
juc笔记前篇_第33张图片

Unsafe.cpp

juc笔记前篇_第34张图片

orderAccess_ linux_ _x86.inline.hpp:

juc笔记前篇_第35张图片

四大屏障分别是什么意思口:

juc笔记前篇_第36张图片

3、什么叫保证有序性?

通过内存屏障禁重排
上述说明

1重排序有可能影响程序的执行和实现,因此,我们有时候希望告诉JVM你别“自作聪明”给我重排序,我这里不需要排序,听主人的。
2对于编译器的重排序,JMM会根据重排序的规则,禁止特定型的编译器重排序。
3对于处理器的重排序,Java编译器在生成指令序列的适当位置,插入内存屏障指令,来禁止特定类型的处理器排序。

4、happens-before 7之volatile变量规则

juc笔记前篇_第37张图片

5、JMM就将内存屏障插入策略分为4种规则

    • 在每个 volatile 写操作的前⾯插⼊⼀个 StoreStore 屏障
    • 在每个 volatile 写操作的后⾯插⼊⼀个 StoreLoad 屏障
    • 在每个 volatile 读操作的后⾯插⼊⼀个 LoadLoad 屏障
    • 在每个 volatile 读操作的后⾯插⼊⼀个 LoadStore 屏障

4.volatile特性

1.保证可见性

保证不同线程对这个变量进行操作时的可见性,即变量一旦改变所有线程立即可见

public class VolatileSeeDemo
{
    static          boolean flag = true;       //不加volatile,没有可见性
    //static volatile boolean flag = true;       //加了volatile,保证可见性

    public static void main(String[] args)
    {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"\t come in");
            while (flag)
            {

            }
            System.out.println(Thread.currentThread().getName()+"\t flag被修改为false,退出.....");
        },"t1").start();

        //暂停2秒钟后让main线程修改flag值
        try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }

        flag = false;

        System.out.println("main线程修改完成");
    }
}
  • 不加volatile,没有可见性,程序无法停止
  • 加了volatile,保证可见性,程序可以停止
线程t1中为何看不到被主线程main修改为false的flag的值?
 
问题可能:
1. 主线程修改了flag之后没有将其刷新到主内存,所以t1线程看不到。
2. 主线程将flag刷新到了主内存,但是t1一直读取的是自己工作内存中flag的值,没有去主内存中更新获取flag最新的值。
 
我们的诉求:
1.线程中修改了工作内存中的副本之后,立即将其刷新到主内存;
2.工作内存中每次读取共享变量时,都去主内存中重新读取,然后拷贝到工作内存。
 
解决:
使用volatile修饰共享变量,就可以达到上面的效果,被volatile修改的变量有以下特点:
1. 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存
2. 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存

2.volatile变量的读写过程

Java内存模型中定义的8种工作内存与主内存之间的原子操作 read(读取)→load(加载)→use(使用)→assign(赋值)→store(存储)→write(写入)→lock(锁定)→unlock(解锁)

juc笔记前篇_第38张图片

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

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

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

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

store: 作用于工作内存,将赋值完毕的工作变量的值写回给主内存

write: 作用于主内存,将store传输过来的变量值赋值给主内存中的变量 由于上述只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令:

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

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

3、没有原子性

1、volatile变量的复合操作(如i++)不具有原子性
class MyNumber
{
    volatile int number = 0;

    public void addPlusPlus()
    {
        number++;
    }
}

public class VolatileNoAtomicDemo
{
    public static void main(String[] args) throws InterruptedException
    {
        MyNumber myNumber = new MyNumber();

        for (int i = 1; i <=10; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    myNumber.addPlusPlus();
                }
            },String.valueOf(i)).start();
        }
        
        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println(Thread.currentThread().getName() + "\t" + myNumber.number);
    }
}

从i++的字节码角度说明
juc笔记前篇_第39张图片

原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
public void add()
{
        i++; //不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分3步完成
 }
如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,
并执行相同值的加1操作,这也就造成了线程安全失败,因此对于add方法必须使用synchronized修饰,以便保证线程安全.

juc笔记前篇_第40张图片

多线程环境下,"数据计算""数据赋值"操作可能多次出现,即操作非原子。若数据在加载之后,若主内存count变量发生修改之后,由于线程工作内存中的值在此前已经加载,从而不会对变更操作做出相应变化,即私有内存和公共内存中变量不同步,进而导致数据不一致
对于volatile变量,JVM只是保证从主内存加载到线程工作内存的值是最新的,也就是数据加载时是最新的。
由此可见volatile解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改共享变量的场景必须使用加锁同步
2.读取赋值一个普通变量的情况

当线程1对主内存对象发起read操作到write操作第一套流程的时间里,线程2随时都有可能对这个主内存对象发起第二套操作

juc笔记前篇_第41张图片

3、既然一修改就是可见,为什么还不能保证原子性?

volatile主要是对其中部分指令做了处理

use(使用)一个变量的时候必需load(载入),要载入的时候必需从主内存read(读取)这样就解决了读的可见性。 
写操作是把assign和store做了关联(assign(赋值)后必需store(存储))store(存储)write(写入)。
也就是做到了给一个变量赋值的时候一串关联指令直接把变量值写到主内存。
就这样通过用的时候直接从主内存取,在赋值到直接写回主内存做到了内存可见性。注意蓝色框框的间隙。。。。。。o(╥﹏╥)o

4、读取赋值一个volatile变量的情况

juc笔记前篇_第42张图片

 read-load-use 和 assign-store-write 成为了两个不可分割的原子操作,但是在use和assign之间依然有极小的一段真空期,有可能变量会被其他线程读取,导致写丢失一次...o(╥﹏╥)o
但是无论在哪一个时间点主内存的变量和任一工作内存的变量的值都是相等的。这个特性就导致了volatile变量不适合参与到依赖当前值的运算,如i = i + 1; i++;之类的那么依靠可见性的特点volatile可以用在哪些地方呢? 通常volatile用做保存某个状态的boolean值or int值。
《深入理解Java虚拟机》提到:
5、面试回答

JVM的字节码,i++分成三步,间隙期不同步非原子操作(i++)

juc笔记前篇_第43张图片

4、指令禁重排

重排序
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序
不存在数据依赖关系,可以重排序;
存在数据依赖关系,禁止重排序
但重排后的指令绝对不能改变原有的串行语义!这点在并发设计中必须要重点考虑!
    
重排序的分类和执行流程

image-20220807093140628

重排序
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序
不存在数据依赖关系,可以重排序;
存在数据依赖关系,禁止重排序
但重排后的指令绝对不能改变原有的串行语义!这点在并发设计中必须要重点考虑!
    
重排序的分类和执行流程
重排前 重排后
int a = 1; //1
int b = 20; //2
int c = a + b; //3
int b = 20; //1
int a = 1; //2
int c = a + b; //3
结论:编译器调整了语句的顺序,但是不影响程序的最终结果。 重排序OK
存在数据依赖关系,禁止重排序===> 重排序发生,会导致程序运行结果不同。
编译器和处理器在重排序时,会遵守数据依赖性,不会改变存在依赖关系的两个操作的执行,但不同处理器和不同线程之间的数据性不会被编译器和处理器考虑,其只会作用于单处理器和单线程环境,下面三种情况,只要重排序两个操作的执行顺序,程序的执行结果就会被改变。

juc笔记前篇_第44张图片

1、volatile的底层实现是通过内存屏障

juc笔记前篇_第45张图片

当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序。这个操作保证了volatile读之后的操作不会被重排到volatile读之前。

当第二个操作为volatile写时,不论第一个操作是什么,都不能重排序。这个操作保证了volatile写之前的操作不会被重排到volatile写之后。

当第一个操作为volatile写时,第二个操作为volatile读时,不能重排。

四大屏障的插入情况

  • 在每一个volatile写操作前面插入一个StoreStore屏障
    • StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存中。
  • 在每一个volatile写操作后面插入一个StoreLoad屏障
    • StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序
  • 在每一个volatile读操作后面插入一个LoadLoad屏障
    • LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。
  • 在每一个volatile读操作后面插入一个LoadStore屏障
    • LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。
//模拟一个单线程,什么顺序读?什么顺序写?
public class VolatileTest {
    int i = 0;
    volatile boolean flag = false;
    public void write(){
        i = 2;
        flag = true;
    }
    public void read(){
        if(flag){
            System.out.println("---i = " + i);
        }
    }
}

juc笔记前篇_第46张图片

5.如何正确使用volatile

1、单一赋值可以,but含复合运算赋值不可以(i++之类)
volatile int a = 10
volatile boolean flag = false
2.状态标志,判断业务是否结束
/**
 *
 * 使用:作为一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或任务结束
 * 理由:状态标志并不依赖于程序内任何其他状态,且通常只有一种状态转换
 * 例子:判断业务是否结束
 */
public class UseVolatileDemo
{
    private volatile static boolean flag = true;

    public static void main(String[] args)
    {
        new Thread(() -> {
            while(flag) {
                //do something......
            }
        },"t1").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(2L); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            flag = false;
        },"t2").start();
    }
}
3、开销较低的读-写锁策略
public class UseVolatileDemo
{
    /**
     * 使用:当读远多于写,结合使用内部锁和 volatile 变量来减少同步的开销
     * 理由:利用volatile保证读取操作的可见性;利用synchronized保证复合操作的原子性
     */
    public class Counter
    {
        private volatile int value;

        public int getValue()
        {
            return value;   //利用volatile保证读取操作的可见性
              }
        public synchronized int increment()
        {
            return value++; //利用synchronized保证复合操作的原子性
               }
    }
}
4.DCL双端锁的发布

问题代码

public class SafeDoubleCheckSingleton
{
    private static SafeDoubleCheckSingleton singleton;
    //私有化构造方法
    private SafeDoubleCheckSingleton(){
    }
    //双重锁设计
    public static SafeDoubleCheckSingleton getInstance(){
        if (singleton == null){
            //1.多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象
            synchronized (SafeDoubleCheckSingleton.class){
                if (singleton == null){
                    //隐患:多线程环境下,由于重排序,该对象可能还未完成初始化就被其他线程读取
                    singleton = new SafeDoubleCheckSingleton();
                }
            }
        }
        //2.对象创建完毕,执行getInstance()将不需要获取锁,直接返回创建对象
        return singleton;
    }
}

单线程看问题代码

单线程环境下(或者说正常情况下),在"问题代码处",会执行如下操作,保证能获取到已完成初始化的实例

juc笔记前篇_第47张图片

由于存在指令重排序…

多线程看问题代码

隐患:多线程环境下,在"问题代码处",会执行如下操作,由于重排序导致2,3乱序,后果就是其他线程得到的是null而不是完成初始化的对象

juc笔记前篇_第48张图片

解决01

加volatile修饰

面试题,反周志明老师的案例,你还有不加volatile的方法吗

解决02 – 采用静态内部类的方式实现

//现在比较好的做法就是采用静态内部内的方式实现
 
public class SingletonDemo
{
    private SingletonDemo() { }

    private static class SingletonDemoHandler
    {
        private static SingletonDemo instance = new SingletonDemo();
    }

    public static SingletonDemo getInstance()
    {
        return SingletonDemoHandler.instance;
    }
}

6.小总结

volatile可见性

image-20220807110449078

volatile没有原子性
volatile禁重排
写指令

juc笔记前篇_第49张图片

读指令

juc笔记前篇_第50张图片

凭什么我们java写了一个volatile关键字
字节码层面

juc笔记前篇_第51张图片

系统底层加入内存屏障?两者关系怎么勾搭上的?
内存屏障是什么

内存屏障:是一种屏障指令,它使得CPU或编译器对屏障指令的前和后所发出的内存操作执行一个排序的约束 。也叫内存栅栏 或栅栏指令

内存屏障能干嘛

阻止屏障两边的指令重排序
写数据时加入屏障,强制将线程私有工作内存的数据刷回主物理内存
读数据时加入屏障,线程私有工作内存的数据失效,重新到主物理内存中获取最新数据

内存屏障四大指令

在每一个volatile写操作前面插入一个StoreStore屏障
在每一个volatile写操作后面插入一个StoreLoad屏障
在每一个volatile读操作后面插入一个LoadLoad屏障
在每一个volatile读操作后面插入一个LoadStore屏障

3句话总结
  • volatile写之前的操作,都禁止重排序到volatile之后
  • volatile读之后的操作,都禁止重排序到volatile之前
  • volatile写之后volatile读,禁止重排序的

7.CAS

原子类

java.util.concurrent. atomic

没有CAS之前

多线程环境不使用原子类保证线程安全i++ (基本数据类型)

juc笔记前篇_第52张图片

使用CAS之后

多线程环境使用原子类保证线程安全i++ (基本数据类型)

juc笔记前篇_第53张图片

类似我们的乐观锁

是什么

说明

compareandswap的缩写,中文翻译成比较并交换,实现并发算法时常用到的一种技术。
它包含三个操作数--内存位置、预期原值及更新值。
执行CAS操作的时候,将内存位置的值与预期原值比较:
如果相匹配,那么处理器会自动将该位置值更新为新值,
如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功。

原理

CAS有3个操作数,位置内存值V,旧的预期值A,要修改的更新值B。
当且仅当旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做或重来
当它重来重试的这种行为成为—自旋! !

juc笔记前篇_第54张图片

CASDemo

AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5, 2022)+"\t"+atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 2022)+"\t"+atomicInteger.get());

juc笔记前篇_第55张图片

硬件级别保证

CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。
它是非阻塞的且自身具有原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。
CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的
CAS方法( 如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。
执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会
对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,
比起用synchronized重量级锁,这 里的排他时间要短很多,所以在 多线程情况下性能会比较好。

源码分析compareAndSet(int expect,int update)

juc笔记前篇_第56张图片

juc笔记前篇_第57张图片

CAS底层原理?如果知道,谈谈你对UnSafe的理解

1.UnSafe

  1. Unsafe 是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。 注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务juc笔记前篇_第58张图片

  2. 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。juc笔记前篇_第59张图片

  3. 变量value用volatile修饰,保证了多线程之间的内存可见性。

2、我们知道i++线程不安全的,那atomicInteger.getAndIncrement()

CAS的全称为Compare-And-Swap,它是一条CPU并发原语。 它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。 AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。

juc笔记前篇_第60张图片

CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

juc笔记前篇_第61张图片

假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上):

  1. AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
  2. 线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
  3. 线程B也通过getIntVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
  4. 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值已经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
  5. 线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。

3.底层汇编

native修饰的方法代表是底层方法

Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp中

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
// 先想办法拿到变量value在内存中的地址,根据偏移量valueOffset,计算 value 的地址
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
// 调用 Atomic 中的函数 cmpxchg来进行比较交换,其中参数x是即将更新的值,参数e是原内存的值
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

(Atomic::cmpxchg(x, addr, e)) == e;

cmpxchg

// 调用 Atomic 中的函数 cmpxchg来进行比较交换,其中参数x是即将更新的值,参数e是原内存的值
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;

unsigned Atomic::cmpxchg(unsigned int exchange_value,volatile unsigned int* dest, unsigned int compare_value) {
    assert(sizeof(unsigned int) == sizeof(jint), "more work to do");
  /*
   * 根据操作系统类型调用不同平台下的重载函数,这个在预编译期间编译器会决定调用哪个平台下的重载函数*/
    return (unsigned int)Atomic::cmpxchg((jint)exchange_value, (volatile jint*)dest, (jint)compare_value);
}

在不同的操作系统下会调用不同的cmpxchg重载函数,本次用的是win10系统

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
  //判断是否是多核CPU
  int mp = os::is_MP();
  __asm {
    //三个move指令表示的是将后面的值移动到前面的寄存器上
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    //CPU原语级别,CPU触发
    LOCK_IF_MP(mp)
    //比较并交换指令
    //cmpxchg: 即“比较并交换”指令
    //dword: 全称是 double word 表示两个字,一共四个字节
    //ptr: 全称是 pointer,与前面的 dword 连起来使用,表明访问的内存单元是一个双字单元 
    //将 eax 寄存器中的值(compare_value)与 [edx] 双字内存单元中的值进行对比,
    //如果相同,则将 ecx 寄存器中的值(exchange_value)存入 [edx] 内存单元中
    cmpxchg dword ptr [edx], ecx
  }
}

到这里应该理解了CAS真正实现的机制了,它最终是由操作系统的汇编指令完成的

4.总结

你只需要记住:CAS是靠硬件实现的从而在硬件层面提升效率,最底层还是交给硬件来保证原子性和可见性
实现方式是基于硬件平台的汇编指令,在intel的CPU中(X86机器上),使用的是汇编指令cmpxchg指令。 
 
核心思想就是:比较要更新变量的值V和预期值E(compare),相等才会将V的值设为新值N(swap)如果不相等自旋再来。

原子引用

AtomicInteger原子整型,可否有其它原子类型?

  • AtomicBook
  • AtomicOrder
  • 。。。
@Getter
@ToString
@AllArgsConstructor
class User
{
    String userName;
    int    age;
}


public class AtomicReferenceDemo
{
    public static void main(String[] args)
    {
        User z3 = new User("z3",24);
        User li4 = new User("li4",26);

        AtomicReference<User> atomicReferenceUser = new AtomicReference<>();

        atomicReferenceUser.set(z3);
        System.out.println(atomicReferenceUser.compareAndSet(z3,li4)+"\t"+atomicReferenceUser.get().toString());
        System.out.println(atomicReferenceUser.compareAndSet(z3,li4)+"\t"+atomicReferenceUser.get().toString());
    }
}

CAS与自旋锁,借鉴CAS思想

自旋锁(spinlock)

是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁, 当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

CAS缺点

1.、循环时间长开销很大

我们可以看到getAndAddInt方法执行时,有个do while

juc笔记前篇_第62张图片

如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

2.引出来ABA问题???

CAS会导致“ABA问题”。
 
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
 
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,
然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
 
尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
public class ABADemo
{
    static AtomicInteger atomicInteger = new AtomicInteger(100);
    static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);

    public static void main(String[] args)
    {
        new Thread(() -> {
            atomicInteger.compareAndSet(100,101);
            atomicInteger.compareAndSet(101,100);
        },"t1").start();

        new Thread(() -> {
            //暂停一会儿线程
            try { Thread.sleep( 500 ); } catch (InterruptedException e) { e.printStackTrace(); };            System.out.println(atomicInteger.compareAndSet(100, 2019)+"\t"+atomicInteger.get());
        },"t2").start();

        //暂停一会儿线程,main彻底等待上面的ABA出现演示完成。
        try { Thread.sleep( 2000 ); } catch (InterruptedException e) { e.printStackTrace(); }

        System.out.println("============以下是ABA问题的解决=============================");

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t 首次版本号:"+stamp);//1
            //暂停一会儿线程,
            try { Thread.sleep( 1000 ); } catch (InterruptedException e) { e.printStackTrace(); }
            atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t 2次版本号:"+atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t 3次版本号:"+atomicStampedReference.getStamp());
        },"t3").start();

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t 首次版本号:"+stamp);//1
            //暂停一会儿线程,获得初始值100和初始版本号1,故意暂停3秒钟让t3线程完成一次ABA操作产生问题
            try { Thread.sleep( 3000 ); } catch (InterruptedException e) { e.printStackTrace(); }
            boolean result = atomicStampedReference.compareAndSet(100,2019,stamp,stamp+1);
            System.out.println(Thread.currentThread().getName()+"\t"+result+"\t"+atomicStampedReference.getReference());
        },"t4").start();
    }
}

8.原子操作类

atomic

  1. AtomicBoolean
  2. AtomicInteger
  3. AtomicIntegerArray
  4. AtomicIntegerFieldUpdater
  5. AtomicLong
  6. AtomicLongArray
  7. AtomicLongFieldUpdater
  8. AtomicMarkableReference
  9. AtomicReference
  10. AtomicReferenceArray
  11. AtomicReferenceFieldUpdater
  12. AtomicStampedReference
  13. DoubleAccumulator
  14. DoubleAdder
  15. LongAccumulator
  16. LongAdder

再分类

基本类型原子类

AtomicBoolean
AtomicInteger
AtomicLong
常用API简介
public final int get() /获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() 1/获取当前的值,并自减
public final int getAndAdd(int delta) 1/获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
Case
public class AtomicIntegerDemo {
    public static final int SIZE = 50;

    public static void main(String[] args) throws InterruptedException
    {
        MyNumber myNumber = new MyNumber();
        CountDownLatch countDownLatch = new CountDownLatch(SIZE);

        for (int i = 1; i <=SIZE; i++) {
            new Thread(() -> {
                try {
                    for (int j = 1; j <=1000; j++) {
                        myNumber.addPlusPlus();
                    }
                } finally {
                    countDownLatch.countDown();
                }
            },String.valueOf(i)).start();
        }
        //等待上面50个线程全部计算完成后,再去获得最终值

        //暂停几秒钟线程
        //try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }

        countDownLatch.await();

        System.out.println(Thread.currentThread().getName()+"\t"+"result: "+myNumber.atomicInteger.get());
    }
}
class MyNumber
{
    AtomicInteger atomicInteger = new AtomicInteger();

    public void addPlusPlus()
    {
        atomicInteger.getAndIncrement();
    }
}

数组类型原子类

AtomicIntegerArray
Atomicl ongArray
AtomicReferenceArray
Case

public class AtomicIntegerArrayDemo
{
public static void main(String[] args)
{
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[5]);
//AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(5);
//AtomicIntegerArray atomicIntegerArray = 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();
    System.out.println();
    System.out.println();
    int tmpInt = 0;

    tmpInt = atomicIntegerArray.getAndSet(0,1122);
    System.out.println(tmpInt+"\t"+atomicIntegerArray.get(0));
    atomicIntegerArray.getAndIncrement(1);
    atomicIntegerArray.getAndIncrement(1);
    tmpInt = atomicIntegerArray.getAndIncrement(1);
    System.out.println(tmpInt+"\t"+atomicIntegerArray.get(1));
}

}

引用类型原子类

AtomicReference
@Getter
@ToString
@AllArgsConstructor
class User
{
    String userName;
    int    age;
}

public class AtomicReferenceDemo
{
    public static void main(String[] args)
    {
        User z3 = new User("z3",24);
        User li4 = new User("li4",26);

        AtomicReference<User> atomicReferenceUser = new AtomicReference<>();

        atomicReferenceUser.set(z3);
        System.out.println(atomicReferenceUser.compareAndSet(z3,li4)+"\t"+atomicReferenceUser.get().toString());
        System.out.println(atomicReferenceUser.compareAndSet(z3,li4)+"\t"+atomicReferenceUser.get().toString());
    }
}

自旋锁SpinLockDemo

/**
 * 题目:实现一个自旋锁
 * 自旋锁好处:循环比较获取没有类似wait的阻塞。
 *
 * 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进来后发现
 * 当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到。
 */
public class SpinLockDemo
{
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void myLock()
    {
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"\t come in");
        while(!atomicReference.compareAndSet(null,thread))
        {

        }
    }

    public void myUnLock()
    {
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread,null);
        System.out.println(Thread.currentThread().getName()+"\t myUnLock over");
    }

    public static void main(String[] args)
    {
        SpinLockDemo spinLockDemo = new SpinLockDemo();

        new Thread(() -> {
            spinLockDemo.myLock();
            //暂停一会儿线程
            try { TimeUnit.SECONDS.sleep( 5 ); } catch (InterruptedException e) { e.printStackTrace(); }
            spinLockDemo.myUnLock();
        },"A").start();
        //暂停一会儿线程,保证A线程先于B线程启动并完成
        try { TimeUnit.SECONDS.sleep( 1 ); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            spinLockDemo.myLock();
            spinLockDemo.myUnLock();
        },"B").start();

    }
}
AtomicStampedReference
  • 携带版本号的引用类型原子类,可以解决ABA问题
  • 解决修改过几次
  • 状态戳原子引用

ABADemo

public class ABADemo
{
    static AtomicInteger atomicInteger = new AtomicInteger(100);
    static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);

    public static void main(String[] args)
    {
        abaProblem();
        abaResolve();
    }

    public static void abaResolve()
    {
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println("t3 ----第1次stamp  "+stamp);
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            atomicStampedReference.compareAndSet(100,101,stamp,stamp+1);
            System.out.println("t3 ----第2次stamp  "+atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println("t3 ----第3次stamp  "+atomicStampedReference.getStamp());
        },"t3").start();

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println("t4 ----第1次stamp  "+stamp);
            //暂停几秒钟线程
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            boolean result = atomicStampedReference.compareAndSet(100, 20210308, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName()+"\t"+result+"\t"+atomicStampedReference.getReference());
        },"t4").start();
    }

    public static void abaProblem()
    {
        new Thread(() -> {
            atomicInteger.compareAndSet(100,101);
            atomicInteger.compareAndSet(101,100);
        },"t1").start();

        try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            atomicInteger.compareAndSet(100,20210308);
            System.out.println(atomicInteger.get());
        },"t2").start();
    }
}
AtomicMarkableReference
  • 原子更新带有标记位的引用类型对象
  • 解决是否修改过 它的定义就是将状态戳简化为true|false – 类似一次性筷子

状态戳(true/false)原子引用

public class ABADemo
{
    static AtomicInteger atomicInteger = new AtomicInteger(100);
    static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1);
    static AtomicMarkableReference<Integer> markableReference = new AtomicMarkableReference<>(100,false);

    public static void main(String[] args)
    {
        new Thread(() -> {
            atomicInteger.compareAndSet(100,101);
            atomicInteger.compareAndSet(101,100);
            System.out.println(Thread.currentThread().getName()+"\t"+"update ok");
        },"t1").start();

        new Thread(() -> {
            //暂停几秒钟线程
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            atomicInteger.compareAndSet(100,2020);
        },"t2").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }

        System.out.println(atomicInteger.get());

        System.out.println();
        System.out.println();
        System.out.println();

        System.out.println("============以下是ABA问题的解决,让我们知道引用变量中途被更改了几次=========================");
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"\t 1次版本号"+stampedReference.getStamp());
            //故意暂停200毫秒,让后面的t4线程拿到和t3一样的版本号
            try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }

            stampedReference.compareAndSet(100,101,stampedReference.getStamp(),stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t 2次版本号"+stampedReference.getStamp());
            stampedReference.compareAndSet(101,100,stampedReference.getStamp(),stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t 3次版本号"+stampedReference.getStamp());
        },"t3").start();

        new Thread(() -> {
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t =======1次版本号"+stamp);
            //暂停2秒钟,让t3先完成ABA操作了,看看自己还能否修改
            try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
            boolean b = stampedReference.compareAndSet(100, 2020, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName()+"\t=======2次版本号"+stampedReference.getStamp()+"\t"+stampedReference.getReference());
        },"t4").start();

        System.out.println();
        System.out.println();
        System.out.println();

        System.out.println("============AtomicMarkableReference不关心引用变量更改过几次,只关心是否更改过======================");

        new Thread(() -> {
            boolean marked = markableReference.isMarked();
            System.out.println(Thread.currentThread().getName()+"\t 1次版本号"+marked);
            try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
            markableReference.compareAndSet(100,101,marked,!marked);
            System.out.println(Thread.currentThread().getName()+"\t 2次版本号"+markableReference.isMarked());
            markableReference.compareAndSet(101,100,markableReference.isMarked(),!markableReference.isMarked());
            System.out.println(Thread.currentThread().getName()+"\t 3次版本号"+markableReference.isMarked());
        },"t5").start();

        new Thread(() -> {
            boolean marked = markableReference.isMarked();
            System.out.println(Thread.currentThread().getName()+"\t 1次版本号"+marked);
            //暂停几秒钟线程
            try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
            markableReference.compareAndSet(100,2020,marked,!marked);
            System.out.println(Thread.currentThread().getName()+"\t"+markableReference.getReference()+"\t"+markableReference.isMarked());
        },"t6").start();
    }
}

对象的属性修改原子类

  • AtomicIntegerFieldUpdater
    • 原子更新对象中int类型字段的值
  • AtomicLongFieldUpdater
    • 原子更新对象中Long类型字段的值
  • AtomicReferenceFieldUpdater
    • 原子更新引用类型字段的值
使用目的

以一种线程安全的方式操作非线程安全对象内的某些字段

使用要求.

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

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

面试官问你:你在哪里用了volatile

AtomicReferenceFieldUpdater

Case
AtomicIntegerFieldUpdaterDemo
class BankAccount
{
    private String bankName = "CCB";//银行
    public volatile int money = 0;//钱数
    AtomicIntegerFieldUpdater<BankAccount> accountAtomicIntegerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(BankAccount.class,"money");

    //不加锁+性能高,局部微创
    public void transferMoney(BankAccount bankAccount)
    {
        accountAtomicIntegerFieldUpdater.incrementAndGet(bankAccount);
    }
}

/**
 * @auther zzyy
 * @create 2020-07-14 18:06
 * 以一种线程安全的方式操作非线程安全对象的某些字段。
 * 需求:
 * 1000个人同时向一个账号转账一元钱,那么累计应该增加1000元,
 * 除了synchronized和CAS,还可以使用AtomicIntegerFieldUpdater来实现。
 */
public class AtomicIntegerFieldUpdaterDemo
{

    public static void main(String[] args)
    {
        BankAccount bankAccount = new BankAccount();

        for (int i = 1; i <=1000; i++) {
            int finalI = i;
            new Thread(() -> {
                bankAccount.transferMoney(bankAccount);
            },String.valueOf(i)).start();
        }

        //暂停毫秒
        try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }

        System.out.println(bankAccount.money);

    }
}
AtomicReferenceFieldUpdater
class MyVar
{
    public volatile Boolean isInit = Boolean.FALSE;
    AtomicReferenceFieldUpdater<MyVar,Boolean> atomicReferenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(MyVar.class,Boolean.class,"isInit");


    public void init(MyVar myVar)
    {
        if(atomicReferenceFieldUpdater.compareAndSet(myVar,Boolean.FALSE,Boolean.TRUE))
        {
            System.out.println(Thread.currentThread().getName()+"\t"+"---init.....");
            //暂停几秒钟线程
            try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t"+"---init.....over");
        }else{
            System.out.println(Thread.currentThread().getName()+"\t"+"------其它线程正在初始化");
        }
    }


}


/**
 * 多线程并发调用一个类的初始化方法,如果未被初始化过,将执行初始化工作,要求只能初始化一次
 */
public class AtomicIntegerFieldUpdaterDemo
{
    public static void main(String[] args) throws InterruptedException
    {
        MyVar myVar = new MyVar();

        for (int i = 1; i <=5; i++) {
            new Thread(() -> {
                myVar.init(myVar);
            },String.valueOf(i)).start();
        }
    }
}

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

  • DoubleAccumulator
  • DoubleAdder
  • LongAccumulator
  • LongAdder

juc笔记前篇_第63张图片

热点商品点赞计算器,点赞数加加统计,不要求实时精确

一个很大的ist,里面都是int类型,如何实现加加,说说思路

源码、原理分析
架构

juc笔记前篇_第64张图片

原理(LongAdder为什么这么快)

官网说明和阿里要求

juc笔记前篇_第65张图片

LongAdder是Striped64的子类

Striped64

juc笔记前篇_第66张图片

striped64中的一些变量或者方法的定义

juc笔记前篇_第67张图片

Cell

LongAdder为什么这么快

juc笔记前篇_第68张图片

底层分散热点。

源码解读深度分析

小总结

LongAdder在 无竞争的情况,跟AtomicLong- -样,对同一 .个base进行操作, 当出现竞争关系时则是采用化整为零分散热点的做法,用空间换时间,用一个数组cells, 将- -个value拆分进这个数组cells。 多个线程需要同时对value进行操作时候,可以对线程id进行hash得到hash值, 再根据hash值映射到这个数组ells的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组ells的所有值和base都加起来作为最终结果。

juc笔记前篇_第69张图片

juc笔记前篇_第70张图片

longAdder.increment()

1、add(1L)

juc笔记前篇_第71张图片

juc笔记前篇_第72张图片

juc笔记前篇_第73张图片

juc笔记前篇_第74张图片

  1. 最初无竞争时只更新base;
  2. 如果更新base失败后,首次新建一个Cell[]数组
  3. 当多个线程竞争同一个Cell比较激烈时,可能就要对Cell[]扩容
2、longAccumulate

longAccumulate入参说明

在这里插入图片描述

Striped64中一些变量或者方法的定义

juc笔记前篇_第75张图片

线程hash值:probe

juc笔记前篇_第76张图片

juc笔记前篇_第77张图片

总纲

juc笔记前篇_第78张图片

上述代码首先给当前线程分配一个hash值,然后进入一个for(;;)自旋,这个自旋分为三个分支:
CASE1:Cell[]数组已经初始化
CASE2:Cell[]数组未初始化(首次新建)
CASE3:Cell[]数组正在初始化中

刚刚要初始化Cell[]数组(首次新建)

未初始化过Cell[]数组,尝试占有锁并首次初始化cells数组

juc笔记前篇_第79张图片

如果上面条件都执行成功就会执行数组的初始化及赋值操作, Cell[] rs = new Cell[2]表示数组的长度为2,
rs[h & 1] = new Cell(x) 表示创建一个新的Cell元素,value是x值,默认为1。
h & 1类似于我们之前HashMap常用到的计算散列桶index的算法,通常都是hash & (table.len - 1)。同hashmap一个意思。

兜底

多个线程尝试CAS修改失败的线程会走到这个分支

image-20220827222037258

该分支实现直接操作base基数,将值累加到base上,也即其它线程正在初始化,多个线程正在更新base的值。

Cell数组不再为空且可能存在Cell数组扩容

多个线程同时命中一个cell的竞争

juc笔记前篇_第80张图片
juc笔记前篇_第81张图片

上面代码判断当前线程hash后指向的数据位置元素是否为空,
如果为空则将Cell数据放入数组中,跳出循环。
如果不空则继续循环。

juc笔记前篇_第82张图片

image-20220827222245774

说明当前线程对应的数组中有了数据,也重置过hash值,
这时通过CAS操作尝试对当前数中的value值进行累加x操作,x默认为1,如果CAS成功则直接跳出循环。

juc笔记前篇_第83张图片

juc笔记前篇_第84张图片

juc笔记前篇_第85张图片

image-20220827222411886

3、sum

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

为啥在并发情况下sum的值不精确

sum执行时,并没有限制对base和cells的更新(一句要命的话)。所以LongAdder不是强一致性的,它是最终一致性的。

首先,最终返回的sum局部变量,初始被复制为base,而最终返回时,很可能base已经被更新了,而此时局部变量sum不会更新,造成不一致。 其次,这里对cell的读取也无法保证是最后一次写入的值。所以,sum方法在没有并发的情况下,可以获得正确的结果

juc笔记前篇_第86张图片

使用总结
  • AtomicLong
    • 线程安全,可允许一些性能损耗,要求高精度时可使用
    • 保证精度,性能代价
    • AtomicLong是多个线程针对单个热点值value进行原子操作
  • LongAdder
    • 当需要在高并发下有较好的性能表现,且对值的精确度要求不高时,可以使用
    • 保证性能,精度代价
    • LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作

总结

1、AtomicLong
  • 原理
    • CAS+自旋
    • incrementAndGet
  • 场景
    • 低并发下的全局计算
    • AtomicLong能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性的问题。
  • 缺陷
    • 高并发后性能急剧下降
    • AtomicLong的自旋会成为瓶颈
    • N个线程CAS操作修改线程的值,每次只有一个成功过,其它N - 1失败,失败的不停的自旋直到成功,这样大量失败自旋的情况,一下子cpu就打高了。
2、LongAdder
  • 原理
    • CAS+Base+Cell数组分散
    • 空间换时间并分散了热点数据
  • 场景
    • 高并发下的全局计算
  • 缺陷
    • sum求和后还有计算线程修改结果的话,最后结果不够准确

9.ThreadLocal

ThreadLocal简介

恶心的大厂面试题

  • ThreadL ocal中ThreadLocalMap的数据结构和关系?
  • ThreadL ocal的key是弱引用,这是为什么?
  • ThreadLocal内存泄露问题你知道吗?
  • ThreadL ocal中最后为什么要加remove方法?

是什么

juc笔记前篇_第87张图片

能干嘛

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

juc笔记前篇_第88张图片

api介绍

juc笔记前篇_第89张图片

永远的helloworld讲起

5个销售卖房子,集团高层只关心销售总量的准确统计数,按照总销售额统计,方便集团公司给部分发送奖金

  • 群雄逐鹿起紛争
  • Code上述需求变化了…

上述需求该如何处理???

code1

public class ThreadLocalDemo {
    public static void main(String[] args) throws InterruptedException {
        House house = new House();

        for (int i = 1; i <= 5; i++) {
            new Thread(() -> {
                int size = new Random().nextInt(5) + 1;
                try {
                    for (int j = 1; j <= size; j++) {
                        house.saleHouse();
                        house.saleVolumeByThreadLocal();
                    }
                    System.out.println(Thread.currentThread().getName() + "\t" + "号销售卖出:" + house.saleVolume.get());
                } finally {
                    house.saleVolume.remove();
                }
            }, String.valueOf(i)).start();
        }
        ;

        //暂停毫秒
        try {
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "\t" + "共计卖出多少套: " + house.saleCount);
    }
}

class House {
    int saleCount = 0;

    public synchronized void saleHouse() {
        ++saleCount;
    }

    ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);

    public void saleVolumeByThreadLocal() {
        saleVolume.set(1 + saleVolume.get());
    }
}

code2

public class ThreadLocalDemo2 {
    public static void main(String[] args) throws InterruptedException{
        MyData myData = new MyData();

        ExecutorService threadPool = Executors.newFixedThreadPool(3);

        try
        {
            for (int i = 0; i < 10; i++) {
                threadPool.submit(() -> {
                    try {
                        Integer beforeInt = myData.threadLocalField.get();
                        myData.add();
                        Integer afterInt = myData.threadLocalField.get();
                        System.out.println(Thread.currentThread().getName()+"\t"+"beforeInt:"+beforeInt+"\t afterInt: "+afterInt);
                    } finally {
                        myData.threadLocalField.remove();
                    }
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}
class MyData
{
    ThreadLocal<Integer> threadLocalField = ThreadLocal.withInitial(() -> 0);
    public void add()
    {
        threadLocalField.set(1 + threadLocalField.get());
    }
}

通过上面代码总结

  • 因为每个Thread内有自己的实例副本且该副本只由当前线程自己使用
  • 既然其它Thread不可访问,那就不存在多线程间共享的问题。
  • 统一-设置初始值,但是每个线程对这个值的修改都是各自线程互相独立的

一句话

如何才能不争抢

  • 加入synchronized或者Lock控制资源的访问顺序

  • 人手一份,大家各自安好,没必要抢夺+

ThreadLocal源码分析

源码解读

Thread, ThreadLocal,ThreadLocalMap关系

Thread和ThreadLocal

juc笔记前篇_第90张图片

ThreadLocal和ThreadLocalMap

juc笔记前篇_第91张图片

All三者总概括

juc笔记前篇_第92张图片

小总结

threadLocalMap实际上就是一个以threadLocal实例为key,任意对象为value的Entry对象。 当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放

近似的可以理解为: ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以ThreadLocal为Key),不过是经过了两层包装的ThreadLocal对象:

juc笔记前篇_第93张图片

JVM内部维护了一个线程版的Map(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中),每个线程要用到这个T的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。

ThreadLocal内存泄露问题

从阿里面试题开始讲起

juc笔记前篇_第94张图片

什么是内存泄漏

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

谁惹的祸?

  • why

juc笔记前篇_第95张图片

  • 强引用、软引用、弱引用、虚引用分别是什么?

    juc笔记前篇_第96张图片

    ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以它为Key),不过是经过了两层包装的ThreadLocal对象: (1)第一层包装是使用 WeakReference> 将ThreadLocal对象变成一个弱引用的对象; (2)第二层包装是定义了一个专门的类 Entry 来扩展 WeakReference

juc笔记前篇_第97张图片

每个Thread对象维护着一个ThreadLocalMap的引用
ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值Value是传递进来的对象
调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
ThreadLocal本身并不存储值,它只是自己作为一个key来让线程从ThreadLocalMap获取value,正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响~

为什么要用弱引用?不用如何?

public void function01()
{
    ThreadLocal tl = new ThreadLocal<Integer>();    //line1
    tl.set(2021);                                   //line2
    tl.get();                                       //line3
}
//line1新建了一个ThreadLocal对象,t1 是强引用指向这个对象;
//line2调用set()方法后新建一个Entry,通过源码可知Entry对象里的k是弱引用指向这个对象。

当function01方法执行完毕后,栈帧销毁强引用 tl 也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象,若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的雷)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null。

此后我们调用get,set或remove方法时,就会尝试删除key为null的entry,可以释放value对象所占用的内存。

1.弱引用就万事大吉了吗?
  1. 当我们为threadLocal变量赋值,实际上就是当前的Entry(threadLocal实例为key,值为value)往这个threadLocalMap中存放。Entry中的key是弱引用,当threadLocal外部强引用被置为null(tl=null),那么系统 GC 的时候,根据可达性分析,这个threadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
  2. 当然,如果当前thread运行结束,threadLocal,threadLocalMap,Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收。
  3. 但在实际使用中我们有时候会用线程池去维护我们的线程,比如在Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以threadLocal内存泄漏就值得我们小心
2.key为null的entry,原理解析

juc笔记前篇_第98张图片

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如正好用在线程池),这些key为null的Entry的value就会一直存在一条强引用链。

虽然弱引用,保证了key指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value,因此弱引用不能100%保证内存不泄露。我们要在不使用某个ThreadLocal对象后,手动调用remoev方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。

3.set、get方法会去检查所有键为null的Entry对象

set()

juc笔记前篇_第99张图片
juc笔记前篇_第100张图片

get()

juc笔记前篇_第101张图片

juc笔记前篇_第102张图片

juc笔记前篇_第103张图片

remove()

juc笔记前篇_第104张图片

结论

从前面的set,getEntry,remove方法看出,在threadLocal的生命周期里,针对threadLocal存在的内存泄漏的问题, 都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法清理掉key为null的脏entry。

juc笔记前篇_第105张图片

最佳实践

用完记得手动remove

小总结

  • ThreadLocal 并不解决线程间共享数据的问题
  • ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
  • ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
  • 每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题
  • ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题
  • 都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏,属于安全加固的方法
  • 群雄逐鹿起纷争,人各一份天下安

ThreadLocal和InheritableThreadLocal

需要解决的问题

  • 我们还是以解决问题的方式来引出ThreadLocalInheritableThreadLocal,这样印象会深刻一些。

目前java开发web系统一般有3层,controller、service、dao,请求到达controller,controller调用service,service调用dao,然后进行处理。

我们写一个简单的例子,有3个方法分别模拟controller、service、dao。代码如下:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class Demo1 {
    static AtomicInteger threadIndex = new AtomicInteger(1);
    //创建处理请求的线程池子
    static ThreadPoolExecutor disposeRequestExecutor = new ThreadPoolExecutor(3,
            3,
            60,
            TimeUnit.SECONDS,
            new LinkedBlockingDeque<>(),
            r -> {
                Thread thread = new Thread(r);
                thread.setName("disposeRequestThread-" + threadIndex.getAndIncrement());
                return thread;
            });
    //记录日志
    public static void log(String msg) {
        StackTraceElement stack[] = (new Throwable()).getStackTrace();
        System.out.println("****" + System.currentTimeMillis() + ",[线程:" + Thread.currentThread().getName() + "]," + stack[1] + ":" + msg);
    }
    //模拟controller
    public static void controller(List<String> dataList) {
        log("接受请求");
        service(dataList);
    }
    //模拟service
    public static void service(List<String> dataList) {
        log("执行业务");
        dao(dataList);
    }
    //模拟dao
    public static void dao(List<String> dataList) {
        log("执行数据库操作");
        //模拟插入数据
        for (String s : dataList) {
            log("插入数据" + s + "成功");
        }
    }
    public static void main(String[] args) {
        //需要插入的数据
        List<String> dataList = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            dataList.add("数据" + i);
        }
        //模拟5个请求
        int requestCount = 5;
        for (int i = 0; i < requestCount; i++) {
            disposeRequestExecutor.execute(() -> {
                controller(dataList);
            });
        }
        disposeRequestExecutor.shutdown();
    }
}
****1565338891286,[线程:disposeRequestThread-2],com.itsoku.chat24.Demo1.controller(Demo1.java:36):接受请求
****1565338891286,[线程:disposeRequestThread-1],com.itsoku.chat24.Demo1.controller(Demo1.java:36):接受请求
****1565338891287,[线程:disposeRequestThread-2],com.itsoku.chat24.Demo1.service(Demo1.java:42):执行业务
****1565338891287,[线程:disposeRequestThread-1],com.itsoku.chat24.Demo1.service(Demo1.java:42):执行业务
****1565338891287,[线程:disposeRequestThread-3],com.itsoku.chat24.Demo1.controller(Demo1.java:36):接受请求
****1565338891287,[线程:disposeRequestThread-1],com.itsoku.chat24.Demo1.dao(Demo1.java:48):执行数据库操作
****1565338891287,[线程:disposeRequestThread-1],com.itsoku.chat24.Demo1.dao(Demo1.java:51):插入数据数据0成功
****1565338891287,[线程:disposeRequestThread-1],com.itsoku.chat24.Demo1.dao(Demo1.java:51):插入数据数据1成功
****1565338891287,[线程:disposeRequestThread-2],com.itsoku.chat24.Demo1.dao(Demo1.java:48):执行数据库操作
****1565338891287,[线程:disposeRequestThread-1],com.itsoku.chat24.Demo1.dao(Demo1.java:51):插入数据数据2成功
****1565338891287,[线程:disposeRequestThread-3],com.itsoku.chat24.Demo1.service(Demo1.java:42):执行业务
****1565338891288,[线程:disposeRequestThread-1],com.itsoku.chat24.Demo1.controller(Demo1.java:36):接受请求
****1565338891287,[线程:disposeRequestThread-2],com.itsoku.chat24.Demo1.dao(Demo1.java:51):插入数据数据0成功
****1565338891288,[线程:disposeRequestThread-1],com.itsoku.chat24.Demo1.service(Demo1.java:42):执行业务
****1565338891288,[线程:disposeRequestThread-3],com.itsoku.chat24.Demo1.dao(Demo1.java:48):执行数据库操作
****1565338891288,[线程:disposeRequestThread-1],com.itsoku.chat24.Demo1.dao(Demo1.java:48):执行数据库操作
****1565338891288,[线程:disposeRequestThread-2],com.itsoku.chat24.Demo1.dao(Demo1.java:51):插入数据数据1成功
****1565338891288,[线程:disposeRequestThread-1],com.itsoku.chat24.Demo1.dao(Demo1.java:51):插入数据数据0成功
****1565338891288,[线程:disposeRequestThread-3],com.itsoku.chat24.Demo1.dao(Demo1.java:51):插入数据数据0成功
****1565338891288,[线程:disposeRequestThread-1],com.itsoku.chat24.Demo1.dao(Demo1.java:51):插入数据数据1成功
****1565338891288,[线程:disposeRequestThread-2],com.itsoku.chat24.Demo1.dao(Demo1.java:51):插入数据数据2成功
****1565338891288,[线程:disposeRequestThread-1],com.itsoku.chat24.Demo1.dao(Demo1.java:51):插入数据数据2成功
****1565338891288,[线程:disposeRequestThread-3],com.itsoku.chat24.Demo1.dao(Demo1.java:51):插入数据数据1成功
****1565338891288,[线程:disposeRequestThread-2],com.itsoku.chat24.Demo1.controller(Demo1.java:36):接受请求
****1565338891288,[线程:disposeRequestThread-3],com.itsoku.chat24.Demo1.dao(Demo1.java:51):插入数据数据2成功
****1565338891288,[线程:disposeRequestThread-2],com.itsoku.chat24.Demo1.service(Demo1.java:42):执行业务
****1565338891289,[线程:disposeRequestThread-2],com.itsoku.chat24.Demo1.dao(Demo1.java:48):执行数据库操作
****1565338891289,[线程:disposeRequestThread-2],com.itsoku.chat24.Demo1.dao(Demo1.java:51):插入数据数据0成功
****1565338891289,[线程:disposeRequestThread-2],com.itsoku.chat24.Demo1.dao(Demo1.java:51):插入数据数据1成功
****1565338891289,[线程:disposeRequestThread-2],com.itsoku.chat24.Demo1.dao(Demo1.java:51):插入数据数据2成功

代码中调用controller、service、dao 3个方法时,来模拟处理一个请求。main方法中循环了5次模拟发起5次请求,然后交给线程池去处理请求,dao中模拟循环插入传入的dataList数据。

问题来了:开发者想看一下哪些地方耗时比较多,想通过日志来分析耗时情况,想追踪某个请求的完整日志,怎么搞?

上面的请求采用线程池的方式处理的,多个请求可能会被一个线程处理,通过日志很难看出那些日志是同一个请求,我们能不能给请求加一个唯一标志,日志中输出这个唯一标志,当然可以。

如果我们的代码就只有上面示例这么简单,我想还是很容易的,上面就3个方法,给每个方法加个traceId参数,log方法也加个traceId参数,就解决了,代码如下:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class Demo2 {
    static AtomicInteger threadIndex = new AtomicInteger(1);
    //创建处理请求的线程池子
    static ThreadPoolExecutor disposeRequestExecutor = new ThreadPoolExecutor(3,
            3,
            60,
            TimeUnit.SECONDS,
            new LinkedBlockingDeque<>(),
            r -> {
                Thread thread = new Thread(r);
                thread.setName("disposeRequestThread-" + threadIndex.getAndIncrement());
                return thread;
            });
    //记录日志
    public static void log(String msg, String traceId) {
        StackTraceElement stack[] = (new Throwable()).getStackTrace();
        System.out.println("****" + System.currentTimeMillis() + "[traceId:" + traceId + "],[线程:" + Thread.currentThread().getName() + "]," + stack[1] + ":" + msg);
    }
    //模拟controller
    public static void controller(List<String> dataList, String traceId) {
        log("接受请求", traceId);
        service(dataList, traceId);
    }
    //模拟service
    public static void service(List<String> dataList, String traceId) {
        log("执行业务", traceId);
        dao(dataList, traceId);
    }
    //模拟dao
    public static void dao(List<String> dataList, String traceId) {
        log("执行数据库操作", traceId);
        //模拟插入数据
        for (String s : dataList) {
            log("插入数据" + s + "成功", traceId);
        }
    }
    public static void main(String[] args) {
        //需要插入的数据
        List<String> dataList = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            dataList.add("数据" + i);
        }
        //模拟5个请求
        int requestCount = 5;
        for (int i = 0; i < requestCount; i++) {
            String traceId = String.valueOf(i);
            disposeRequestExecutor.execute(() -> {
                controller(dataList, traceId);
            });
        }
        disposeRequestExecutor.shutdown();
    }
}
****1565339559773[traceId:0],[线程:disposeRequestThread-1],com.itsoku.chat24.Demo2.controller(Demo2.java:36):接受请求
****1565339559773[traceId:1],[线程:disposeRequestThread-2],com.itsoku.chat24.Demo2.controller(Demo2.java:36):接受请求
****1565339559773[traceId:2],[线程:disposeRequestThread-3],com.itsoku.chat24.Demo2.controller(Demo2.java:36):接受请求
****1565339559774[traceId:1],[线程:disposeRequestThread-2],com.itsoku.chat24.Demo2.service(Demo2.java:42):执行业务
****1565339559774[traceId:0],[线程:disposeRequestThread-1],com.itsoku.chat24.Demo2.service(Demo2.java:42):执行业务
****1565339559774[traceId:1],[线程:disposeRequestThread-2],com.itsoku.chat24.Demo2.dao(Demo2.java:48):执行数据库操作
****1565339559774[traceId:2],[线程:disposeRequestThread-3],com.itsoku.chat24.Demo2.service(Demo2.java:42):执行业务
****1565339559774[traceId:1],[线程:disposeRequestThread-2],com.itsoku.chat24.Demo2.dao(Demo2.java:51):插入数据数据0成功
****1565339559774[traceId:0],[线程:disposeRequestThread-1],com.itsoku.chat24.Demo2.dao(Demo2.java:48):执行数据库操作
****1565339559774[traceId:1],[线程:disposeRequestThread-2],com.itsoku.chat24.Demo2.dao(Demo2.java:51):插入数据数据1成功
****1565339559774[traceId:2],[线程:disposeRequestThread-3],com.itsoku.chat24.Demo2.dao(Demo2.java:48):执行数据库操作
****1565339559774[traceId:1],[线程:disposeRequestThread-2],com.itsoku.chat24.Demo2.dao(Demo2.java:51):插入数据数据2成功
****1565339559774[traceId:0],[线程:disposeRequestThread-1],com.itsoku.chat24.Demo2.dao(Demo2.java:51):插入数据数据0成功
****1565339559775[traceId:3],[线程:disposeRequestThread-2],com.itsoku.chat24.Demo2.controller(Demo2.java:36):接受请求
****1565339559775[traceId:2],[线程:disposeRequestThread-3],com.itsoku.chat24.Demo2.dao(Demo2.java:51):插入数据数据0成功
****1565339559775[traceId:3],[线程:disposeRequestThread-2],com.itsoku.chat24.Demo2.service(Demo2.java:42):执行业务
****1565339559775[traceId:0],[线程:disposeRequestThread-1],com.itsoku.chat24.Demo2.dao(Demo2.java:51):插入数据数据1成功
****1565339559775[traceId:3],[线程:disposeRequestThread-2],com.itsoku.chat24.Demo2.dao(Demo2.java:48):执行数据库操作
****1565339559775[traceId:2],[线程:disposeRequestThread-3],com.itsoku.chat24.Demo2.dao(Demo2.java:51):插入数据数据1成功
****1565339559775[traceId:3],[线程:disposeRequestThread-2],com.itsoku.chat24.Demo2.dao(Demo2.java:51):插入数据数据0成功
****1565339559775[traceId:0],[线程:disposeRequestThread-1],com.itsoku.chat24.Demo2.dao(Demo2.java:51):插入数据数据2成功
****1565339559775[traceId:3],[线程:disposeRequestThread-2],com.itsoku.chat24.Demo2.dao(Demo2.java:51):插入数据数据1成功
****1565339559775[traceId:2],[线程:disposeRequestThread-3],com.itsoku.chat24.Demo2.dao(Demo2.java:51):插入数据数据2成功
****1565339559775[traceId:3],[线程:disposeRequestThread-2],com.itsoku.chat24.Demo2.dao(Demo2.java:51):插入数据数据2成功
****1565339559775[traceId:4],[线程:disposeRequestThread-1],com.itsoku.chat24.Demo2.controller(Demo2.java:36):接受请求
****1565339559776[traceId:4],[线程:disposeRequestThread-1],com.itsoku.chat24.Demo2.service(Demo2.java:42):执行业务
****1565339559776[traceId:4],[线程:disposeRequestThread-1],com.itsoku.chat24.Demo2.dao(Demo2.java:48):执行数据库操作
****1565339559776[traceId:4],[线程:disposeRequestThread-1],com.itsoku.chat24.Demo2.dao(Demo2.java:51):插入数据数据0成功
****1565339559776[traceId:4],[线程:disposeRequestThread-1],com.itsoku.chat24.Demo2.dao(Demo2.java:51):插入数据数据1成功
****1565339559776[traceId:4],[线程:disposeRequestThread-1],com.itsoku.chat24.Demo2.dao(Demo2.java:51):插入数据数据2成功

上面我们通过修改代码的方式,把问题解决了,但前提是你们的系统都像上面这么简单,功能很少,需要改的代码很少,可以这么去改。但事与愿违,我们的系统一般功能都是比较多的,如果我们都一个个去改,岂不是要疯掉,改代码还涉及到重新测试,风险也不可控。那有什么好办法么?

ThreadLocal

还是拿上面的问题,我们来分析一下,每个请求都是由一个线程处理的,线程就相当于一个人一样,每个请求相当于一个任务,任务来了,人来处理,处理完毕之后,再处理下一个请求任务。人身上是不是有很多口袋,人刚开始准备处理任务的时候,我们把任务的编号放在处理者的口袋中,然后处理中一路携带者,处理过程中如果需要用到这个编号,直接从口袋中获取就可以了。那么刚好java中线程设计的时候也考虑到了这些问题,Thread对象中就有很多口袋,用来放东西。Thread类中有这么一个变量:

ThreadLocal.ThreadLocalMap threadLocals = null;

如何来操作Thread中的这些口袋呢,java为我们提供了一个类ThreadLocal,ThreadLocal对象用来操作Thread中的某一个口袋,可以向这个口袋中放东西、获取里面的东西、清除里面的东西,这个口袋一次性只能放一个东西,重复放东西会将里面已经存在的东西覆盖掉。

常用的3个方法:

//向Thread中某个口袋中放东西
public void set(T value);
//获取这个口袋中目前放的东西
public T get();
//清空这个口袋中放的东西
public void remove()

我们使用ThreadLocal来改造一下上面的代码,如下:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class Demo3 {
    //创建一个操作Thread中存放请求任务追踪id口袋的对象
    static ThreadLocal<String> traceIdKD = new ThreadLocal<>();
    static AtomicInteger threadIndex = new AtomicInteger(1);
    //创建处理请求的线程池子
    static ThreadPoolExecutor disposeRequestExecutor = new ThreadPoolExecutor(3,
            3,
            60,
            TimeUnit.SECONDS,
            new LinkedBlockingDeque<>(),
            r -> {
                Thread thread = new Thread(r);
                thread.setName("disposeRequestThread-" + threadIndex.getAndIncrement());
                return thread;
            });
    //记录日志
    public static void log(String msg) {
        StackTraceElement stack[] = (new Throwable()).getStackTrace();
        //获取当前线程存放tranceId口袋中的内容
        String traceId = traceIdKD.get();
        System.out.println("****" + System.currentTimeMillis() + "[traceId:" + traceId + "],[线程:" + Thread.currentThread().getName() + "]," + stack[1] + ":" + msg);
    }
    //模拟controller
    public static void controller(List<String> dataList) {
        log("接受请求");
        service(dataList);
    }
    //模拟service
    public static void service(List<String> dataList) {
        log("执行业务");
        dao(dataList);
    }
    //模拟dao
    public static void dao(List<String> dataList) {
        log("执行数据库操作");
        //模拟插入数据
        for (String s : dataList) {
            log("插入数据" + s + "成功");
        }
    }
    public static void main(String[] args) {
        //需要插入的数据
        List<String> dataList = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            dataList.add("数据" + i);
        }
        //模拟5个请求
        int requestCount = 5;
        for (int i = 0; i < requestCount; i++) {
            String traceId = String.valueOf(i);
            disposeRequestExecutor.execute(() -> {
                //把traceId放入口袋中
                traceIdKD.set(traceId);
                try {
                    controller(dataList);
                } finally {
                    //将tranceId从口袋中移除
                    traceIdKD.remove();
                }
            });
        }
        disposeRequestExecutor.shutdown();
    }
}
****1565339644214[traceId:1],[线程:disposeRequestThread-2],com.itsoku.chat24.Demo3.controller(Demo3.java:41):接受请求
****1565339644214[traceId:2],[线程:disposeRequestThread-3],com.itsoku.chat24.Demo3.controller(Demo3.java:41):接受请求
****1565339644214[traceId:0],[线程:disposeRequestThread-1],com.itsoku.chat24.Demo3.controller(Demo3.java:41):接受请求
****1565339644214[traceId:2],[线程:disposeRequestThread-3],com.itsoku.chat24.Demo3.service(Demo3.java:47):执行业务
****1565339644214[traceId:1],[线程:disposeRequestThread-2],com.itsoku.chat24.Demo3.service(Demo3.java:47):执行业务
****1565339644214[traceId:2],[线程:disposeRequestThread-3],com.itsoku.chat24.Demo3.dao(Demo3.java:53):执行数据库操作
****1565339644214[traceId:0],[线程:disposeRequestThread-1],com.itsoku.chat24.Demo3.service(Demo3.java:47):执行业务
****1565339644214[traceId:2],[线程:disposeRequestThread-3],com.itsoku.chat24.Demo3.dao(Demo3.java:56):插入数据数据0成功
****1565339644214[traceId:0],[线程:disposeRequestThread-1],com.itsoku.chat24.Demo3.dao(Demo3.java:53):执行数据库操作
****1565339644214[traceId:1],[线程:disposeRequestThread-2],com.itsoku.chat24.Demo3.dao(Demo3.java:53):执行数据库操作
****1565339644215[traceId:0],[线程:disposeRequestThread-1],com.itsoku.chat24.Demo3.dao(Demo3.java:56):插入数据数据0成功
****1565339644215[traceId:2],[线程:disposeRequestThread-3],com.itsoku.chat24.Demo3.dao(Demo3.java:56):插入数据数据1成功
****1565339644215[traceId:0],[线程:disposeRequestThread-1],com.itsoku.chat24.Demo3.dao(Demo3.java:56):插入数据数据1成功
****1565339644215[traceId:1],[线程:disposeRequestThread-2],com.itsoku.chat24.Demo3.dao(Demo3.java:56):插入数据数据0成功
****1565339644215[traceId:0],[线程:disposeRequestThread-1],com.itsoku.chat24.Demo3.dao(Demo3.java:56):插入数据数据2成功
****1565339644215[traceId:2],[线程:disposeRequestThread-3],com.itsoku.chat24.Demo3.dao(Demo3.java:56):插入数据数据2成功
****1565339644215[traceId:1],[线程:disposeRequestThread-2],com.itsoku.chat24.Demo3.dao(Demo3.java:56):插入数据数据1成功
****1565339644215[traceId:4],[线程:disposeRequestThread-3],com.itsoku.chat24.Demo3.controller(Demo3.java:41):接受请求
****1565339644215[traceId:3],[线程:disposeRequestThread-1],com.itsoku.chat24.Demo3.controller(Demo3.java:41):接受请求
****1565339644215[traceId:4],[线程:disposeRequestThread-3],com.itsoku.chat24.Demo3.service(Demo3.java:47):执行业务
****1565339644215[traceId:1],[线程:disposeRequestThread-2],com.itsoku.chat24.Demo3.dao(Demo3.java:56):插入数据数据2成功
****1565339644215[traceId:4],[线程:disposeRequestThread-3],com.itsoku.chat24.Demo3.dao(Demo3.java:53):执行数据库操作
****1565339644215[traceId:3],[线程:disposeRequestThread-1],com.itsoku.chat24.Demo3.service(Demo3.java:47):执行业务
****1565339644215[traceId:4],[线程:disposeRequestThread-3],com.itsoku.chat24.Demo3.dao(Demo3.java:56):插入数据数据0成功
****1565339644215[traceId:3],[线程:disposeRequestThread-1],com.itsoku.chat24.Demo3.dao(Demo3.java:53):执行数据库操作
****1565339644215[traceId:4],[线程:disposeRequestThread-3],com.itsoku.chat24.Demo3.dao(Demo3.java:56):插入数据数据1成功
****1565339644215[traceId:3],[线程:disposeRequestThread-1],com.itsoku.chat24.Demo3.dao(Demo3.java:56):插入数据数据0成功
****1565339644215[traceId:4],[线程:disposeRequestThread-3],com.itsoku.chat24.Demo3.dao(Demo3.java:56):插入数据数据2成功
****1565339644215[traceId:3],[线程:disposeRequestThread-1],com.itsoku.chat24.Demo3.dao(Demo3.java:56):插入数据数据1成功
****1565339644215[traceId:3],[线程:disposeRequestThread-1],com.itsoku.chat24.Demo3.dao(Demo3.java:56):插入数据数据2成功

可以看出输出和刚才使用traceId参数的方式结果一致,但是却简单了很多。不用去修改controller、service、dao代码了,风险也减少了很多。

代码中创建了一个ThreadLocal traceIdKD,这个对象用来操作Thread中一个口袋,用这个口袋来存放tranceId。在main方法中通过traceIdKD.set(traceId)方法将traceId放入口袋,log方法中通traceIdKD.get()获取口袋中的traceId,最后任务处理完之后,main中的finally中调用traceIdKD.remove();将口袋中的traceId清除。

ThreadLocal的官方API解释为:

“该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。”

InheritableThreadLocal

继续上面的实例,dao中循环处理dataList的内容,假如dataList处理比较耗时,我们想加快处理速度有什么办法么?大家已经想到了,用多线程并行处理dataList,那么我们把代码改一下,如下:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class Demo4 {
    //创建一个操作Thread中存放请求任务追踪id口袋的对象
    static ThreadLocal<String> traceIdKD = new ThreadLocal<>();
    static AtomicInteger threadIndex = new AtomicInteger(1);
    //创建处理请求的线程池子
    static ThreadPoolExecutor disposeRequestExecutor = new ThreadPoolExecutor(3,
            3,
            60,
            TimeUnit.SECONDS,
            new LinkedBlockingDeque<>(),
            r -> {
                Thread thread = new Thread(r);
                thread.setName("disposeRequestThread-" + threadIndex.getAndIncrement());
                return thread;
            });
    //记录日志
    public static void log(String msg) {
        StackTraceElement stack[] = (new Throwable()).getStackTrace();
        //获取当前线程存放tranceId口袋中的内容
        String traceId = traceIdKD.get();
        System.out.println("****" + System.currentTimeMillis() + "[traceId:" + traceId + "],[线程:" + Thread.currentThread().getName() + "]," + stack[1] + ":" + msg);
    }
    //模拟controller
    public static void controller(List<String> dataList) {
        log("接受请求");
        service(dataList);
    }
    //模拟service
    public static void service(List<String> dataList) {
        log("执行业务");
        dao(dataList);
    }
    //模拟dao
    public static void dao(List<String> dataList) {
        CountDownLatch countDownLatch = new CountDownLatch(dataList.size());
        log("执行数据库操作");
        String threadName = Thread.currentThread().getName();
        //模拟插入数据
        for (String s : dataList) {
            new Thread(() -> {
                try {
                    //模拟数据库操作耗时100毫秒
                    TimeUnit.MILLISECONDS.sleep(100);
                    log("插入数据" + s + "成功,主线程:" + threadName);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            }).start();
        }
        //等待上面的dataList处理完毕
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        //需要插入的数据
        List<String> dataList = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            dataList.add("数据" + i);
        }
        //模拟5个请求
        int requestCount = 5;
        for (int i = 0; i < requestCount; i++) {
            String traceId = String.valueOf(i);
            disposeRequestExecutor.execute(() -> {
                //把traceId放入口袋中
                traceIdKD.set(traceId);
                try {
                    controller(dataList);
                } finally {
                    //将tranceId从口袋中移除
                    traceIdKD.remove();
                }
            });
        }
        disposeRequestExecutor.shutdown();
    }
}

看一下上面的输出,有些traceId为null,这是为什么呢?这是因为dao中为了提升处理速度,创建了子线程来并行处理,子线程调用log的时候,去自己的存放traceId的口袋中拿去东西,肯定是空的了。

那有什么办法么?可不可以这样?

父线程相当于主管,子线程相当于干活的小弟,主管让小弟们干活的时候,将自己兜里面的东西复制一份给小弟们使用,主管兜里面可能有很多牛逼的工具,为了提升小弟们的工作效率,给小弟们都复制一个,丢到小弟们的兜里,然后小弟就可以从自己的兜里拿去这些东西使用了,也可以清空自己兜里面的东西。

Thread对象中有个inheritableThreadLocals变量,代码如下:

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

inheritableThreadLocals相当于线程中另外一种兜,这种兜有什么特征呢,当创建子线程的时候,子线程会将父线程这种类型兜的东西全部复制一份放到自己的inheritableThreadLocals兜中,使用InheritableThreadLocal对象可以操作线程中的inheritableThreadLocals兜。

InheritableThreadLocal常用的方法也有3个:

//向Thread中某个口袋中放东西
public void set(T value);
//获取这个口袋中目前放的东西
public T get();
//清空这个口袋中放的东西
public void remove()

使用InheritableThreadLocal解决上面子线程中无法输出traceId的问题,只需要将上一个示例代码中的ThreadLocal替换成InheritableThreadLocal即可,代码如下:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class Demo4 {
    //创建一个操作Thread中存放请求任务追踪id口袋的对象,子线程可以继承父线程中内容
    static InheritableThreadLocal<String> traceIdKD = new InheritableThreadLocal<>();
    static AtomicInteger threadIndex = new AtomicInteger(1);
    //创建处理请求的线程池子
    static ThreadPoolExecutor disposeRequestExecutor = new ThreadPoolExecutor(3,
            3,
            60,
            TimeUnit.SECONDS,
            new LinkedBlockingDeque<>(),
            r -> {
                Thread thread = new Thread(r);
                thread.setName("disposeRequestThread-" + threadIndex.getAndIncrement());
                return thread;
            });
    //记录日志
    public static void log(String msg) {
        StackTraceElement stack[] = (new Throwable()).getStackTrace();
        //获取当前线程存放tranceId口袋中的内容
        String traceId = traceIdKD.get();
        System.out.println("****" + System.currentTimeMillis() + "[traceId:" + traceId + "],[线程:" + Thread.currentThread().getName() + "]," + stack[1] + ":" + msg);
    }
    //模拟controller
    public static void controller(List<String> dataList) {
        log("接受请求");
        service(dataList);
    }
    //模拟service
    public static void service(List<String> dataList) {
        log("执行业务");
        dao(dataList);
    }
    //模拟dao
    public static void dao(List<String> dataList) {
        CountDownLatch countDownLatch = new CountDownLatch(dataList.size());
        log("执行数据库操作");
        String threadName = Thread.currentThread().getName();
        //模拟插入数据
        for (String s : dataList) {
            new Thread(() -> {
                try {
                    //模拟数据库操作耗时100毫秒
                    TimeUnit.MILLISECONDS.sleep(100);
                    log("插入数据" + s + "成功,主线程:" + threadName);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            }).start();
        }
        //等待上面的dataList处理完毕
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        //需要插入的数据
        List<String> dataList = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            dataList.add("数据" + i);
        }
        //模拟5个请求
        int requestCount = 5;
        for (int i = 0; i < requestCount; i++) {
            String traceId = String.valueOf(i);
            disposeRequestExecutor.execute(() -> {
                //把traceId放入口袋中
                traceIdKD.set(traceId);
                try {
                    controller(dataList);
                } finally {
                    //将tranceId从口袋中移除
                    traceIdKD.remove();
                }
            });
        }
        disposeRequestExecutor.shutdown();
    }
}

你可能感兴趣的:(笔记,java)