Java 高并发编程 十三 CompletableFuture、ThreadLocal·

CompletableFuture·

2.1 Future接口理论知识复习·

Future接口(FutureTask实现类)定义了操作异步任务执行一些方法,如获取异步任务的执行结果、取消异步任务的执行、判断任务是否被取消、判断任务执行是否完毕等。举例:比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就去做其他事情了,忙完其他事情或者先执行完,过了一会再才去获取子任务的执行结果或变更的任务状态(老师上课时间想喝水,他继续讲课不结束上课这个主线程,让学生去小卖部帮老师买水完成这个耗时和费力的任务)。 image.png

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

2.2.1 Future接口能干什么·

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

2.2.2 Future接口相关架构·

  • 目的:异步多线程任务执行且返回有结果,三个特点:多线程、有返回、异步任务(班长为老师去买水作为新启动的异步多线程任务且买到水有结果返回)
  • 代码实现:Runnable接口+Callable接口+Future接口和FutureTask实现类。

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @author Guanghao Wei
* @create 2023-04-10 11:21
*/
public class CompletableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask = new FutureTask(new MyThread());
Thread t1 = new Thread(futureTask); //开启一个异步线程
t1.start();

System.out.println(futureTask.get()); //有返回hello Callable
}
}


class MyThread implements Callable<String> {

@Override
public String call() throws Exception {
System.out.println("--------come in");
return "hello Callable";
}
}

2.2.3 Future编码实战和优缺点分析·

  • 优点:Future+线程池异步多线程任务配合,能显著提高程序的运行效率。
  • 缺点:
    • get()阻塞—一旦调用get()方法求结果,一旦调用不见不散,非要等到结果才会离开,不管你是否计算完成,如果没有计算完成容易程序堵塞。
    • isDone()轮询—轮询的方式会耗费无谓的cpu资源,而且也不见得能及时得到计算结果,如果想要异步获取结果,通常会以轮询的方式去获取结果,尽量不要阻塞。
  • 结论:Future对于结果的获取不是很友好,只能通过阻塞或轮询的方式得到任务的结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* @author Guanghao Wei
* @create 2023-04-10 11:41
*/
public class FutureApiDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
FutureTask<String> futureTask = new FutureTask<>(() -> {
System.out.println(Thread.currentThread().getName() + "--------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() + " ------忙其他任务");
// System.out.println(futureTask.get(3,TimeUnit.SECONDS));//只愿意等待三秒,计算未完成直接抛出异常
while (true) {//轮询
if(futureTask.isDone()){
System.out.println(futureTask.get());
break;
}else{
TimeUnit.MILLISECONDS.sleep(500);
System.out.println("正在处理中,不要催了,越催越慢");
}
}
/* 轮询结果
* main ------忙其他任务
t1--------come in
正在处理中,不要催了,越催越慢
正在处理中,不要催了,越催越慢
正在处理中,不要催了,越催越慢
正在处理中,不要催了,越催越慢
正在处理中,不要催了,越催越慢
正在处理中,不要催了,越催越慢
正在处理中,不要催了,越催越慢
正在处理中,不要催了,越催越慢
正在处理中,不要催了,越催越慢
正在处理中,不要催了,越催越慢
task over
Process finished with exit code 0
* */
}
}

2.2.4 完成一些复杂的任务·

  • 对于简单的业务场景使用Future完全ok
  • 回调通知:
    • 应对Future的完成时间,完成了可以告诉我,也就是我们的回调通知
    • 通过轮询的方式去判断任务是否完成这样非常占cpu并且代码也不优雅
  • 创建异步任务:Future+线程池组合
  • 多个任务前后依赖可以组合处理(水煮鱼—>买鱼—>调料—>下锅):
    • 想将多个异步任务的结果组合起来,后一个异步任务的计算结果需要钱一个异步任务的值
    • 想将两个或多个异步计算合并成为一个异步计算,这几个异步计算互相独立,同时后面这个又依赖前一个处理的结果
  • 对计算速度选最快的:
    • 当Future集合中某个任务最快结束时,返回结果,返回第一名处理结果
  • 结论
    • 使用Future之前提供的那点API就囊中羞涩,处理起来不够优雅,这时候还是让CompletableFuture以声明式的方式优雅的处理这些需求。
    • 从i到i++
    • Future能干的,CompletableFuture都能干

2.3 CompletableFuture对Future的改进·

2.3.1 CompletableFuture为什么会出现·

  • get()方法在Future计算完成之前会一直处在阻塞状态下,阻塞的方式和异步编程的设计理念相违背。
  • isDone()方法容易耗费cpu资源(cpu空转),
  • 对于真正的异步处理我们希望是可以通过传入回调函数,在Future结束时自动调用该回调函数,这样,我们就不用等待结果

jdk8设计出CompletableFuture,CompletableFuture提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方。

2.3.2 CompletableFuture和CompletionStage介绍·

类架构说明image.png

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

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

四个静态构造方法image.png 对于上述Executor参数说明:若没有指定,则使用默认的ForkJoinPoolcommonPool()作为它的线程池执行异步代码,如果指定线程池,则使用我们自定义的或者特别指定的线程池执行异步代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* @author Guanghao Wei
* @create 2023-04-10 12:16
*/
public class CompletableFutureBuildDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(3);

CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
System.out.println(Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
},executorService);

System.out.println(completableFuture.get()); //null


CompletableFuture<String> objectCompletableFuture = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hello supplyAsync";
},executorService);

System.out.println(objectCompletableFuture.get());//hello supplyAsync

executorService.shutdown();

}
}

CompletableFuture减少阻塞和轮询,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* @author Guanghao Wei
* @create 2023-04-10 12:28
*/
public class CompletableFutureUseDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(3);
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "---come in");
int result = ThreadLocalRandom.current().nextInt(10);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (result > 5) { //模拟产生异常情况
int i = 10 / 0;
}
System.out.println("----------1秒钟后出结果" + result);
return result;
}, executorService).whenComplete((v, e) -> {
if (e == null) {
System.out.println("计算完成 更新系统" + v);
}
}).exceptionally(e -> {
e.printStackTrace();
System.out.println("异常情况:" + e.getCause() + " " + e.getMessage());
return null;
});
System.out.println(Thread.currentThread().getName() + "先去完成其他任务");
executorService.shutdown();
}
}

/**
* 无异常情况
* pool-1-thread-1---come in
* main先去完成其他任务
* ----------1秒钟后出结果9
* 计算完成 更新系统9
*/

/**
* 有异常情况
*pool-1-thread-1---come in
* main先去完成其他任务
* java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
* 异常情况:java.lang.ArithmeticException: / by zero java.lang.ArithmeticException: / by zero
*/

CompletableFuture优点:

  • 异步任务结束时,会自动回调某个对象的方法
  • 主线程设置好回调后,不用关心异步任务的执行,异步任务之间可以顺序执行
  • 异步任务出错时,会自动回调某个对象的方法

2.4 案例精讲-从电商网站的比价需求展开·

2.4.1 函数式编程已成为主流·

Lambda表达式+Stream流式调用+Chain链式调用+Java8函数式编程 函数时接口:

  • Runnable:无参数、无返回值

image.png

  • Function:接受一个参数,并且有返回值

image.png

  • Consumer:接受一个参数,没有返回值

image.png

  • BiConsumer:接受两个参数,没有返回值

image.png

  • Supplier:没有参数,有返回值

image.png 小结: image.png chain链式调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @author Guanghao Wei
* @create 2023-04-10 12:53
*/
public class CompletableFutureMallDemo {
public static void main(String[] args) {
Student student = new Student();
student.setId(1).setStudentName("z3").setMajor("english"); //链式调用
}
}


@AllArgsConstructor
@NoArgsConstructor
@Data
@Accessors(chain = true)//开启链式调用
class Student {
private Integer id;
private String studentName;
private String major;
}

2.4.2 大厂业务需求说明·

切记:功能—>性能(完成—>完美)电商网站比价需求分析:

  1. 需求说明:
    1. 同一款产品,同时搜索出同款产品在各大电商平台的售价
    2. 同一款产品,同时搜索出本产品在同一个电商平台下,各个入驻卖家售价是多少
  2. 输出返回:
    1. 出来结果希望是同款产品的在不同地方的价格清单列表,返回一个List

例如:《Mysql》 in jd price is 88.05 《Mysql》 in taobao price is 90.43

  1. 解决方案,对比同一个产品在各个平台上的价格,要求获得一个清单列表
    1. step by step,按部就班,查完淘宝查京东,查完京东查天猫…
    2. all in,万箭齐发,一口气多线程异步任务同时查询

2.4.3 一波流Java8函数式编程带走-比价案例实战Case·

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/**
* @author Guanghao Wei
* @create 2023-04-10 12:53
* 这里面需要注意一下Stream流方法的使用
* 这种异步查询的方法大大节省了时间消耗,可以融入简历项目中,和面试官有所探讨
*/
public class CompletableFutureMallDemo {
static List<NetMall> list = Arrays.asList(new NetMall("jd"), new NetMall("taobao"), new NetMall("dangdang"));

/**
* step by step
* @param list
* @param productName
* @return
*/
public static List<String> getPrice(List<NetMall> list, String productName) {
//《Mysql》 in jd price is 88.05
return list
.stream()
.map(netMall ->
String.format("《" + productName + "》" + "in %s price is %.2f",
netMall.getNetMallName(),
netMall.calcPrice(productName)))
.collect(Collectors.toList());
}

/**
* all in
* 把list里面的内容映射给CompletableFuture()
* @param list
* @param productName
* @return
*/
public static List<String> getPriceByCompletableFuture(List<NetMall> list, String productName) {
return list.stream().map(netMall ->
CompletableFuture.supplyAsync(() ->
String.format("《" + productName + "》" + "in %s price is %.2f",
netMall.getNetMallName(),
netMall.calcPrice(productName)))) //Stream<CompletableFuture<String>>
.collect(Collectors.toList()) //List<CompletableFuture<String>>
.stream()//Stream<String>
.map(s -> s.join()).collect(Collectors.toList()); //List<String>
}

public static void main(String[] args) {
/**
* 采用step by setp方式查询
* 《masql》in jd price is 110.11
* 《masql》in taobao price is 109.32
* 《masql》in dangdang price is 109.24
* ------costTime: 3094 毫秒
*/
long StartTime = System.currentTimeMillis();
List<String> list1 = getPrice(list, "masql");
for (String element : list1) {
System.out.println(element);
}
long endTime = System.currentTimeMillis();
System.out.println("------costTime: " + (endTime - StartTime) + " 毫秒");

/**
* 采用 all in三个异步线程方式查询
* 《mysql》in jd price is 109.71
* 《mysql》in taobao price is 110.69
* 《mysql》in dangdang price is 109.28
* ------costTime1009 毫秒
*/
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) + " 毫秒");

}
}

@AllArgsConstructor
@NoArgsConstructor
@Data
class NetMall {
private String netMallName;

public double calcPrice(String productName) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}

return ThreadLocalRandom.current().nextDouble() * 2 + productName.charAt(0);
}
}

2.4.4 CompletableFuture常用方法·

  • 获得结果和触发计算
    • 获取结果
      • public T get()
      • public T get(long timeout,TimeUnit unit)
      • public T join() —>和get一样的作用,只是不需要抛出异常
      • public T getNow(T valuelfAbsent) —>计算完成就返回正常值,否则返回备胎值(传入的参数),立即获取结果不阻塞
    • 主动触发计算
      • public boolean complete(T value) ---->是否打断get/join方法立即返回括号值
  • 对计算结果进行处理
    • thenApply —>计算结果存在依赖关系,这两个线程串行化---->由于存在依赖关系(当前步错,不走下一步),当前步骤有异常的话就叫停
    • handle —>计算结果存在依赖关系,这两个线程串行化---->有异常也可以往下走一步
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* @author Guanghao Wei
* @create 2023-04-10 13:43
*/
public class CompletableFutureApiDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1;
}, threadPool).thenApply(f -> {
System.out.println("222");
return f + 2;
}).handle((f, e) -> {
System.out.println("3333");
int i=10/0;
return f + 2;

// thenApply(f -> {
// System.out.println("3333");
// return f + 2;
}).whenComplete((v, e) -> {
if (e == null) {
System.out.println("----计算结果" + v);
}
}).exceptionally(e -> {
e.printStackTrace();
System.out.println(e.getCause());
return null;
});
System.out.println(Thread.currentThread().getName() + "------主线程先去做其他事情");
}
}

  • 对计算结果进行消费
    • 接受任务的处理结果,并消费处理,无返回结果
    • thenAccept
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @author Guanghao Wei
* @create 2023-04-10 13:59
*/
public class CompletableFutureApi2Demo {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture.supplyAsync(() -> {
return 1;
}, threadPool).thenApply(f -> {
return f + 2;
}).thenApply(f -> {
return f + 2;
}).thenAccept(r -> {
System.out.println(r);//5
});
}
}
  • 对比补充
    • thenRun(Runnable runnable) :任务A执行完执行B,并且不需要A的结果
    • thenAccept(Consumer action): 任务A执行完执行B,B需要A的结果,但是任务B没有返回值
    • thenApply(Function fn): 任务A执行完执行B,B需要A的结果,同时任务B有返回值
1
2
3
4
5
6
7
8
9
10
11
/**
* @author Guanghao Wei
* @create 2023-04-10 13:59
*/
public class CompletableFutureApi2Demo {
public static void main(String[] args) {
System.out.println(CompletableFuture.supplyAsync(() -> "result").thenRun(() -> {}).join());//null
System.out.println(CompletableFuture.supplyAsync(() -> "result").thenAccept(r -> System.out.println(r)).join());//result null
System.out.println(CompletableFuture.supplyAsync(() -> "result").thenApply(f -> f + 2).join());//result2
}
}
  • CompletableFuture和线程池说明
    • 如果没有传入自定义线程池,都用默认线程池ForkJoinPool
    • 传入一个线程池,如果你执行第一个任务时,传入了一个自定义线程池
      • 调用thenRun方法执行第二个任务时,则第二个任务和第一个任务时共用同一个线程池。(或者说使用上一步的线程池)
      • 调用thenRunAsync执行第二个任务时,则第一个任务使用的是你自定义的线程池,第二个任务使用的是ForkJoin线程池。(或者说不使用上一步的线程池,使用默认或自定义的线程池)
    • 备注:可能是线程处理太快,系统优化切换原则, 直接使用main线程处理,thenAccept和thenAcceptAsync,thenApply和thenApplyAsync等,之间的区别同理。
  • 对计算速度进行选用
    • 谁快用谁
    • applyToEither
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* @author Guanghao Wei
* @create 2023-04-10 14:11
* 可以合并写在一起,不必拆分
*/
public class CompletableFutureApiDemo {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture<String> playA = CompletableFuture.supplyAsync(() -> {
try {
System.out.println("A come in");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "playA";
}, threadPool);


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

CompletableFuture<String> result = playA.applyToEither(playB, f -> {
return f + " is winner";
});

/**
* A come in
* B come in
* main-----------winner:playA is winner
*/
System.out.println(Thread.currentThread().getName() + "-----------winner:" + result.join());
}
}
  • 对计算结果进行合并
    • 两个CompletableStage任务都完成后,最终能把两个任务的结果一起交给thenCombine来处理
    • 先完成的先等着,等待其他分支任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* @author Guanghao Wei
* @create 2023-04-10 14:28
* 可以合并写在一起,不必拆分
*/
public class CompletableFutureApi3Demo {
public static void main(String[] args) {
CompletableFuture<Integer> completableFuture1 = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + " 启动");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 10;
});

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

CompletableFuture<Integer> finalResult = completableFuture1.thenCombine(completableFuture2, (x, y) -> {
System.out.println("----------开始两个结果合并");
return x + y;
});
System.out.println(finalResult.join());

}
}

ThreadLocal·

9.1 ThreadLocal简介·

9.1.1 面试题·

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

9.1.2 是什么?·

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

9.1.3 能干吗?·

实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不用麻烦别人,不和其他人共享,人人有份,人各一份)。主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其改为当前线程所存的副本的值从而避免了线程安全问题。比如8锁案例中,资源类是使用同一部手机,多个线程抢夺同一部手机,假如人手一份不是天下太平?

9.1.4 API介绍·

image.png

9.1.5 永远的helloworld讲起·

  • 问题描述:5个销售买房子,集团只关心销售总量的准确统计数,按照总销售额统计,方便集团公司给部分发送奖金--------群雄逐鹿起纷争------为了数据安全只能加锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* @author Guanghao Wei
* @create 2023-04-13 14:06
* 需求:5个销售卖房子,集团只关心销售总量的精确统计数
*/
class House {
int saleCount = 0;

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

}

public class ThreadLocalDemo {
public static void main(String[] args) {
House house = new House();
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
int size = new Random().nextInt(5) + 1;
System.out.println(size);
for (int j = 1; j <= size; j++) {
house.saleHouse();
}
}, String.valueOf(i)).start();

}
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "共计卖出多少套: " + house.saleCount);
}
}
/**
* 3
* 4
* 2
* 4
* 2
* main 共计卖出多少套: 15
*/

  • 需求变更:希望各自分灶吃饭,各凭销售本事提成,按照出单数各自统计-------比如房产中介销售都有自己的销售额指标,自己专属自己的,不和别人参和。----人手一份天下安
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* @author Guanghao Wei
* @create 2023-04-13 14:06
* 需求:需求变更:希望各自分灶吃饭,各凭销售本事提成,按照出单数各自统计-------比如房产中介销售都有自己的销售额指标,自己专属自己的,不和别人参和。
*/
class House {
int saleCount = 0;

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

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

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


}

public class ThreadLocalDemo {
public static void main(String[] args) {
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);
}
}
/**
* 3 号销售卖出:1
* 4 号销售卖出:3
* 5 号销售卖出:4
* 2 号销售卖出:3
* 1 号销售卖出:5
* main 共计卖出多少套: 16
*/

9.1.6 总结·

  • 因为每个Thread内有自己的实例副本且该副本只有当前线程自己使用
  • 既然其他ThreadLocal不可访问,那就不存在多线程间共享问题
  • 统一设置初始值,但是每个线程对这个值得修改都是各自线程互相独立得
  • 如何才能不争抢
    • 加入synchronized或者Lock控制资源的访问顺序
    • 人手一份,大家各自安好,没有必要争抢

9.2 ThreadLocal源码分析·

9.2.1 源码解读·

9.2.2 Thread、ThreadLocal、ThreadLocalMap关系·

  • Thread和ThreadLocal,人手一份
    • image.png
  • ThreadLocal和ThreadLocalMap
    • image.png
  • 三者总概括
    • image.png
    • ThreadLocalMap实际上就是一个以ThreadLocal实例为Key,任意对象为value的Entry对象
    • 当我们为ThreadLocal变量赋值,实际上就是以当前ThreadLocal实例为Key,值为value的Entry往这个ThreadLocalMap中存放
    • image.png

9.2.3 总结·

  • ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以ThreadLocal为Key),不过是经过了两层包装的ThreadLocal对象:
    • image.png
  • JVM内部维护了一个线程版的Map<ThreadLocal, Value>(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当作Key,放进了ThreadLocalMap中),每个线程要用到这个T的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。

9.3 ThreadLocal内存泄漏问题·

9.3.1 什么是内存泄漏·

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

9.3.2 谁惹的祸?·

  • 再回首ThreadLocalMap
    • image.png
  • 强软弱虚引用
    • image.png
    • 强引用:
      • 对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收,当一个对象被强引用变量引用时,它处于可达状态,是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到,JVM也不会回收,因此强引用是造成Java内存泄露的主要原因之一。
    • 软引用:
      • 是一种相对强引用弱化了一些的引用,对于只有软引用的对象而言,当系统内存充足时,不会被回收,当系统内存不足时,他会被回收,软引用通常用在对内存敏感的程序中,比如高速缓存,内存够用就保留,不够用就回收。
    • 弱引用:
      • 比软引用的生命周期更短,对于只有弱引用的对象而言,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。
    • 软引用和弱引用的使用场景----->假如有一个应用需要读取大量的本地图片:
      • 如果每次读取图片都从硬盘读取则会严重影响性能
      • 如果一次性全部加载到内存中又可能会造成内存溢出
      • 此时使用软应用来解决,设计思路时:用一个HashMap来保存图片的路径和与相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,有效避免了OOM的问题
    • 虚引用:
      • 虚引用必须和引用队列联合使用,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都有可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象。
      • 虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize后,做某些事情的通知机制。换句话说就是在对象被GC的时候会收到一个系统通知或者后续添加进一步的处理,用来实现比finalize机制更灵活的回收操作。

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

image.png

  • 为什么要用弱引用:
    • 当方法执行完毕后,栈帧销毁,强引用t1也就没有了,但此时线程的ThreadLocalMap里某个entry的Key引用还指向这个对象,若这个Key是强引用,就会导致Key指向的ThreadLocal对象即V指向的对象不能被gc回收,造成内存泄露
    • 若这个引用时弱引用就大概率会减少内存泄漏的问题(当然,还得考虑key为null这个坑),使用弱引用就可以使ThreadLocal对象在方法执行完毕后顺利被回收且entry的key引用指向为null
  • 这里有个需要注意的问题:
    • 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对象后,手动调用remove方法来删除它,尤其是在线程池中,不仅仅是内存泄漏的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。
    • 清除脏Entry----key为null的entry
      • set()方法
        • image.png
      • get()方法
        • image.png
      • remove()
        • image.png

9.3.4 最佳实践·

  • ThreadLocal一定要初始化,避免空指针异常。
  • 建议把ThreadLocal修饰为static
  • 用完记得手动remove

9.4 小总结·

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