JUC
一、线程与进程
1.概念
- 程序是由指令和数据组成,指令的运行需要将指令加载到CPU,数据的读写要加载到内存。同时还需要用到磁盘、网络等。进程就是用来加载指令、管理内存、管理IO的。
- 进程是资源分配的最小单元,一个进程中可以分为多个线程。
- 线程是最小调度单元。一个线程就是一个指令流,负责将指令流中的指令以指定的顺序交给CPU执行
- 在windows中进程是不活动的,只是作为线程的容器。
2.比较
- 进程是相互独立的,而线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,比如内存空间等,供其内部的线程共享
- 进程间的通信较为复杂:
- 同一个计算机内的进程通信为IPC
- 不同计算机之间的通信需要通过网络,并遵守共同的协议,比如HTTP等通信协议。
- 线程之间的通信相对简单,共享进程的内存,多个线程可以访问同一个共享变量(所以存在并发问题)
- 线程更加轻量,线程上下文切换成本一般比进程上下文切换消耗的资源少。
3.并行(parallel)和并发(concurrent)------单核并发,多核并行
- 并行:是指CPU同时执行多个线程,比如8核的CPU他可以同时处理8个线程,这就是一种并行
- 并发:是指CPU在不同线程间切换,看上去是同时发生的。比如单核的CPU去处理两个线程的任务,是通过程序计数器来记录不同线程指令的执行位置,来回切换(轮询)使得不同线程几乎在同一时刻执行。(微观上是串行的,宏观上的并发的)

4.异步调用
- 同步:方法调用时,需要等待结果返回才能继续运行,就是同步
- 异步:方法调用时,不需要等待结果返回,就能继续运行执行其他代码逻辑,这就是异步
- 设计:
多线程可以让方法执行变成异步:比如读取磁盘文件时,新开一个线程单独去处理读取任务,而主线程则继续执行其他任务,执行结束直接返回。
如果不加,这个读取的任务如果耗时5秒,主线程在读取之后的任务都需要等待5秒,读取完毕才继续实现。
5.提高效率
- 在多核CPU下,如果一个任务中总数是需要通过计算3个数之后汇总得到,比如nums1 耗时11ms,nums2耗时10ms,nums3耗时9ms,汇总耗时1ms。
- 如果串行则需要21ms,但是如果是多线程执行就取决于耗时最长的任务,11+1=12ms
二、线程的创建方法
- 当运行一个Java程序时,一启动默认就是一个线程(主线程),如果想单独再创建一个线程就需要自己额外去创建一个线程
1.new Thread()重写run()方法
- 使用
Thread重写run()方法 - 当使用
Thread创建一个线程时,此时仅仅是创建了一个Java的线程对象,还没有真正与操作系统的线程相关联(没有交给CPU去调度执行) - 使用
t1.start()启动线程,将该线程交给任务调度器,让任务调度器分配时间片交给CPU执行
java
package cn.itcast.test;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test1")
public class Test1 {
public static void main(String[] args) {
Thread t1 = new Thread(){
@Override
public void run() {
log.debug("running");
}
};
t1.setName("t1");
t1.start();
log.debug("running");
}
}
2.使用Runnable接口,配合Thread
- 先创建
Runnable对象,负责任务执行 - 然后将
Runnable对象交给Thread对象 - 调用
Thread对象的start()方法,将该线程与操作系统的线程相关联,分配时间片交给CPU执行
java
@Slf4j(topic = "c.Test1")
public class Test1 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
public void run() {
log.debug("running");
}
};
Thread t1 = new Thread(runnable);
t1.setName("t1");
t1.start();
log.debug("t1 started");
}
}
lambda简化写法(Java8以后)
java
@Slf4j(topic = "c.Test1")
public class Test1 {
public static void main(String[] args) {
Runnable runnable = () -> log.debug("running");
Thread t1 = new Thread(runnable, "t1");
t1.start();
log.debug("t1 started");
}
}
3.总结
- 两种方法将来在执行过程中其实走的都是线程对象的run方法:
target是传进去的任务对象

- 通过
new Thread并重写run方法是将线程和任务合并到了一起,通过runnable提交任务对象给线程是将线程和任务分开 - 使用
Runnable更加容易与线程池等高级API配合,并且让任务类脱离了Thread继承体系,更加灵活
4.使用FutureTask配合Callable创建线程
Runnable的run方法是没有返回值的,那么线程之间的交互就无法实现,FutureTask是实现了FutureRunnable,而FutureRunnable实现了Runnable接口,所以相当于是对Runnable的优化。- 所以使用
FutureTask来接收Callable对象,Callable的call()方法是有返回值的,所以该任务对象就是一个有返回值的任务对象 - 可以通过
get()方法来获取到该任务对象的返回值
java
@Slf4j(topic = "c.Test2")
public class Test2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
public Integer call() throws Exception {
log.debug("futureTask thread is running");
Thread.sleep(2000);
return 20251002;
}
});
Thread thread = new Thread(futureTask);
thread.start();
log.debug("{}", futureTask.get());
}
}
- 这里
futureTask返回的结果,哪个线程需要使用到,就由哪个线程调用futureTask的get()方法。

三、进程和线程的查看方式
1.Windows系统
- 使用任务管理器查看,并且可以杀死进程
- cmd:
Tasklist:查看进程- 查看指定的进程:
Tasklist | findstr java:查到Java有关的进程
- 查看指定的进程:
Taskkill:杀死进程Taskkill /F /PID xxxx:/F强制杀死,/PID指定进程号(PID)xxxx
2.Linux系统
-
top:动态查看进程占用系统的信息top -H -p xxxx:查看指定进程的线程信息,-H表示查看线程,-p表示查看的进程ID

-
ps -fe: 查看进程信息 -
ps -fe | grep java:使用管道查看相关的进程信息 -
kill xxxx:杀死指定PID的进程 -
jstack:抓取一个快照的进程信息(JDK提供)
3.Java
-
jconsole: -
通过
win+r唤起Java提供的图形化监视管理控制台:

-
可以通过选择线程或者内存等查看具体的状态:

四、线程运行原理
1.Java虚拟机栈
-
在JVM中每个线程的开启都会为线程分配一个Java虚拟机栈(线程私有)
-
线程中的方法执行则会向Java虚拟机栈中插入一个栈帧
-
栈帧包含着:
- 1.局部变量表(用于维护方法中的局部变量和方法的参数)
- 2.方法出口(也就是方法的返回地址)
- 3.操作数栈:方法内部的工作区,用于运算、方法调用、对象字段访问等,比如方法内执行加法时,会从栈中弹出两个int值,相加之后再把结果压回栈中
- 4.动态链接:
.class文件中,方法调用和字段访问等指令不是直接写死实际的内存地址,而是使用符号引用。方法运行时JVM需要将这些符号引用解析为真正的内存地址从而进行访问,动态链接区域中保存的就是指向运行时常量池的引用,方便字节码在执行时快速定位目标方法或者字段。

-
线程栈帧具体流程:

2.线程上下文切换(Context switch)
- 上下文切换是指CPU中断当前线程的执行,去执行其他线程
- 上下文切换的原因:
- 当前分配的时间片用完了,CPU自动切换到其他线程
- 垃圾回收
- 有更高优先级的线程需要执行
- 人为控制:调用
sleep(), yield(), wait(), join(), synchronized, lock()等方法
- 当context switch发生时,需要由操作系统保存当前线程的状态,并恢复另外一个线程的状态。比如Java虚拟机中的程序计数器:记住当前线程下一条jvm指令的执行地址,方便线程的上下文切换
- 维护线程的状态包括:程序计数器和虚拟机栈中的栈帧信息,比如局部变量表、操作数栈、返回地址等
3.线程常用方法
start():启动一个线程,只是让线程进入就绪状态,具体运行还需要等调度器分配CPU时间片才能运行sleep():让出当前线程的时间片,过一段时间再执行join():等待某个线程结束,常用于线程之间的通信,等待某个线程结束之后获取它的返回值。join(long n)可以设置等待线程运行结束时间ngetId():获取当前线程的唯一id


1)start() 与 run()
- 启动线程必须使用
start()方法,而run()方法则是线程启动后需要调用的方法,而在线程的run()方法中,实际调用的还是我们自己重写的run()方法的逻辑。 start()方法会让线程从NEW状态进入RUNNABLE状态->从新建状态变为就绪状态。start()方法不能多次调用,否则会报错
java
@Slf4j(topic = "c.Test2")
public class Test2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
public Integer call() throws Exception {
log.debug("futureTask thread is running");
Thread.sleep(2000);
return 20251002;
}
});
Thread thread = new Thread(futureTask);
System.out.println(thread.getState());
thread.start();
System.out.println(thread.getState());
log.debug("{}", futureTask.get());
}
}

2)sleep()与 yield()
1.sleep()
sleep():将线程状态由Running变为Timed_waiting状态
java
@Slf4j(topic = "c.Test4")
public class Test4 {
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
t1.start();
log.debug("t1 state: {}", t1.getState());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("t1 state: {}", t1.getState());
}
}

- 其他线程也可以打断正在睡眠的线程,通过调用线程的
interrupt()方法:
java
@Slf4j(topic = "c.InterruptMethod")
public class InterruptMethod {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread() {
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
log.debug("interrupted wake up ...");
throw new RuntimeException(e);
}
}
};
thread.start();
Thread.sleep(1000);
log.debug("interrupted");
thread.interrupt();
}
}

- 睡眠结束之后的线程并不是直接获取到CPU的时间片并执行,而是仍要等待任务调度器分配
- 使用
TimeUnit使用的sleep获取更好的可读性:TimeUnit.SECONDS.sleep(3)
2.yield()
- 将线程从
Running状态变成Runnable就绪状态,让CPU去调度执行其他线程 - 但不是一定会,因为任务调度器仍有可能继续将CPU时间片分配给当前线程,从而继续执行
3.两个方法的区别
sleep()方法让线程进入Timed_waiting状态,yield()让线程进入Runnable状态- 任务调度器会将CPU时间片分配给
Runnable状态的线程,而不会分配给Timed_waiting状态的线程。
4.线程优先级
- 线程优先级会提示(hint)调度器优先调度该线程,但是具体分配还是看调度器
- 如果CPU比较忙时,优先级高的线程会获得更多的时间片,但是CPU空闲时,优先级几乎没什么作用。
t1.setPriority(Thread.MIN_PRIORITY); t2.setPriority(Thread.MAX_PRIORITY);通过这种方式来设置线程的优先级。
3)join()
- 使用场景:异步调用场景下,当前线程需要得到另外一个线程运行之后的返回值时,使用
join() - 在方法1中,调用了一个线程去赋值result,但是该线程执行较慢(假设),如果不等该线程执行完毕就获取result的值,那么获取的只是初始值
java
@Slf4j(topic = "c.JoinMethod")
public class JoinMethod {
static int result = 1;
public static void main(String[] args) throws InterruptedException {
method1();
}
public static void method1() throws InterruptedException {
log.debug("method1 start...");
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
result = 10;
});
t1.start();
t1.join();
log.debug("result is {}", result);
log.debug("method1 end...");
}
}
-
当调用
join()方法之后,就会等到t1线程执行完毕,也就是完成给result的赋值之后才会获取result的值:

-
使用
join()方法就能够实现同步调用: -
当前线程中请求的数据需要等待发送数据的线程将数据发送完才能继续执行
-
而异步则是当前线程请求的数据无需等待返回结果就继续执行了。

-
当进行多个线程的同步时,消耗的时长为耗时最长的线程所用的时间:(并行执行的)

有等待时间的join()
- 可以设置一个等待线程返回结果的时间,超时则不再等待
- 同时,如果等待时间超出线程返回结果的时间,则以返回结果的时间为准,不是非要等到设置的时间。比如线程1返回结果需要2s,而主线程在等待时,设置
t1.join(3000),那么会在2s的时候返回结果,而不是等3s。
4)Interrupt()
1.打断处于阻塞状态的线程:
比如调用了sleep(), wait(), join()等方法从而进入阻塞状态的线程。会清空打断状态,一般被打断的线程会被标记为t1.isInterrupted() = true,而打断上述方法而进入阻塞状态的线程则会清空打断状态,也就是t1.isInterrupted() = false
java
@Slf4j(topic = "c.JoinMethod")
public class JoinMethod {
static int result = 1;
public static void main(String[] args) throws InterruptedException {
method1();
}
public static void method1() throws InterruptedException {
log.debug("method1 start...");
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
result = 10;
});
t1.start();
t1.interrupt();
log.debug("result is {}", result);
log.debug("t1 interrupt is {}", t1.isInterrupted());
log.debug("method1 end...");
}
}
sleep(),wait(),join()等方法导致线程进入阻塞状态,而此时如果使用interrupt()方法进行打断,则会清空打断状态,使得原本应该是isInterrupted() = true的,变为isInterrupted() = false默认。

2. 打断正常运行的线程:
正常运行线程的打断只是由调用线程做了一个标记,而真正决定是否停止运行还是由被打断的线程本身根据标记去判断是否需要中止:
- 比如下面的代码,线程t1如果不进行打断标记的判断,单凭在主线程中调用
t1.interrupt()只是将t1的isInterrupted() = true,只有当t1内部自己去判断打断标记并做出对应的中止操作,t1线程才能被打断。 - 也就是调用打断方法只是通知该线程需要被打断,从而让该线程去判断要不要打断并且处理一些打断前的操作。
java
@Slf4j(topic = "c.JoinMethod")
public class JoinMethod {
static int result = 1;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted) {
break;
}
}
}, "t1");
t1.start();
Thread.sleep(1000);
t1.interrupt();
log.debug("t1 isInterrupted ? " + t1.isInterrupted());
}
}
设计模式------ 线程的两阶段终止模式
- 让线程在被停止前能够处理一些善后工作

- 假设场景为:监控事件需要不断监视,但是如果按下停止按钮则保存数据并停止,但是在监视过程中如果出现异常情况也要进行停止监视:
java
@Slf4j(topic = "c.TwoStage")
public class TwoStage {
public static void main(String[] args) throws InterruptedException {
TwoStageInterrupt t = new TwoStageInterrupt();
t.start();
Thread.sleep(3500);
log.debug("interrupt...");
t.stop();
}
}
@Slf4j(topic = "c.TwoStageInterrupt")
class TwoStageInterrupt {
private Thread mintor;
public void start() {
mintor = new Thread(() -> {
while (true) {
Thread thread = Thread.currentThread();
if (thread.isInterrupted()) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("mintor is running...");
} catch (InterruptedException e) {
//如果睡眠被打断则会清空打断标记,则还是false,不会陷入终止状态
//于是需要重新再打断
thread.interrupt();
e.printStackTrace();
}
}
});
mintor.start();
}
public void stop() {
mintor.interrupt();
}
}
- 在使用
interrupt()对sleep中的线程进行打断之后,由于是抛出异常并清空打断标记,所以正常是无法停止执行的,因为打断标记被重置为false,此时我们需要在抛出异常之后再次调用interrupt(),停止当前正在运行的线程。这样它的打断标记就为true了,下次循环判断时就会判定为打断 ,从而停止线程。

isInterrupted()和interrupted()
isInterrupted()只会判定是否被打断,不会清除标记,而interrupted()判断当前线程是否被打断并且会清除打断标记。
4.守护线程
- 正常线程都是执行完本身的任务之后才会停止,而不会管其他线程的状态,比如在主线程main中开启了一个一直循环的线程t1,那么当前main执行完自己的任务之后就会停止,而t1则仍会一直循环直到中断或者进程停止
- 而守护线程则是如果当前其他线程停止了,不管守护线程的任务有没有执行完都会随之停止。
- 垃圾回收器就是一种守护线程,Java中的垃圾回收机制,当堆内存中的对象没有被引用或者长时间没有使用,并且内存不足时就会触发垃圾回收线程进行垃圾回收,如果程序停止了,垃圾回收也立即强制停止

五、线程的状态
1.操作系统层面------五种
初始状态:语言层面创建线程对象,还未和操作系统的线程关联可运行状态:(就绪状态)指该线程已经和操作系统的线程进行关联,只需要等待任务调度器分配时间片就可以由CPU调度执行运行状态:指该线程正在CPU时间片运行中的状态- 当分配的CPU时间片用完,就会从运行状态变为可运行状态,等待任务调度器的CPU时间片分配
阻塞状态:调用了一些阻塞API,比如BIO读写文件,这时的线程并不会用到CPU,就会导致线程的上下文切换,进而转变为阻塞状态。- 当阻塞状态结束,线程会变回到可运行状态,等待任务调度器重新分配CPU时间片。只有重新获得CPU时间片才能进入运行状态。
终止状态:线程顺利执行完所有的任务,从而自动停止,线程结束,不会再转换为其他状态。

2.Java层面------六种
-
NEW:线程刚被创建,还没有调用start()方法(此时还未和操作系统的线程关联起来) -
RUNNABLE:涵盖了可运行状态、运行状态、阻塞状态,比如由于BIO导致的线程阻塞,在Java中无法区分,仍认为是可运行状态。
-
BLOCKED:当前线程尝试获取锁,但是该锁被其他线程占用,正在阻塞获取 -
WAITING:等待唤醒状态 -
TIMED_WAITING:有时间的等待状态 -
TERMINATED:终止状态,表示线程结束 -
后面三种都是JavaAPI层面堆阻塞状态的细分

-
六种状态发送的情形:
java
@Slf4j(topic = "c.ThreadState")
public class TreadState {
public static void main(String[] args) {
//NEW
Thread t1 = new Thread(() -> {
log.debug("t1");
},"t1");
//TERMINATED
Thread t2 = new Thread(() -> {
log.debug("t2");
}, "t2");
t2.start();
//RUNNABLE
Thread t3 = new Thread(() -> {
while (true) {
}
}, "t3");
t3.start();
//TIMED_WAITING
Thread t4 = new Thread(() -> {
synchronized (TreadState.class) {
try {
Thread.sleep(1000000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t4");
t4.start();
//WAITING
Thread t5 = new Thread(() -> {
try {
t3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t5");
t5.start();
Thread t6 = new Thread(() -> {
synchronized (TreadState.class) {
try {
log.debug("t6 running ...");
} catch (Exception e) {
e.printStackTrace();
}
}
}, "t6");
t6.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1 state is {}" , t1.getState());
log.debug("t2 state is {}" , t2.getState());
log.debug("t3 state is {}" , t3.getState());
log.debug("t4 state is {}" , t4.getState());
log.debug("t5 state is {}" , t5.getState());
log.debug("t6 state is {}" , t6.getState());
}
}

小结
-
线程的三种创建方式:
new Thread():直接创建新线程并重写run()方法new Thread()创建线程,并创建Runnable任务对象重写run()方法,将任务对象交给thread去执行调用任务对象里面的run()new Thread()创建线程,并创建FutureTask对象,传入Callable对象,在Callable对象中重写有返回值的call()方法,将FutureTask对象交给thread进行执行。
-
线程上下文切换(程序计数器)
-
进程是资源分配的最小单元,线程是最小的调度执行单元
-
常用的线程方法:
sleep(),join(),interrupt(),start(),run(),yield() -
线程在操作系统层面的五种状态:初始状态、可运行状态、运行状态、阻塞状态、终止状态
-
线程在Java层面的六种状态:
NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED -
线程的设计模式:两阶段终止模式------在线程停止之前需要让线程处理一些事情在停止,而不是立马停止。
-
栈帧:局部变量表、动态链接、返回地址、操作数栈
共享模型
一、共享问题
-
多个线程访问共享资源时就会很容易发送共享问题
-
因为Java中的一行指令对于的字节码指令可能是多条,那么 多线程去进行上下文切换时,就会导致多指令的乱序交错执行(比如t1线程刚准备写入result值时,切换到了t2去操作result值,下次再切换到t1线程时它会直接写入result值,从而覆盖掉t2线程的操作数组,使得最终的结果不一致),从而发生并发问题。
-
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。

- 竞态条件:多个线程在临界区执行,由于代码的执行序列不同而导致结果无法预测,称为发生了竞态条件。
二、synchronized解决方案
- 阻塞式的解决方案:synchronized, lock
- 非阻塞式的解决方案:原子变量
synchronized:俗称对象锁------采用互斥的方式让同一时刻只能有一个线程持有对象锁,其他线程再想去获取这个对象锁就会被阻塞住(BLOCKED)。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程的上下文切换(因为即使被切换了,对象锁还是在该线程手里,其他线程没有锁无法执行临界区代码对共享资源进行修改。只有当该线程执行完将结果定下来,释放锁,并唤醒其他阻塞线程去获取锁对象并操作对应的共享资源)。
对象锁------由多个线程共享的对象,通过锁定该对象阻止其他线程获取,从而达到互斥的效果。

synchronized是使用对象锁保证了临界区内代码的原子性,使得临界区内的代码是不可分割的,并且不会被上下文切换所打断。
java
@Slf4j(topic = "c.Test17")
public class Test17 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.increment();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", room.getCounter());
}
}
class Room {
private int counter = 0;
public synchronized void increment() {
counter++;
}
public synchronized void decrement() {
counter--;
}
public synchronized int getCounter() {
return counter;
}
}

- 问题1:如果放在for循环的外面,那么相当于锁的范围是5000* 4,原本的范围只是锁住4条对共享资源current的读写。锁的范围越大其他线程所等待当前线程执行完这些任务的时间增大,性能就大幅降低。
- 问题2:这样两个线程相当于拿到的不是同一把锁,不能互斥也就保证不了对共享资源的安全操作。
- 问题3:在线程t1拿到锁时,t2相当于不用锁就能访问共享资源,于是还是会发生读写的冲突。
线程八锁------考察锁的对象是否是同一个
- 将
synchronized加在成员方法上就相当于锁住的是this对象 - 将
synchronized加在静态方法上就相当于锁住的是class对象


- 情况分析:如果是线程1先获得锁,那么会先睡1秒,然后打印1,然后线程2获得锁打印2;如果是线程2先获得锁,那么会先打印2,然后线程1获得锁,睡1秒然后打印1。

- 情况分析:线程3调用的c方法没有加锁肯定是得到CPU时间片之后就会打印3,然后线程1和2的情况则和上面的一样。

- 情况分析:先打印2然后1s后打印1

- 情况分析:线程1锁住的是
class对象,线程2锁住的是this对象

- 情况分析:锁住的都是
class对象,存在互斥,是可以保护共享资源读写的原子性的

- 情况分析:虽然n1和n2指向同一个
class对象,但a()和b()锁住的不是同一个对象,一个是this对象,一个是class对象。

- 情况分析:n1和n2都是
Number()类对象,a()和b()都是静态方法加synchronized锁住的是class对象,所以是同一个对象,会存在互斥。那么先睡1秒然后打印1,再打印2,或者是先打印2,然后睡1秒打印1。
六、线程安全问题分析
1.成员变量和静态变量是否线程安全?
- 如果它们没有共享,则线程安全
- 如果存在共享,根据状态是否改变分为两种情况:
- 只有读操作,则线程安全
- 存在读写操作,则这段代码是临界区,需要考虑线程安全
2.局部变量是否安全?
- 局部变量是线程安全的
- 但是局部变量引用的对象则不一定线程安全:
- 如果该对象没有逃离方法的作用范围,它是线程安全的
- 如果该对象逃离方法的作用范围,则需要考虑线程安全问题
3.成员变量和局部变量的共享问题
java
public class TestThreadSafe {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadSafeSubClass test = new ThreadSafeSubClass();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + (i+1)).start();
}
}
}
class ThreadUnsafe {
ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
method2();
method3();
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
}
class ThreadSafe {
public void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
System.out.println(1);
list.remove(0);
}
}
-
1.当成员变量被多个线程访问(比如上述代码中
ThreadUnSafe类中的list):由于是成员变量,类对象被创建时只会创建一个list集合,那么调用method1方法时里面的method2和method3都会操作该集合,同时其他线程的method2和method3也会操作该集合,就会出现method3移除list中的元素之后,其他线程的method3又进行移除,此时就会报异常。
-
也就是无论哪个线程中的method2引用的都是同一个对象中的list成员变量

-
2.当局部变量被多个线程访问(比如上述代码中
ThreadSafe类中的list):由于是局部变量,每次执行method1方法都会创建一个list集合,并且method2和method3都是由method1创建的线程然后传递过去的,这样每个线程的list集合指向的对象就不再是同一个对象,由此就不会产生线程安全问题。

- 当子类覆盖父类的方法,导致局部变量的引用暴露给了其他线程,从而发生线程安全问题:
java
public class TestThreadSafe {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadSafeSubClass test = new ThreadSafeSubClass();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + (i+1)).start();
}
}
}
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list) {
list.add("1");
}
//private
public void method3(ArrayList<String> list) {
System.out.println(1);
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe{
// @Override
public void method3(ArrayList<String> list) {
System.out.println(2);
new Thread(() -> {
list.remove(0);
}).start();
}
}
- 比如上述的代码中,
ThreadSafeSubClass重写了父类ThreadSafe的method3方法,并新开了一个线程,那么由于创建的是ThreadSafeSubClass对象,它在执行method3的时候会调用重写的方法,然后新开线程,由于将局部变量list传递过去了,所以新开线程也能对局部变量list进行修改,也就产生了临界区,容易出现线程安全问题。
修饰符对于线程安全的保障------开闭原则中的【闭】(不想让子类改变父类的行为)
- 子类不能重写父类中
private的方法:也就是说将某些可能涉及到并发修改的方法设置为私有,就可以避免子类去覆盖掉这些方法,从而防止出现线程安全问题 - 子类中不能重写父类中
fianl修饰的方法:同时对一些需要对外开放方法进行final修饰,防止子类对该方法进行修改从而出现线程安全问题。
4.常用的线程安全类
StringIntegerStringBufferRandomVectorHashtablejava.util.concurrent包下的类
这里的类线程安全是指:多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为它们的每个方法是原子的。
- 但是不同方法的组合可能就不是线程安全的了。比如:

- 多个线程通过
get()方法去判断当前是否存在这个键,但是当两个线程都判断为null时,都会去往Hashtable中存放同一个键,那么就会产生冲突。
1.不可变类线程安全性
String等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的。String的replace()、substring()等方法是如何保证线程安全的?- 在方法底层是直接将其中的一部分直接复制出来重新创建一个
String对象然后赋值。
- 在方法底层是直接将其中的一部分直接复制出来重新创建一个
2.实例分析
- 例1:

- 分析:HttpServlet只有一份并且是需要在多个tomcat中使用的,所以该类中的成员变量都会被当成共享资源被访问。
- map 不安全
- S1 安全 S2 安全
- D1 不安全
- D2 不安全 ------
final修饰引用数据类型,只是固定了它引用地址不变,但是Date()类型中的值还是有可能被修改,比如年月日被不同线程修改
- 例2:

- 分析:HttpServlet只有一份并且是需要在多个tomcat中使用,而UserService属于成员变量因此也只有一份。所以该类中的成员变量会被当成共享资源被访问。
- 而在UserService中又有一个成员变量count,当多个线程去执行update()时就会发生共享资源的读写,从而出现线程安全问题。
- 例3:

- 分析:这是使用AOP进行切面通知的常用做法,但是也是存在线程安全问题的
- 因为在Spring中通常是
单例Bean,因此MyAspect通常只有一个,那么就会涉及到多个线程对它的成员变量的修改,导致起始时间发生改变。 - 一般使用环绕通知进行包裹,使得整个记时是原子的
- 例4:

- 分析:上述三个地方都是线程安全的
- 因为在UserDao中,没有成员变量,而它的update()方法中,conn是属于局部变量,不会被多个线程共享,因此是线程安全的
- 例5:

- 分析:由于UserDao中conn是定义成了成员变量,因此就会存在多个线程访问并改写,最终出现线程安全问题,所以是不安全的。
- 例6:

- 分析:在UserService中,每次执行update()方法都创建一个UserDao作为局部变量,因此即使UserDao中是可能存在安全问题的,但是由于不是同一个UserDao所以还仍是安全的。
- 例7:

- 分析:虽然代码中sdf是局部变量,但是将局部变量传递给了一个抽象方法,而如果抽象方法的实现方法中如果对该局部变量进行了改写就会导致不安全。
3.总结:谨记开闭原则中的【闭】
String类中为什么设置为final?
String类本身是安全的,但是如果子类中覆盖了父类中的方法,子类就会破坏原本方法的安全性,导致出现线程安全问题。------所以String使用final修饰,体现了开闭原则。
4.练习
售票:
java
@Slf4j(topic = "c.ExerciseSell")
public class ExerciseSell {
public static void main(String[] args) throws InterruptedException {
// 模拟多人买票
TicketWindow window = new TicketWindow(1000);
// 所有线程的集合
List<Thread> threadList = new ArrayList<>();
// 卖出的票数统计
List<Integer> amountList = new Vector<>();
for (int i = 0; i < 2000; i++) {
Thread thread = new Thread(() -> {
// 买票
int amount = window.sell(random(5));
// 统计买票数
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());
}
// Random 为线程安全
static Random random = new Random();
// 随机 1~5
public static int random(int amount) {
return random.nextInt(amount) + 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;
}
}
}
-
分析:在上述代码中,分析出共享资源的读写
-
1.
amountList.add(amount);:由于amountList使用的是Vector<>包装类是线程安全的,所以对该资源进行读写时不会发生线程安全问题。

-
2.
int amount = window.sell(random(5));:window中,该sell()方法对成员变量count进行了读写,所以是属于临界区,容易发生线程安全问题。- 解决办法:我们需要保护count,由于count是属于
TicketWindow的成员变量,锁住该this对象即可,于是我们在sell()方法上加锁,sychronized加在成员方法上即锁住的是this对象。
- 解决办法:我们需要保护count,由于count是属于
-
3.
threadList.add(thread);:是由主线程进行操作,并不会由多个线程共享。
转账
java
@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
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();
// 查看转账2000次后的总金额
log.debug("total:{}", (a.getMoney() + b.getMoney()));
}
// Random 为线程安全
static Random random = new Random();
// 随机 1~100
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);
}
}
}
}
- 分析:上述代码中,a和b的money都是共享资源会被其他线程修改,那么像之前一样仅仅锁住
this对象的话每次只能锁住一个money,另外一个仍会产生共享问题,那么我们就需要对Account加synchronized对象锁,将两个account对象都锁住,这样当进行转账时,对两个account进行修改时就不允许其他线程来修改任意一个account对象里面的money值,就能避免线程安全问题。(只是这样效率较低,现在使用初级的方法来解决,进阶再学其他)
七、锁进阶
1.对象头
以32位的虚拟机为例(一个字节8bits)
Java中每个对象在JVM中的包含了对象头的,对象头中存放对象的相关信息,比如:
-
1.普通对象中:对象头由64bits组成,包含
Mark word(32bits)和Klass word(32bits)*
Klass word是一个指向该对象属于哪个class的指针(通过该指针可以找到类对象)

-
2.数组对象中:对象头由93bits组成,包含
Mark word(32bits)和Klass word(32bits)以及Array length(32bits)Array length:记录的是数组的长度

-
而在
Mark word中:- 正常状态下:有25位是记录该对象的
hashcode, 然后age是代表在新生代中断年龄,当年龄到达15就会通过GC进入老年代,还记录了biased_lock::是否加了偏向锁,最后的01两位是代表该对象的加锁状态
- 正常状态下:有25位是记录该对象的

2.Monitor(锁)
-
又称为监视器 或者管程
-
每个Java对象都可以关联一个
Monitor对象(操作系统提供),如果使用synchronized给对象上锁(重量级锁)之后,该对象的Mark word中就被设置一个指向Monitor对象的指针,而对象中原本的GC年龄和hashcode信息被缓存到Monitor对象中。 -
当一个
obj对象加了synchronized之后,JVM会尝试为它寻找一个Monitor对象,找到了就会在该obj对象的对象头的Mark word中设置该Monitor的地址(将其他清空),并且锁的标识位01变为10,表示加了重量级锁。 -
当执行完毕,释放锁时,会重置
obj对象的Mark word,从Monitor中恢复它的hashcode、对象头等信息。

-
比如图中三个线程都来访问被加锁的
obj对象,那么当一个thread2来获取锁的时候,由于obj中的Mark word中存放了Monitor的地址,那么thread2会去访问Monitor,判断里面的属性Owner是否有线程占用,如果没有则thread2成为该Monitor的owner。 -
当
thread1和thread3执行到该临界区时,都会去访问Monitor而此时Monitor对象的Owner已经存在了,thread1和thread3就会被放入到EntryList链表中,进入Java线程的BLOCKED状态。 -
只有当
thread2执行完,释放锁之后,会唤醒EntryList中的等待线程thread1和thread3去尝试成为Owner,竞争锁,竞争时是公平的,不是谁在前面就能先获得锁。 -
而之前获得过锁,但条件不满足进入
WAITING状态的线程,比如thread4、thread5就会进入到WaitSet中。 -
注意:
- 只有
synchronized加的是同一个obj对象才会关联同一个Monitor对象 - 没有加
synchronized的对象不会关联监视器
- 只有


- 如何保证锁的释放:
- 当临界区代码正常执行完毕之后,会通过
monitorexit将lock对象的Mark word重置,恢复回原来的形式,并唤醒EntryList中等待的线程来竞争锁,然后通过go to 24返回 - 当代码执行中间出现异常:由
Exception table来维护可能出现异常的字节码指令区间,比如6 -16如果出现异常就会跳转到19将异常赋值给临时变量,然后通过monitorexit将lock对象的Mark word重置,恢复回原来的形式,释放锁并唤醒EntryList中等待的线程来竞争锁,最后athrow将临时变量中的异常抛出。
- 当临界区代码正常执行完毕之后,会通过
3.锁升级
- Java6之后对
synchronized关键字获取锁的方式进行了改进。 - 重量级锁
- 轻量级锁
- 偏向锁
1)故事引入

- 老王更加贴合的表述应该是:JVM

- 理解:
- 当没有锁竞争时,为了减少消耗,使用轻量级锁,出现了锁竞争时,就升级为重量级锁
- 当某个资源长期由固定线程进行访问时,使用偏向锁(标识符),当有其他线程尝试访问时,就升级为轻量级锁。
4.轻量级锁
- 当一个对象虽然有多个线程访问,但是每个线程访问的时间都是错开的(比如线程1访问加锁执行释放锁之后,线程2才来访问),没有竞争,那么可以使用轻量级锁来优化。
- 轻量级锁对使用者是透明的,语法仍然是
synchronized,JVM底层会先尝试去给对象加轻量级锁,当加轻量级锁失败时才会加重量级锁。
轻量级锁加锁过程

- 当某个线程执行到加锁部分时,该线程的执行该加锁部分的方法栈帧中会创建一个锁记录
Lock Record用于存储加锁对象的指针以及加锁对象的Mark Word

- 当
Lock Record被创建时,包含两个部分:一个是当前锁记录的地址lock record 地址 00,以及Object reference(后续用于记录加锁对象的地址)

- 加锁时,会让
Lock Record中的Object reference指向加锁对象地址,并且尝试用CAS (compare and switch) 替换Object对象头中的Mark word,将Mark word的值存入锁记录(当锁释放时再换回来)。
加锁成功
- 如果
CAS替换成功,Object的Mark word中存放的是lock record锁记录地址和状态00,表示加了轻量级锁。只有当Object对象头中的Mark word的锁标识位为01,也就是无锁状态时才能替换成功。

加锁失败
- 如果加锁失败,有两种情况:
- 一种情况是:
Object对象头的Mark word中指向的是其他线程的锁记录地址并且状态是00,说明已经有其他线程给该对象加了轻量级锁,产生了竞争,就会进入锁膨胀。 - 第二种情况:自己本身执行了
synchronized锁重入,也就是持有锁的线程又再次访问该加锁对象,那么会判断Object对象头中的Mark word中锁记录地址对应的栈帧是否属于当前线程,如果是当前线程中的栈帧,则会再添加一条新的Lock Record作为重入的计数,并且该Lock Record原本保存Object对象头的Mark word,但是如果是锁重入则存放null作为锁重入的标识。
- 一种情况是:

- 当退出
synchronized代码块(解锁时),如果有取值为null的Lock Record,表示该锁记录是重入的锁记录,这时重置锁记录,表示重入计数减一

解锁
- 当解锁时,
Lock Record锁记录中的值不为null时,会使用CAS将Mark word的值恢复给对象头(CAS大致是判断Object对象头的Mark word中的锁标识位是否为00)- 成功恢复则说明解锁成功。
- 失败,则说明
Object对象头的Mark word中的锁标识位不是00,说明当前轻量级锁进行了锁膨胀或者已经升级为重量级锁,进入重量级锁解锁流程。
5.锁膨胀
- 在尝试添加轻量级锁的过程中,CAS操作失败,就表示有其他线程已经为该加锁对象添加了轻量级锁(有竞争),这时就会进入锁膨胀过程,将轻量级锁变成重量级锁。
- 当线程0已经对
obj对象加了轻量级锁,线程1再尝试给obj对象加轻量级锁时,线程1的Java虚拟机栈中的栈帧里也会为obj对象创建lock record,并将lock record的object reference指向obj对象,开始使用CAS去交换locke record地址和Mark word中的信息,但是此时obj的对象头中的Mark word已经变成00加轻量级锁状态了,就会加锁失败,那么线程1由于获取不到锁就需要进入阻塞状态。

- 锁膨胀过程:
- 首先会为
obj对象申请Monitor锁,让obj地址指向重量级锁地址,并且将原本存储在锁记录中的Mark word(包含哈希码、GC年龄等)转移到Monitor中保存。 - 然后线程1则会进入
Monitor的EntryList进入BLOCKED状态
- 首先会为

- 当线程0执行完进行解锁时,使用CAS尝试将Mark word中的值恢复到对象头中时,此时失败了,因为
obj对象头中的轻量级锁地址被替换为了重量级锁的地址,则会进入重量级锁的解锁流程:- 先重置Owner为null
- 唤醒在EntryList中等待的其他线程去获取锁。
6.锁自旋
- 重量级锁竞争时,可以使用自旋进行优化,如果当前线程自旋成功(即这时候刚好持有锁的线程退出了同步块,释放了锁),此时当前线程就可以避免进入阻塞状态(需要进行上下文切换),减少CPU的消耗(需要先获取
Monitor对象地址然后尝试获取等开销较大)。

- 每次线程来访问加锁对象时,会尝试进行自旋,但是如果自旋几次之后还是无法获取到锁就会进入EntryList进入阻塞状态。

- 锁的自旋只有在多核CPU时才有性能优势。
7.偏向锁
-
轻量级锁在没有竞争时,每次进入重入仍然需要进行CAS操作,并且将lock record中的地址与
obj中的地址尝试交换。 -
在Java6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark word头,之后如果发现这个线程ID是自己的线程ID就表示没有竞争,不用重新CAS,以后只要不发生竞争这个对象就归该线程所有。

-
原本的轻量级锁中,同一个线程中发生锁重入,每次需要先生成锁记录,然后使用CAS去替换
Mark word,由于是锁重入,markword并不会发生替换,而是在lock record中置为null,表示发生锁重入。 -
而偏向锁,则是在第一次获取到该锁对象时,会使用当前线程的ID去替换markword中的信息,而后面会判断当前对象头中的线程ID是否是自己,是则

偏向锁-状态

- 加偏向锁的对象,在对象被刚创建时,对象头中都是正常赋值为0,然后Mark word中后面3位就为
101表示偏向锁标记,当执行到同步代码块中时,就会将该对象的对象头前54位变成当前线程的ID,后续如果是同一个线程访问时,发现该对象的Mark word中的线程ID是本身,就无需再使用CAS进行比较交换。 - 而当加了偏向锁的对象,在当前线程执行完进行锁释放之后,该对象的
Mark word中还是保存的是当前线程的id。

禁用偏向锁
- 偏向锁适用场景:只有当前一个线程对共享资源进行访问,很少有其他线程来进行读写,就可以将该对象加上偏向锁。

- 禁用偏向锁之后,默认给共享对象加的就是轻量级锁

####撤销偏向锁
- 1.使用加锁对象的
obj.hashcode()方法,就会禁用调偏向锁,因为使用该方法,就需要给该对象生成hashcode放到对象头中,此时线程ID就会被剔除(空间不够)。


- 2.其他线程使用对象
当其他线程使用到该加偏向锁的对象时,


调用wait()或者notify()方法
- 当调用
wait()和notify()方法时,就进入到重量级锁的范畴,自然而然地就使得偏向锁被撤销。
8.锁消除
- 锁消除是JVM
即时编译器(JIT)的一种优化技术 - 核心作用:在编译阶段自动移除那些被判断为不可能存在竞争的锁,从而减少同步操作带来的性能开销。

- 比如上述列子中,方法a不加锁对x进行修改,而方法b则在其内部创建局部变量加锁之后对x进行修改,其实方法b中加锁的是局部变量,而其他线程无法获取到该局部变量,不会存在多线程竞争的可能,于是JIT在编译期就会抹除加锁状态。

- 可以看出两个方法的性能消耗是差不多的,说明此时底层并没有进行同步代码块的操作,两个方法的执行流程差不多

- 当使用JVM参数取消锁消除的优化方式,那么就会使得方法b每次进行修改时就会执行同步代码块的逻辑,增加性能的消耗。
八、wait()和notify()
1.概念引入


- 通俗的来讲就是当一个持有锁的线程需要等待其他条件满足时(比如他需要的资源被其他线程持有了,只有等待其他线程释放之后,该线程获取到之后才能继续往下执行),他会进入
waitset()中进行等待,当他的条件满足之后,就会被唤醒,重新去尝试获取锁。
2.wait(),notify()原理

- 持有锁的线程发现当前条件不满足时,就会调用加锁对象的
wait()方法,进入WaitSet中变为Java中的WAITTING或者TIMEDWAITTING状态。 BLOCKED和WATTING的线程都处于阻塞状态,不会占用CPU时间片BLOCKED线程会在持有锁线程进行锁释放时被唤醒WAITTING线程则会在Owner线程调用notify()或者notifyAll()方法时被唤醒,唤醒之后也同样是进入到EntryList中进行锁的竞争获取
3.常用API
wait()、notify()、notifyAll()都属于Object的方法,因此只有获取到该对象的锁对象,之后,才能调用这些方法。obj.wait()让进入obj监视器的线程进入WaitSet进行等待obj.notify()会从WaitSet中挑选一个线程唤醒,使其重新进入到EntryList中去竞争锁obj.notifyAll()会唤醒WaitSet中的所有线程,去竞争锁。obj.notify(long time),等待指定的时间,等待时间内如果没有线程将他唤醒,则继续执行代码,不会再等待。
4.wait(long n)和sleep(long n)的区别
-
1.
sleep()是Thread的静态方法,而wait()是Object的方法 -
2.
sleep()不需要强制和sychronized配合使用,而wait()需要和sychronized一起使用 -
3.
sleep()期间不会释放锁,而是一直持有,其他线程就无法获取锁;而使用wait()时会释放锁,使得其他线程能够获取锁 -
共同点:使用两个方法之后,线程进入的状态都是
TIMED_WAITTING状态。
九、设计模式------保护性暂停
Guarded Suspension:用在一个线程需要等待另外一个线程的返回结果时- 当有一个结果需要从一个线程传递到另外一个线程时,就需要让这两个线程关联到同一个
Guarded Object - 如果有结果不断从一个线程传递到另外一个线程,就需要使用到消息队列(生产者和消费者模式)
- JDK中
join()和Future的实现就是采用的保护性暂停模式 - 因为一个线程需要等待到另外一方的结果,所以分为同步模式。
1.设计模式实现案例
- 创建一个
GuardedObject对象,使得两个线程关联同一个对象,并调用GuardedObject的方法实现线程间的结果传递。(在两个线程之间实现结果的交互) - 这种设计模式的好处 :如果使用
join()也就是等待另外一个线程执行完结果并返回,那么主线程需要等待执行任务的线程结束才能继续执行。而使用这种保护模式则不需要等待执行任务的线程结束,只需要执行任务的线程执行完这个任务发送消息通知(notifyAll())就可以继续执行。这种情况下,执行任务的线程还可以执行其他的任务或者代码,可以一人担多职。 join()方法下,执行任务的线程如果分配其他任务那么,主线程也要等待其他无关任务执行完才能获取到结果并继续执行。
java
package cn.itcast.MyTest;
import cn.itcast.pattern.Downloader;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
@Slf4j(topic = "c.TestGuardedSuspension")
public class TestGuardedSuspension {
public static void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
new Thread(new Runnable() {
public void run() {
log.debug("获取结果");
List<String> response = (List<String>) guardedObject.get();
log.debug("文件大小为: {}", response.size());
}
}, "t1").start();
new Thread(new Runnable() {
public void run() {
log.debug("正在下载。。。");
try {
Object response = Downloader.download();
guardedObject.set(response);
} catch (Exception e) {
e.printStackTrace();
}
}
}, "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 set(Object response) {
synchronized(this) {
this.response = response;
this.notifyAll();
}
}
}
2.超时扩展
- 当执行任务的线程时间超过指定时间之后,主线程放弃等待,退出循环的等待结果返回
- 同时还可以避免因为虚假唤醒导致的等待时间重置(waitTime = timeOut - responseTime)------每一轮应该等待的时间。
java
package cn.itcast.MyTest;
import cn.itcast.n2.util.Sleeper;
import cn.itcast.pattern.Downloader;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
@Slf4j(topic = "c.TestGuardedSuspension")
public class TestGuardedSuspension {
public static void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
new Thread(new Runnable() {
public void run() {
log.debug("获取结果");
Object response = guardedObject.get(2000l);
System.out.println(response);
}
}, "t1").start();
new Thread(new Runnable() {
public void run() {
log.debug("正在下载。。。");
try {
Thread.sleep(3000);
guardedObject.set(new Object());
} catch (Exception e) {
e.printStackTrace();
}
}
}, "t2").start();
}
}
class GuardedObject {
private Object response;
public Object get(long timeOut) {
long start = System.currentTimeMillis();
synchronized(this) {
long responseTime = 0;
while (response == null) {
//如果消耗时间比设置等待时间大,则退出等待-> 即等待时间小于等于0时,说明已经超时了,退出等待
long waitTime = timeOut - responseTime; //表示这一轮循环应该等待的时间
if (waitTime <= 0) {
break;
}
try {
this.wait(waitTime);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//更新此轮的消耗时间
responseTime = (System.currentTimeMillis() - start);
}
return response;
}
}
// public Object get(long timeOut) {
// long start = System.currentTimeMillis();
// synchronized(this) {
// long responseTime = 0;
// while (response == null) {
// //如果消耗时间比设置等待时间大,则退出等待
// if (responseTime <= timeOut) {
// break;
// }
// try {
// this.wait(timeOut - responseTime);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
// //更新此轮的消耗时间
// responseTime = (System.currentTimeMillis() - start);
// }
// return response;
// }
// }
public void set(Object response) {
synchronized(this) {
this.response = response;
this.notifyAll();
}
}
}
3.join()原理------保护性暂停思想
join()是一个线程等待另外一个线程的结束并获取结果返回- 而使用保护性暂停是一个线程等待另外一个线程的结果,而返回结果的线程在返回结果之后可以继续执行其他任务。
join()源码:
java
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
4.解耦
- 当多个线程之间的数据传递时,怎么使用
GuardedObject进行管理分发 - 就比如说一个楼里的多个用户接收信件,而有多个邮递员进行信件的分发,那么如何管理使得信件分发不会发生错误(使用中间级------邮箱),邮递员将信件分发给对应用户的邮箱中,而用户则从属于自己的邮箱中获取信件即可。

- 定义一个邮箱网格
Mailboxes,给每个邮箱地址GuardedObject绑定一个id,比如指定了某个GuardedObject的归属用户是A,那么邮递员PostMan1去派送信件时,就先根据id获取到该邮箱地址GuardedObject,然后将该信件发送给对应邮箱地址。
java
@Slf4j(topic = "c.Postman")
public class PostMan {
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 Postman1(id, "内容" + id).start();
}
}
}
@Slf4j(topic = "c.People")
class People extends Thread {
@Override
public void run() {
GuardedObject1 guardedObject1 = Mailboxes.createGuardedObject();
log.debug("开始收信 id:{}", guardedObject1.getId());
Object mail = guardedObject1.get(5000);
log.debug("收到信 id:{}, 内容:{}", guardedObject1.getId(), mail);
}
}
@Slf4j(topic = "c.postman")
class Postman1 extends Thread {
private int id;
private String mail;
public Postman1(int id, String mail) {
this.id = id;
this.mail = mail;
}
@Override
public void run() {
//根据id找到对应的邮箱地址
GuardedObject1 guardedObject1 = Mailboxes.getGuardedObject(id);
//发送信件内容
log.debug("送信:id:{}, 内容:{}", id, mail);
guardedObject1.set(mail);
}
}
//使用boxes将id和邮箱地址一一绑定
class Mailboxes {
private static int id;
private static Map<Integer,GuardedObject1> boxes = new Hashtable<>();
//根据id找到对应的邮箱格子
public static GuardedObject1 getGuardedObject(int id) {
return boxes.remove(id);
}
public static Set<Integer> getIds() {
return boxes.keySet();
}
//产生自增id
public static synchronized int generateId() {
return id++;
}
//创建信件
public static GuardedObject1 createGuardedObject() {
GuardedObject1 go = new GuardedObject1(generateId());
boxes.put(go.getId(), go);
return go;
}
}
class GuardedObject1 {
private Object response;
private int id;
public GuardedObject1(int id) {
this.id = id;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public Object get(long timeOut) {
long startTime = System.currentTimeMillis();
long responseTime = 0;
synchronized (this) {
while (response == null) {
long waitTime = timeOut - responseTime;
if (waitTime <= 0) {
break;
}
try {
this.wait(waitTime);
} catch (Exception e) {
e.printStackTrace();
}
responseTime = System.currentTimeMillis() - startTime;
}
}
return response;
}
public void set(Object response) {
synchronized (this) {
this.response = response;
this.notifyAll();
}
}
}
十、设计模式------生产者消费者模式
-
与
GuardObject不同,生产者消费者模式不需要生产结果和消费结果的线程一一对应 -
一般使用消费队列来平衡生产和消费的线程资源
-
生产者仅负责生产结果数据,不关心数据该如何处理,而消费者专心处理结果数据
-
消息队列是有容量限制的,消息队列中消息达到容量上限时,就不会再加入数据,而消息队列中没有数据时,也不会再消耗数据,而是阻塞等待消息的添加
-
JDK中的各种阻塞队列实现就是采用的这种模式。

-
生产者消费者模式实现及测试:
-
使用
Message类封装消息,然后使用MessageQueue来管理线程之间的消息交互,并设置容量capcity,将消息存储在LinkedList<Message>中,并且对该集合对象进行加锁,保持线程间通信的安全性,生产者通过put()方法添加消息,消费者通过take()方法获取消息进行消费。 -
每次添加消息通过
addLast()方法添加到双向链表的尾部,如果消息集合的大小等于容量了(也就是已经满了),则会使得生产者线程进入阻塞状态等待唤醒。 -
获取消息则是每次只要消息集合不为空,就通过
removeFirse()获取并删除掉链表头部的消息,表示消费掉了,并唤醒生产者线程可以继续生产消息。
java
package cn.itcast.MyTest;
import lombok.extern.slf4j.Slf4j;
import java.util.LinkedList;
import static java.lang.Thread.sleep;
@Slf4j(topic = "c.ConsumerTest")
public class ConsumerTest {
public static void main(String[] args) {
MessageQueue mq = new MessageQueue(2);
for (int i = 0; i < 3; i++) {
int id = i;
new Thread(() -> {
Message msg = new Message(id, "包裹");
mq.put(msg);
}, "生产者" + i).start();
}
new Thread(() -> {
while (true) {
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Message msg = mq.take();
}
}, "消费者").start();
}
}
@Slf4j(topic = "c.MessageQueue")
class MessageQueue {
private int capcity;
private LinkedList<Message> queue = new LinkedList<>();
public MessageQueue(int capcity) {
this.capcity = capcity;
}
//从消息队列中获取结果 ------ 消费者
public Message take() {
synchronized (queue) {
while (queue.isEmpty()) {
try {
log.debug("消息队列为空,消费者等待生产者产生结果。。。");
queue.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
//获取并移除队首元素,并唤醒生产者去生产消息补充消息队列
Message msg = queue.removeFirst();
log.debug("已消费消息:{}", msg.getId());
queue.notifyAll();
return msg;
}
}
//往消息队列中存放消息 ------ 生产者
public void put(Message msg) {
synchronized (queue) {
//当消息队列的长度和容量相等时,就停止生产
while (queue.size() == capcity) {
try {
log.debug("消息队列已满,生产者等待消费者消费消息。。。");
queue.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
//当生产消息并放入消息队列之后,就需要唤醒消费者去消费消息
log.debug("已添加消息: {}", msg.getId());
queue.addLast(msg);
queue.notifyAll();
}
}
}
class Message {
private int id;
private Object message;
public Message(int id, Object message) {
this.id = id;
this.message = message;
}
public int getId() {
return id;
}
public Object getMessage() {
return message;
}
@Override
public String toString() {
return "Message{" +
"id=" + id +
", message=" + message +
'}';
}
}
十一、park()和unpark()
- 是
LockSupport类中的方法,用于暂停当前线程和恢复某个线程的运行 - 使用
park()使得当前线程进入WAITTING状态,执行unpark()之后,线程恢复为RUNNABLE状态。
java
package cn.itcast.n4;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.LockSupport;
import static cn.itcast.n2.util.Sleeper.sleep;
@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);
}
}
- 存在的问题:先对当前线程执行
unpark(),再执行park(),该线程就不会被暂停。
与wait()和notify()的区别
wait()、notify()、notifyAll()必须配合ObjectMonitor使用,而park、unpark不用park()和unpark()是以线程为单位来阻塞和唤醒线程,而notify()只能随机唤醒一个等待线程park() 和unpark()可以先unpark(),而wait()和notify()必须先执行wait()再由notify()去唤醒。
park()和unpark()原理

- 比如当前线程调用
park()方法时,首先会去判断_counter是否为1,如果为1,就重置_counter=0,然后继续执行,如果_counter=0,就会进入到_mutex的_cond条件变量中阻塞住。

- 调用
unpark(),先将_counter置为1,如果已经是1,则不变,再去判断_cond中是否有线程在阻塞,如果有则直接将该线程唤醒,并重新将_counter置为0。

- 如果先调用
unpark(),此时会先将_counter置为1,然后由于cond中没有线程阻塞,就会直接返回到当前线程中继续执行。等下次当前线程再执行park()时,由于counter=1,所以直接会将counter重置为0,然后继续运行,不会暂停。

十二、线程状态转换

1.RUNNABLE<->WAITTING

1.调用wait()、notify()方法发生线程状态变化
- 当线程t使用
synchronized(obj)获得锁成功之后,使用obj.wait()方法就会使得当前线程从RUUNABLE进入WAITTING状态。 - 而如果调用
notify()、notifyAll()、interrupt()唤醒当前线程时:- 如果当前线程竞争锁成功,就会从
WAITTING状态变成RUNNABLE状态 - 如果竞争失败,就会从
WAITTING状态进入BLOCKED状态
- 如果当前线程竞争锁成功,就会从
-
- 线程 1 的初始操作:线程 1 获取锁后,调用 wait() 方法,此时会发生两件事:
释放持有的锁;
从 RUNNABLE 状态 进入 WAITING 状态,并加入到该锁的 waitset 中。
- 线程 1 的初始操作:线程 1 获取锁后,调用 wait() 方法,此时会发生两件事:
-
- 其他线程唤醒操作:当其他线程持有该锁并调用 notifyAll() 时,waitset 中的所有线程(包括线程 1)会被唤醒,从 WAITING 状态 退出 waitset。
-
- 唤醒后的锁竞争:被唤醒的线程(线程 1)需要重新竞争锁:
竞争成功:直接进入 RUNNABLE 状态,继续执行 wait() 之后的代码;
竞争失败:进入 BLOCKED 状态,并加入到该锁的同步队列(如 EntryList)中,等待下一次锁释放时再次竞争。
- 唤醒后的锁竞争:被唤醒的线程(线程 1)需要重新竞争锁:
简言之,唤醒后的线程若未抢到锁,会从等待状态(WAITING)转为阻塞状态(BLOCKED),并在同步队列中排队,这与其他未抢到锁的线程的行为一致。
2.调用join()和interrupt()
- 调用
join()时,会让当前调用join()的线程进入WAITTING状态,因为当前线程需要等待t1.join()也就是t1线程执行完毕返回结果才会被唤醒。 - 调用
interrupt()或者t1运行结束时,当前线程t就会从WAITTING状态便成为RUNNABLE状态。
3.调用park()和unpark()
- 当前线程
t调用park(),会让当前线程从RUNNABLE变成WAITTING状态,如果之前已经unpark()过一次(也就是_counter=1了,那么就会再WAITTING而是继续执行了。 - 当前线程调用
unpark()或者intterupt()方法就会让目标线程从WAITTING变成RUNNABLE。
2.RUNNABLE<->TIMED_WAITTING
- 获取对象锁,调用
obj.wait(long n)时,线程从RUNNALE变成TIMED_WAITTING - 当前前程调用
t.jion(long n)时,会等待指定的时间TIMED_WAITTING,然后在该时间内没有返回结果,会继续运行从TIMED_WAITTING转换成RUNNABLE - 调用
Thread.sleep(long n),当前线程会从RUNNABLE<->TIMED_WAITTING - 调用
parkNanos(long n)或者parkUntil(long millis)从RUNNABLE变成TIMED_WAITTING
3.RUNNABLE<->BLOCKED
- 当前线程尝试竞争锁,但是根据
synchronized(obj),去访问obj的Monitor时,发现他已经有Owner了,则说明已经有其他线程获取到锁了,竞争锁失败,进入到Monitor的EntryList中,从RUNNABLE变成BLOCKED状态。 - 当持有锁的线程执行完释放锁时,会唤醒
EntryList中的线程去重新竞争锁,此时线程从BLOCKED变成RUNNABLE状态。
4.RUNNABLE->TERMINATED
- 当前线程执行完所有代码运行完毕之后,进入
TERMINATED状态。
十三、锁的活跃性
- 锁的细粒度:可以增强并发度,但是容易发生死锁。
死锁的发生
- 当一个线程需要获取多把锁时,就容易出现,持有一把锁A的同时尝试获取另外一把锁B时,B锁被其他线程持有,并且该线程在等待获取锁A,于是二者就僵持住了,陷入了死锁。
java
@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {
public static void main(String[] args) {
test1();
}
private static void test1() {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("lock A");
sleep(1);
synchronized (B) {
log.debug("lock B");
log.debug("操作...");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug("lock B");
sleep(0.5);
synchronized (A) {
log.debug("lock A");
log.debug("操作...");
}
}
}, "t2");
t1.start();
t2.start();
}
}
定位死锁
1.使用jps定位进程id,再使用jstack定位死锁

- 先通过
jps查到当前所有的Java进程,然后使用jstack通过进程id查看进程中的线程状态。

- 可以发现JVM通过
jstack发现了该死锁并列出了详细的信息,我们就可以根据这个信息进行对应的排查。

2.使用jconsole工具查看线程状态
- 使用
jconsole工具连接到对应的进程上,然后切换到线程界面,点击检测死锁,就可以检测到可能存在的死锁。

3.死锁示例------哲学家就餐问题

- 会出现的情况:每个哲学家(五个线程)手里都各拿了一个筷子(加锁共享资源),那么这五个哲学家都持有一个资源并需要等待对方释放资源,那么就会导致死锁(所有人都吃不了饭也无法思考。
java
public class TestDeadLock {
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();
}
}
@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true) {
// 尝试获得左手筷子
if (left.tryLock()) {
try {
// 尝试获得右手筷子
if (right.tryLock()) {
try {
eat();
} finally {
right.unlock();
}
}
} finally {
left.unlock();
}
}
}
}
private void eat() {
log.debug("eating...");
Sleeper.sleep(1);
}
}
class Chopstick extends ReentrantLock {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
- 运行一段时间之后就会发现,所有人突然都停下了,不在继续吃饭和思考
- 使用
jconsole检测就会发现造成了死锁:

活锁
- 活锁出现在两个线程互相改变对方的结束条件,最后导致谁也无法结束。
- 示例:
java
@Slf4j(topic = "c.TestLiveLock")
public class TestLiveLock {
static volatile int count = 10;
static final Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
// 期望减到 0 退出循环
while (count > 0) {
sleep(0.2);
count--;
log.debug("count: {}", count);
}
}, "t1").start();
new Thread(() -> {
// 期望超过 20 退出循环
while (count < 20) {
sleep(0.2);
count++;
log.debug("count: {}", count);
}
}, "t2").start();
}
}
饥饿
- 当一个线程由于优先级太低,始终得不到CPU的调度执行,也不能结束,就发生了线程饥饿问题。
- 也就是说并没有发生死锁,但是该线程一直分配不到CPU时间片去获取到锁。
十四、ReentrantLock
-
相对于
synchronized,ReentrantLock具有- 可中断
- 可设置超时时间
- 可以设置公平锁
- 可以支持多个条件变量
-
基本语法:
java
//获取锁
reentrantLock.lock();
try {
//临界区
} finally {
//释放锁
reentrantLock.unlock();
}
1.可重入
- 可重入指的是:拥有锁的线程,还是可以再次获取这把锁,而不会被持有的锁锁住
- 如果是不可重入锁,那么第二次需要获得锁时,自己也会被锁住。
synchronized和reentrantLock都是可重入锁。
java
@Slf4j(topic = "c.ReentrantLockTest ")
public class ReentrantLockTest {
private final static ReentrantLock lock = new ReentrantLock(true);
public static void main(String[] args) {
lock.lock();
try {
log.debug("begin main...");
m1();
} finally {
lock.unlock();
}
}
public static void m1() {
lock.lock();
try {
m2();
log.debug("begin m1...");
} finally {
lock.unlock();
}
}
public static void m2() {
try {
lock.lock();
log.debug("begin m2...");
} finally {
lock.unlock();
}
}
}
2.可中断
- 在某个线程在等待锁的过程中,可以由其他线程打断
synchronized和常规的ReentrantLock是不能被外界打断的,而使用ReentrantLock的lockInterrupibly()加锁时,该锁就可以由外界进行打断。- 可以避免某个线程一直死等,避免死锁。
java
@Slf4j(topic = "c.Interrupt")
public class Interrupt {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
//如果没有竞争,则获取到lock锁
//如果有其他线程持有锁了,该线程进入锁的阻塞队列,可以被其他线程用interrupt方法打断
log.debug("try to get the lock");
lock.lockInterruptibly();
} catch (Exception e) {
e.printStackTrace();
log.debug("get lock fail");
return;
}
try {
log.debug("success");
} catch (Exception e) {
lock.unlock();
}
});
//主线程获取锁
lock.lock();
t1.start();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t1.interrupt();
log.debug("interrupt t1 to giveup get the lock");
lock.unlock();
}
}
3.锁超时
-
主动设置尝试获取锁的时间,如果某个线程获取锁时超过指定时间还是未获取到,就主动中止对锁的获取。
-
立即放弃获取
java
@Slf4j(topic = "c.TryLockTest")
public class TryLockTest {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("try to get lock");
if (!lock.tryLock()) {
//如果获取锁失败,则直接放弃
log.debug("failed to obtain lock");
return;
}
//获取锁成功
try {
log.debug("success to obtain lock");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("main thread obtain lock");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t1.start();
}
}
- 当t1线程尝试获取锁时,由于主线程先行获取到了锁,然后并没有释放,t1去尝试获取时发现锁被占用了就立马放弃了对锁的等待,也就是直接结束了运行。
- 等待一段时间的获取
lock.tryLock(long n, TimeUnitl)
java
@Slf4j(topic = "c.TryLockTest")
public class TryLockTest {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("try to get lock");
try {
//在规定的时间内尝试去获取锁
if (!lock.tryLock(1, TimeUnit.SECONDS)) {
//如果获取锁失败,则直接放弃
log.debug("failed to obtain lock");
return;
}
} catch (InterruptedException e) {
//如果在指定时间内没有获取到锁,也是直接返回
e.printStackTrace();
log.debug("failed to obtain lock");
return;
}
//获取锁成功
try {
log.debug("success to obtain lock");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("main thread obtain lock");
t1.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
log.debug("main thread released lock");
lock.unlock();
}
}
}
-
使用加时间的获取锁的方法时,t1线程会在指定的时间内去尝试获取锁。
-
由于主线程先获取到锁,所以t1刚开始会被阻塞一段时间,但是0.5s之后,主线程释放了锁,t1成功获取到锁。

-
如果在2s内,锁一直被主线程持有,那么t1线程就会放弃对锁的竞争,直接结束。

4.解决哲学家就餐问题------解决死锁
- 使用
Chopstick继承ReentrantLock,使筷子类变成锁,当创建左右手的筷子时就创建了锁对象 - 那么当持有左手筷子,再去获取右手筷子失败时,就会释放掉左手的筷子,从而避免一直持有左手筷子,而无法获取右手筷子导致所有线程进入阻塞等待,避免造成死锁状态。
java
@Slf4j(topic = "c.Test23")
public class Test23 {public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();
}
}
@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true) {
// 尝试获得左手筷子
if(left.tryLock()) {
try {
// 尝试获得右手筷子
if(right.tryLock()) {
try {
eat();
} finally {
right.unlock();
}
}
} finally {
left.unlock(); // 释放自己手里的筷子
}
}
}
}
Random random = new Random();
private void eat() {
log.debug("eating...");
Sleeper.sleep(0.5);
}
}
class Chopstick extends ReentrantLock {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
5.条件变量
-
synchronized中类似于条件变量的就说waitset,当线程获取到锁但是需要满足其他条件时,为了避免阻塞其他线程的执行,就会进入到waitset中等待条件满足之后被唤醒。 -
ReentrantLock的条件变量比synchronized更多,就好比:synchronized只有一间等候室,而ReentrantLock支持多间等候室,有专门等烟的休息室,有专门等早餐的实验室,唤醒时也是根据休息室来唤醒。比如专门唤醒等到早餐的休息室中的线程,而不需要全部唤醒。
-
使用实例:
java
@Slf4j(topic = "c.ReentrantLock2")
public class ReentrantLock2 {
static ReentrantLock lock = new ReentrantLock();
static Condition CigaretSet = lock.newCondition();
static Condition TakeoutSet = lock.newCondition();
static boolean hasCigaret = false;
static boolean hasTakeout = false;
public static void main(String[] args) {
new Thread(() -> {
lock.lock();
try {
log.debug("有没有烟?");
while (!hasCigaret) {
log.debug("没烟,干不了活");
try {
CigaretSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟可以开始干活了");
} finally {
lock.unlock();
}
},"小明").start();
new Thread(() -> {
lock.lock();
log.debug("有没有东西填肚子");
try {
while (!hasTakeout) {
log.debug("肚子饿了,没力气干活");
try {
TakeoutSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("吃饱了,开始干活");
} finally {
lock.unlock();
}
}, "小红").start();
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
lock.lock();
try {
log.debug("烟送到了");
hasCigaret = true;
CigaretSet.signal();
} finally {
lock.unlock();
}
}, "送烟的").start();
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
lock.lock();
try {
log.debug("外卖到了");
hasTakeout = true;
TakeoutSet.signal();
} finally {
lock.unlock();
}
}, "送外卖的").start();
}
}
- 小明执行任务时等待另外一个条件(烟),由于刚开始未满足则进入到
CigaretSet中进行等待,而当送烟线程输出结果会到CigaretSet中唤醒小明去继续执行任务,而不会打扰到小红。 - 小红执行任务时等到不同的条件(食物),刚开始未满足则进入到
TakeoutSet中进行等到,当外卖送到时,外卖员会到TakeoutSet中唤醒小红让她填饱肚子继续干活,而不会打扰到小明。

6.固定顺序输出
- 要求:有两个线程,一个输出1,一个输出2,但是必须先输出2再输出1
wait()和notify()
- 使用一个标识符,当线程2输出2之后,设置标识符为true,同时使用
notify()唤醒线程1,而线程1则只要标识符不为true就使用wait()阻塞等待。
java
package cn.itcast.MyTest;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.backToPrint")
public class backToPrint {
private static Object lock = new Object();
private static boolean t2Runned = false;
public static void main(String[] args) {
new Thread(() -> {
synchronized (lock) {
while(!t2Runned) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("1");
}
}, "t1").start();
new Thread(() -> {
synchronized (lock) {
t2Runned = true;
System.out.println("2");
lock.notify();
}
}, "t2").start();
}
}
park()和unpark()
- 线程t1直接调用
park()方法进入阻塞等待状态,如果线程t2先执行,那么会先执行unpark(t1),无论t1先后执行,等到t1执行park()时,都有一份"干粮",可以打印1
java
@Slf4j(topic = "c.backToPrint")
public class backToPrint {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
LockSupport.park();
System.out.println("1");
});
t1.start();
new Thread(() -> {
LockSupport.unpark(t1);
System.out.println("2");
}, "t2").start();
}
}
ReentrantLock
- 使用
ReentrantLock的condition条件变量,当轮到线程1时,根据标识符判定线程t2是否运行了,如果没有就进入阻塞状态,直到t2运行,将标识符置为true,并唤醒线程t1进行打印。
java
@Slf4j(topic = "c.backToPrint")
public class backToPrint {
private static ReentrantLock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
private static boolean t2Runned = false;
public static void main(String[] args) {
new Thread(() -> {
lock.lock();
try {
System.out.println("2");
t2Runned = true;
condition.signal();
} finally {
lock.unlock();
}
}, "t2").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
lock.lock();
try {
while (!t2Runned) {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("1");
} finally {
lock.unlock();
}
}, "t1").start();
}
}
7.交替输出
- 要求:三个线程依次输出a,b,c,循环5次:abcabcabcabcabc
- 定义一个共享资源类,同时使用一个数字作为线程切换的条件,比如当flag=1时,唤醒线程1打印a,并将flag变成2,从而唤醒线程2打印b,然后再唤醒线程3打印c。
wait()和notify()
java
@Slf4j(topic = "c.PrintOrder")
public class PrintOder {
public static void main(String[] args) {
WaitNotify wn = new WaitNotify(1,5);
new Thread(() -> {
wn.print("a", 1,2);
}, "t1").start();
new Thread(() -> {
wn.print("b", 2, 3);
},"t2").start();
new Thread(() -> {
wn.print("c", 3, 1);
}, "t3").start();
}
}
class WaitNotify {
private int flag;
private int loopNumber;
public WaitNotify(int flag, int loopNumber) {
this.flag = flag;
this.loopNumber = loopNumber;
}
public void print(String str, int workFlag, int nextFlag) {
synchronized (this) {
for (int i = 0; i < loopNumber; i++) {
while (workFlag != flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(str);
flag = nextFlag;
this.notifyAll();
}
}
}
}
await()和signal()
java
@Slf4j(topic = "rlPrintOrder")
public class rlPrintOrder {
public static void main(String[] args) {
AwaitSignal as = new AwaitSignal(5);
Condition a = as.newCondition();
Condition b = as.newCondition();
Condition c = as.newCondition();
new Thread(() -> {
as.print("a", a, b);
}, "t1").start();
new Thread(() -> {
as.print("b", b, c);
}, "t2").start();
new Thread(() -> {
as.print("c", c, a);
}, "t3").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
as.lock();
try {
a.signal();
} finally {
as.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.print(str);
next.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
unlock();
}
}
}
}
park()和unpark()
- 使用park当当前线程执行时令当前线程等待,打印当前字符,并唤醒下一个需要打印的线程,如此循环。
- 最后使用主线程唤醒t1线程,从t1开始先打印a,然后unpark唤醒t2,t2打印b,然后t2唤醒t3,t3打印c,然后t3再唤醒t1,循环5次。
java
package cn.itcast.MyTest;
import java.util.concurrent.locks.LockSupport;
public class ParkUnpark {
private static Thread t1;
private static Thread t2;
private static Thread t3;
public static void main(String[] args) {
Park pp = new Park(5);
t1 = new Thread(() -> {
pp.print("a", t2);
});
t2 = new Thread(() -> {
pp.print("b", t3);
});
t3 = new Thread(() -> {
pp.print("c", t1);
});
t1.start();
t2.start();
t3.start();
LockSupport.unpark(t1);
}
}
class Park {
private int loopNumber;
public Park(int loopNumber) {
this.loopNumber = loopNumber;
}
public void print(String str, Thread next) {
for (int i = 0; i < loopNumber; i++) {
LockSupport.park();
System.out.print(str);
LockSupport.unpark(next);
}
}
}
十五、小结
1.基本概念和使用
-
多线程访问共享资源时,对共享资源既有读又有写的代码区就属于临界区
-
使用
synchronized互斥解决临界区的线程安全问题:synchronized的锁对象语法synchronized加在成员方法上是对this对象加锁,加在静态方法上是对class对象进行加锁synchronized的同步方法:使用wait()和notify()解决线程的同步问题,当有条件不满足时,使得当前线程进入等待waitset,当条件满足则继续运行,同时使用while(true)避免虚假唤醒等
-
使用
lock(ReentrantLock)解决临界区的线程安全问题:- lock的特点:可以打断(
lock.Interrupibly())、锁超时(tryLock())、公平锁(先等待的线程先获得锁)、条件变量(Condition)。
- lock的特点:可以打断(
-
变量的线程安全性分析:判断该变量是否属于线程安全的
-
常见线程安全类:
StringIntegerStringBufferRandomVectorHashtablejava.util.concurrent包下的类
-
线程活跃性问题:
- 死锁:多线程并发时,彼此之间互相持有对方需要的资源锁,由于对方无法释放锁,本身也无法中止,导致双方陷入无限阻塞等待。
- 活锁:由于线程之间互相修改对方的中止条件,使得其无法终止。
- 饥饿:由于优先级太低,一直无法分配到CPU时间片去调度执行,导致线程一直处于等待状态
- 检测方法:
- 使用
jps查看当前java进程,并使用jstack查看对应进程的线程情况 - 使用
jconsole查看对应进程的线程情况
- 使用
2.应用方面
- 互斥:使用
synchronized或者lock达到共享资源互斥效果 - 同步:使用
wait()/notify()或者await()/signal()(Lock的条件变量)来达到线程间通信
3.原理
synchronized的原理是关联操作系统层面的monitor来实现的ReentrantLock是在Java层面去实现的monitor,实现了如monitor的waitset和EntryList等