预备知识
- 不是一个初学者
- 线程安全问题,需要接触过Java Web开发,Jdbc开发,Web服务器,分布式框架时才会遇到
- 基于JDK8,对函数式编程,lambda有一定的了解
- 采用slf4j打印日志,是好的实践
- 采用了lombok简化java bean的书写
- 给每个线程一个好名字,也是一个好的实践
pom.xml的依赖如下
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
<optional>true</optional>
</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" lever="debug" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>
<root lever="ERROR">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
2.1 进程与线程
进程
当一个程序被运行,从磁盘加载代码到内存,就开启了一个进程
线程
一个进程中有多个线程,一个线程就是一个指令流,将指令流中的一个个指令按照一定的顺序交给CPU执行
2.2 并行与并发
在单核CPU下,线程实际上还是串行执行的,操作系统中有一个组件叫做任务调度器,将CPU分给不同的线程使用,只是由于CPU之间的切换非常快,所以人类感觉是同时运行的。总结一句话,微观串行,宏观并行
一般会将这种线程轮流使用CPU的做法称为并发,concurrent
2.3应用
从方法调用的角度来看,如果
*需要等待结果返回摩擦,才能继续运行就是同步
*不需要等待结果返回摩擦,就能继续运行就是异步
*同步在多线程中还有另外一层意思,是让多个线程步调一致
- 应用之异步调用示例
@Slf4j
public class Main {
public static void main(String[] args){
for(int i = 0 ; i < 5 ; i++){
log.debug("do things");
}
log.debug("do other things");
}
}
上面代码的输出结果为
可以看到do other things必须要等待循环结束才能够执行
我们用异步方法做一个对比
@Slf4j
public class Main {
public static void main(String[] args){
new Thread(()->{
for(int i = 0 ; i < 5 ; i++){
log.debug("do things");
}
}).start();
log.debug("do other things");
}
}
此时代码的输出结果为
可以看到,do other things并没有等待循环的执行
当然线程的语法后面会说,现在只是先演示一下
结论
- 比如在项目中,视频文件等需要转换格式等操作比较费时间,此时新开一个线程处理视频转换,避免阻塞主线程
- tomcat的servlet也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞tomcat的工作线程
- ui程序中,开线程进行其他操,避免阻塞ui线程
3.Java线程
本章内容介绍
- 创建和运行线程
- 查看线程
- 线程API
- 线程状态
3.1 创建和运行线程
方法1,直接使用Thread
//创建线程对象
Thread t = new Thread(){
@Override
public void run() {
//要执行的任务
}
};
//启动线程
t.start();
例如
//创建线程对象 最好给线程起个名字 这样在输出的结果中比较清晰
Thread t = new Thread("t1"){
@Override
public void run() {
log.debug("running");
}
};
//启动线程
t.start();
log.debug("running");
输出结果为
可以看出分别是main和t1两个不同的线程进行的
方法2,使用Runnable配合Thread
即把【线程】和【任务】分开
Thread代表线程,Runnable代表可运行的任务
查看源码后可以发现,Runnable是一个接口,没有返回值也不能抛出异常
@Slf4j
public class Main {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
//要执行的任务
}
};
//创建线程对象
Thread t = new Thread(runnable);
//执行线程
t.start();
}
}
例如
@Slf4j
public class Main {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
log.debug("running");
}
};
//创建线程对象
Thread t = new Thread(runnable,"t1");
//执行线程
t.start();
}
}
此代码输出结果与之前类似
回到刚才的Runnable源码,可以发现Runnable接口只有一个抽象方法,在JDK中,会对于这样的接口加入一个@FunctionalInterface的注解,带有此注解的就可以用lambda表达式进行简化
如下
@Slf4j
public class Main {
public static void main(String[] args) {
Runnable runnable = () -> {
log.debug("running");
};
//创建线程对象
Thread t = new Thread(runnable,"t1");
//执行线程
t.start();
}
}
原理之Thread与Runnable的关系
查看Thread的源码可以发现有此方法
让我们看看他把此runnable对象传到了哪里去
传递到了另一个被重载的init函数,继续跟踪
可以发现他在这个方法里面被赋值给了target对象
可以发现,这个target是被他Thread的run方法里面用到了,Thread若发现runnable不为空,那么会优先使用runnable的任务
小结
- 用Runnable更容易与线程池等高级API配合
- 用Runnable让任务类脱离了Thread继承体系,更灵活(java中组合优于继承)
方法3,FutureTask配合Thread
FutureTask能够接收Callable类型的参数,用来处理有返回结果的类型
查看其源码可以发现他是一个实现类,实现了RunnableFuture接口
再查看RunnableFuture源码,可以发现他间接实现了Runnable接口,所以他也可以作为一个任务对象,此外他多了一个Future接口,是用来返回结果的
我们再去查看Callable源码
可以发现他与Runnable接口类似,但是有返回类型并且可以抛出异常
代码示例如下
@Slf4j
public class Main {
public static void main(String[] args) {
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("running");
return 100;
}
});
Thread t = new Thread(task);
t.start();
}
}
输出结果为
我们可以发现,似乎这个返回值并没有任何体现,这是因为我们并没有使用他
如果我们想要主线程使用则可以使用get()方法,如下
@Slf4j
public class Main {
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");
return 100;
}
});
Thread t = new Thread(task);
t.start();
log.debug("{}",task.get());
}
}
若task方法执行较慢,则get方法会一直等待他执行完成返回结果才会继续往下执行,也就是阻塞主线程,如下示例
@Slf4j
public class Main {
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 t = new Thread(task);
t.start();
log.debug("{}",task.get());
}
}
输出结果为
可以发现,主线程输出的100是等待了两秒之后的。
观察两个线程交替执行
代码如下
@Slf4j
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
new Thread(()->{
while(true){
log.debug("running");
}
},"t1").start();
new Thread(()->{
while(true){
log.debug("running");
}
},"t2").start();
}
}
输出结果为
可以发现两个线程是交替执行的
查看进程线程的方法
Windows
任务管理器可以用来查看进程和线程数,也可以用来杀死进程
- tasklist 查看进程
- taskkill 杀死进程
输入tasklist我们可以看见有这么多的进程
我们将刚刚的java程序启动,由于tasklist的进程太多,我们需要筛选一下,
但是这里的java我们仍然不知道哪个才是我们运行中的进程,所以我们用jdk中的命令jps可以发现具体哪个才是我们运行的
然后使用taskkill通过PID杀死进程
此时进程也会结束
linux
- ps -fe 查看所有进程
- ps -fT -p < PID > 查看某个进程的全部线程
- kill 杀死进程
- top 按大写H切换是否显示线程
- top -H -p < PID > 查看某个进程的所有线程
Java
- jps 查看所有java进程
- jstack < PID > 查看某个Java进程所有的线程状态
- jconsole 查看某个Java进程中线程的运行情况(图形界面)
原理之线程运行
栈与栈帧
Java Virtual Machine Stacks(Java虚拟机栈)
我们都知道JVM中由堆,栈,方法区组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为他分配一块栈内存
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
这样说比较模糊,我们用代码来演示一下
@Slf4j
public class Main {
public static void main(String[] args){
method1(10);
}
private static void method1(int x){
int y = x + 1;
Object m = method2();
System.out.println(m);
}
private static Object method2(){
Object n = new Object();
return n;
}
}
我们在method1上打一个断点,用debug方式来运行,在运行过程中我们可以发现
随着方法的进行,会多出几个栈帧,并且存储他们的局部变量
在方法执行结束之后,就释放其内存,每个栈帧对应一次方法的调用
线程上下文切换(Thread Context Switch)
因为以下一些原因导致cpu不再执行当前的线程,转而执行另一个线程的代码
- 线程的cpu时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了sleep,yield,wait,join,park,synchronized,lock等方法
当Context Switch发生,需要由操作系统保存他当前的状态,并恢复另一个线程的状,Java中对应的概念就是程序计数器(Program Counter Register),他的作用是记住下一条jvm指令的执行地址,是线程私有的 - 状态包括程序计数器,虚拟机栈用每个栈帧的信息,如局部变量,操作数栈,返回地址等
- Context Switch频繁发生会影响性能
3.4 常见方法
3.4.1 start vs run
调用run
既然t1里面有run方法我们为何不直接调用这个run方法而是用start呢
@Slf4j
public class Main {
public static void main(String[] args) {
Thread t1 = new Thread(){
@Override
public void run() {
log.debug("running");
}
};
t1.run();
log.debug("do other things");
}
}
运行结果如下
我们可以发现是主线程进行的输出,所以run方法并不能够起到异步的作用,并不能启动新的线程,也不能够提高性能,因此启动线程必须是用start(),才能够启动新的线程
调用start
调用start后我们可以用getState()方法来获取线程的状态,代码如下
@Slf4j
public class Main {
public static void main(String[] args) {
Thread t1 = new Thread(){
@Override
public void run() {
log.debug("running");
}
};
System.out.println(t1.getState());
t1.start();
System.out.println(t1.getState());
}
}
输出结果为
当一个线程没有被start调用时的状态是NEW,被调用了则是RUNNABLE
此外,一个线程的start方法不能被调用两次,否则会报错
@Slf4j
public class Main {
public static void main(String[] args) {
Thread t1 = new Thread(){
@Override
public void run() {
log.debug("running");
}
};
System.out.println(t1.getState());
t1.start();
t1.start();
System.out.println(t1.getState());
}
}
报错为Exception in thread “main” java.lang.IllegalThreadStateException,如下
3.4.2 sleep与yield
sleep
- 调用sleep会让当前线程从Running状态进入Timed Waiting状态(阻塞)(任务调度器不会考虑这种状态)
- 其他线程可以使用interrupt方法打断正在睡眠的进程,这时sleep方法会抛出InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用TimeUnit的sleep代替Thread的sleep来获得更好的可读性
yield
- 调用yield会让当前线程从Running状态进入Runnable就绪状态(任务调度器仍有可能分配时间片给他),然后调度执行其他同优先级的线程,如果这时没有同优先级的线程,那么不能保证让线程暂停的效果
- 具体的实现依赖于操作系统的任务调度器
线程优先级
- 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略他
- 如果cpu比较忙,那么优先级高的线程会获得更多的时间片,但cpu闲的时候,优先级几乎没有作用
sleep方法示例1
@Slf4j
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(){
@Override
public void run() {
try {
//单位为毫秒
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("running");
}
};
t1.start();
//防止主线程先被调用
Thread.sleep(500);
System.out.println(t1.getState());
}
}
输出结果为
可以发现线程状态为TIMED WAITING
sleep方法示例2
@Slf4j
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(){
@Override
public void run() {
log.debug("enter sleep");
try {
//单位为毫秒
Thread.sleep(2000);
} catch (InterruptedException e) {
log.debug("wake up");
throw new RuntimeException(e);
}
}
};
t1.start();
//防止主线程先被调用
Thread.sleep(1000);
t1.interrupt();
}
}
输出结果为
发现这个线程在一秒后被叫醒
sleep方法示例3
@Slf4j
public class Main {
public static void main(String[] args) throws InterruptedException {
log.debug("begin");
TimeUnit.SECONDS.sleep(1);
log.debug("end");
}
}
输出结果为
可以发现用TimeUnit的效果是相同的,并且可读性更高
yield方法以及线程优先级示例
@Slf4j
public class Main {
public static void main(String[] args) throws InterruptedException {
Runnable task1 = () -> {
int count = 0;
for(;;){
System.out.println("----------->1" + count++);
}
};
Runnable task2 = () -> {
int count = 0;
for(;;){
System.out.println(" ----------->2" + count++);
}
};
Thread t1 = new Thread(task1,"t1");
Thread t2 = new Thread(task2,"t2");
//t1.setPriority(Thread.MAX_PRIORITY);
//t2.setPriority(Thread.MIN_PRIORITY);
t1.start();
t2.start();
}
}
可以看到,在不加优先级的时,两个线程被调用的次数类似
但是在设置了优先级之后,他们被调用的次数差距就变大了
或者在线程2中添加一个yield方法,让他把线程让出去,也可以发现两个线程被调用的次数差距拉大
@Slf4j
public class Main {
public static void main(String[] args) throws InterruptedException {
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.start();
t2.start();
}
}
总结
不管是优先级还是yield,都只是对任务调度器的一个提示而已,没有必定的效果
3.4.3 join
先提出一个问题,下面的代码执行,打印出的r的值是多少
@Slf4j
public class Main {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(()->{
log.debug("开始");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("结束");
r = 10;
});
t1.start();
log.debug("结果为:{}",r);
log.debug("结束");
}
}
结果为r = 0
分析:主线程和t1线程是并行执行的,但是t1线程需要一秒后才能算出结果 r = 10,而主线程一开始就会打印r的结果,因此打印出来的结果为r = 0
解决方法:
用sleep()行不行? 当然可以,但是不太好,因为你不知道线程会在多久之后得到正确的结果,可是join()方法就可以完美的解决这个问题,join()的作用就是等待线程运行结束。
只需要在原来的代码的基础上,加一行t1.join();
public class Main {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(()->{
log.debug("开始");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("结束");
r = 10;
});
t1.start();
t1.join();
log.debug("结果为:{}",r);
log.debug("结束");
}
}
输出结果为
可以发现输出结果确实等待了其一秒钟
除此之外join还可以添加一个参数,代表最大的等待时间
@Slf4j
public class Main {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(()->{
log.debug("开始");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("结束");
r = 10;
});
t1.start();
t1.join(1500);
log.debug("结果为:{}",r);
log.debug("结束");
}
}
输出结果为
由于线程中睡眠了两秒钟,但是最大等待时间为一点五秒,所以他输出的结果依然为 0
3.4.4 interrupt
打断阻塞线程(sleep,wait,join)
@Slf4j
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
log.debug("sleep");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"t1");
t1.start();
Thread.sleep(500);
log.debug("interrupt");
t1.interrupt();
log.debug("打断标记:{}",t1.isInterrupted());
}
}
按理来说打断标记应该为true,但是对于阻塞线程,他们会用异常方式来代表自己被打断了,因此他们会将打断标记设置为false,这个打断标记可以用来判断线程被打断后是继续运行还是就此终止
打断正常运行的线程
@Slf4j
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while(true){
}
},"t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
}
}
可以发现再被打断后,线程并没有结束,此时我们便可以用上打断标记来终止他
使用打断标记来终止的代码如下
@Slf4j
public class Main {
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();
}
}
输出结果为
interrupt细节
Thread的interrupt有两个类似的方法,一个叫做isInterrupted(),另一个叫做interrupted()。他们的区别为,后者为静态方法,且会清除打断标记
两阶段终止模式(Two Phase Termination)
在一个线程T1中如何“优雅”的终止线程T2?这里的优雅指的是给T2一个料理后事的机会
错误思路
- 使用线程对象的stop()方法停止线程
stop()方法会真正杀死线程,如果这时线程锁住了共享资源,那么当他被杀死后就再也没有机会释放锁,其他线程将永远无法获取锁 - 使用System.exit(int)方法停止线程
目的仅是停止一个线程,但这种做法会让整个程序都停止
两阶段终止模式运行流程分析
代码如下
@Slf4j
public class TwoPhaseTermination {
public static void main(String[] args) throws InterruptedException {
twoPhaseTermination twoPhaseTermination = new twoPhaseTermination();
twoPhaseTermination.start();
Thread.sleep(3500);
twoPhaseTermination.stop();
}
}
@Slf4j
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);//情况1
log.debug("执行监控");//情况2
} catch (InterruptedException e) {
e.printStackTrace();
//重新设置打断标记,因为如果在睡眠状态被interrupt()会导致其打断标记仍然为false
current.interrupt();
}
}
});
}
public void stop(){
monitor.interrupt();
}
}
interruput常见方法-----打断park
@Slf4j
public class InterruptPark {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
log.debug("park...");
//park并不是线程里面的方法,而是锁支持里面的
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}",Thread.currentThread().isInterrupted());
LockSupport.park();
log.debug("park...");
},"t1");
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
}
打印结果为
可以发现,打断状态标记为true的线程是不可以再次被park的,如果想要再次被park,则可以将Thread.currentThread().isInterrupted()改为上文提到的可以清除打断标记的Thread.interrupted()方法
3.4.5 过时方法
还有一些不推荐使用的方法,这些方法已经过时,容易破坏同步代码块,造成线程死锁,一定要避免使用
- stop() :停止线程运行
- suspend() :挂起(暂停)线程运行
- resume():恢复线程运行
3.4.6 主线程与守护线程
默认情况下,Java进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即便守护进程的代码没有执行完,也会强制结束。
例如
@Slf4j
public class DaemonThread {
public static void main(String[] args) throws InterruptedException {
log.debug("开始运行");
Thread t1 = new Thread(()->{
log.debug("开始运行");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("运行结束");
},"daemon");
//设置为守护线程,默认为false
t1.setDaemon(true);
t1.start();
Thread.sleep(1000);
log.debug("运行结束");
}
}
输出
可以发现他daemon线程里面没有打印运行结束,即其没有运行结束就结束了
注意
- 垃圾回收器就是一种守护线程
- Tomcat中的Acceptor和Pollar线程都是守护线程,所以Tomcat接收到shutdown命令后,不会等待它们处理完当前的请求
3.4.6 五种状态(这是从操作系统层面来描述的)
- 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
- 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程相关联),可以由CPU调度执行
- 【运行状态】指获取了CPU时间片运行中的状态
当时间片用完,会从【运行状态】转换到【可运行状态】,会导致线程的上下文切换 - 【阻塞状态】
如果调用了阻塞API,如BIO读写文件,这时该线程实际不会用到CPU,会导致线程上下文切换,进入【阻塞状态】。等BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至可运行状态。与【可运行状态】的区别是,对【阻塞状态】的线程来说只要他们一直不唤醒,调度器就一直不会考虑调度他们 - 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其他状态
3.4.6 六种状态(这是从Java API层面来描述的)
根据Thread.State枚举,分为六种状态
- 【NEW】线程刚被创建,还没有调用start()方法
- 【RUNNABLE】当调用了start()方法后,注意,Java API层面的RUNNABLE状态涵盖了操作系统层面的【可运行状态】,【运行状态】和【阻塞状态】(由于BIO导致的线程阻塞,在Java里面无法区分,仍然认为是可运行)
- 【BLOCKED】,【WAITING】,【TIMED_WAITING】都是Java API层面对【阻塞状态】的细分,后面会进一步解释
- 【TERMINATED】当线程代码运行结束
六种状态演示
代码如下
@Slf4j
public class TestState {
public static void main(String[] args) throws InterruptedException {
// 1
Thread t1 = new Thread("t1"){
@Override
public void run() {
log.debug("running");
}
};
// 2
Thread t2 = new Thread("t2"){
@Override
public void run() {
while(true){ // runnable
}
}
};
t2.start();
// 3
Thread t3 = new Thread("t3"){
@Override
public void run() {
log.debug("running");
}
};
t3.start();
// 4
Thread t4 = new Thread("t4"){
@Override
public void run() {
synchronized (TestState.class){
try {
Thread.sleep(1000000); //timed_waiting
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
t4.start();
// 5
Thread t5 = new Thread("t5"){
@Override
public void run() {
try {
t2.join(); // waiting
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
t5.start();
// 6
Thread t6 = new Thread("t6"){
@Override
public void run() {
synchronized (TestState.class){ // blocked
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
t6.start();
Thread.sleep(500);
log.debug("t1 state {}",t1.getState());
log.debug("t2 state {}",t2.getState());
log.debug("t3 state {}",t3.getState());
log.debug("t4 state {}",t4.getState());
log.debug("t5 state {}",t5.getState());
log.debug("t6 state {}",t6.getState());
}
}
输出结果为
本章总结
本章的重点在于掌握
- 线程创建
- 线程重要的api,如start,run,sleep,join,interrupt等
- 线程状态
- 应用方面
a)异步调用:主线程执行期间,其他线程异步执行耗时操作
b)提高效率:并行计算,缩短运算时间
c)同步等待:join
d)统筹规划:合理使用线程,得到最优效果 - 原理方面
a)线程运行流程:栈,栈帧,上下文切换,程序计数器
b)Thread两种创建方式的源码 - 模式方面
a)两阶段终止
4 共享模型之管程
4.1 上下文切换
如下代码中的两个线程分别对counter做五千次++和–操作,按理来说输出的结果应该为0,但实际的输出结果
@Slf4j
public class Test {
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);
}
}
并不是0
问题分析
以上的结果可能是正数,负数,也有可能是零。为什么呢?因为从Java中对静态变量的自增,自减,并不是原子操,要彻底理解,必须从字节码进行分析
例如对i++而言(i是静态变量),实际会产生如下的JVM字节码指令
getstatic i //获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
对应的i–也十分类似
getstatic i //获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
而Java的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换
如果是单线程顺序执行(不会交错) 是没有问题的
但多线程下这些代码可能交错运行,但根本原因还是上下文切换
出现负数的情况:
出现负数的情况:
*
临界区 Critical Section
- 一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问共享资源
a)多个线程读共享资源本身也是没有问题的
b)在多个线程对共享资源读写操作时发生指令交错,就会出现问题 - 一个代码内如果存在对共享资源的多线程读写操作,那称这块代码块为临界区
例如,下面代码中的临界区
public class CriticalSection {
static int counter = 0;
static void increment(){
// 临界区
counter++;
}
static void decrement(){
// 临界区
counter--;
}
}
竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
4.2 synchronized 解决方案
本节课采用阻塞式的解决方案:synchronzied,来解决上述问题,即俗称的【对象锁】,他采用互斥的方法让同一时刻之多只有一个线程能持有【对象锁】,其他线程再想获取这个【对象锁】时就会阻塞住。这样就可以保证用有锁的线程可以安全的执行临界区内的代码,不用担心线程的上下文切换
synchronized语法介绍
语法
synchronized(对象){ // 线程1 执行代码时会获得这个锁,当有其他线程再来到时,就不能获取这个锁处于阻塞状态(blocked)
临界区
}
解决刚刚的问题
@Slf4j
public class Test {
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);
}
}
运行后会发现问题已被解决
synchronized 理解
synchronized思考
synchronized实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换打断
- 如果把synchronized(obj)放在for循环的外面会怎么样?
——线程会将循环体内的代码全部执行完才会释放锁 - 如果线程1 synchronized(obj1) 而线程2 synchronized(obj2)会怎么样运作?
——这样不可以保护线程的安全运行 - 如果线程1 synchronized(obj1) 而线程2没有加会怎么样?
——不可以,这样做线程2不会去尝试获取锁
面对对象改进
把需要保护的共享变量放入一个类
@Slf4j
public class Test1 {
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;
//this 即Room对象
public void increment(){
synchronized (this){
counter++;
}
}
public void decrement(){
synchronized (this){
counter--;
}
}
public int getCounter(){
synchronized (this){
return counter;
}
}
}
4.3 方法上的synchronized
public synchronized void test(){
}
//等价于
public void test(){
synchronized(this){
}
}
public synchronized static void test(){
}
//等价于
public static void test(){
synchronized(Test.class){
}
}
不加synchronized的方法是没有办法保证原子性的
所谓的“线程八锁”
1
@Slf4j
public class Test8Locks {
public static void main(String[] args) {
Number number = new Number();
new Thread(()->{
log.debug("begin");
number.a();
}).start();
new Thread(()->{
log.debug("begin");
number.b();
}).start();
}
}
@Slf4j
class Number{
public synchronized void a(){
log.debug("1");
}
public synchronized void b(){
log.debug("2");
}
}
他的输出结果为“12”或者“21”
2
@Slf4j
public class Test8Locks {
public static void main(String[] args) {
Number number = new Number();
new Thread(()->{
log.debug("begin");
try {
number.a();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
new Thread(()->{
log.debug("begin");
number.b();
}).start();
}
}
@Slf4j
class Number{
public synchronized void a() throws InterruptedException {
Thread.sleep(1000);
log.debug("1");
}
public synchronized void b(){
log.debug("2");
}
}
输出结果“一秒后12”或者“2一秒后1”
3
@Slf4j
public class Test8Locks {
public static void main(String[] args) {
Number number = new Number();
new Thread(()->{
log.debug("begin");
try {
number.a();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
new Thread(()->{
log.debug("begin");
number.b();
}).start();
new Thread(()->{
log.debug("begin");
number.c();
}).start();
}
}
@Slf4j
class Number{
public synchronized void a() throws InterruptedException {
Thread.sleep(1000);
log.debug("1");
}
public synchronized void b(){
log.debug("2");
}
public void c(){
log.debug("3");
}
}
输出结果为“3一秒后12”或“32一秒后1”或“23一秒后1”
4
@Slf4j
public class Test8Locks {
public static void main(String[] args) {
Number number = new Number();
Number number1 = new Number();
new Thread(()->{
log.debug("begin");
try {
number.a();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
new Thread(()->{
log.debug("begin");
number1.b();
}).start();
}
}
@Slf4j
class Number{
public synchronized void a() throws InterruptedException {
Thread.sleep(1000);
log.debug("1");
}
public synchronized void b(){
log.debug("2");
}
}
输出结果为“2一秒后1”
5
@Slf4j
public class Test8Locks {
public static void main(String[] args) {
Number number = new Number();
new Thread(()->{
log.debug("begin");
try {
number.a();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
new Thread(()->{
log.debug("begin");
number.b();
}).start();
}
}
@Slf4j
class Number{
public static synchronized void a() throws InterruptedException {
Thread.sleep(1000);
log.debug("1");
}
public synchronized void b(){
log.debug("2");
}
}
输出结果为“2一秒后1” ,因为对于静态方法,锁的是类对象
6
@Slf4j
public class Test8Locks {
public static void main(String[] args) {
Number number = new Number();
new Thread(()->{
log.debug("begin");
try {
number.a();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
new Thread(()->{
log.debug("begin");
number.b();
}).start();
}
}
@Slf4j
class Number{
public static synchronized void a() throws InterruptedException {
Thread.sleep(1000);
log.debug("1");
}
public static synchronized void b(){
log.debug("2");
}
}
输出结果“一秒后12”或者“2一秒后1”
7
@Slf4j
public class Test8Locks {
public static void main(String[] args) {
Number number = new Number();
Number number1 = new Number();
new Thread(()->{
log.debug("begin");
try {
number.a();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
new Thread(()->{
log.debug("begin");
number1.b();
}).start();
}
}
@Slf4j
class Number{
public static synchronized void a() throws InterruptedException {
Thread.sleep(1000);
log.debug("1");
}
public synchronized void b(){
log.debug("2");
}
}
输出结果为“2一秒后1”
8
@Slf4j
public class Test8Locks {
public static void main(String[] args) {
Number number = new Number();
Number number1 = new Number();
new Thread(()->{
log.debug("begin");
try {
number.a();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
new Thread(()->{
log.debug("begin");
number1.b();
}).start();
}
}
@Slf4j
class Number{
public static synchronized void a() throws InterruptedException {
Thread.sleep(1000);
log.debug("1");
}
public static synchronized void b(){
log.debug("2");
}
}
输出结果“一秒后12”或者“2一秒后1”,因为他们锁住的都是类对象,而类对象只有一个
4.4 变量的线程安全分析
成员变量和静态变量是否线程安全
- 如果他们没有共享,则线程安全
- 如果他们被共享了,根据他们的状态是否能够改变,又分为两种情况
a)如果只有读操作则线程安全
b)如果有读写操作,则这段代码时临界区,需要考虑线程安全
局部变量是否线程安全
- 局部变量是线程安全的
- 但局部变量的引用对象则未必
a)如果该对象没有逃离方法的作用访问,它是线程安全的
b) 如果该对象逃离方法的作用范围,需要考虑线程安全
局部变量线程安全分析
public static void test1(){
int i = 10;
i++;
}
每个线程调用test1()方法时局部变量i,会在每个线程的栈帧内存中被创建多份,因此不存在共享
如图
局部变量的引用则稍有不同
先看一个成员变量的例子
public class TestThreadUnsafe {
public static void main(String[] args) {
ThreadUnsafe test = new ThreadUnsafe();
for(int i = 0 ; i < 2 ; i++){
new Thread(()->{
test.method1(200);
},"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);
}
}
其中一种情况是线程1还没有add线程2就remove就会发生报错
分析:
- 无论哪个线程中的method2引用的都是同一个对象中的list变量
- method3同理
改为局部变量之后此问题就会得到解决
class ThreadSafe{
public void method1(int loopNumber){
ArrayList<String> list = new ArrayList<>();
for(int i = 0 ; i < loopNumber ; i++){
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list){
list.add("1");
}
private void method3(ArrayList<String> list){
list.remove(0);
}
}
分析:
- list是局部变量,每个线程调用时会创建不同的实例,没有共享
- 而method2中的参数是从method1中传进来的,与method1中引用同一个对象
- method3同method2
方法修饰符带来的思考,如果把method2和method3改为Public会不会代理线程安全问题 - 其他线程调用method2或者method3 ————没有问题,并不是同一个List对象
public class TestThreadUnsafe {
public static void main(String[] args) {
ThreadSageSubClass test = new ThreadSageSubClass();
for(int i = 0 ; i < 2 ; i++){
new Thread(()->{
test.method1(200);
},"Thread" + (i + 1)).start();
}
}
}
class ThreadSafe{
public void method1(int loopNumber){
ArrayList<String> list = new ArrayList<>();
for(int i = 0 ; i < loopNumber ; i++){
method2(list);
//调用method3后,在子类中又会产生一个新的线程,并且list对于新线程和原来的线程是一样的
method3(list);
}
}
public void method2(ArrayList<String> list){
list.add("1");
}
public void method3(ArrayList<String> list){
list.remove(0);
}
}
class ThreadSageSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(()->{
list.remove(0);
}).start();
}
}
从这个例子可以看出private和final提供【安全】的意义所在,体会开闭原则中的【闭】
常见线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent包下的包装类
这里说他们是线程安全的是指,多个线程调用他们同一个实例的某个方法时,是线程安全的,也可以理解为
- 他们的每个方法都是原子的
- 但注意他们多个方法的组合不是原子的,后面会进一步分析
4.5 线程安全类方法的组合
分析下面的代码是否线程安全?
Hashtable table = new Hashtable();
//线程1 线程2
if(table,get("key") == null){
table.put("key",value);
}
有可能会导致另一个线程的put覆盖了本来的意图
不可变类线程安全性
String,Interger等都是不可变类,因为其内部的状态不可以改变,因此他们的方法都是线程安全的
有人可能会有疑问,String有replace,substring等方法【可以】改变值啊,那么这些方法又是怎么保证线程安全的呢?
我们用String的substring方法作为一个例子,查看其源码
可以发现substring方法最后返回的是一个新对象,并没有修改原来的对象
4.6 Monitor概念
Java对象头
以32位虚拟机为例
- 普通对象
- 数组对象
其中Mark Word结构为
Monitor
Monitor被翻译为监视器或管程
每个Java对象都可以关联一个Monitor对象,如果使用stnchronized给对象上锁(重量级)之后,该对象的MarkWord就被设置珍指向Monitor的指针
Monitor结构如下
- 刚开始Monitor中Owner为null
- 当Thread-2执行synchronized(obj)就会将Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner
- 在Thread-2上锁的过程中,如果Thread-3,Thread-4,Thread-5也来执行synchronized(obj),就会进入EntryLIst BLOCKED
- Thread-2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争时是非公平的
- 途中的Thread-0,Thread-1是之前获得过锁,但条件不满足进入WAITING状态的线程,后面wait-notify时会分析
注意: - synchronized必须是进入同一个对象的monitor才能有效果
- 不加synchronized的对象不会关联监视器,不遵守以上规则
原理之synchronized
代码示例
public class Test2 {
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock){
counter++;
}
}
}
其代码所对应的字节码为
0 getstatic #2 // <-lock引用(synchronized)开始
3 dup
4 astore_1 // lock引用 -> slot 1
5 monitorenter //将lock对象MarkWord置为Monitor的指针
6 getstatic #3 // <- i
9 iconst_1 // 准备常数1
10 iadd // +1
11 putstatic #3 //-> i
14 aload_1 // <- lock引用
15 monitorexit //将lock对象MarkWord重置,唤醒EntryList
16 goto 24 (+8)
// 由异常表可知,若6-16行的代码出现异常则进入19行
19 astore_2 // 异常对象e -> slot 2
20 aload_1 // <- lock引用
21 monitorexit //将lock对象MarkWord重置,唤醒EntryList
22 aload_2 // <- slot 2(e)
23 athrow // throw e
24 return
其对应的异常表为
4.7 synchronized优化原理
4.7.1 轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是synchronized(即会优先使用轻量级锁,当轻量级锁加锁失败时才会改变)
假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();
public static void method1(){
synchronized(obj){
//同步块A
method2();
}
}
public static void method2(){
synchronized(obj){
//同步块B
}
}
加锁流程为
- 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
- 让锁记录中的Object reference指向锁对象,并尝试用cas替换Object的Mark Word,将Mark Word的值存入锁记录
- 如果cas(compare and set)替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁,图示如下。
- 如果cas失败,有两种情况
a)如果是其他线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程
b)如果是自己执行了synchronized锁重入(自己给自己加锁),那么再添加一条Lock Record作为重入的计数
- 当退出synchronized代码块(解锁时)如果有取值为null的锁记录,表示有重入,这是重置锁记录,表示重入数量减一
- 当退出synchronzied代码块(解锁时)锁记录不为null,这时使用cas将Mark Word的值回复给对象头
a)成功,则恢复成功
b)失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
4.7.2 锁膨胀
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这是一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这是需要进行锁膨胀,将轻量级锁变成重量级锁
static Object obj = new Object();
public static void method1(){
synchronized(obj){
//同步块
}
}
-
当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁
-
这时Thread-1 加轻量级锁失败,进入锁膨胀流程
a)即为Object对象申请Monitor锁,让Object指向重量级锁地址
b)然后自己进入Monitor的EntryList的BLOCKED
-
当Thread-0退出同步块解锁时,使用cas将Mark Word的值回复给对象头,失败。此时会进入重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中BLOCKED线程
4.7.3 自旋优化(多核CPU)
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞
自旋重试成功的情况
自旋重试失败的情况
总结:
- 在Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就会多自旋几次;反之,就少自旋甚至不自旋,总之比较智能
- 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势
- Java7之后不能控制是否开启自旋功能
4.7.4 偏向锁
轻量级锁在没有竞争的时(就自己这个线程),每次重入仍然需要执行CAS操作
Java6中引入了偏向锁来进一步优化;只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有
例如:
static final Object obj = new Object();
public static void m1(){
synchronized(obj){
// 同步块A
m2();
}
}
public static void m2(){
synchronized(obj){
//同步块B
m3();
}
}
public static void m3(){
synchronized(obj){
//同步块C
}
}
对比
偏向状态
一个对象创建时:
- 如果开启了偏向锁(默认开启),那么对象创建后,markword值为0x05即最后三位是101,这时他的thread,epoch,age都为0
- 偏向锁的开启默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数,XX:BiasedLockingStartupDealy=0来禁用延迟
- 如果没有开启偏向锁,那么对象创建后,markword值为0x01即最后三位为001,这时他的hashcode,age都为0,第一次用到hashcode时才会赋值
- 禁用偏向锁,添加VM参数,-XX:-UseBiasedLocking
- 调用对象的hashcode()方法后,会使偏向锁失效,因为在对象头中,hashcode要占用31位,会导致线程id无处存放
撤销-调用对象hashCode
调用了对象的hashCode,但偏向锁的对象MarkWord中存储的是线程id,如果调用hashCode会导致线程锁被撤销
- 轻量级锁会在锁记录中记录hashCode
- 重量级锁会在Monitor中记录hashCode
在调用hashCode后使用偏向锁,要记得去掉-XX:-UseBiasedLocking
撤销-其他线程使用对象
当有其他线程使用偏向锁对象时,会将偏向锁升级成为轻量级锁
调用wait/notify
批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的ThreadID
当撤销偏向锁阈值超过20次后,jvm会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程
批量撤销
当撤销偏向锁40次后,jvm会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,心间的对象也是不可偏向的。
4.7.5 锁消除
即时编译器(JIT)会在运行过程中判断代码块是否需要加锁,来进行锁消除操作,增加性能
4.8 wait notify
4.8.1 原理之wait/notify
- Owner线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态
- BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
- BLOCKED线程会在Owner线程释放锁时唤醒
- WAITING线程会在Owner线程调用notify或者notifyAll方法时唤醒,但唤醒后不以为着立刻获得锁,仍需进入EntryList重新竞争
API介绍
- obj.wait() 让进入object监视器的线程到waitSet等待
- obj.wait(time) 让进入object监视器的线程到waitSet等待一定时间,若时间到达没被唤醒会自动唤醒
- obj.notify()在object上正在waitSet等待的线程中挑一个唤醒
- obj.notifyAll()让object上正在waitSet等待的线程全部唤醒
他们都是线程之间进行写作的手段,都属于object对象的方法。必须获得此对象的锁,才能调用这几个方法
@Slf4j
public class WaitTest {
static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
lock.wait();
}
}
以上代码会抛出异常Exception in thread “main” java.lang.IllegalMonitorStateException,因为还没有获取到他的锁
必须要获取到锁才能够正常运行,如下
@Slf4j
public class WaitTest {
static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
synchronized (lock){
lock.wait();
}
}
}
notify与notifyAll的区别
@Slf4j
public class WaitTest {
static final Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
synchronized (obj){
log.debug("正在执行");
try {
obj.wait(); //让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其他代码");
}
},"t1").start();
new Thread(()->{
synchronized (obj){
log.debug("正在执行");
try {
obj.wait();//让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其他代码");
}
},"t2").start();
Thread.sleep(2000);
log.debug("唤醒obj上其他线程");
synchronized (obj){
obj.notify(); //唤醒obj上一个线程
//obj.notifyAll(); //唤醒obj上所有线程
}
}
}
notify()输出结果
notifyAll()输出结果
4.8.2 wait和notify的正确姿势
开始前先看看sleep(long n)和wait(long n)的区别
- sleep是Thread的方法,而wait是Object的方法
- sleep不需要强制和synchronized配合使用,但wait需要
- sleep在睡眠的同时,不会释放对象锁,但wait会释放
- 他们的状态都是TIMED_WAITING
@Slf4j
public class WaitNotifyStep1 {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
synchronized (room){
log.debug("没烟,先歇会");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]",hasCigarette);
if(hasCigarette){
log.debug("可以开始干活了");
}
},"小南").start();
for(int i = 0 ; i < 5 ; i++){
new Thread(()->{
synchronized (room){
log.debug("可以开始干活了");
}
},"其他人").start();
}
Thread.sleep(1000);
new Thread(()->{
synchronized (room){
hasCigarette = true;
log.debug("烟到了哦");
room.notify();
}
},"送烟的").start();
}
}
输出结果为
- 解决了其他干活的线程的阻塞问题
- 但如果有其他线程也在等待条件呢?
@Slf4j
public class WaitNotifyStep2 {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
//虚假唤醒
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
synchronized (room){
log.debug("有烟没?[{}]",hasCigarette);
if(!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();
Thread.sleep(1000);
new Thread(()->{
synchronized (room){
hasTakeout = true;
log.debug("外卖到了哦");
room.notify();
}
},"送外卖的").start();
}
}
输出结果为
那么如何解决虚假唤醒呢
@Slf4j
public class WaitNotifyStep3 {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
//虚假唤醒
public static void main(String[] args) throws InterruptedException {
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);
while(!hasTakeout){
log.debug("没外卖先歇会");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]",hasTakeout);
if(hasTakeout){
log.debug("可以开始干活了");
}else{
log.debug("没干成活");
}
}
},"小女").start();
Thread.sleep(1000);
new Thread(()->{
synchronized (room){
hasTakeout = true;
log.debug("外卖到了哦");
room.notifyAll();
}
},"送外卖的").start();
}
}
只需要将判断条件改为while就可以了,这就是wait,notify使用的正确姿势
伪代码如下
synchronized(lock){
while(条件不成立){
lock.wait();
}
//干活
}
//另一个线程
synchronized(lock){
lock.notifyAll();
}
同步模式之保护性暂停
即Guarded Suspension,用在一个线程等待另一个线程的执行结果
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject
- 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(后面说)
- JDK中,join的实现,Future的实现,采用的就是此模式
- 因为到等待另一方的结果,所以归类到同步模式
代码实现
@Slf4j
public class Test3 {
public static void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
new Thread(()->{
log.debug("等待结果");
guardedObject.get();
log.debug((String) guardedObject.get());
},"t1").start();
new Thread(()->{
log.debug("执行下载");
String test = "123";
guardedObject.complete(test);
},"t2").start();
}
}
class GuardedObject{
private Object response;
public Object get(){
synchronized (this){
while(response == null){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return response;
}
}
public void complete(Object response){
synchronized (this){
this.response = response;
this.notifyAll();
}
}
}
输出结果为
相比于join的优点
- 下载线程下载之后还可以干其他的事情,不需要一定等待他结束线程1才可以干其他的事情
- 等待的结果的变量可以为局部变量
功能增强(超时效果)
// 增强超时效果
public Object get(long timeout){
synchronized (this){
//开始时间
long begin = System.currentTimeMillis();
//经历时间
long passedTime = 0;
while(response == null){
// 这一轮循环应该等待的时间
long waitTime = timeout - passedTime;
//经历的时间超过了最大等待时间就退出循环
if(waitTime <= 0){
break;
}
try {
// 防虚假唤醒
this.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 求得经历时间
passedTime = System.currentTimeMillis() - begin;
}
return response;
}
}
扩展
途中Futures就好比居民楼每一层的信箱(每个信箱有房间编号),左侧的t0,t2,t4就好比等待邮件的居民,右侧的t1,t3,t5就好比邮递员
如果需要在多个类之间使用GuardedObject对象,作为参数传递不是很方便,因此设计一个用来解耦的中间件,这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理
代码如下
@Slf4j
public class Test4 {
public static void main(String[] args) throws InterruptedException {
for(int i = 0 ; i < 3 ; i++){
new People().start();
}
Thread.sleep(1000);
for (Integer id : MailBoxes.getIds()) {
new Postman(id,"内容" + id).start();
}
}
}
@Slf4j
class People extends Thread{
@Override
public void run() {
// 收信
GuardedObject1 guardedObject1 = MailBoxes.generateGuardedObject();
log.debug("开始收信 id:{}",guardedObject1.getId());
Object mail = guardedObject1.get(5000);
log.debug("收到信 id{},内容:{}",guardedObject1.getId(),mail);
}
}
@Slf4j
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() {
GuardedObject1 guardedObject = MailBoxes.getGuardedObject(id);
guardedObject.complete(mail);
log.debug("送信 id:{},内容:{}",id,mail);
}
}
class MailBoxes{
private static Map<Integer,GuardedObject1> boxes = new HashMap<>();
// 产生唯一id
private static int id = 1;
private static synchronized int generateId(){
return id++;
}
public static GuardedObject1 getGuardedObject(int id){
return boxes.remove(id);
}
public synchronized static GuardedObject1 generateGuardedObject(){
GuardedObject1 guardedObject1 = new GuardedObject1(generateId());
boxes.put(guardedObject1.getId(),guardedObject1);
return guardedObject1;
}
public static Set<Integer> getIds(){
return boxes.keySet();
}
}
class GuardedObject1{
private int id;
private Object response;
public GuardedObject1(int id){
this.id = id;
}
public int getId() {
return id;
}
public Object get(){
synchronized (this){
while(response == null){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return response;
}
}
// 增强超时效果
public Object get(long timeout){
synchronized (this){
//开始时间
long begin = System.currentTimeMillis();
//经历时间
long passedTime = 0;
while(response == null){
// 这一轮循环应该等待的时间
long waitTime = timeout - passedTime;
//经历的时间超过了最大等待时间就退出循环
if(waitTime <= 0){
break;
}
try {
// 防虚假唤醒
this.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 求得经历时间
passedTime = System.currentTimeMillis() - begin;
}
return response;
}
}
public void complete(Object response){
synchronized (this){
this.response = response;
this.notifyAll();
}
}
}
输出结果为
4.9 异步模式之生产者/消费者
要点
- 与前面的保护性暂停中的GuardObject不同,不需要产生结果和消费结果的线程一一对应
- 消费队列可以用来平衡生产和消费的线程资源
- 生产者仅负责生产数据,不关心数据如何处理,而消费者专心处理消费结果数据
- 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
- JDK中各种阻塞队列,采用的就是这种模式
代码如下
public class Test5 {
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){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Message message = queue.take();
}
},"消费者").start();
}
}
// 消息队列类,用于处理java线程之间的通信
@Slf4j
class MessageQueue{
// 消息的队列集合
private LinkedList<Message> list = new LinkedList<>();
// 队列容量
public MessageQueue(int capcity){
this.capcity = capcity;
}
private int capcity;
// 获取消息
public Message take(){
// 检查队列是否为空
synchronized (list){
while(list.isEmpty()){
try {
log.debug("队列为空,消费者线程等待");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 从队列的头部获取消息返回
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) {
e.printStackTrace();
}
}
list.addLast(message);
log.debug("已生产消息:{}",message);
list.notifyAll();
}
}
}
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 +
'}';
}
}
输出结果为
4.10 Park & Unpark
基本使用
他们是LockSupport类中的方法
// 暂停当前线程
LockSupport.park();
// 回复某个线程的运行
LockSupport.unpark(暂停线程对象);
先park后unpark的基本使用
@Slf4j
public class ParkTest {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
log.debug("start...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("park...");
LockSupport.park();
log.debug("resume");
});
t1.start();
Thread.sleep(2000);
log.debug("unpark...");
LockSupport.unpark(t1);
}
}
输出结果为
特点
与Object的wait¬ify相比
- wait,notify,notifyAll必须配合Object Monitor一起使用,而park,unpark不必
- park & unpark是以线程为单位来【阻塞】和【唤醒】线程,而notify只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】
- park&unpark可以先unpark,但是wait¬ify不能先notify
原理之park & unpark
每个线程都有一个自己的一个Parker对象,由三部分组成,_counter,_cond,_mutex,打个比喻
- 线程就像一个旅人,Parker就像他随身携带的书包,条件变量就好比背包中的帐篷。_counter就好比背包中的备用干粮(0为耗尽,1为充足)
- 调用park就是看需不需要停下来休息
a)如果备用干粮耗尽。那么钻进帐篷里休息
b)如果备用干粮充足,那么不需停留,继续前进。 - 调用unpark,就好比令干粮充足
a)如果这时线程还在帐篷,就唤醒他让他继续前进
b)如果这时线程还在运行,那么他下次调用park时,仅是消耗掉备用干粮,不需停留继续前进
c)因为背包空间有限,多次调用unaprk只会补充一份备用干粮
1 当前线程调用Unsafe.park()方法
2 检查_counter.本情况为0,这时获得_mutex互斥锁
3 线程进入_cond条件变量阻塞
4 设置_couner为0
1 调用Unsafe.unpark()方法,设置_counter为1
2 当前线程调用Unsafe.park()方法
3 检查_counter,本情况为1,这时线程无需阻塞,继续运行
4 设置_counter为0
4.11 重新理解线程状态转换
假设有线程Thread t
情况1 NEW --> RUNNABLE
当调用t.start()方法时,由NEW --> RUNNABLE
情况2 RUNNABLE <–> WAITING
t线程用synchronized(obj)获取了对象锁之后
调用obj.wait()方法时,t线程从RUNNABLE --> WAITING
调用obj.notify(),obj.notifyAll(),t.interrupt()时
a)竞争锁成功,t线程从WAITING --> RUNNABLE
b)竞争锁失败,t线程从 WAITING --> BLOCKED
情况3 RUNNABLE<–> WAITING
当前线程调用t.join()方法时,当前线程从RUNNABLE --> WAITING
注意是当前线程在t线程对象的监视器上等待
t线程运行结束,或调用了当前线程的interrupt()时,当前线程从WAITING --> RUNNING
情况4 RUNNABLE <–> RUNNING
当前线程调用LockSupport.park()方法会让当前线程从 RUNNBALE --> RUNNING
调用LockSupport.unpark(目标线程)或调用了线程的interrupt(),会让目标线程从WAITING --> RUNNABLE
情况5 RUNNABLE <–> TIMED_WAITING
t线程用synchronized(obj)获取了对象锁之后
调用obj.wait(long n)方法时,t线程从RUNNABLE --> TIMED_WAITING
调用obj.notify(),obj.notifyAll(),t.interrupt()时
a)竞争锁成功,t线程从TIMED_WAITING --> RUNNABLE
b)竞争锁失败,t线程从 TIMED_WAITING --> BLOCKED
情况6 RUNNABLE <–> TIMED_WAITING
当前线程调用t.join(long n)方法时,当前线程从RUNNABLE --> TIMED_WAITING
注意是当前线程在t线程对象的监视器上等待
当前线程等待时间超过n毫秒,或者t线程运行结束,或调用了当前线程的interrupt()时,当前线程从TIMED_WAITING–> RUNNABLE
情况7 RUNNABLE <–> TIMED_WAITING
当前线程调用Thread.sleep(long n),当前线程从RUNNBALE --> TIMED_WAITING
当前线程等待时间超过了n毫秒,当前线程从TIMED_WAITING --> RUNNABLE
情况8 RUNNABLE <–> TIMED_WAITING
当前线程调用LockSupport.parkNanos(long nanos)或LockSupport.parkUntil(long millis)时,当前线程从RUNNABLE --> TIMED_WAITING
调用LockSupport.unpark(目标线程)或调用了线程了interrupt(),或是等待超时,会让目标线程从TIMED_WAITING --> RUNNABLE
情况9 RUNNABLE <–> BLOCKED
t线程用synchronized(obj)获取了对象锁时如果竞争失败,从RUNNABLE --> BLOCKED
持obj锁线程的同步代码块执行完毕,会唤醒该对象上所有的BLOCKED的线程重新竞争,如果其中t线程竞争成功,从BLOCKED --> RUNNABLE,其他失败的线程仍然是 BLOCKED
情况10 RUNNABLE <–> TERMINATED
当前线程所有代码运行完毕,进入TERMINATED
4.11 多把锁
多把不相干的锁
一间大屋子有两个功能:睡觉,学习,互不相干
现在小南要学习,小女要睡觉,但如果只用一间屋子的话(一个对象锁)的话,那么并发度很低
解决方法是准备多个房间(多个对象锁)
例如
public class TestMutiLock {
public static void main(String[] args) {
BigRoom bigRoom = new BigRoom();
new Thread(()->{
try {
bigRoom.sleepd();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
new Thread(()->{
try {
bigRoom.study();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
}
}
@Slf4j
class BigRoom{
public void sleepd() throws InterruptedException {
synchronized (this){
log.debug("sleeping两个小时");
Thread.sleep(2000);
}
}
public void study() throws InterruptedException {
synchronized (this){
log.debug("学习一个小时");
Thread.sleep(1000);
}
}
}
输出结果为
可以发现其并发度很低,如何改进呢,如下
@Slf4j
class BigRoom{
private final Object bedRoom = new Object();
private final Object studyRoom = new Object();
public void sleepd() throws InterruptedException {
synchronized (bedRoom){
log.debug("sleeping两个小时");
Thread.sleep(2000);
}
}
public void study() throws InterruptedException {
synchronized (studyRoom){
log.debug("学习一个小时");
Thread.sleep(1000);
}
}
}
现在的输出结果为
可以发现他们是同时进行的,将锁的粒度细分
- 好处是可以增强并发度
- 坏处是如果一个线程需要同时获得多把锁,就容易发生死锁
4.12 活跃性
死锁
有这样的情况:一个线程需要获得多把锁,这时就容易发生死锁
t1线程获得A对象的锁,接下来像获取B对象的锁
t2线程获得B对象的锁,接下来像获取A对象的锁
例如
@Slf4j
public class TestDeadLock {
public static void main(String[] args) {
test();
}
public static void test(){
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(()->{
synchronized (A){
log.debug("lock A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B){
log.debug("lock B");
log.debug("操作");
}
}
},"t1");
Thread t2 = new Thread(()->{
synchronized (B){
log.debug("lock B");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (A){
log.debug("lock A");
log.debug("操作");
}
}
},"t2");
t1.start();
t2.start();
}
}
就会陷入死锁
定位死锁
检测死锁可以使用jconsole工具,或者使用jps定位进程id,再使用jstack定位死锁
我们先使用jps查看java进程ID
再使用jstack + 进程ID来查看进程信息
其输出的内容很多
这里可以发现两个线程的状态为BLOCKED,他们正在等待锁"waiting to lock < xxxx >"
往下翻一翻可以发现其为我们列出了死锁
并且还告诉了我们是哪一行代码发生了死锁
另一种方法是使用jconsole
我们可以在这里打开jconsole进行查看
在这里点击死锁就可以查看死锁
可以发现其列出了死锁和对应的行数
经典问题 哲学家就餐问题
有五个哲学家,围坐在圆桌旁
- 他们只做两件事,思考和吃饭,思考一会吃个饭,吃完饭后接着思考
- 他们吃饭时要用两根筷子吃,桌上共有五个筷子,每位哲学家左右手边各有一个筷子
- 如果筷子被身边人拿着,自己就得等待
@Slf4j
public class PhilosopherProblem {
public static void main(String[] args) {
ChopSticks c1 = new ChopSticks("1");
ChopSticks c2 = new ChopSticks("2");
ChopSticks c3 = new ChopSticks("3");
ChopSticks c4 = new ChopSticks("4");
ChopSticks c5 = new ChopSticks("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
class Philosopher extends Thread{
ChopSticks left;
ChopSticks right;
public Philosopher(String name,ChopSticks left,ChopSticks right){
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while(true){
synchronized (left){
synchronized (right){
try {
eat();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
private void eat() throws InterruptedException {
log.debug("eating");
Thread.sleep(1000);
}
}
class ChopSticks{
String name;
public ChopSticks(String name){
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + "}";
}
}
运行发现输出结果卡在这里不动了
使用jconsole检测死锁,发现
名称: 王沛
状态: org.example.Test.ChopSticks@4f6b6d07上的BLOCKED, 拥有者: 张峰
总阻止数: 1, 总等待数: 0
堆栈跟踪:
org.example.Test.Philosopher.run(PhilosopherProblem.java:36)
- 已锁定 org.example.Test.ChopSticks@1d99b39b
名称: 张峰
状态: org.example.Test.ChopSticks@59bd0fee上的BLOCKED, 拥有者: 问候
总阻止数: 2, 总等待数: 1
堆栈跟踪:
org.example.Test.Philosopher.run(PhilosopherProblem.java:36)
- 已锁定 org.example.Test.ChopSticks@4f6b6d07
名称: 问候
状态: org.example.Test.ChopSticks@618bfff2上的BLOCKED, 拥有者: 杨勇
总阻止数: 2, 总等待数: 2
堆栈跟踪:
org.example.Test.Philosopher.run(PhilosopherProblem.java:36)
- 已锁定 org.example.Test.ChopSticks@59bd0fee
名称: 杨勇
状态: org.example.Test.ChopSticks@74392739上的BLOCKED, 拥有者: 夏鑫宇
总阻止数: 10, 总等待数: 2
堆栈跟踪:
org.example.Test.Philosopher.run(PhilosopherProblem.java:36)
- 已锁定 org.example.Test.ChopSticks@618bfff2
名称: 夏鑫宇
状态: org.example.Test.ChopSticks@1d99b39b上的BLOCKED, 拥有者: 王沛
总阻止数: 2, 总等待数: 0
堆栈跟踪:
org.example.Test.Philosopher.run(PhilosopherProblem.java:36)
- 已锁定 org.example.Test.ChopSticks@74392739
这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题,除了死锁以外,还有活锁和接着两个情况
活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,如
@Slf4j
public class TestLikeLock {
static volatile int count = 10;
public static void main(String[] args) {
new Thread(()->{
// 期望到0退出循环
while(count > 0){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
log.debug("count : {}",count);
}
},"t1").start();
new Thread(()->{
// 期望到20退出循环
while(count < 20){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
log.debug("count : {}",count);
}
},"t2").start();
}
}
饥饿
很多教程把饥饿定义为,一个线程由于优先级太低,始终得不到CPU调度执行也,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题
下面我讲一下我遇到的一个饥饿线程的例子,先来看看使用顺序加锁的方式解决之前的死锁问题
顺序加锁的解决方案
但是这样容易发生饥饿现象,拿之前的哲学家问题举例,如果将第五个哲学家的c5,c1改成c1,c5就不会出现死锁
@Slf4j
public class PhilosopherProblem {
public static void main(String[] args) {
ChopSticks c1 = new ChopSticks("1");
ChopSticks c2 = new ChopSticks("2");
ChopSticks c3 = new ChopSticks("3");
ChopSticks c4 = new ChopSticks("4");
ChopSticks c5 = new ChopSticks("5");
new Philosopher("张峰",c1,c2).start();
new Philosopher("问候",c2,c3).start();
new Philosopher("杨勇",c3,c4).start();
new Philosopher("夏鑫宇",c4,c5).start();
new Philosopher("王沛",c1,c5).start();
}
}
但是输出结果基本上都是夏鑫宇哲学家在吃饭,而王沛哲学家几乎没吃过饭,就发生了饥饿现象
4.13 ReentrantLock
相对于synchronized他具备一下特点
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
与synchronized一样,都支持可重入
基本语法
//获取锁
reentarntLock.lock();
try{
//临界区
}finally{
//释放锁
reentrantLock.unlock();
}
可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为他是这把锁的拥有者,因此有权利再次获得这把锁
如果是不可重入锁,那么第二次获得锁时,自己也会被挡住
例如
@Slf4j
public class ReentrantLockTest {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
log.debug("enter main");
m1();
}finally {
lock.unlock();
}
}
public static void m1(){
lock.lock();
try {
log.debug("enter m1");
m2();
}finally {
lock.unlock();
}
}
public static void m2(){
lock.lock();
try {
log.debug("enter m2");
}finally {
lock.unlock();
}
}
}
输出结果为
证明其可以被同一个线程进入多次同一把锁
可打断
@Slf4j
public class LockInterruptly {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
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();
Thread.sleep(1000);
log.debug("打断t1");
t1.interrupt();
}
}
输出结果为
可以发现线程t1中的锁被打断了,不会无限制的等待下去,这也是一种防止死锁的有效方式
锁超时
@Slf4j
public class ReentrantTryLock {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
log.debug("尝试获得锁");
if(!lock.tryLock()){
log.debug("获取不到锁");
return;
}
try {
log.debug("获得到锁");
}finally {
lock.unlock();
}
},"t1");
lock.lock();
log.debug("获得到锁");
t1.start();
}
}
tryLock也有携带参数的方法,表示最大等待时间
锁超时解决哲学家就餐问题
@Override
public void run() {
while(true){
// 尝试获得左手筷子
if(left.tryLock()){
try {
// 尝试获得右手筷子
if(right.tryLock()){
try {
eat();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
right.unlock();
}
}
}finally {
left.unlock(); // 释放自己左手的筷子
}
}
}
}
使用tryLock成功解决哲学家就餐问题
公平锁
ReentrantLock默认是不公平(后来的也可以先运行)的,公平(排队)锁一般是没有必要的,会降低并发度,后面再进行具体分析
条件变量
synchronized中也有条件变量,就是我们讲原理时那个waitSet休息室,当条件不满足的时候进入waitSet等待
ReentrantLock的条件变量比synchronized强大之处在于,它是支持多个条件变量的,这就好比
a)synchronized是那些不满足条件的线程都在一间休息室休息
b)而ReentrantLock支持多间休息室,有专门等烟的休息室,专门等早餐的休息室,唤醒时也是根据休息室来唤醒
使用流程
- await前需要获得锁
- await执行后,会释放锁,进入conditionObject等待
- await的线程被唤醒(或打断,或超时)重新竞争lock锁
- 竞争lock锁成功后,从await后继续执行
例如
public class ReentrantLockCondition {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
// 创建一个新的条件变量(休息室)
Condition condition = lock.newCondition();
Condition condition1 = lock.newCondition();
lock.lock();
// 进入休息室等待
condition1.await();
condition1.await(1000, TimeUnit.MINUTES);
condition1.signal();
condition1.signalAll();
}
}
使用例子
@Slf4j
public class ReentrantLockConditionTest {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
static ReentrantLock ROOM = new ReentrantLock();
static Condition waitCigaretteSet = ROOM.newCondition();
static Condition waitTakeoutSet = ROOM.newCondition();
//虚假唤醒
public static void main(String[] args) throws InterruptedException {
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();
Thread.sleep(1000);
new Thread(()->{
ROOM.lock();
try {
waitTakeoutSet.signal();
hasTakeout = true;
}finally {
ROOM.unlock();
}
},"送外卖的").start();
Thread.sleep(1000);
new Thread(()->{
ROOM.lock();
try {
waitCigaretteSet.signal();
hasCigarette = true;
}finally {
ROOM.unlock();
}
},"送烟的").start();
}
}
同步模式之顺序控制
4.14固定运行顺序
比如必须先2后1打印
wait notify版打印
@Slf4j
public class Test6 {
static final Object lock = new Object();
// 表示t2是否运行过
static boolean t2run = false;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock){
while(!t2run){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("1");
}
},"t1");
Thread t2 = new Thread(()->{
synchronized (lock){
log.debug("2");
t2run = true;
lock.notifyAll();
}
},"t2");
t1.start();
t2.start();
}
}
park unpark版打印
@Slf4j
public class Test7 {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
LockSupport.park();
log.debug("1");
},"t1");
Thread t2 = new Thread(()->{
log.debug("2");
LockSupport.unpark(t1);
},"t2");
t1.start();
t2.start();
}
}
交替输出
三个线程交替输出abc五次
wait notify版
@Slf4j
public class Test8 {
public static void main(String[] args) throws InterruptedException {
WaitNotify waitNotify = new WaitNotify(5,1);
Thread t1 = new Thread(()->{
try {
waitNotify.print("a",1,2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"t1");
Thread t2 = new Thread(()->{
try {
waitNotify.print("b",2,3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"t2");
Thread t3 = new Thread(()->{
try {
waitNotify.print("c",3,1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"t3");
t1.start();
t2.start();
t3.start();
}
}
class WaitNotify{
private int loopNumber;
private int flag;
public WaitNotify(int loopNumber,int flag){
this.flag = flag;
this.loopNumber = loopNumber;
}
public void print(String str,int waitFlag,int nextFlag) throws InterruptedException {
for(int i = 0 ; i < loopNumber ; i++){
synchronized (this){
while(flag != waitFlag){
this.wait();
}
System.out.print(str);
flag = nextFlag;
this.notifyAll();
}
}
}
}
await signal版
public class Test9 {
public static void main(String[] args) throws InterruptedException {
WaitSignal waitSignal = new WaitSignal(5);
Condition a = waitSignal.newCondition();
Condition b = waitSignal.newCondition();
Condition c = waitSignal.newCondition();
new Thread(()->{
try {
waitSignal.print("a",a,b);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
waitSignal.print("b",b,c);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
waitSignal.print("c",c,a);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
Thread.sleep(1000);
waitSignal.lock();
try {
System.out.println("开始...");
a.signal();
}finally {
waitSignal.unlock();
}
}
}
class WaitSignal extends ReentrantLock{
private int loopNumber;
public WaitSignal(int loopNumber){
this.loopNumber = loopNumber;
}
public void print(String str, Condition current,Condition next) throws InterruptedException {
for(int i = 0 ; i < loopNumber ; i++){
lock();
try {
current.await();
System.out.print(str);
next.signal();
}finally {
unlock();
}
}
}
}
park unpark版
public class Test10 {
static Thread t1;
static Thread t2;
static Thread t3;
public static void main(String[] args) throws InterruptedException {
WaitUnpark waitUnpark = new WaitUnpark(5);
t1 = new Thread(() -> {
waitUnpark.print("a",t2);
});
t2 = new Thread(() -> {
waitUnpark.print("b",t3);
});
t3 = new Thread(() -> {
waitUnpark.print("c",t1);
});
t1.start();
t2.start();
t3.start();
Thread.sleep(1000);
LockSupport.unpark(t1);
}
}
class WaitUnpark{
private int loopNumber;
public WaitUnpark(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);
}
}
}
本章小节
本章我们需要重点掌握的是
- 分析多线程访问共享资源时那么,哪些代码属于临界区
- 使用synchronized互斥解决临界区的线程安全问题
a)掌握synchronized锁对象语法
b)掌握synchronized加载成员方法和静态方法语法
c)掌握wait/notify同步方法 - 使用lock互斥解决临界区的线程安全问题
a)掌握lock的使用细节:可打断,锁超时,公平锁,条件变量 - 学会分析变量的线程安全性,掌握常见线程安全类的使用
- 了解线程活跃性问题:死锁,活锁,饥饿
- 应用方面
a)互斥:使用synchronized或Lock达到共享资源互斥效果
b)同步:使用wait/notify或Lock的条件变量达到线程间通信效果 - 原理方面
a)monitor,synchronized,wait/notify原理
b)synchronized进阶原理
c)park & unpark原理 - 模式方面
a)同步模式之保护性暂停
b)异步模式之生产者消费者
c)同步模式之顺序控制