JUC并发编程,深入学习Java并发编程,与视频每一P对应,全系列6w+字。
【Java速通大厂必备知识免费下载:夸克网盘分享】
P1-5 为什么学+特色+预备知识 进程线程概念
进程:
一个程序被运行,从磁盘加载这个程序的代码到内存,就开起了一个进程。
进程可以视为程序的一个实例,大部分程序可以同时运行多个实例进程(笔记本,记事本,图画,浏览器等),也有的程序只能启动一个实例进程(网易云音乐,360安全卫士等)。
线程:
一个进程内可以分为一到多个线程。
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行。
Java中线程是最小调度单元,进程作为资源分配的最小单位。在windows中进程是不活动的,知识作为线程的容器。
对比:
进程拥有共享的资源,如内存空间等,供其内部线程共享。
进程间通信较为复杂:同一台计算机的进程通信称为IPC。不同计算机之间的进程通信,需要通过网络,遵守共同的协议。
线程通信简单,因为共享进程内的内存,多个线程可以访问同一个共享变量。
线程更轻量,上下文切换成本要比进程上下文切换低。
给项目引入如下pom依赖:
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration
xmlns="http://ch.qos.logback/xml/ns/logback"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback logback.xsd">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date{HH:mm:ss} [%t] %logger - %m%n</pattern>
</encoder>
</appender>
<logger name="c" level="debug" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>
<root level="ERROR">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
P6 并发并行概念
操作系统任务调度器,可以把CPU时间交给不同线程使用,线程可以轮流使用CPU资源。
假如CPU为单核,同一时间段应对多件事情叫并发。同一时间段处理多件事情的能力。一个人做多件事。
假如CPU为多核,多个核心同时执行任务,叫作并行。同一时间同时做多件事情的能力。多个人做多件事。
P7 线程应用异步调用
同步:需要等待结果返回,才能继续运行。
异步:不需要等待结果返回,就能继续运行。
多线程可以让方法执行变为异步的,不会干巴巴等着,比如读取磁盘要花费5秒,如果没有线程调度机制,这5秒什么事情都做不了。
视频文件要转换格式操作比较费时,可以开一个新线程处理视频转换,避免阻塞主线程。
P8 线程应用提升效率
P9 P10 线程应用提升效率验证和小结
单核多线程比单核单线程的速度慢。
多核多线程比多核单线程快。
P11 创建线程方法1
源代码是在如下位置:
一开始默认有一个主线程在运行。
@Slf4j(topic = "c.Test1")
public class Test1 {
public static void main(String[] args){
Thread t = new Thread(){
@Override
public void run(){
log.debug("running");
}
};
t.setName("t1");
t.start();
log.debug("running");
}
}
P12 创建线程方法2
使用Runnable配合Thread创建线程:
@Slf4j(topic="c.Test2")
public class test2 {
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
log.debug("running");
}
};
Thread t = new Thread(r,"t2");
t.start();
}
}
将任务和线程分离:
P13 创建线程lambda简化
@Slf4j(topic="c.Test2")
public class test2 {
public static void main(String[] args) {
Runnable r =()->{
log.debug("running");
};
Thread t = new Thread(r,"t2");
t.start();
}
}
超级简化版:
@Slf4j(topic="c.Test2")
public class test2 {
public static void main(String[] args) {
Thread t = new Thread(()->{
log.debug("running");
},"t2");
t.start();
}
}
P14 创建线程方法1,2-原理
P15 创建线程方法3
FutureTask配合Thread,FutureTask能够接收Callable类型的参数,用来处理有返回结果的情况。
@Slf4j(topic="c.Test2")
public class Test3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("running...");
Thread.sleep(2000);
return 100;
}
});
Thread t1 = new Thread(task,"t1");
t1.start();
log.debug("{}",task.get());//阻塞住,等待线程,直到线程返回结果
}
}
P16 线程运行现象
交替运行。
P17 线程运行windows查看和杀死
查看方式:1.通过任务管理器。2.在控制台输入tasklist
找到java进程:
tasklist | findstr java
查看所有java进程:
jps
杀死某个进程:
taskkill /F /PID PID号
P18 线程运行linux查看和杀死
列出所有正在执行的进程信息:
ps -fe
用grep关键字进行筛选:
ps -fe | grep 关键字
查看java进程页可以用Jps。
杀死某个进程:
kill PID号
查看进程内的线程信息:
top -H -p PID号
P19 线程运行jconsole
输入win+r,键入jconsole,可以打开图形化界面。
可以远程连接到服务器监控信息。
P20 线程运行原理栈帧debug
JVM由堆、栈、方法区组成。栈内存是给线程用的,每个线程启动后,虚拟机会为其分配一块栈内存。
栈由栈帧组成,对应每次方法调用时所占用的内存。
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
P21 线程运行原理栈帧图解
返回地址对应的是方法区中的方法,局部变量对应的是堆中的对象。
P22 线程运行原理多线程
P23 线程运行原理上下文切换
CPU不再执行当前的线程,转而执行另一个线程的代码:
1.线程的CPU时间片用完。
2.垃圾回收。暂停当前所有的工作线程,让垃圾回收的线程去回收垃圾。
3.有更高优先级的线程需要运行。
4.线程自己调用了sleep,yield,wait,join,park,synchronized,lock等方法。
当Context Switch发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应概念是程序计数器,作用是记住下一条jvm指令的执行地址,是线程私有的。
状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址。
P24 常见方法概述
start() 启动一个新线程,在新的线程运行run方法中的代码。start方法只能让线程进入就绪,代码不一定立即执行(只有等CPU的时间片分配给它才能运行)。每个线程对象的start方法只能调用一次。
join()等待线程运行结束。假如当前的主线程正在等待某个线程执行结束后返回的结果,就可以调用这个join方法。join(long n)表示最多等待n毫秒。
getId()获得线程id,getName()获得线程名称,setName()设置线程名称,getPriority()获得优先级,setPriority(int)设置线程优先级,getStatus()获取线程状态,isInterupted()判断是否被打断,isAlive()判断线程是否存活,interrupt()打断线程,interrupted()判断当前线程是否被打断。
currentThread()获取当前正在执行的线程,sleep(long n)让当前执行的线程休眠n毫秒,休眠时让出其cpu的时间片给其它线程。
yield()提示线程调度器让出当前线程对CPU的使用。
P25 常见方法start vs run
用run时是主线程来执行run方法。无法做到异步。
@Slf4j(topic="c.Test4")
public class Test4 {
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running...");
}
};
t1.run();
}
}
下面是使用start方法启动,可以异步执行任务。
@Slf4j(topic="c.Test4")
public class Test4 {
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running...");
}
};
System.out.println(t1.getState());
t1.start();
System.out.println(t1.getState());
}
}
在new之后start之前是NEW状态,在start之后是RUNNABLE状态。
P26 常见方法sleep状态
sleep让线程从running状态变成time waiting状态,从运行状态变到有时限(因为会传递一个参数)的等待状态。
P27 常见方法sleep打断
正在睡眠的线程可以由其它线程用interrupt方法打断唤醒。此时睡眠的方法会抛出InterruptException。
程序思路,t1.start执行完,输出begin,然后休眠,执行t1的run方法输出enter slee...,然后休眠,1秒到后输出interrupt,最终t1.interrupt方法被调用,休眠线程立刻被打断,开始执行wake up....
@Slf4j(topic="c.Test7")
public class Test6 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1") {
public void run() {
log.debug("enter sleep....");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
log.debug("wake up...");
throw new RuntimeException(e);
}
}
};
t1.start();
log.debug("begin");
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
}
}
P28 常见方法sleep可读性
建议用TimeUnit的sleep代替Thread的sleep来获得更好的可读性。
@Slf4j(topic = "c.Test8")
public class Test7 {
public static void main(String[] args) throws InterruptedException {
log.debug("enter");
TimeUnit.SECONDS.sleep(1);
log.debug("end");
}
}
P29 常见方法yield_vs_sleep
1.yield
某个线程调用yield,可以让出CPU的使用权。
调用yield会让当前线程从Running进入Runnable就绪状态,然后调度执行其它线程。
2.sleep
调用sleep会让当前线程从Running进入Timed Waitring状态(阻塞)
P30 常见方法线程优先级
线程优先级会提示(hint)调度器优先调度该线程,但它仅仅只是一个提示,调度器可以忽略它。
如果cpu较忙,优先级高的线程会获得更多的时间片,但cpu如果闲时,优先级几乎没作用。
@Slf4j(topic="c.Test4")
public class test4 {
public static void main(String[] args) {
Runnable task1 =()->{
int count=0;
for(;;){
System.out.println("------>1"+count++);
}
};
Runnable task2 =()->{
int count=0;
for(;;){
//Thread.yield();
System.out.println(" ------>2"+count++);
}
};
Thread t1 = new Thread(task1,"t1");
Thread t2 = new Thread(task2,"t2");
//t1.setPriority(Thread.MIN_PRIORITY);
//t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
}
}
P31 常见方法sleep应用
在没有利用cpu来计算时,不要让while(true)空转浪费cpu,这时可以使用yield或sleep来让出cpu的使用权给其它程序。
可以用wait或者条件变量达到类似的效果。但需要加锁,并且需要设置相应的唤醒操作,一般适用于要进行同步的场景。sleep适合无锁同步的场景。
P32 常见方法join
join等待某个线程执行结束。
下面这个例子因为t1线程睡了1秒,对r的更改不会发生,主线程会直接输出r的结果r=0。此时若想让r=10,则需要在t1.start()的下面加上t1.join()表示等待t1执行结束返回结果,主线程再执行。
@Slf4j(topic="c.Test5")
public class test5 {
static int r=0;
public static void main(String[] args) throws InterruptedException{
test1();
}
public static void test1() throws InterruptedException{
log.debug("开始");
Thread t1 = new Thread(()->{
log.debug("开始");
sleep(1);
log.debug("结束");
r=10;
},"t1");
t1.start();
t1.join();
log.debug("结果为:{}",r);
log.debug("结果");
}
}
P33 常见方法join同步应用
需要等待结果返回,才能继续运行是同步。
不需要等待结果返回,就能继续运行是异步。
@Slf4j(topic = "c.TestJoin")
public class TestJoin {
static int r = 0;
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test2();
}
private static void test2() throws InterruptedException {
Thread t1 = new Thread(() -> {
sleep(1);
r1 = 10;
});
Thread t2 = new Thread(() -> {
sleep(2);
r2 = 20;
});
t1.start();
t2.start();
long start = System.currentTimeMillis();
log.debug("join begin");
t1.join();
log.debug("t1 join end");
t2.join();
log.debug("t2 join end");
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
}
P34 常见方法join限时同步
下面给t1.join()设置了1500毫秒等待时间,因为小于线程睡眠时间,所以没法能线程苏醒改变r,输出结果为r1=0。
@Slf4j(topic = "c.TestJoin")
public class TestJoin {
static int r = 0;
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test3();
}
public static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
sleep(2);
r1 = 10;
});
long start = System.currentTimeMillis();
t1.start();
// 线程执行结束会导致 join 结束
log.debug("join begin");
t1.join(1500);
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
}
P35 常见方法interrupt打断阻塞
如果线程是在睡眠中被打断会以报错的形式出现,打断标记为false。
@Slf4j(topic="c.Test6")
public class test6 {
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(() -> {
log.debug("sleep...");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
log.debug("打断标记:{}",t1.isInterrupted());
}
}
P36 常见方法interrupt打断正常
如果在main方法中调用t1的interrupt方法,t1线程只是会被告知有线程想打断,不会强制被退出。此时isinterrupted状态会被设为true,此时可以利用该状态来让线程决定是否退出。
@Slf4j(topic="c.Test7")
public class Test7 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while(true){
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted){
log.debug("被打断了,退出循环");
break;
}
}
},"t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
}
}
P37 设计模式两阶段终止interrupt
在一个线程T1中如何优雅的终止线程T2,这里的优雅指的是给T2一个料理后事的机会。
错误思路:
1.使用线程对象的stop方法停止线程。stop方法会真正杀死线程,如果线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁。
2.使用System.exit(int)方法会直接把方法停止,直接把进程停止。
P38 设计模式两阶段终止interrupt分析
在工作中被打断,打断标记是false,会进入到料理后事。
在睡眠是被打断,会抛出异常,此时打断标记是true,此时可以重新设置打断标记为false。
P39 设计模式两阶段终止interrupt实现
@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination{
private Thread monitor;
public void start(){
monitor = new Thread(()->{
while(true) {
Thread current = Thread.currentThread();
if (current.isInterrupted()) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("执行监控记录");
} catch (InterruptedException e) {
e.printStackTrace();
//重新设置打断标记
current.interrupt();
}
}
});
monitor.start();
}
public void stop(){
monitor.interrupt();
}
}
P40 设计模式两阶段终止interrupt细节
P41 常见方法interrupt打断park
@Slf4j(topic="c.Test9")
public class java9 {
public static void main(String[] args) throws InterruptedException{
test1();
}
public static void test1() throws InterruptedException{
Thread t1 = new Thread(()->{
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}",Thread.interrupted());
LockSupport.park();
log.debug("unpark...");
},"t1");
t1.start();
sleep(1);
t1.interrupt();
}
}
P42 常见方法过时方法
切忌用stop,suspend方法。
P43 常见方法守护线程
默认情况下,Java进程需要等待所有的线程都运行结束,才会结束。
有一种特殊的线程叫守护线程,只要其它非守护线程执行结束了,即时守护线程的代码没有执行完,也会强制结束。
在t1启动前调用setDaemon方法开启守护线程,如果主线程运行结束,守护线程也会结束。
@Slf4j(topic="c.Test15")
public class Test10 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while(true){
if(Thread.currentThread().isInterrupted()){
break;
}
}
});
t1.setDaemon(true);
t1.start();
Thread.sleep(1000);
log.debug("结束");
}
}
垃圾回收器线程是一种守护线程。如果程序停止,垃圾回收线程也会被强制停止。
P44 线程状态五种
初始状态:在语言层面上创建线程对象,还没与操作系统中的线程关联,仅停留在对象层面。比如new了一个Thread对象,但没调用start方法。
可运行状态:就绪状态,线程已经被创建,与操作系统线程关联,可以由CPU调度器调度执行,可以获得CPU时间片,但暂时没获得时间片。
运行状态:指获取了CPU时间片,运行中的状态。
阻塞状态:调用了阻塞API,比如BIO读写文件,线程不会用到CPU,会导致上下文切换,进入阻塞状态。等BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至可运行状态。
终止状态:线程已经执行完毕,生命周期结束,不会再转换为其它状态。
P45 线程状态六种
从Java的层面进行描述:
NEW:指被创建,还没调用Start方法。
RUNNABLE:涵盖了操作系统层面的可运行、运行、阻塞状态。
TERMINATED:指被终止状态,不会再转化为其它状态。
3种阻塞的状态:
BLOCKED(想获得锁,但获得不了,拿不到锁会陷入block状态)
WAITING(这个是join等待时的状态)
TIMED_WAITING(这个是sleep时的状态,有时限的等待)
P46 线程状态六种演示
P47 习题应用之统筹分析
P48 习题应用之统筹实现
@Slf4j(topic = "c.Test16")
public class Test11 {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
log.debug("洗水壶");
Sleeper.sleep(1);
log.debug("烧开水");
Sleeper.sleep(5);
},"老王");
Thread t2 = new Thread(()->{
log.debug("洗茶壶");
Sleeper.sleep(1);
log.debug("洗茶杯");
Sleeper.sleep(2);
log.debug("拿茶叶");
Sleeper.sleep(1);
try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("泡茶");
},"小王");
t1.start();
t2.start();
}
}
缺点:上面模拟的是小王等老王的水烧开了,小王泡茶,如果反过来要实现老王等小王的茶叶拿过来,老王泡茶呢?代码最好能适应2种情况。
上面的两个线程各执行各的,如果要模拟老王把水壶交给小王泡茶,或模拟小王把茶叶交给老王泡茶呢?
P49 第三章小节
P50 本章内容
P51 小故事线程安全问题
多线程下访问共享资源,因为分时系统导致的数据不一致等安全问题。
P52 上下文切换分析
@Slf4j(topic="c.Test12")
public class Test12 {
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", counter);
}
}
结果并不唯一如下:
i++和i--编译成字节码不是一条代码:
造成数据不一致的原因是:
某个线程的事情还没干完,数据还没来得及写入,上下文就切换了。根本原因:上下文切换导致指令交错。
P53 临界区与竞态条件
问题出现在多个线程访问共享资源。
在多个线程对共享资源读写操作时发生指令交错,出现问题。
一段代码内如果存在对共享资源的多线程读写操作,称这段代码为临界区。
竞态条件:多个
P54 上下文切换synchronized解决
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
1.阻塞式的解决方案:synchronized,Lock。
2.非阻塞式的解决方案:原子变量。
本次课使用的是synchronized来解决问题,即对象锁,它采用互斥的方式来让同一时刻至多只能有1个线程持有对象锁,其它线程想获取对象锁会被阻塞。这样保证拥有锁的线程可以安全的执行临界区内的代码,不用担心上下文切换。
synchronized(对象){
临界区
}
@Slf4j(topic="c.Test12")
public class Test12 {
static int counter = 0;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock){
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock){
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", counter);
}
}
P55 上下文切换synchronized理解
假如t1通过synchronized拿到锁以后,但是时间片不幸用完了,但这个锁仍旧是t1的,只有时间片下次重新轮到t1时才能继续执行。
只有当t1执行完synchronized()块内的代码,会释放锁。其它线程才能竞争。
P56 上下文切换synchronized理解
当锁被占用时,就算指令没执行完上下文切换,其它线程也获取不到锁,只有当拥有锁的线程的所有代码执行完才能释放锁。
P57 上下文切换synchronized思考
1.把加锁提到for循环外,相当于5000次for循环都视为一个原子操作。
2.如果线程1加锁,线程2没加锁会导致的情况:线程2去访问临界资源时,不会尝试获取对象锁,因此不会被阻塞住,仍然能继续访问。
P58 锁对象面向对象改进
@Slf4j(topic="c.Test12")
public class Test12 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room){
room.increment();
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room){
room.decrement();
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", room.getCounter());
}
}
class Room{
private int counter = 0;
public void increment(){
synchronized (this){
counter++;
}
}
public void decrement(){
synchronized (this){
counter--;
}
}
public int getCounter(){
synchronized (this){
return counter;
}
}
}
P59 synchronized 加在方法上
synchronized可以加在方法上,相当于锁住方法。
synchronized加在静态方法上,相当于所住类。
P60 P61 P62上下文切换synchronized习题1~8
线程1锁的是类,线程2锁的是方法。
下面一个锁类一个锁方法,锁的仍然是不同对象,所以会并行执行。
锁的是同一个Number对象,锁的是静态方法,所以锁的是类。
P63 线程安全分析
P64 线程安全分析局部变量
局部变量的i++只有一行字节码,不同于静态变量的i++。
P65 线程安全分析局部变量引用
创建2个线程,然后每个线程去调用method1:
如果method1还没把数据放入,method2就要取出数据,此时集合为空,会报错。
将list改为局部变量后,放到方法内:
list是局部变量,每个线程会创建不同实例,没有共享。
method2和method3的参数从method1中传递过啦,与method1中引用同一个对象。
P66 线程安全分析 局部变量暴露引用
因为下面ThreadSafeSubClass继承了ThreadSafe类,然后重写了method3方法,导致出现了问题。
必须要改为private和final防止子类去重写和修改,满足开闭原则,不让子类改变父类的行为。
P67 线程安全分析 局部变量组合调用
常见线程安全的类:
String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent包下的类
注意:每个方法是原子的,单多个方法的组合不是原子的。
下面Hashtable的get和put单个方法是线程安全的,但二者组合在一起仍然会受到线程上下文的切换的影响。
P68 线程安全分析 常见类 不可变
String、Integer等都是不可变类,即时被线程共享,因为其内部的状态不可以改变,因此它们的方法是线程安全的。
String的replace和substring等方法看似可以改变值,实则是创建了一个新的字符串对象,里面包含了截取后的结果。
P69 线程安全分析 实例分析1~3
线程不安全:Map<String,Object> map = new HashMap<>();
线程不安全:Date
下面这段非线程安全:
下面这段非线程安全:
Spring里某一个对象没有加Scope都是单例的,只有1份,成员变量需要被共享。
P70 线程安全分析 实例分析4~7
下面这个方法是线程安全,因为没有成员变量,也就是类下没有定义变量。变量在方法内部,各自都在线程的栈内存中,因此是线程安全的。
下面是线程安全的,因为UserDaoImpl里面没有可以更改的成员变量(无状态)。
下面是线程安全的,因为是通过new来创建对象,相当于每个线程拿到的是不一样的副本。
P71 习题 卖票 读题
证明方法:余票数和卖出去的票数相等,代表前后一致,没有线程安全问题。
@Slf4j(topic="c.ExerciseSell")
public class ExerciseTransfer {
public static void main(String[] args) throws InterruptedException {
//模拟多人买票
TicketWindow window = new TicketWindow(100000);
//所有线程的集合
List<Thread> threadList = new ArrayList<>();
//卖出的票数统计
List<Integer> amountList = new Vector<>();
for(int i=0;i<20000;i++){
Thread thread = new Thread(()->{
int amount = window.sell(randomAmount());//买票
try {
Thread.sleep(randomAmount());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
amountList.add(amount);
});
threadList.add(thread);
thread.start();
}
for (Thread thread :threadList) {
thread.join();
}
log.debug("余票:{}",window.getCount());
log.debug("卖出的票数:{}",amountList.stream().mapToInt(i->i).sum());
}
static Random random = new Random();
public static int randomAmount(){
return random.nextInt(5)+1;
}
}
class TicketWindow{
private int count;
public TicketWindow(int count){
this.count = count;
}
public int getCount(){
return count;
}
public int sell(int amount){
if(this.count >= amount){
this.count -= amount;
return amount;
}else{
return 0;
}
}
}
P72 习题 卖票 测试方法
老师用的是一个测试脚本进行测试。
P73 习题 卖票 解题
临界区:多个线程对共享变量有读写操作。
在sell方法中存在对共享变量的读写操作,因此只需要在方法上加synchronized:
P74 习题 转账
这道题的难点在于有2个共享变量,一个是a的账户中的money,一个是b的账户中的money。
@Slf4j(topic="c.ExerciseTransfer")
public class ExerciseTransfer1{
public static void main(String[] args) throws InterruptedException {
Account a = new Account(1000);
Account b = new Account(1000);
Thread t1 = new Thread(()->{
for (int i = 0; i < 1000; i++) {
a.transfer(b,randomAmount());
}
},"t1");
Thread t2 = new Thread(()->{
for (int i = 0; i < 1000; i++) {
b.transfer(a,randomAmount());
}
},"t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("total:{}",(a.getMoney()+b.getMoney()));
}
static Random random = new Random();
public static int randomAmount(){return random.nextInt(100)+1;}
}
class Account {
private int money;
public Account(int money){
this.money = money;
}
public int getMoney(){
return money;
}
public void setMoney(int money){
this.money = money;
}
public void transfer(Account target,int amount){
synchronized(Account.class) {
if (this.money >= amount) {
this.setMoney(this.getMoney() - amount);//自身余额,减去转账金额
target.setMoney(target.getMoney() + amount);//对方余额加上转账金额
}
}
}
}
偷懒的方法加入下面:synchronized(Account.class),相当于锁住两个账户的临界资源,缺点是n个账户只能有2个账户进行交互。
P75 Monitor 对象头
Klass word是一个指针,指向某个对象从属的Class,找到类对象,每个对象通过Klass来辨明自己的类型。
在32位虚拟机中:Integer:8+4,int:4。
P76 Monitor 工作原理
Monitor是锁,Monitor被翻译为监视器或管程。每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁之后,该对象头的Mark Word中就被设置指向Monitor对象的指针。
obj是java的对象,Monitor是操作系统提供的监视器,调用synchronized是将obj和Monitor进行关联,相当于在MarkWord里面记录Monitor里面的指针地址。
Monitor里面的Owner记录的是当前哪个线程享有这个资源,EntryList是一个线程队列,来一个线程就进入到阻塞队列。
P77 Monitor 工作原理 字节码角度
下面是源代码:
getstatic #2 拿到lock锁的对象的引用,将来会关联monitor。
dup 复制了一份
astore_1 存储到slot 1中,为了将来解锁用
monitorenter 将lock对象MarkWord置为Monitor指针
monitorexit是充值MarkWord,唤醒EntryList让线程能够重新竞争锁。
如果第6到16行出现问题,会到第19行执行,astore_2是将异常对象e存放到slot 2中。
第20行是找到lock对象的引用地址,第21行是重置MarkWord,唤醒EntryList,第23行athrow是抛出处理不了的异常。
P78 synchronized 优化原理 小故事
P79 synchronized 优化原理 轻量级锁
轻量级锁使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的,也就是没有竞争,那么可以使用轻量级锁优化。
轻量级锁对使用者是透明的,语法仍然是synchronized
对象(Object)分为对象头和对象体。对象头由2部分组成,Mark Word(包含哈希码、分代年龄、状态位)和Klass Word(类型指针)。对象体(存储成员变量的信息)。
线程(Thread)会在栈帧里生成锁记录(Lock Record)对象,包含对象指针(Object reference)和要加锁记录地址。
对象指针是为了后期加锁之后,记录对象(Object)的地址。
加锁之后,要让锁记录中的对象指针(Object reference)指向锁对象(Object)。
然后要让锁记录里的“lock record 地址 00”和锁对象里的“Hashcode Age Bias 01”进行交换。交换是为了表示加锁。
这么做的意义是:对对象(Object)来说,它能知道是哪个线程锁住了自己。对线程来说它能知道锁对象的信息。
锁状态01代表无锁状态,00代表轻量级锁。
现在的问题是:假如现在同一个线程中,又创建了一个栈帧,它想把自己的锁记录地址和锁对象中的Mark Word进行替换,但此时锁对象没有Mark Word,因为原先Mark Word的位置中是当前线程前一个锁记录的地址。
这种情况会cas失败,当这种自身线程执行了synchronized锁重入的情况发生,会再添加一条锁记录(Lock Record)作为重入的计数。
新的锁记录的原先的地址处会为null(如上图),当退出synchronized代码块,也就是解锁时,如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减1。
当一直退出,一直退出,退出直到synchronized代码块的锁记录不为null,此时会进行锁清除,线程会把Mark Word(“Hashcode Age Bias 01”)这部分内容还给Object,Object会把锁记录地址(“lock record 地址 00”)还给线程。
P80 synchronized 优化原理 锁膨胀
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时代表有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变成重量级锁。
如上图Thread1想加锁,但由于锁对象(Object)已经被Thread0加锁,Thread1的锁记录地址(“lock record 地址00”)无法与Object的Mark Word进行替换,因此会进入锁膨胀的阶段。
如下图加的是重量级锁,首先要为锁对象(Object)申请Monitor锁,让Object指向重量级锁的地址。
Monitor的Owner会指向线程0,然后线程1会挂载到EntryList上。
当Thread0退出同步块解锁时,使用cas将Mark Word的值(“Hashcode Age Bias 01”)恢复给锁对象(Object),因为原先的“lock record 地址00”变成了“Monitor 地址”,所以失败了。
此时会进入重量级锁的解锁流程,首先会根据Monitor地址找到Monitor对象,设置Owner为null,同时唤醒EntryList中BLOCKED线程。
P81 synchronized 优化原理 自旋优化
自旋:多次cas尝试修改Mark Word,让Mark Word指向Lock Record
重量级锁竞争的时候(线程2请求加锁,此时线程1已经上了锁),还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞(避免上下文切换)。
下图是自旋成功的情况:
下图是自旋失败的情况:
在Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次。反之则少自旋甚至不自旋,比较智能。
自旋会占用CPU时间,单核CPU自旋会浪费,多核CPU自旋才有优势。
P82 synchronized 优化原理 偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作。
Java 6中引入了偏向锁来做进一步优化,只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS,以后只要不发生竞争,这个对象就归该线程所有。
P83 synchronized 优化原理 偏向锁 状态
如果开启了偏向锁(默认开启),那么对象创建后,MarkWord值为0x05即最后3位为101,这时它的thread、epoch、age都为0。
偏向锁是默认延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数,-XX:BiasedLockingStartupDelay=0来禁用延迟。
如果没有开启偏向锁,那么对象创建后,MarkWord值为0x01即最后3位为001,这时它的hashcode、age都为0,第一次用到hashcode时才会赋值。
偏向锁可以理解为:某个锁对象“偏向于”只给某个线程使用。
如果有其它线程来使用了锁对象,偏向锁会变成轻量锁。
如果线程之间存在竞争关系,轻量锁就会编程重量锁。
当偏向锁调用hashcode()方法之后偏向锁会撤销。
P84 synchronized 优化原理 偏向锁 撤销
假如一开始没有任何线程和线程1争抢对象,那么这个锁对象就是线程1独有的,此时是偏向锁,因为没有任何其它线程争抢。
假如突然来了一个线程2,此时锁就会升级为轻量级锁。
这里可以这么理解,第2个线程运行到wait方法处会进行等待,一直等到线程1调用notify方法才会继续往下执行,拿到线程1释放的锁。
会发现会从101结尾(偏向锁),变成000(轻量级)。
P85 synchronized 优化原理 偏向锁 批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍然有机会重新偏向T2,重偏向会重置对象的Thread ID。
假如原本某个锁对象是偏向线程1,假如后面有线程2来访问,此时要注意:线程2每来执行一次,都是让锁对象从偏向锁变为轻量级锁(而这个锁对象仍然是线程1的偏向锁)。
但当撤销偏向锁阈值超过20次之后,jvm会觉得,我是不是偏向错了(不应该偏向给线程1),于是会在给这些对象加锁时重新偏向至加锁线程。
现在设计的实验是:
原本只有线程1加锁打印30个数,然后notify唤醒一直在wait的线程2。
此时线程2进入打印30个数,此时每打印一个数,锁都会从偏向线程1的偏向锁(结尾101)变成轻量级锁(结尾000)。
直到20次之后,锁对象不再发生切换,变成了线程2的偏向锁(结尾000)。
P86 synchronized 优化原理 偏向锁 批量重偏向
当撤销偏向锁阈值超过40次后,jvm会觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。
调用park()方法是用于阻塞某个线程,调用unpark(线程名)用来唤醒某个线程。
实验设计:
线程1给0~38加锁,要给39个对象加锁,被加锁完的对象,末尾是101,此时的39个对象都加的是线程1的偏向锁。
线程2的前19次会对前19个偏向锁(101)对象进行加锁,加的是轻量级锁(00),偏向锁被解锁后会变成无锁状态(001)。给第20到39个对象加的是线程2的偏向锁(101,因为在20次后锁会批量重偏向)。
然后线程3运行,前19次起初都是无锁状态(001),加锁时轻量级锁(00),解锁后同样是无锁状态(001)。从第20个开始因为线程2给对象加的是偏向锁,所以每个对象初始为101,然后被加锁会加的是轻量级锁(00),最后解锁变成无锁状态(001)
进入到第40次的时候,jvm会觉得这个类的竞争激烈,直接变成无锁状态(001)。
P87 synchronized 优化原理 锁消除
JIT即时编译器会发现o这个对象只有在b方法中使用,没有其它的地方用到,所以加这个锁毫无意义,因此在字节码中会把加锁的代码去掉。
在启动jvm的时候加入如下参数,会关闭这种锁消除优化,然后会发现性能的差距比较明显:
P88 小故事wait notify
wait方法可以理解为让线程进入休息室等待,然后让其它线程继续工作。
当另一个线程调用notify会将wait方法的线程唤醒。
P89 wait notify api 工作原理
Owner线程如果发现条件不满足,会调用wait方法,线程会进入WaitSet变为WAITING状态。
BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片。
BLOCKED线程会在Owner线程释放锁时唤醒。
WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味着立即获得锁,仍需进入EntryList重新竞争。
P90 wait notify api 1
obj.wait() 让进入object监视器的线程到waitSet等待。
obj.notify() 在object上正在waitSet等待的线程中挑一个唤醒。
obj.notifyAll()让object上正在waitSet等待的线程全部唤醒。
要注意一点,某个线程要先成为Owner,才有资格进入WaitSet。
只有某个线程成为Owner,才有资格唤醒WaitSet中的线程。
它们都是线程之间协作的手段,都属于Object对象的方法。必须获得此对象的所,才能调用这几个方法。
@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
final static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t2").start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t1").start();
// 主线程两秒后执行
sleep(0.5);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
//obj.notify(); // 唤醒obj上一个线程
obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
}
P91 wait notify api 2
无参的wait,默认传入0,表示无限等待。
带参的wait,比如wait(1000),就是只等待1秒,如果等不到唤醒,就继续往下执行。
P92 wait vs sleep
sleep是Thread的静态方法,而wait是Object方法。
sleep不需要强制和synchronized配合使用,但wait需要和synchronized一起使用。
sleep在睡眠同时,不会释放锁,但wait在等待时会释放锁。
@Slf4j(topic="c.Test19")
public class Test14 {
static final Object lock = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (lock) {
log.debug("获得锁");
try {
Thread.sleep(10000);
//lock.wait(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t1").start();
Sleeper.sleep(1);
synchronized (lock){
log.debug("获得锁");
}
}
}
P93 wait notify 正确姿势 step1
某个线程调用sleep,会导致仍然带着锁没释放,别的线程会被阻塞,需要干等着,导致效率低。
P94 P95 wait notify 正确姿势 step2~4
@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep1 {
static final Object room = new Object();
static boolean hasCigarette = false; // 有没有烟
static boolean hasTakeout = false;
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人").start();
}
sleep(1);
new Thread(() -> {
// 这里能不能加 synchronized (room)?
synchronized (room) {
hasCigarette = true;
room.notify();
log.debug("烟到了噢!");
}
}, "送烟的").start();
}
}
P96 wait notify 正确姿势 step5
调用notifyAll方法唤醒所有线程,然后通过while循环来保持等待。
@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep4 {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
if (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notifyAll();
}
}, "送外卖的").start();
}
}
P97 设计模式 保护性暂停 定义
保护性暂停即Guarded Suspension,用在一个线程等待另一个线程的执行结果。
要点:
1.有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject。
2.如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)。
3.JDK中join的实现、Future的实现,采用的就是此模式。
4.因为要等待另一方的结果,因此归类到同步模式。
P98 设计模式 保护性暂停 实现
t1等待GuardedObject中response的值,t2为response赋值,会通知t1。
下面程序的思路是:t2线程会调用Downloader的download方法下载资源,complete方法用于给成员变量赋值,然后通知所有线程。t1线程会调用get方法获取成员变量的值,如果成员变量没有值就wait等待,获取到值后打印代码行数。
@Slf4j(topic = "c.Test20")
public class Test15 {
public static void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
new Thread(()->{
//等待结果
log.debug("等待结果");
List<String> list = (List<String>) guardedObject.get();
log.debug("结果大小:{}",list.size());
},"t1").start();
new Thread(()->{
log.debug("执行下载");
try {
List<String> list = Downloader.download();
guardedObject.complete(list);
} catch (IOException e) {
throw new RuntimeException(e);
}
},"t2").start();
}
}
class GuardedObject{
//结果
private Object response;
//获取结果
public Object get(){
synchronized (this){
while(response==null){
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
return response;
}
}
//产生结果
public void complete(Object response){
synchronized (this){
//给结果成员变量赋值
this.response = response;
this.notifyAll();
}
}
}
P99 设计模式 保护性暂停 扩展 增加超时
首先考虑2点:
1.设置超时事件后如何退出while循环:判断当前时间是否大于所设定的超时来判断。
2.虚假唤醒问题,假如wait方法中传入的参数是timeout,假如线程在前一次被唤醒,参数还没准备好,在此休眠仍然会有2秒的超时时间,不符合要求。必须是timeout减去之前经过的时间。
public Object get(long timeout){
synchronized (this){
//开始时间
long begin = System.currentTimeMillis();
//经历的时间
long passedTime = 0;
while(response==null){
//经历的时间超过最大等待时间,退出循环
if(passedTime>=timeout){
break;
}
try {
this.wait(timeout-passedTime);//防止虚假唤醒,唤醒之后结果数据还没准备好。
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//求得经历时间
passedTime = System.currentTimeMillis() - begin;
}
return response;
}
}
P100 设计模式 保护性暂停 扩展 增加超时 测试
修改main代码如下,模拟虚假唤醒的情况(唤醒了但数据没准备好):
public static void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
new Thread(()->{
log.debug("begin");
Object response = guardedObject.get(2000);
log.debug("结果是:{}",response);
},"t1").start();
new Thread(()->{
log.debug("begin");
Sleeper.sleep(1);
guardedObject.complete(null);
},"t2").start();
}
刚好是间隔2秒输出:
P101 设计模式 join原理
P102 设计模式 保护性暂停 扩展 解耦等待和生产 分析
如果需要在多个类之间使用GuardedObject对象,作为参数传递不是很方便,因此设计一个用来解耦的类,这样不仅能够解耦结果等待者和结果生产者,还能够同时支持多个任务的管理。
P103 设计模式 保护性暂停 扩展 解耦等待和生产 实现
完整代码如下:
@Slf4j(topic = "c.Test20")
public class Test15 {
public static void main(String[] args) {
for(int i=0;i<3;i++){
new People().start();
}
Sleeper.sleep(1);
for(Integer id : Mailboxes.getIds()){
new Postman(id,"内容"+id).start();
}
}
}
@Slf4j(topic="c.People")
class People extends Thread{
@Override
public void run() {
//收信
GuardedObject guardedObject = Mailboxes.createGuardedObject();
log.debug("开始收信 id:{}",guardedObject.getId());
Object mail = guardedObject.get(5000);
log.debug("收到信 id:{} , 内容:{}",guardedObject.getId(),mail);
}
}
@Slf4j(topic="c.Postman")
class Postman extends Thread{
private int id;
private String mail;
public Postman(int id,String mail){
this.id = id;
this.mail = mail;
}
@Override
public void run() {
GuardedObject guardedObject = Mailboxes.getGuardedObject(id);
log.debug("开始收信 id:{},内容:{}",id,mail);
guardedObject.complete(mail);
}
}
class Mailboxes{
private static Map<Integer,GuardedObject> boxes = new Hashtable<>();
private static int id = 1;
//产生唯一id
private static synchronized int generateId(){
return id++;
}
public static GuardedObject getGuardedObject(int id){
return boxes.remove(id);
}
public static GuardedObject createGuardedObject(){
GuardedObject go = new GuardedObject(generateId());
boxes.put(go.getId(),go);
return go;
}
public static Set<Integer> getIds(){
return boxes.keySet();
}
}
class GuardedObject{
private int id;
public GuardedObject(int id){
this.id = id;
}
public int getId(){
return id;
}
//结果
private Object response;
//获取结果
public Object get(long timeout){
synchronized (this){
//开始时间
long begin = System.currentTimeMillis();
//经历的时间
long passedTime = 0;
while(response==null){
//经历的时间超过最大等待时间,退出循环
if(passedTime>=timeout){
break;
}
try {
this.wait(timeout-passedTime);//防止虚假唤醒,唤醒之后结果数据还没准备好。
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//求得经历时间
passedTime = System.currentTimeMillis() - begin;
}
return response;
}
}
//产生结果
public void complete(Object response){
synchronized (this){
//给结果成员变量赋值
this.response = response;
this.notifyAll();
}
}
}
P104 设计模式 保护性暂停 扩展 解耦等待和生产 测试
P105 设计模式 生产者消费者 定义
保护性暂停是同步的,生产者/消费者是异步的:
与前面的保护性暂停中的GuardObject不同,不需要产生结果和消费结果的线程一一对应(比如之前的保护性暂停中要求1个居民要配1个快递员,如果有几百个居民,则需要配几百个快递员)。
消费队列可以用来平衡生产和消费的线程资源。
生产者仅负责产生结果数据,不关心数据如何处理,消费者专心处理结果数据。
消息队列有容量限制,满时不再加入数据,空时不会再消耗数据。
JDK中各种阻塞队列,采用的是这种模式。
P106 设计模式 生产者消费者 实现
线程之间通信id很重要,线程之间不知道,id作为桥梁,可以检查消息受到了没有,因此设置一个Message类,在类里加入id属性。
在Message类前加final(不能有子类),仅有get方法,因此是线程安全的。
双向队列在Java里的实现是LinkedList
@Slf4j(topic = "c.Test21")
public class Test16 {
public static void main(String[] args) {
MessageQueue queue = new MessageQueue(2);
for (int i = 0; i < 3; i++) {
int id = i;
new Thread(()->{
queue.put(new Message(id,"值"+id));
},"生产者"+i).start();
}
new Thread(()->{
while(true) {
sleep(1);
Message message = queue.take();
}
},"消费者").start();
}
}
@Slf4j(topic = "c.MessageQueue")
class MessageQueue{
//消息的队列集合
private LinkedList<Message> list = new LinkedList<>();
//队列容量
private int capcity;
public MessageQueue(int capcity){
this.capcity=capcity;
}
//获取消息
public Message take(){
//检查队列是否为空
synchronized (list) {
while (list.isEmpty()) {
try {
log.debug("队列为空,消费者线程等待");
list.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//从队列头部获取消息返回。
Message message = list.removeFirst();
log.debug("已消费消息{}",message);
list.notifyAll();
return message;
}
}
//存入消息
public void put(Message message){
synchronized (list){
//检查队列是否已满
while(list.size()==capcity){
try {
log.debug("队列为满,生产者线程等待");
list.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//将消息加入队列尾部
list.addLast(message);
log.debug("已生产消息{}",message);
list.notifyAll();
}
}
}
final class Message{
private int id;
private Object value;
public Message(int id,Object value){
this.id=id;
this.value=value;
}
public int getId(){
return id;
}
public Object getValue(){
return value;
}
@Override
public String toString() {
return "Message{" +
"id=" + id +
", value=" + value +
'}';
}
}
P107 设计模式 生产者消费者 测试
P108 park & unpark 基本使用
park和unpark是在LockSupport类中的方法。
LockSupport.part() 是暂停当前线程
LockSupport.unpart() 是恢复某个线程运行
首先会执行main方法启动t1线程输出start,然后main方法休眠1秒,t1线程休眠2秒,1秒后main方法输出unpark,然后执行unpark,再过1秒t1线程信赖输出park和resume。
@Slf4j(topic = "c.TestParkUnpark")
public class TestParkUnpark {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("start...");
sleep(2);
log.debug("park...");
LockSupport.park();
log.debug("resume...");
}, "t1");
t1.start();
sleep(1);
log.debug("unpark...");
LockSupport.unpark(t1);
}
}
与Obeject的wait和notify相比:
1.wait,notify和notifyAll必须配合Object Monitor一起使用(也就是要先给对象上锁),而park和unpark不必。
2.park和unpark是以线程为单位来阻塞和唤醒线程,而notify只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,不那么精确。
3.park和unpark可以先unpark,而wait和notify不能先notify。
P109 park & unpark 原理
每个线程都有自己的一个Parker对象,由3部分组成_counter,_cond,_mutex。
比喻:
线程就像司机开的车。
_mutex是互斥锁。
_cond是条件变量,等待队列,类似加油站。
_counter=0代表汽油不足,需要休息。
_counter=1代表汽油充足,不需要休息。
情况1:
当前线程调用Unsafe.park()方法。检查_counter,本情况为0,代表汽油不足,此时就会获得互斥锁。线程就像司机开的车要进入_cond加油,所以条件变量阻塞。再确保设置_counter=0。
情况2:
调用unpark之后,首先会把_counter置为1,代表汽油补充完毕。然后会唤醒_cond中等待的变量。然后该线程恢复运行。最后会设置_counter为0。
情况3:
如果先调用unpark,相当于给汽车加油,设置_counter为1。然后调用park方法,此时会检查_counter会发现_counter为1,相当于汽车有油,此时不需要加油,汽车继续上路。最后会把counter置为0,代表汽车上路油逐步会消耗关。
P110 线程状态转换1
NEW 初始状态,创建java线程对象还没与操作系统对象关联起来。调用start方法后与操作系统底层线程关联。状态变成RUNNABLE。
RUNNABLE 包含可运行状态(可以被调度器调度,分得CPU时间片)、运行状态、阻塞状态。
P111 线程状态转换2
从RUNNABLE到WAITING:
2调用的是wait notify。注意锁竞争失败后会变成
P112 线程状态转换3~4
从RUNNABLE到WAITING:
3调用的是join()方法,当前线程会从RUNNABLE -> WATING。
4调用的是park()方法。
P113 线程状态转换
TIMED_WAITING是有时限的waiting。
RUNNABLE到TIMED_WAITING的转换:
情况5:调用wait,如果等待时间超过n能参与锁竞争。
情况6:调用join方法
情况7:调用sleep方法
情况8:调用park方法。
从RUNNABLE到BLOCKED
P114 多把锁
如下图是用细粒度的锁来增强程序的并发性。
P115 活跃性 死锁现象
各自持有一把锁,但还想获得对方的锁:
P116 活跃性 定位死锁
检测死锁可以用jconsole工具,或者使用.jps定位进程id,再用jstack定位死锁。
P117 活跃性 死锁 哲学家就餐
如果所有哲学家都拿着一双筷子,死锁发生。
P118 活跃性 活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束:
P119 活跃性 饥饿
一个线程由于优先级太低,始终得不到CPU调度执行,也不能够结束。
P120 ReentrantLock 简介
Reentrant 表示可重入,ReentrantLock表示可重入锁。
ReentrantLock是juc并发工具包下的类。
相较于具有如下特点:
1.可中断(别的线程可以中断当前线程获得的锁)。
2.可设置超时时间(规定时间获取不到锁,放弃争抢锁)。
3.可设置为公平锁(防止线程饥饿)
4.支持多个条件变量。
与synchronized一样,都支持可重入(自己加的锁,没释放前,下一次还能进去)。
P121 ReentrantLock 可重入
synchronized是在关键字级别保护临界区,reentrantLock是在对象级别保护临界区。
首先创建reentrantLock对象,然后调用lock方法。
lock放在try块外面或里面都可以,lock和unlock要成对出现,要在finally中unlock。
可重入是指如果同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获得这把锁。
如果是不可重入锁,那么第二次获得锁时,自己也会被锁住。
P122 ReentrantLock 可打断
可打断:在等待锁的过程中,其它线程可以用interrupt的方法终止线程的等待,防止死锁。
@Slf4j(topic="c.Test18")
public class Test18 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
//如果没有竞争那么此方法会获取lock对象锁
//如果有竞争就进入阻塞队列,可以被其它线程用interrupt方法打断
log.debug("尝试获取锁");
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("没有获得锁,返回");
return;
}
try{
log.debug("获取到锁");
}finally{
lock.unlock();
}
}, "t1");
lock.lock();
t1.start();
sleep(1);
log.debug("打断t1");
t1.interrupt();
}
}
P123 ReentrantLock 锁超时
某线程尝试获得锁,如果未获取到,不会立刻死亡,会等待一段时间,如果锁仍未释放,就放弃等待,表示锁获取失败。避免线程无限次等待,避免死锁。
@Slf4j(topic="c.Test19")
public class Test19 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
log.debug("尝试获得锁");
try {
if(!lock.tryLock(2, TimeUnit.MINUTES.SECONDS)){
log.debug("获取不到锁");
return ;
}
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("获取不到锁");
return;
}
try{
log.debug("获得到锁");
}finally{
lock.unlock();
}
},"t1");
lock.lock();
log.debug("获得到锁");
t1.start();
sleep(1);
lock.unlock();
log.debug("释放了锁锁");
}
}
P124 ReentrantLock 锁超时 解决哲学家就餐问题
首先要给筷子类继承一个ReentanceLock类,它可以被当作锁。
lock.tryLock()会返回一个Boolean值,值为true代表拿到锁,值为false代表拿不到锁。
假如一个哲学家拿到了左手的筷子,但没拿到右手的筷子,会执行到finally内的代码,释放左筷子(放下左筷子)。
P125 ReentrantLock 公平锁
公平锁:先来先获得锁,大家都有均等的机会获得锁。但没有必要,会降低并发度。推荐使用ReentrantLock来实现。
P126 ReentrantLock 条件变量 简介
synchronized中也有条件变量,waitSet休息室,当条件不满足时进入waitSet等待。
ReentrantLock的条件变量比synchronized强大,支持多个条件变量(举例来说ReentrantLock支持多间休息室,有专门等烟的休息室,有专门等早餐的休息室,唤醒时是按休息室来唤醒)。
在调用awai()方法前需要先获得锁
调用条件变量的await()会释放锁,进入conditionObject等待,(相当于进入休息室等待)。
调用条件变量的signal()会唤醒线程,唤醒之后会重新竞争lock锁。
竞争lock锁成功后会从awit后继续执行
P127 ReentrantLock 条件变量 使用例子
public static void main(String[] args) {
new Thread(() -> {
ROOM.lock();
try{
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
waitCigaretteSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了");
}finally {
ROOM.unlock();
}
}, "小南").start();
new Thread(() -> {
ROOM.lock();
try{
log.debug("外卖送到没?[{}]", hasTakeout);
while (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
waitTakeoutSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了");
}finally {
ROOM.unlock();
}
}, "小女").start();
sleep(1);
new Thread(() -> {
ROOM.lock();
try {
hasTakeout = true;
waitTakeoutSet.signal();
} finally {
ROOM.unlock();
}
}, "送外卖的").start();
new Thread(() -> {
ROOM.lock();
try {
hasCigarette = true;
waitCigaretteSet.signal();
} finally {
ROOM.unlock();
}
}, "送烟的").start();
}
P128 设计模式 固定运行顺序 wait notify
使用wait和notify:
@Slf4j(topic="c.Test21")
public class Test21 {
static final Object lock = new Object();
//表示t2是否运行过
static boolean t2runned = false;
public static void main(String[] args) {
Thread t1 = new Thread(()->{
synchronized (lock) {
while(!t2runned) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("1");
}
},"t1");
Thread t2 = new Thread(()->{
synchronized (lock){
if(!t2runned){
t2runned=true;
log.debug("2");
lock.notify();
}
}
},"t2");
t1.start();
t2.start();
}
}
P129 设计模式 固定运行顺序 park&unpark
调用park就相当于车没油了进入加油站停车加油,unpark就相当于上路。
如果先执行1,就会停车加油,保证2先输出。如果是先执行2代表提前加好油,运行到1的时候就可以直接上路,调用park就没用了。
@Slf4j(topic="c.Test23")
public class Test23 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
LockSupport.park();
log.debug("1");
}, "t1");
t1.start();
new Thread(()->{
log.debug("2");
LockSupport.unpark(t1);
},"t2").start();
}
}
P130 设计模式 交替输出 wait notify
要求线程1输出5次a,线程2输出5次b,线程3输出5次c。输出结果为abcabcabcabcabcabc。
@Slf4j(topic="c.Test22")
public class Test22 {
public static void main(String[] args) {
WaitNotify wn = new WaitNotify(1,5);
new Thread(()->{
wn.print("a",1,2);
}).start();
new Thread(()->{
wn.print("b",2,3);
}).start();
new Thread(()->{
wn.print("c",3,1);
}).start();
}
}
class WaitNotify{
public void print(String str,int waitFlag,int nextFlag){
for(int i=0;i<loopNumber;i++){
synchronized(this){
while(flag != waitFlag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(str);
flag=nextFlag;
this.notifyAll();
}
}
}
//等待标记
private int flag;
//循环次数
private int loopNumber;
public WaitNotify(int flag, int loopNumber) {
this.flag = flag;
this.loopNumber = loopNumber;
}
}
P131 设计模式 交替输出 await&signal
public class Test24 {
public static void main(String[] args) throws InterruptedException {
AwaitSignal awaitSignal = new AwaitSignal(5);
Condition a = awaitSignal.newCondition();
Condition b = awaitSignal.newCondition();
Condition c = awaitSignal.newCondition();
new Thread(()->{
awaitSignal.print("a",a,b);
}).start();
new Thread(()->{
awaitSignal.print("b",b,c);
}).start();
new Thread(()->{
awaitSignal.print("c",c,a);
}).start();
Thread.sleep(1000);
awaitSignal.lock();
try{
System.out.println("开始...");
a.signal();
}finally{
awaitSignal.unlock();
}
}
}
class AwaitSignal extends ReentrantLock{
private int loopNumber;
public AwaitSignal(int loopNumber){
this.loopNumber = loopNumber;
}
public void print(String str,Condition current,Condition next){
for(int i=0;i<loopNumber;i++){
lock();
try{
current.await();
System.out.println(str);
next.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
unlock();
}
}
}
}
P132 设计模式 交替输出 park&unpark
public class Test25 {
static Thread t1;
static Thread t2;
static Thread t3;
public static void main(String[] args) {
ParkUnpark pu = new ParkUnpark(5);
t1 = new Thread(()->{
pu.print("a",t2);
});
t2 = new Thread(()->{
pu.print("b",t3);
});
t3 = new Thread(()->{
pu.print("c",t1);
});
t1.start();
t2.start();
t3.start();
LockSupport.unpark(t1);
}
}
class ParkUnpark{
private int loopNumber;
public ParkUnpark(int loopNumber){
this.loopNumber = loopNumber;
}
public void print(String str,Thread next){
for(int i=0;i<loopNumber;i++){
LockSupport.park();
System.out.println(str);
LockSupport.unpark(next);
}
}
}
P133 第四章小结
synchronized互斥保护临界区的代码不会因为线程上下文切换导致交错。
wait/notify同步是让条件不满足时线程等待。
lock:可打断、锁超时、公平锁、条件变量。
P134 本章内容
JMM即Java Memory Model,Java内存模型。它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。
JMM体现在以下几个方面:
1.原子性:保证指令不会受到线程上下文切换的影响。
2.可见性:保证指令不会受cpu缓存的影响。
3.有序性:保证指令不会受cpu指令并行优化的影响。
P135 可见性 问题
可见性问题:一个线程对主存的数据进行修改,但对另一个线程不可见。
因为t线程会频繁从主存中读取run的值,所有JIT会将run的值缓存到自己工作内存中的高速缓存中,从而减少对主存run的访问,提高效率。
main线程修改了run值,同步到主存,但t线程仍旧从自己工作内存中的高速缓存中读值,结果永远是旧值。
P136 可见性 解决
可以在共享的变量上加修饰符volatile(易变关键字),代表这个变量是容易变化的。
它可以用来修饰成员变量和静态成员变量,加了volatile之后线程不能从自己工作缓存中读取变量的值,必须去到主内存中获取变量的最新值。
线程操作volatile变量都是直接操作主存。
@Slf4j(topic="c.Test32")
public class Test26 {
volatile static boolean run = true;
public static void main(String[] args) {
Thread t = new Thread(()->{
while(run){
}
});
t.start();
Sleeper.sleep(1);
log.debug("停止 t");
run = false;
}
}
加synchronized之后同样可以改变变量的可见性。
P137 可见性vs原子性
可见性指的是一个线程对volatile变量的修改对另一个线程可见,不能保证原子性,用在一个写线程,多个读线程的情况。
P138 设计模式 两阶段终止 volatile
通过修改中间变量来使得程序继续执行,为了让不同线程间数据修改可见,于是加了volatile。
P139 设计模式 犹豫模式
Balking(犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件同样的事,那么本线程就无需再做了,直接结束返回。
实现逻辑:设置一个标记变量,来判断是否执行过某个方法。具体实现如下:
但容易产生问题:在多线程情况下,容易2个线程同时。加volatile没用,比如第1个线程来了starting为false,还没来得及改starting,第2个线程来了,同样也是false,防不住。所以要加synchronized。
P140 设计模式 犹豫模式 应用
单例模式:在jvm只产生一个实例。
构造方法设为私有,定义静态实例变量。
P141 有序性 指令重排
JVM会在不影响正确性的前提下,调整语句的执行顺序。
在多线程下指令重排会影响正确性。
P142 有序性 指令重排原理 指令并行优化
P143 有序性 指令重排 问题
r1的结果有1和4和0。
0的情况是当actor2还没来得及执行num=2时此时num还为0,ready为true,r1=0+0还是为0。
P144 有序性 指令重排 验证
用压测工具可以看到结果:
P145 有序性 指令重排 禁用
只需要在ready上加volatile,可以防止其之前的代码被重排序,加了写屏障。
P146 volatile 原理 保证可见性
写屏障:保证在该屏障之前,对共享变量的改动,都同步到主存中。
读屏障:保证在屏障之后,对共享变量的读取,加载的是主存中最新数据。
P147 volatile 原理 保证有序性
读屏障和写屏障只能保证代码的可见性(只能保证读到最新的结果)和有序性(只能保证本线程内代码不重排)不能解决指令交错(不能保证原子性)。
P148 volatile 原理 dcl 简介
像下面这么写有问题是因为,在synchronized外面的变量不会受到同步代码块的保护。会发生指令重排,导致有序性出现问题。
P149 volatile 原理 dcl 问题分析
出现问题:没等t1完成构造方法的调用,t2发现已有对象直接返回对象使用,发生错误。
P150 volatile 原理 dcl 问题纠正
synchronized内的字节码仍旧可以被重排序,只有volatile才可以阻止重排序,如果共享变量是完全被synchronized保护。
P151 volatile 原理 dcl 问题解决
加一个写屏障,使得写屏障前的代码不能被重排序到后面。
在INSTANCE前面加一个volatile相当于写屏障,防止指令重排序。
P152 happens before规则
P153 习题 balking模式
有问题,如果initialized=true还没执行,t2进来了,会导致问题。
P154 习题 线程安全单例1
饿汉式:类加载就会导致该单实例被创建。
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会被创建。
1.单例类加final原因:怕将来有子类,子类不适当覆盖父类方法,破坏单例。
2.序列化接口反序列化的时候也会生成新的对象。
采用指定的对象返回,而不会把真正反序列字节码生成的对象当作结果。
3.设为非private的别的类能无限创建对象。不能防止反射创建新的实例。
4.可以保证线程安全,静态成员变量初始化是在类加载阶段完成。
P155-156 习题 线程安全单例2~5
了解即可。
P157 第五章小结
P158 本章内容
P159 保护共享资源 加锁实现
P160 保护共享资源 无锁实现
无锁比加锁的速度更快:
P161 cas工作方式
AtomicInteger内部没有锁来保护共享变量的线程安全,实现的关键是compareAndSet:
compareAndSet简称是CAS(也有Compare And Swap的说法)必须是原子操作。
如上图线程1先把100减10获得90。线程1想执行cas指令用90这个结果替换原先100的值。但在此之前,cas会通过比较发现:线程2已经把共享变量余额改为了90,如果线程1再把90覆盖这个结果没有意义,必须要在90的基础上减1。
所以本次的cas修改是失败的,判断的标准是将线程1自身的变量值和共享变量上最新结果的值对比。然后线程1会获取最新的余额90,减10后,调用cas尝试把旧值90替换为新值80。
CAS的底层是lock cmpxchg指令,在单核CPU和多核CPU下都能保证比较-交换的原子性。
P162 cas慢动作分析
P163 cas volatile
CAS必须借助volatile才能获取到共享变量的最新值,从而实现比较并交换的效果。
获取共享变量时,为了保证变量的可见性(它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取值,一个线程对volatile变量的修改,对灵一个线程可见)。
P164 cas 效率分析
cas的效率比synchronized效率高,因为synchronized会让没有获得锁的线程发生上下文切换,会进入阻塞。
线程就像高速跑道上的赛车,高速运行时一旦发生上下文切换,赛车就要停下来熄火,等待线程被唤醒重新打火启动加速,代价大。
无锁情况下是用while循环不断进行尝试,如果线程要保持不断运行,需要额外的CPU的支持,如果没有分到CPU时间片,仍然会进入可运行状态,导致上下文切换。
cas只有在多核才能发挥功效,并且最好线程数少于CPU核心数。
P165 cas 特点
结合CAS和volatile可以实现无锁并发,适用于线程数少,多核CPU的使用场景。
CAS是基于乐观锁的思想:不怕别的线程来修改共享变量,改了也没关系,再重新尝试。
synchronized是基于悲观锁的思想:防止其它线程来修改共享变量,上完锁其它线程不能修改,只有释放锁了才能修改。
因为没有使用synchronized所以线程不会陷入阻塞,这个提升效率的因素。
但如果竞争激烈,重试过于频繁,效率会受到影响。
P166 原子整数 AtomicInteger
调用incrementAndGet相当于先自增再获取:++i。
调用getAndIncrement相当于先获取再自增:i++。
调用getAndAdd输出的是加前的数据。
调用addAndGet输出的是加后的数据。
P167 原子整数 AtomicInteger updataAndGet
比如value初始是5,调用i.updateAndGet(value->value*10)会得到的修改后的值50。
如果调用i.getAndUpdate()会得到的是修改前的值。
P168 原子整数 AtomicInteger updataAndGet原理
调用operator的applyAsInt方法只需要传入数值参数,具体的操作(加法,减法,乘法,除法)会由applyAsInt的实现决定。
P169 原子引用 AtomicReference
因为要保护的数据不都是基本类型的,比如想保护小数类型,可以用引用类型。
P170 原子引用 ABA问题
实现原理是拿main线程的变量与共享变量进行对比,判断值是否一致。
假如线程t1把共享变量的值从A改为B,线程t2把共享变量的值从B改为A,但main线程无法感知是否有线程对共享变量进行过修改,它只能根据最终结果进行比较。
P171 原子引用 AtomicStampedReference
如果希望实现有其它线程修改过共享变量,那么自己的cas就算失败,此时还需要添加一个版本号。
可以使用AtomicStampedReference来校验版本号,可以知道引用变量中途被更改了几次。
通过getStamp来获得版本号,如果线程中的版本号与共享变量的版本号不一致,修改失败。
P172 原子引用 AtomicMarkableReference
但有时候我们并不关心引用变量被更改了几次,只是单纯关心是否被更改过,所以就有了AtomicMarkableReference。、
P173 原子数组
有时候我们要修改的不是引用本身,而是修改引用内部的元素。比如数组,不想修改数组地址本身,而是想修改数组里面的元素。
P174 原子数组
函数式接口:
supplier 提供者 无中生有 ()->结果
function 函数 一个参数一个结果 (参数)->结果
BiFunction 函数 两个参数一个结果 (参数1,参数2)->结果
consumer 消费者 一个参数没结果 (参数)->
P175 原子更新器
字段更新器保护的是:某个对象里的属性,成员变量。能够保证多个线程访问某个成员变量时的线程安全性。
如下图对Student类中的name变量进行保护,因为name初始值为null,可以被更改为张三。
P176 原子累加器
经过测试累加器LongAddr()的性能要比getAndIncrement更好。
原理:CAS在有竞争时要用while true循环不断尝试更新能否成功,如果竞争激烈,只往一个共享变量上累加竞争激烈,重试的次数一多,累加的速度会降下来。
LongAdder会在有竞争时,设置多个累加单元,线程0累加Cell[0],线程1累加Cell[1],最后把结果汇总。在累加时操作不同的Cell变量,减少了CAS重试失败,从而提高性能。
P177 LongAdder原理 cas锁
volatile是为了保证可见性,transient是序列化时不会把变量进行序列化。
P178 LongAdder原理 缓存行伪共享
CPU于内存的速度差异很大,需要靠预读数据到缓存来提升效率。
而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是64byte(8个long)。
缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中。
CPU要保证数据的一致性,如果某个CPU核心更改了数据,其它CPU核心对应的整个缓存行必须失效。
假如CPU核心占用的是同一个缓存行,其中一个核心对该行中的cell进行修改,都会使得另一个核心该缓存行中的数据失效,降低了效率。
解决方法是用@sun.misc.Contended注解来让对象预读至缓存行时占用不同的缓存行,从而避免缓存行失效的问题。该注解原理是在使用该注解的对象或字段的前后增加128字节大小的padding,使得占用不同缓存行。
P179~P183 LongAdder源码
与面试关系不大,可以选择跳过。
P184 unsafe对象 获取
Unsafe对象提供了非常底层的,操作内存、线程的方法,Unsafe对象不能直接调用(Unsafe的意思是操作是不安全的),只能通过反射获得。
public class Test26 {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
System.out.println(unsafe);
}
}
P185 unsafe对象 cas相关方法
public class Test26 {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
System.out.println(unsafe);
//1.获得域的偏移地址
long idOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("id"));
long nameOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("name"));
Teacher t = new Teacher();
//2.执行cas操作
unsafe.compareAndSwapInt(t,idOffset,0,1);
unsafe.compareAndSwapObject(t,nameOffset,null,"张三");
//3.验证
System.out.println(t);
}
}
@Data
class Teacher{
volatile int id;
volatile String name;
}
P186 unsafe对象 模拟实现原子整数
public class Test26 {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Account.demo(new MyAtomicInteger(10000));
}
}
@Data
class MyAtomicInteger {
private volatile int value;
private static final long valueOffset;
private static final Unsafe UNSAFE;
static{
UNSAFE = UnsafeAccessor.getUnsafe();
try {
valueOffset = UNSAFE.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
} catch (NoSuchFieldException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public int getValue(){
return value;
}
public void decrement(int amount){
while(true){
int prev = this.value;
int next = prev - amount;
if(UNSAFE.compareAndSwapInt(this,valueOffset,prev,next)){
break;
}
}
}
public MyAtomicInteger(int value){
this.value = value;
}
public Integer getBalance(){
return getValue();
}
public void withdraw(Integer amount){
decrement(amount);
}
}
P187 第六章小结
P188 本章内容
SimpleDateFormat在多线程下不安全。
@Slf4j(topic = "c.Test1")
public class Test1 {
public static void main(String[] args) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
log.debug("{}", sdf.parse("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}
}).start();
}
}
}
P189 不可变对象 使用
this class is immutable(不可变) and thread-safe
public class Test1 {
public static void main(String[] args){
DateTimeFormatter stf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for(int i=0;i<10;i++){
new Thread(()->{
TemporalAccessor parse = stf.parse("1951-04-21");
log.debug("{}",parse);
}).start();
}
}
}
P190 不可变对象 设计
属性用final修饰保证了该属性是只读的,不能修改。
类用final修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性。
采用复制的方法给value赋上新的数组:
substring是调用String内部的构造方法创建了一个新的字符串,没有对原字符串进行修改。
通过创建副本对象来避免共享的手段叫作保护性拷贝。
P191 享元模式 定义和体现
保护性拷贝的缺点是:创建的对象会比较多,占用内存。
享元模式:
定义:A flyweight is an object that minimizes memory usage by sharing as much data as possible with other similar objects。大致的意思是通过共享具有相同值的对象实现占用内存的最小化。
包装类:
在JDK中Boolean,Byte,Short,Integer,Long,Character等包装类提供了valueOf方法,例如Long的valueOf会缓存-128~127之间的Long对象,在这个范围内会重用对象,大于这个范围,才会新建Long对象。
Long的底层源码时,首先创建一个256字节长度的cache数组,提前先把256个数创建玩完毕存入。
P192 享元模式 不可变线程安全辨析
BigDecimal单个方法是原子的,但多个方法的组合不一定是原子的,不能保证线程安全。
3个组合操作要用原子引用类对其进行保护:
P193 享元模式 自定义连接池 分析
P194 享元模式 自定义连接池 实现
public class Test3 {
public static void main(String[] args) {
Pool pool = new Pool(2);
for (int i = 0; i < 5; i++) {
new Thread(()->{
Connection conn = pool.borrow();
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
pool.free(conn);
}).start();
}
}
}
@Slf4j(topic = "c.Pool")
class Pool {
//1.连接池大小
private final int poolSize;
//2.连接对象数组
private Connection[] connections;
//3.连接状态数组 0表示空闲 1表示繁忙
private AtomicIntegerArray states;
//4.构造方法初始化
public Pool(int poolSize){
this.poolSize = poolSize;
this.connections=new Connection[poolSize];
this.states = new AtomicIntegerArray(new int[poolSize]);
for(int i=0;i<poolSize;i++){
connections[i] = new MockConnection("连接"+(i+1));
}
}
//5.借连接
public Connection borrow(){
while(true){
for(int i=0;i<poolSize;i++){
//获取空闲连接
if(states.get(i)==0) {
if (states.compareAndSet(i, 0, 1)) {//不能用set,不然会有线程安全问题
log.debug("borrow {}",connections[i]);
return connections[i];
}
}
}
//如果没有空闲连接,当前线程进入等待
synchronized (this){
try {
log.debug("wait...");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//6.归还连接
public void free(Connection conn){
for(int i=0;i<poolSize;i++){
if(connections[i]==conn){
states.set(i,0);
synchronized (this){
log.debug("free {}",conn);
this.notifyAll();
}
break;
}
}
}
}
class MockConnection implements Connection {
private String name;
public MockConnection(String name) {
this.name = name;
}
}
P195 享元模式 自定义连接池 测试
轮流获取:
P196 享元模式 自定义连接池 总结
之所以要加下面连接是为了避免CPU空转,让线程获取不到资源就放弃对资源的抢占:
P197 final 原理
如果一个变量被声明为final,会在赋值之后加入一个写屏障,保证写屏障前的指令不会被重排序到写屏障后面。
能保证在写屏障之前的操作(修改、赋值....)能够被更新到主存中,其它线程可见。
如果没有加final,声明一个变量分2步走:1.分配空间,初始值为0。2.赋值。
P198 第七章小节
P199 本章内容
P200 自定义线程池 阻塞队列
线程池不是越大越好,要与CPU核数适配。
class BlockingQueue<T>{
// 1.任务队列
private Deque<T> queue = new ArrayDeque<>();
// 2.锁
private ReentrantLock lock = new ReentrantLock();
// 3.生产者条件变量
private Condition fullWaitSet = lock.newCondition();
// 4.消费者条件变量
private Condition emptyWaitSet = lock.newCondition();
// 5.容量
private int capcity;
public BlockingQueue(int capcity) {
this.capcity = capcity;
}
//阻塞获取
public T take(){
lock.lock();
try{
while(queue.isEmpty()){
try {
emptyWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal();
return t;
}finally{
lock.unlock();
}
}
//阻塞添加
public void put(T element){
lock.lock();
try{
while(queue.size()==capcity){
try {
fullWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.addLast(element);
emptyWaitSet.signal();
}finally {
lock.unlock();
}
}
//获取大小
public int size(){
lock.lock();
try{
return queue.size();
}finally {
lock.unlock();
}
}
}
P201 自定义线程池 阻塞队列 poll增强
//带超时的阻塞获取
public T poll(long timeout, TimeUnit unit){
lock.lock();
try{
long nanos = unit.toNanos(timeout);//将timeout时间统一转化为纳秒
while(queue.isEmpty()){
try {
//没等到直接返回
if(nanos<=0){
return null;
}
//返回的是剩余时间
nanos = emptyWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal();
return t;
}finally{
lock.unlock();
}
}
P202 自定义线程池 线程池 实现
下面是线程池的基本框架:
class TreadPool{
//任务队列
private BlockingQueue<Runnable> taskQueue;
//线程集合
private HashSet<Worker> workers = new HashSet<>();
//核心线程数
private int coreSize;
//获取任务的超时时间
private long timeout;
//时间单位
private TimeUnit timeUnit;
public void execute(Runnable task){
}
public TreadPool( int coreSize, long timeout, TimeUnit timeUnit,int queueCapcity) {
this.taskQueue = new BlockingQueue<>(queueCapcity);
this.coreSize = coreSize;
this.timeout = timeout;
this.timeUnit = timeUnit;
}
class Worker{
}
}
P203 自定义线程池 线程池 任务提交 Worker实现
@Slf4j(topic="c.ThreadPool")
class ThreadPool{
//任务队列
private BlockingQueue<Runnable> taskQueue;
//线程集合
private HashSet<Worker> workers = new HashSet<>();
//核心线程数
private int coreSize;
//获取任务的超时时间
private long timeout;
//时间单位
private TimeUnit timeUnit;
//执行任务
public void execute(Runnable task){
//当任务数没有超过coreSize时,直接交给worker对象执行
//如果任务数超过coreSize时,加入任务队列暂存
synchronized (workers){
if(workers.size()<coreSize){
Worker worker = new Worker(task);
log.debug("新增 worker{},{}",worker,task);
workers.add(worker);
worker.start();
}else{
log.debug("加入任务队列 {}",task);
taskQueue.put(task);
}
}
}
public ThreadPool( int coreSize, long timeout, TimeUnit timeUnit,int queueCapcity) {
this.taskQueue = new BlockingQueue<>(queueCapcity);
this.coreSize = coreSize;
this.timeout = timeout;
this.timeUnit = timeUnit;
}
class Worker extends Thread{
private Runnable task;
public Worker(Runnable task) {
this.task = task;
}
public void run(){
//执行任务
// 1)当task不为空,执行任务
// 2)当task执行完毕,再接着从任务队列获取任务
while(task !=null || (task = taskQueue.take() )!= null){
try{
log.debug("正在执行...{}",task);
task.run();
}catch(Exception e){
e.printStackTrace();
}finally {
task=null;
}
}
synchronized (workers){
log.debug("worker被移除{}",this);
workers.remove(this);
}
}
}
}
P204 自定义线程池 线程池 take死等 poll超时
下面是执行代码:
@Slf4j(topic="c.TestPool")
public class TestPool {
public static void main(String[] args){
ThreadPool threadPool = new ThreadPool(2,1000,TimeUnit.MICROSECONDS,1);
for (int i = 0; i < 5; i++) {
int j = i;
threadPool.execute(()->{
log.debug("{}",j);
});
}
}
}
因为线程池线程数为2,所以是创建了2个worker线程。
然后把创建出来的2个线程加入到任务队列中等待执行。
然后Thread1和Thread2分别执行。输出结果1和0
执行完一个任务,新的任务会被继续加入任务队列。
总共设置了5个任务,全部比线程执行完毕。
现在有一个问题,线程会无限死等:
只需要把take方法替换为poll方法即可:
自动停止:
P205 自定义线程池 线程池 当任务队列已满
现在假如核心线程数为2,队列容量大小为10,假如处理一个任务的耗时很长,生成了15个任务,必然会有10个任务在任务队列中阻塞,而有3个任务等待加入任务队列。
应该添加一个拒绝策略。
P206 自定义线程池 线程池 offer增强
//带超时时间的阻塞添加
public boolean offer(T task,long timeout,TimeUnit timeUnit){
lock.lock();
try{
long nanos = timeUnit.toNanos(timeout);
while(queue.size()==capcity){
try {
log.debug("等待加入任务队列{}...",task);
if(nanos<=0){
return false;
}
nanos = fullWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("加入任务队列 {}",task);
queue.addLast(task);
emptyWaitSet.signal();
return true;
}finally {
lock.unlock();
}
}
P207 自定义线程池 线程池 拒绝策略
如果把拒绝策略写死在执行方法里需要很多的if-else判断,现在的思路是将拒绝策略的选择交给用户端,由用户来决定要用哪种拒绝策略。
现在可以用策略模式,把操作抽象为接口,具体的实现由调用者传递进来。
主要是修改main方法和execute方法:
P208 自定义线程池 线程池 拒绝策略 演示
@Slf4j(topic="c.TestPool")
public class TestPool {
public static void main(String[] args){
ThreadPool threadPool = new ThreadPool(1,1000,TimeUnit.MILLISECONDS,1,(queue,task)->{
//1.死等
// queue.put(task);
// 2)带超时等待
// queue.offer(task,1500,TimeUnit.MILLISECONDS);
// 3)让调用者 放弃任务执行
// log.debug("放弃{}",task); //队列一满就自动放弃执行
// 4)让调用者 抛出异常
// throw new RuntimeException("任务执行失败 "+task);
// 5)让调用者自己执行任务
task.run();
});
for (int i = 0; i < 4; i++) {
int j = i;
threadPool.execute(()->{
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("{}",j);
});
}
}
}
@FunctionalInterface //拒绝策略
interface RejectPolicy<T>{
void reject(BlockingQueue<T> queue,T task);
}
@Slf4j(topic="c.ThreadPool")
class ThreadPool{
//任务队列
private BlockingQueue<Runnable> taskQueue;
//线程集合
private HashSet<Worker> workers = new HashSet<>();
//核心线程数
private int coreSize;
//获取任务的超时时间
private long timeout;
//时间单位
private TimeUnit timeUnit;
private RejectPolicy<Runnable> rejectPolicy;
//执行任务
public void execute(Runnable task){
//当任务数没有超过coreSize时,直接交给worker对象执行
//如果任务数超过coreSize时,加入任务队列暂存
synchronized (workers){
if(workers.size()<coreSize){
Worker worker = new Worker(task);
log.debug("新增 worker{},{}",worker,task);
workers.add(worker);
worker.start();
}else{
taskQueue.tryPut(rejectPolicy,task);
}
}
}
public ThreadPool( int coreSize, long timeout, TimeUnit timeUnit,int queueCapcity,RejectPolicy<Runnable> rejectPolicy) {
this.taskQueue = new BlockingQueue<>(queueCapcity);
this.coreSize = coreSize;
this.timeout = timeout;
this.timeUnit = timeUnit;
this.rejectPolicy = rejectPolicy;
}
class Worker extends Thread{
private Runnable task;
public Worker(Runnable task) {
this.task = task;
}
public void run(){
//执行任务
// 1)当task不为空,执行任务
// 2)当task执行完毕,再接着从任务队列获取任务
while(task !=null || (task = taskQueue.poll(timeout,timeUnit) )!= null){
try{
log.debug("正在执行...{}",task);
task.run();
}catch(Exception e){
e.printStackTrace();
}finally {
task=null;
}
}
synchronized (workers){
log.debug("worker被移除{}",this);
workers.remove(this);
}
}
}
}
@Slf4j(topic="c.BlockingQueue")
class BlockingQueue<T>{
// 1.任务队列
private Deque<T> queue = new ArrayDeque<>();
// 2.锁
private ReentrantLock lock = new ReentrantLock();
// 3.生产者条件变量
private Condition fullWaitSet = lock.newCondition();
// 4.消费者条件变量
private Condition emptyWaitSet = lock.newCondition();
// 5.容量
private int capcity;
public BlockingQueue(int capcity) {
this.capcity = capcity;
}
//带超时的阻塞获取
public T poll(long timeout, TimeUnit unit){
lock.lock();
try{
long nanos = unit.toNanos(timeout);//将timeout时间统一转化为纳秒
while(queue.isEmpty()){
try {
//没等到直接返回
if(nanos<=0){
return null;
}
//返回的是剩余时间
nanos = emptyWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal();
return t;
}finally{
lock.unlock();
}
}
//阻塞获取
public T take(){
lock.lock();
try{
while(queue.isEmpty()){
try {
emptyWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal();
return t;
}finally{
lock.unlock();
}
}
//阻塞添加
public void put(T task){
lock.lock();
try{
while(queue.size()==capcity){
try {
log.debug("等待加入任务队列{}...",task);
fullWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.addLast(task);
log.debug("加入任务队列 {}",task);
emptyWaitSet.signal();
}finally {
lock.unlock();
}
}
//带超时时间的阻塞添加
public boolean offer(T task,long timeout,TimeUnit timeUnit){
lock.lock();
try{
long nanos = timeUnit.toNanos(timeout);
while(queue.size()==capcity){
try {
log.debug("等待加入任务队列{}...",task);
if(nanos<=0){
return false;
}
nanos = fullWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("加入任务队列 {}",task);
queue.addLast(task);
emptyWaitSet.signal();
return true;
}finally {
lock.unlock();
}
}
//获取大小
public int size(){
lock.lock();
try{
return queue.size();
}finally {
lock.unlock();
}
}
public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
lock.lock();
try{
//判断队列是否已满
if(queue.size()==capcity){
rejectPolicy.reject(this,task);
}else{ //队列有空闲
log.debug("加入任务队列 {}",task);
queue.addLast(task);
emptyWaitSet.signal();
}
}finally{
lock.unlock();
}
}
}
P209 ThreadPoolExecutor 池状态
ThreadPoolExecutor使用int的高3位来表示线程池状态,低29位表示线程池数量。
SHUTDOWN是温和的停止,不会接收新任务,但会处理阻塞队列中剩余的任务。
STOP是暴力的停止,不会接收新任务,同时会中断正在执行的任务,并抛弃阻塞队列的任务。
TIDYING,任务全部执行完毕,活动线程为0即将进入终结。
因为是int是有符号的,最高位为符号位。
将这些信息存储在一个原子变量中的目的是将线程状态与线程个数合二为一,用一次cas操作进行赋值,保证原子性。
P210 ThreadPoolExecutor 构造方法
很重要,构造参数会决定线程池的行为:
1. corePoolSize 核心线程数目
2.maximumPoolSize 最大线程数目(核心线程数+救急线程数=最大线程数)
3.keepAliveTime 生存时间,针对救急线程(在任务量激增时候,阻塞队列满了,会创建一个临时的救急线程来帮助处理任务,救急线程有生存时间)
4.unit 时间单位,针对救急线程
5.workQueue,阻塞队列
6.threadFactory,线程工厂,可以为线程创建时起个好名字
7.handler,拒绝策略
P211 ThreadPoolExecutor 构造方法
救急线程仅会出现在有界队列(阻塞队列能容纳的任务数有上限)中,当任务数超过队列大小,会创建maximumPoolSize-corePoolSize数目的救急线程来救急。
AbortPolicy抛出异常(告诉使用者线程池无法使用,已满)。
CallerRunsPolicy放弃本次任务。
DiscardPolicy放弃本次任务。
DiscardOldestPolicy放弃队列中最早的任务,本任务取代之。
P212 Executors 固定大小线程池
核心数==最大线程数,没有救急线程被创建,无需超时时间。阻塞队列无界,可以存放任意数量任务。
适用于:任务量已知且相对耗时的任务。
P213 Executors 带缓冲线程池
核心线程数为0,最大线程数为Integer.Max_VALUE
P214 Executors 单线程线程池
只有1个核心线程,没有救急线程,单线程线程池。
希望多个任务排队执行(保证串行,不会并发并行),线程数固定为1,任务数多于1时,会放入无界队列排队,就算任务执行完毕,这唯一的线程也不会被释放。
自己创建一个单线程串行执行任务,如果该线程会因任务执行失败而终止,而线程池会继续新建一个线程,保证池的正常工作。
P215 ThreadPoolExecutor submit
Callable与Runnable的差异在于,Callable会返回结果。
@Slf4j(topic = "c.Test1")
public class Test1 {
public static void main(String[] args) throws ExecutionException,InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<String> future = pool.submit(new Callable<String>() {
@Override
public String call() throws Exception {
log.debug("running");
Thread.sleep(1000);
return "ok";
}
});
log.debug("{}",future.get());
}
}
//Lambda简化
@Slf4j(topic = "c.Test1")
public class Test1 {
public static void main(String[] args) throws ExecutionException,InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<String> future = pool.submit(()->{
log.debug("running");
Thread.sleep(1000);
return "ok";
});
log.debug("{}",future.get());
}
}
P216 ThreadPoolExecutor invokeAll
@Slf4j(topic = "c.Test1")
public class Test1 {
public static void main(String[] args) throws ExecutionException,InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2);
List<Future<String>> futures = pool.invokeAll(Arrays.asList(
()->{
log.debug("begin");
Thread.sleep(1000);
return "1";
},
()->{
log.debug("begin");
Thread.sleep(500);
return "2";
},
()->{
log.debug("begin");
Thread.sleep(2000);
return "3";
}
));
futures.forEach(f->{
try {
log.debug("{}",f.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
}
}
P217 ThreadPoolExecutor invokeAny
哪个任务先被执行完毕,就会返回任务的执行结果,其它任务取消。
简言之就是找到一个最先执行完的任务,其它都结束了。
@Slf4j(topic = "c.Test1")
public class Test1 {
public static void main(String[] args) throws ExecutionException,InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(3);
String result = pool.invokeAny(Arrays.asList(
()->{
log.debug("begin");
Thread.sleep(1000);
log.debug("end");
return "1";
},
()->{
log.debug("begin");
Thread.sleep(500);
log.debug("end");
return "2";
},
()->{
log.debug("begin");
Thread.sleep(2000);
log.debug("end");
return "3";
}
));
log.debug("{}",result);
}
}
如果只有1个执行线程,打印结果是1:
P218 ThreadPoolExecutor 停止
shutdown
线程池状态变为SHUTDOW,不会接收新的任务,但已提交的任务会被执行,此方法不会阻塞调用线程的执行。
shutdowmNow
线程池状态变为STOP,不会接收新的惹怒,会将队列中的任务返回,并用interrupt方式中断正在执行的任务。
P219 ThreadPoolExecutor 停止演示
下面的例子可以说明:调用shutdown之后,已经提交的任务会执行完,但不能加入新的任务:
@Slf4j(topic = "c.Test1")
public class Test1 {
public static void main(String[] args) throws ExecutionException,InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<Integer> result = pool.submit(()->{
log.debug("task 1 running...");
Thread.sleep(1000);
log.debug("task 1 finish...");
return 1;
});
Future<Integer> result2 = pool.submit(()->{
log.debug("task 2 running...");
Thread.sleep(1000);
log.debug("task 2 finish...");
return 2;
});
Future<Integer> result3 = pool.submit(()->{
log.debug("task 3 running...");
Thread.sleep(1000);
log.debug("task 3 finish...");
return 3;
});
log.debug("shutdown");
pool.shutdown();
Future<Integer> result4 = pool.submit(()->{
log.debug("task 4 running...");
Thread.sleep(1000);
log.debug("task 4 finish...");
return 4;
});
}
}
如下图可见调用shutdownNow之后已有的任务便不会再执行,也不会再加入新的任务:
P220 设计模式 工作线程 定义
不同任务类型应该使用不同的线程池,避免饥饿,提升效率。
P221 设计模式 工作线程 饥饿 现象
固定大小的线程池会有饥饿现象。
大致场景:2个线程就像2个全能员工,既能招待客户也能做饭,如果一次性来了2个顾客,都跑去接待了,没有人做饭,就会导致饥饿。
@Slf4j(topic = "c.Test1")
public class Test1 {
static final List<String> MENU=Arrays.asList("地三鲜","宫保鸡丁","辣子鸡丁","烤鸡翅");
static Random RANDOM = new Random();
static String cooking(){
return MENU.get(RANDOM.nextInt(MENU.size()));
}
public static void main(String[] args) throws ExecutionException,InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.execute(()->{
log.debug("处理点餐...");
Future<String> f=pool.submit(()->{
log.debug("做菜");
return cooking();
});
try{
log.debug("上菜:{}",f.get());
}catch (InterruptedException|ExecutionException e){
e.printStackTrace();
}
});
pool.execute(()->{
log.debug("处理点餐...");
Future<String> f=pool.submit(()->{
log.debug("做菜");
return cooking();
});
try{
log.debug("上菜:{}",f.get());
}catch (InterruptedException|ExecutionException e){
e.printStackTrace();
}
});
}
}
一个客人可以完美处理,2个客人就处理不动了:
P222 设计模式 工作线程 饥饿 解决
可以设置2种类型的线程池:
@Slf4j(topic = "c.Test1")
public class Test1 {
static final List<String> MENU=Arrays.asList("地三鲜","宫保鸡丁","辣子鸡丁","烤鸡翅");
static Random RANDOM = new Random();
static String cooking(){
return MENU.get(RANDOM.nextInt(MENU.size()));
}
public static void main(String[] args) throws ExecutionException,InterruptedException {
ExecutorService waitpool = Executors.newFixedThreadPool(1);
ExecutorService cookpool = Executors.newFixedThreadPool(1);
waitpool.execute(()->{
log.debug("处理点餐...");
Future<String> f=cookpool.submit(()->{
log.debug("做菜");
return cooking();
});
try{
log.debug("上菜:{}",f.get());
}catch (InterruptedException|ExecutionException e){
e.printStackTrace();
}
});
}
}
P223 设计模式 工作线程 池大小
线程池过小会导致无法充分利用系统资源,容易导致饥饿。
线程池过大会导致更多的线程上下文切换,占用更多内存。
P224 Timer的缺点
所有任务都是由同一个线程来调度,因此所有的任务都是串行执行的。
只要前面有任务存在延迟或者异常,都会影响到后面的任务。
P225 ScheduledThreadPoolExecutor 延时执行
可以发现尽管任务1存在延时,但两个线程都是并行执行的。
@Slf4j(topic = "c.Test1")
public class Test1 {
public static void main(String[] args) {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
pool.schedule(()->{
log.debug("task1");
sleep(2);
// int i = 1/0;
},1,TimeUnit.SECONDS);
pool.schedule(()->{
log.debug("task2");
},1,TimeUnit.SECONDS);
}
}
可以发现就算是任务1中存在异常也不会影响任务执行。