目录
(2)线程上下文切换(Thread Context Switch)
一、基本概念
1.进程和线程
进程:
- 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。
- 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
- 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
线程:
- 一个进程之内可以分为一到多个线程
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行 。
- Java 中,线程作为小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器
两者对比:
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集。进程拥有共享的资源,如内存空间等,供其内部的线程共享
- 进程间通信较为复杂 同一台计算机的进程通信称为 IPC(Inter-process communication)
- 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
- 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
2.并行和并发
并发:(concurrent)
单核cpu下,线程实际上是串行执行的。操作系统中有一个组件叫做任务调度器,将cpu的时间片(windows下时间片最小约为15ms)分给不同的线程使用,只是cpu在线程间的切换非常快,人类感觉是同时运行的。总结一句话:微观串行,宏观并行。
一般会将这种线程轮流使用cpu的做法成为并发:concurrent。
并行:(parallel)
多核cpu下,每个核都可以调度运行线程,这个时候线程是并行的。
注: 很多时候,并发并行是同时存在的
- 并发(concurrent)是同一时间应对(dealing with)多件事情的能力
- 并行(parallel)是同一时间动手做(doing)多件事情的能力
3.同步和异步
以调用方的角度讲,如果:
- 需要等待结果返回才能继续运行的话就是同步
- 不需要等待就是异步
使用场景:
- 多线程可以使方法的执行变成异步的,比如说读取磁盘文件时,假设读取操作花费了5秒,如果没有线程的调度机制,那么 cpu 只能等 5 秒,啥都不能做。
- 比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程
- tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程
- ui 程序中,开线程进行其他操作,避免阻塞 ui 线程
结论:
- 单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用 cpu ,不至于一个线程总占用 cpu,别的线程没法干活
- 多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的
- IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一 直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化
4.配置文件说明
pom.xml:
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>
logback.xml :
<?xml version="1.0" encoding="UTF-8"?>
<configuration
xmlns="http://ch.qos.logback/xml/ns/logback"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback logback.xsd">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date{HH:mm:ss} [%t] %logger - %m%n</pattern>
</encoder>
</appender>
<logger name="c" level="debug" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>
<root level="ERROR">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
二、java线程
1.创建线程
方法一:使用Thread
public static void main(String[] args) {
// 匿名内部类方式创建 Thread
Thread t = new Thread() {
@Override
public void run() {
log.debug("running");
}
};
t.setName("t1");
t.start();
log.debug("running");
}
方法二:使用Runnable,配合Thread(推荐),实现Runnable的run方法
public static void main(String[] args) {
// 使用 lambda 表达式,因为 Runnable 接口
// 标注了 @FunctionalInterface 这个注解,表示是一个函数式接口,可以使用 lambda 表达式
Runnable r = () -> log.debug("running");
new Thread(r, "t1").start();
}
两种方法比较:
- 方法 1 是把线程和任务合并在了一起,方法 2 是把线程和任务分开了。
- 用 Runnable 更容易与线程池等高级 API 配合,用 Runnable 让任务类脱离了 Thread 继承体系,更灵活。
方法三:FutureTask配合Thread,FutureTask内有Callable对象,因为lambda所以看不到
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1. 使用 FutureTask 传入 Callable 接口方式创建
FutureTask<Integer> future = new FutureTask<Integer>(() -> {
log.debug("running...");
Thread.sleep(2000); // 休眠
return 100;
});
// 2. 传入 future, 因为 FutureTask 这个类是实现了 RunnableFuture 接口,RunnableFuture 继承了 Runnable 接口
Thread t1 = new Thread(future, "t1");
t1.start();
// 3. 获取返回结果时
// 当主线程获取 t1 线程的返回值时, 需要等 2 秒,此时主线程进入阻塞状态
log.debug("{}", future.get());
}
Future 就是对于具体的 Runnable 或者 Callable 任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过 get 方法获取执行结果,该方法会阻塞直到任务返回结果。
public interface Future<V> {
// 取消任务
boolean cancel(boolean mayInterruptIfRunning);
// 获取任务执行结果
V get() throws InterruptedException, ExecutionException;
// 获取任务执行结果,带有超时时间限制
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
// 判断任务是否已经取消
boolean isCancelled();
// 判断任务是否已经结束
boolean isDone();
}
FutureTask 类是 Future 接口和 Runable 接口的实现弥补 runnable 创建线程没有返回值的缺陷
2.查看进程线程的方法
(1)windows
#查看进程
tasklist
tasklist | findstr java
jps
#杀死进程
taskkill /F /PID pid
(2)linux
#查看进程
ps -ef | grep java
jps
#查看线程
top -H -p pid
#查看某一时刻的线程情况
jstack pid
#杀死进程
kill -9 pid
(3)jconsole工具
jconsole可以用来查看线程的相关信息:
*1)打开jconsole
win+R,然后输入jconsole就可以打开。
*2)linux上运行java
注:先关闭防火墙
java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -
Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 -
Dcom.sun.management.jmxremote.authenticate=是否认证 java类
*3)远程连接
输入ip,端口即可
3.线程运行的原理
(1)栈与栈帧
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
-
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
(2)线程上下文切换(Thread Context Switch)
- 被动原因:
- 线程的 cpu 时间片用完
- 垃圾回收,工作线程要暂停给垃圾回收线程
- 有更高优先级的线程需要运行
- 主动原因:
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
4.Thread的常见方法
方法名 | static | 功能说明 | 注意 |
start() | 启动一个新线程,在新线程中运行 run 方法中的代码 | start 方法只是让线程进入就绪状态,里面代码不一定立刻运行,只有当 CPU 将时间片分给线程时,才能进入运行状态,执行代码。每个线程的 start 方法只能调用一次,调用多次就会出现 IllegalThreadStateException | |
run() | 新线程启动会调用的方法 | 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为 | |
join() | 等待线程运行结束 | ||
join(long n) | 等待线程运行结束,最多等待 n 毫秒 | ||
getId() | 获取线程长整型的 id | id 唯一 | |
getName() | 获取线程名 | ||
setName(String) | 修改线程名 | ||
getPriority() | 获取线程优先级 | ||
setPriority(int) | 修改线程优先级 | java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率。默认是5,数字越大,优先级越高 | |
getState() | 获取线程状态 | Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED | |
isInterrupted() | 判断是否被打断 | 不会清除 打断标记 | |
isAlive() | 线程是否存活(还没有运行完毕) | ||
interrupt() | 打断线程 | 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除 打断标记 ;如果打断的正在运行的线程,则会设置 打断标记,park 的线程被打断,也会设置 打断标记 | |
interrupted() | static | 判断当前线程是否被打断 | 会清除 打断标记 |
currentThread() | static | 获取当前正在执行的线程 | |
sleep(long n) | static | 让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程 | |
yield() | static | 提示线程调度器让出当前线程对CPU的使用 | 主要是为了测试和调试 |
(1)start() VS run()
- 直接调用 run 是在主线程中执行了 run,没有启动新的线程
- 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
(2)sleep()与yield()
sleep (放弃线程对cpu使用,让其他线程使用cpu时间)
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞),可通过state()方法查看
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性(TimeUnit.SECONDS.sleep(2))
yield (让出当前线程):
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态(如果没有其他线程运行,仍然有可能被执行),然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器
sleep()与yield()最大的区别:前者阻塞状态,后者就绪状态,cpu不可能执行阻塞状态的线程,但是如果没有其他线程执行还是有可能去执行就绪状态的线程的
sleep使用案例:
对于服务端的一些监听请求的接口,需要我们一直循环监听,如果我们直接while的话,会一直高额占用cpu,如果是单核的话,其他线程基本用不了什么cpu,很浪费资源。
我们一般会加一个Thread.sleep(50);这样可以大幅度降低使用率。
- 也可以使用wait或者条件变量达到类似的效果
- 不同的是,wait和条件变量需要加锁,并且需要相应的唤醒操作,一般适用于同步的场景;而sleep无需锁同步场景,都可以使用
(3)setPriority:线程优先级
setPriority(int):默认优先级5,范围1-10,数字越大优先级越高。
- 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
(4)join()
join():用于等待某个线程结束。哪个线程内调用join()方法,就等待哪个线程结束,然后再去执行其他线程。
t1.join(1000):最多等待 1000ms,如果 1000ms 内线程执行完毕,则会直接执行下面的语句,不会等够 1000ms
如果不用join,用sleep其实也有这个效果,但是你mian线程不知道t1线程要执行多久,这个sleep的时间不好掌控,所以有了join()这个方法。
实例:不加join,结果是0。加了 join,结果是10:
(5)interrupt:打断线程
*1)打断标记
线程有一个Boolean状态:打断标记。用来表示该线程是否被打断。查看线程是否被打断:isInterrupted()
*2)打断线程两种情况:
- 一个线程运行了sleep wait join方法,该线程会处于阻塞状态。对于这些阻塞线程,会打断。同时将打断标记置为 false
- 如果一个正常线程在在运行中被打断,打断标记会被置为 true 。
*3)isInterrupted() 与 interrupted() 比较:
- isInterrupted 是实例方法,interrupted 是静态方法,它们的用处都是查看当前打断的状态
- isInterrupted 方法仅仅是查看线程是否被打断。不会将打断标记清空,也就是置为 false,原来这个状态是什么就还是什么
- interrupted 查看线程打断状态后,会将打断标志置为 false,也就是清空打断标记。
*4)终止模式之两阶段终止模式
所谓两阶段终止模式:考虑在一个线程T1中如何优雅地终止另一个线程T2?这里的优雅指的是给T2一个料理后事的机会(如释放锁),而不是直接强制杀死。
一个线程关闭另一个线程的错误思路:
- t1调用stop()方法直接杀死t2线程。这样做问题在于:如果t2锁住了一些共享资源,直接被杀掉了,则共享资源不会被释放,则其他线程永远访问不到这个共享资源了
- 使用System.exit(int status)方法停止线程。这个方法直接把整个进程都杀掉了,我们的目的只是停下一个线程而已,没必要这样搞
两阶段终止模式:
比如我们有一个监控线程,他的处理流程应该如下:
实现代码:
/**
* 使用 interrupt 进行两阶段终止模式
*/
@Slf4j(topic = "c.Code_13_Test")
public class Code_13_Test {
public static void main(String[] args) throws InterruptedException {
TwoParseTermination twoParseTermination = new TwoParseTermination();
twoParseTermination.start();
Thread.sleep(3500);
twoParseTermination.stop();
}
}
@Slf4j(topic = "c.TwoParseTermination")
class TwoParseTermination {
private Thread monitor;
// 启动线程
public void start() {
monitor = new Thread(() -> {
while (true) {
Thread thread = Thread.currentThread();
if(thread.isInterrupted()) { // 调用 isInterrupted 不会清除标记
log.info("料理后事 ...");
break;
} else {
try {
Thread.sleep(1000);
log.info("执行监控的功能 ...");
} catch (InterruptedException e) {
log.info("设置打断标记 ...");
thread.interrupt();
e.printStackTrace();
}
}
}
}, "monitor");
monitor.start();
}
// 终止线程
public void stop() {
monitor.interrupt();
}
}
*5)打断LockSupport.park()
LockSupport.park()是并发编程里的一个方法,整个方法一旦调用,线程就停在这里了,不往下走了。
整个时候我们通过interrupt()方法可以打断park(),一旦调用interrupt(),被停下来的线程会继续往下走,且这个时候的打断状态是true(类似于正常线程)。但是,一旦这个线程被打断过后,后面再调用park()就没办法再停下来了。如果还想使用park()方法,可以使用Thread.interrupted()方法,这个方法会默认把打断状态清空(设置为false),那样就又可以使用park()了
*6)不推荐使用的方法
以下三个方法会破坏同步代码块,造成锁得不到释放:
- stop():停止线程运行
- suspend():挂起(暂停)线程运行
- resume():恢复线程运行
stop()方法用上面讲的两阶段终止模式替代,下面两个用wait()和notify()来替代
(6)守护线程
默认情况下,java进程需要等待所有的线程结束后才会停止,但是有一种特殊的线程,叫做守护线程,在其他线程全部结束的时候即使守护线程还未结束代码未执行完java进程也会停止。
普通线程t1可以调用 t1.setDeamon(true); 方法变成守护线程。
注意:
- 垃圾回收器线程就是一种守护线程
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等 待它们处理完当前请求
5.线程状态
(1)线程的 5 种状态
从操作系统层划分,线程有 5 种状态:
- 初始状态,仅仅是在语言层面上创建了线程对象,即Thead thread = new Thead();,还未与操作系统线程关联
- 可运行状态,也称就绪状态,指该线程已经被创建,与操作系统相关联,等待cpu给它分配时间片就可运行
- 运行状态,指线程获取了CPU时间片,正在运行
- 当CPU时间片用完,线程会转换至【可运行状态】,等待 CPU再次分配时间片,会导致我们前面讲到的上下文切换
- 阻塞状态
- 如果调用了阻塞API,如BIO读写文件,那么线程实际上不会用到CPU,不会分配CPU时间片,会导致上下文切换,进入【阻塞状态】
- 等待BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
- 与【可运行状态】的区别是,只要操作系统一直不唤醒线程,调度器就一直不会考虑调度它们,CPU就一直不会分配时间片
- 终止状态,表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
(2)线程的 6 种状态
java里有一个枚举:Thread.State。
该枚举有六个值:NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED
- NEW、TERMINATED 跟五种状态里的初始状态、终结状态是一个意思
- RUNNABLE 是当调用了 start() 方法之后的状态,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【io阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
- BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分。
具体代码:
/**
* 演示 java 线程的 6 种状态(NEW, RUNNABLE, TERMINATED, BLOCKED, WAITING, TIMED_WAITING)
*/
@Slf4j(topic = "c.Code_15_Test")
public class Code_15_Test {
public static void main(String[] args) {
// NEW
Thread t1 = new Thread(() -> {
log.info("NEW 状态");
}, "t1");
// RUNNABLE
Thread t2 = new Thread(() -> {
while (true) {
}
}, "t2");
t2.start();
// TERMINATED
Thread t3 = new Thread(() -> {
log.info("running");
}, "t3");
t3.start();
// TIMED_WAITING
Thread t4 = new Thread(() -> {
synchronized (Code_15_Test.class) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t4");
t4.start();
// WAITING
Thread t5 = new Thread(() -> {
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t5");
t5.start();
//想获得锁却获取不到,得到BLOCKED
Thread t6 = new Thread(() -> {
synchronized (Code_15_Test.class) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t6");
t6.start();
// 主线程休眠 1 秒, 目的是为了等待 t3 线程执行完
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("t1 线程状态: {}", t1.getState());
log.info("t2 线程状态: {}", t2.getState());
log.info("t3 线程状态: {}", t3.getState());
log.info("t4 线程状态: {}", t4.getState());
log.info("t5 线程状态: {}", t5.getState());
log.info("t6 线程状态: {}", t6.getState());
}
}
三、共享模型之管程
1.线程共享带来的问题
首先,我们需要明确,count++在字节码层面一共有四个步骤:先拿到count值,再拿到1,做加法,然后把加到的值再赋值给count。
如果前3步做完了,第四步还没来得及做,cpu时间片到了,其他线程读取count,把本该+1而实际还没加的读到了,那就读取的不对。等到时间片切回到count++,不管count这个时候是多少,他会直接把之前的+1的值覆盖掉。这显然不对:
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 1;i < 5000; i++){
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 1;i < 5000; i++){
count--;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("count的值是{}",count);
}
(1)临界区 Critical Section
临界区:一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。
- 一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问共享资源
- 多个线程读共享资源其实也没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
实例:
static int counter = 0;
static void increment()
// 临界区
{
counter++;
}
static void decrement()
// 临界区
{
counter--;
}
(2)竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
2.synchronized 解决方案
(1)解决手段
为了避免临界区中的竞态条件发生,由多种手段可以达到。
- 阻塞式解决方案:synchronized ,Lock
- 非阻塞式解决方案:原子变量
现在讨论使用 synchronized 来进行解决,即俗称的对象锁,它采用互斥的方式让同一时刻至多只有一个线程持有对象锁,其他线程如果想获取这个锁就会阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
注:虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
-
同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
(2)synchronized 语法
synchronized(对象) {
//临界区
}
static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", counter);
}
思考:
(3)面向对象改进
public class Test {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
room.increment();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
room.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("count: {}", room.get());
}
}
class Room {
int value = 0;
public void increment() {
synchronized (this) {
value++;
}
}
public void decrement() {
synchronized (this) {
value--;
}
}
public int get() {
synchronized (this) {
return value;
}
}
}
3.方法上的synchronized
加在成员方法上,锁住的是对象:
public class Test {
// 在方法上加上synchronized关键字
public synchronized void test() {
}
// 等价于
public void test() {
synchronized(this) { // 锁住的是对象
}
}
}
加在静态方法上,锁住的是类:
public class Test {
// 在静态方法上加上 synchronized 关键字
public synchronized static void test() {
}
//等价于
public void test() {
synchronized(Test.class) { // 锁住的是类
}
}
}
4.线程八锁练习题
(1)12 或 21
因为都是同一个this锁
@Slf4j(topic = "c.Number")
class Number {
public synchronized void a() {
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n1.b();
}).start();
}
(2)1s后1 2,或 2 1s后 1
同样都是this锁
@Slf4j(topic = "c.Number")
class Number {
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n1.b();
}).start();
}
(3)3 1s 12 或 23 1s 1 或 32 1s 1
this锁
class Number {
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
public void c() {
log.debug("3");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n1.b();
}).start();
new Thread(() -> {
n1.c();
}).start();
}
(4)2 1s 后 1
不同对象,锁不相关
@Slf4j(topic = "c.Number")
class Number {
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n2.b();
}).start();
}
(5)2 1s 后 1
a是静态方法,锁了类对象。b锁的是方法。锁不同的对象,不互斥
@Slf4j(topic = "c.Number")
class Number {
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n1.b();
}).start();
}
(6)1s 后12, 或 2 1s后 1
@Slf4j(topic = "c.Number")
class Number {
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n1.b();
}).start();
}
(7)2 1s 后 1
@Slf4j(topic = "c.Number")
class Number {
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n2.b();
}).start();
}
(8)1s 后12, 或 2 1s后 1
class Number {
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n2.b();
}).start();
}
5.变量的线程安全分析
(1)成员变量和静态变量的线程安全分析
- 如果变量没有在线程间共享,那么线程对该变量操作是安全的
- 如果变量在线程间共享
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码就是临界区,需要考虑线程安全问题
(2)局部变量线程安全分析
- 局部变量是线程安全的
- 但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
示例1:不安全,共用list,list在堆内
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);
}
}
static final int THREAD_NUMBER=2;
static final int LOOP_NUMBER=200;
public static void main(String[]args){
ThreadUnsafe test=new ThreadUnsafe();
for(int i=0;i<THREAD_NUMBER; i++){
new Thread(()->{
test.method1(LOOP_NUMBER);
},"Thread"+i).start();
}
}
示例2:安全,不共用list,list在栈内
class ThreadSafe {
public final 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);
}
}
实例3:如果method2和method3方法访问修饰符改成public,会有线程问题吗?
没有,就算有另外一个线程来调用method2,但是里面的传参肯定不是method1中的list,参数不同,所以不会有问题
实例4:如果list在method1内,怎样才能让这个代码有线程问题?
有一个子类来继承该类,然后子类调用method3,在method3内子类新开一个线程,然后操作list,这样,子线程新开的线程与method1中的list就是同一个对象了,就会有线程问题。
解决方法:我们可以吧method2,method3设置为private,则子类就不能覆盖他们了,就谈不上新开线程修改了。同时,如果我们不想子类影响我们的类,可以给类加一个final
class ThreadSafe {
public final 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);
}
}
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
(3)常见线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector (List的线程安全实现类)
- Hashtable (Hash的线程安全实现类)
- java.util.concurrent 包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。如:
Hashtable table = new Hashtable();
new Thread(()->{
table.put("key1", "value1");
}).start();
new Thread(()->{
table.put("key2", "value2");
}).start();
*1)线程安全类方法的组合不是安全的
线程1得到null,然后线程2也得到null,线程2put后,线程1又put,则线程2的值不对了
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
table.put("key", value);
}
*2)不可变类的线程安全
String和Integer类都是不可变的类,因为其类内部状态是不可改变的,因此它们的方法都是线程安全的
String 有 replace,substring 等方法【可以】改变值啊,其实调用这些方法返回的已经是一个新创建的对象了,只不过新对象的值是从以前的对象复制而来
(4)实例分析
注:对于单个共享变量,我们可以在方法上加synchronize。
对于多个共享变量,可以synchronized(A.class),将整个类锁住,当然效率不高
*1)实例1
public class MyServlet extends HttpServlet {
// 不安全
Map<String,Object> map = new HashMap<>();
// 安全
String S1 = "...";
// 安全
final String S2 = "...";
// 不安全
Date D1 = new Date();
// 安全
final Date D2 = new Date();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
// 使用上述变量
}
}
*2)实例2
servlet只有一份,因此UserService也是共享的,则UserServiceImpl是不安全的
public class MyServlet extends HttpServlet {
// 不安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 记录调用次数
private int count = 0;
public void update() {
// ...
count++;
}
}
*3)实例3
spring中如果没有设置scope=prototype,则默认所有的实例都是单例。
如果是单例则表示会被共享,其内的成员变量也是需要被共享的,因此不安全。
解决方案:环绕通知,让前置后置作为局部变量
@Aspect
@Component
public class MyAspect {
// 是否安全?
private long start = 0L;
@Before("execution(* *(..))")
public void before() {
start = System.nanoTime();
}
@After("execution(* *(..))")
public void after() {
long end = System.nanoTime();
System.out.println("cost time:" + (end-start));
}
}
*4)实例4
记住一点:是否有成员变量,且这个成员变量被修改。如果是,那就是不安全
public class MyServlet extends HttpServlet {
// 安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 安全
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
public void update() {
String sql = "update user set password = ? where username = ?";
// 安全
try (Connection conn = DriverManager.getConnection("","","")){
// ...
} catch (Exception e) {
// ...
}
}
}
public class MyServlet extends HttpServlet {
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 不安全
private Connection conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
public class MyServlet extends HttpServlet {
// 安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
public void update() {
UserDao userDao = new UserDaoImpl();
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 安全,因为每个UserDao对象都是新创建的
private Connection = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
*5)实例5
虽然sdf是局部变量,但是这个局部变量有可能会暴露给子类,子类然后进行修改
其中 foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法,因为 foo 方法可以被重写,导致线程不安全。在 String 类中就考虑到了这一点,String 类是 final 关键字声明的,子类不能重写它的方法。
public abstract class Test {
public void bar() {
// 不安全
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
foo(sdf);
}
public abstract foo(SimpleDateFormat sdf);
public static void main(String[] args) {
new Test().bar();
}
}
四、Monitor
1.java对象头
以 32 位虚拟机为例,普通对象的对象头结构如下,其中的 Klass Word 为指针,指向对应的 Class 对象:
数组对象:
其中32位 Mark Word 结构为:
其中64位 Mark Word 结构为:
2.Monitor原理
- 刚开始时 Monitor 中的 Owner 为 null
- 当 Thread-2 执行 synchronized(obj){} 代码时就会将 Monitor 的所有者Owner 设置为 Thread-2,上锁成功,Monitor 中同一时刻只能有一个 Owner
- 当 Thread-2 占据锁时,如果线程 Thread-3 ,Thread-4 也来执行synchronized(obj){} 代码,就会进入 EntryList(阻塞队列) 中变成BLOCKED(阻塞) 状态
- Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争时是非公平的
- 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲 wait-notify 时会分析
注:synchronized 必须是进入同一个对象的 monitor 才有上述的效果不加 synchronized 的对象不会关联监视器,不遵从以上规则
注:Monitor是操作系统提供,使用它成本很高
3.synchronize原理
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
该段代码对应字节码:
Code:
stack=2, locals=3, args_size=1
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
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
Exception table:
from to target type
6 16 19 any
19 22 19 any
五、Synchronized原理进阶
1.轻量级锁
轻量级锁的使用场景是:如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的),那么可以使用轻量级锁来进行优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized 。
实例:假设有两个方法同步块,利用同一个对象加锁:
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
流程解析:
(1)首先有一个Object对象,我们可以看到object对象的结构如下图右侧,主要由2个部分组成:上面两个是对象头,下面是对象体。
对象头分成两部分:
- 第一部分MarkWord,记录hash码,分代年龄,加锁状态。
- 第二部分是Klass Word,是类型指针,表示这个对象时什么类型的对象
- 对象体中记录了成员变量。
执行方法时,先在栈帧里生成一个锁对象(Lock Record),锁对象分为两部分:
- 第一部分记录加锁对象的MarkWord
- 第二部分指向加锁对象的地址
(2)让锁记录中的 Object reference 指向对象,并且尝试用 cas(compare and sweep) 替换 Object 对象的 Mark Word ,将 Mark Word 的值存入锁记录中。
(3)如果 cas 替换成功,那么对象的对象头储存的就是锁记录的地址和状态 00 表示轻量级锁,交换锁记录中第一部分和对象头第一部分
(4)如果cas失败,有两种情况:
- 如果是其它线程已经持有了该 Object 的轻量级锁,那么表示有竞争,首先会进行自旋锁,自旋一定次数后,如果还是失败就进入锁膨胀阶段。
- 如果是自己的线程已经执行了 synchronized 进行加锁,那么再添加一条 Lock Record 作为重入的计数。
(5)当线程退出 synchronized 代码块的时候,如果获取的是取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
(6)当线程退出 synchronized 代码块的时候,如果获取的锁记录取值不为 null,那么使用 cas 将 Mark Word 的值恢复给对象
- 成功则解锁成功
- 失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
2.锁膨胀
如果在尝试加轻量级锁的过程中,cas 操作无法成功,这是有一种情况就是其它线程已经为这个对象加上了轻量级锁,这是就要进行锁膨胀,将轻量级锁变成重量级锁。
(1)当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
(2)这时 Thread-1 加轻量级锁失败,进入锁膨胀流程:
- 即为对象申请Monitor锁,让Object指向重量级锁地址
- 然后自己进入Monitor 的EntryList 变成BLOCKED状态
(3)当 Thread-0 退出 synchronized 同步块时,使用 cas 将 Mark Word 的值恢复给对象头,对象的对象头指向 Monitor,那么会进入重量级锁的解锁过程,即按照 Monitor 的地址找到 Monitor 对象,将 Owner 设置为 null ,唤醒 EntryList 中的 Thread-1 线程
3.自旋锁
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即在自旋的时候持锁的线程释放了锁),那么当前线程就可以不用进行上下文切换就获得了锁。
自旋重试成功的情况:
自旋重试失败的情况,自旋了一定次数还是没有等到持锁的线程释放锁:
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能
- Java 7 之后不能控制是否开启自旋功能
4.偏向锁
(1)偏向锁的基本流程
在轻量级的锁中,我们可以发现,如果同一个线程对同一个对象进行重入锁时,也需要执行 CAS 操作,这是有点耗时,因为我们需要生成锁对象,然后再去跟对象中保存的锁记录比较。
ava6 开始引入了偏向锁的东东,只有第一次使用 CAS 时将对象的 Mark Word 头设置为偏向线程 ID,之后这个入锁线程再进行重入锁时,发现线程 ID 是自己的,那么就不用再进行CAS了。即此时对象中的Mark Word既不是锁对象的Mark Word,也不是Monitor的地址,而是线程ID。
实例:
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
}
}
(2)对象头格式解析
第一行Normal是正常状态,第二行Biased是自旋状态。
如果是Normal状态,保存了3部分内容,分别是hashcode,分代年龄,加锁状态,其中biase_lock=0,表示没有加自旋锁。
如果加了自旋锁,就会从Normal状态变成Biased状态,此时保存的内容为:线程ID,epoch(用于批量重偏向和批量撤销),分代年龄,加锁状态,其中biased_lock=1
注:Normal和Biased结尾都是01。
如果加了轻量锁,最后就会变成00。前面是锁记录的地址
如果加了重量锁,最后就会变成10。前面是Monitor的地址
如果是GC,就是11
(3)偏向状态
一个对象的创建过程:
- 如果开启了偏向锁(默认是开启的),那么对象刚创建之后,Mark Word 最后三位的值101,并且这是它的 Thread,epoch,age 都是 0 ,在加锁的时候进行设置这些的值.
- 偏向锁默认是延迟的,不会在程序启动的时候立刻生效,如果想避免延迟,可以添加虚拟机参数来禁用延迟:
-XX:BiasedLockingStartupDelay=0 来禁用延迟 - 注意:处于偏向锁的对象解锁后,线程 id 仍存储于对象头中
-XX:-UseBiasedLocking
(4)撤销偏向状态
*1)使用hashcode()会自动撤销偏向锁状态
如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、 age 都为 0,第一次用到 hashcode 时才会赋值,即调用对象.hashCode()。同时,如果调用了hashcode()方法,会把之前偏向锁线程ID都变成对应的hashcode,同时将偏向锁最后101变成正常的001。原因很简单,如果不把线程ID清掉,没地方存放hashcode
*2)其他线程使用状态
偏向锁设计的目的就是给同一个线程使用的使用降低开销的,当有其他线程也要使用偏向锁对象,就违背了偏向锁的初衷,此时,偏向锁会升级成为轻量级锁。等到其他线程使用完毕,最后3位会从101变成001,即正常可偏向状态变成不可偏向状态。
*3)调用wait/notify
因为wait/notify只有重量级锁才有,当我们使用wait/notify时,偏向锁自然不会生效
(5)批量重偏向
如果对象虽然被多个线程访问,但是线程间不存在竞争,每次t1用完之后,t2来用,此时会从可偏向状态变成轻量锁状态,当t2用完了,就会变成normal状态,从101变成001,且前面所有的值都恢复为0。
当同一个对象有超过20个t1的偏向锁,且这些偏向锁除了被t1访问,还被t2访问,且t1,t2不存在竞争。我们每次t2访问都会把t1的偏向锁给取消掉,换成轻量锁,结束之后再置0。这个操作达到20次之后,jvm会认为是不是偏向锁不应该给t1,后面的所有t1偏向锁会全都批量的冲偏向到t2,即把t1的线程ID直接变成t2的线程ID,不需要经历偏向锁变轻量锁结束再置0的过程。
(6)批量撤销
我们现在有3个线程t1,t2,t3,且他们之间没有竞争。一开始一个对象是t1的偏向锁,然后t2来了,前20次会把偏向锁变成轻量锁,状态从101变成00。后面的就会进行批量重偏向,直接把t1的线程ID变成t2的线程ID,且状态还是101。这就是我们上面讲的批量重偏向。
当t3来了之后,因为t2的前20个已经是轻量锁了,所以继续保持轻量锁。后面是t2的轻量锁,线程ID是t2的ID,t3会先把接下来20个从偏向锁变成轻量锁。这个时候我们偏向锁变成轻量锁的总次数达到40次,jvm设置的阈值就是40。
当超过40次之后,jvm认为这个类变化比较大,整个类对象都改为不可偏向状态(001)。此时,如果我们再新建一个对象,则对象最后3位是001,而不是一开始的101。
(7)锁消除
JIT(即时编译器)在编译代码的时候会分析,你这个加锁对象是否只是方法内部对象,且不会逃逸。如果是,那么这个对象肯定是安全的,就没有加锁的必要,因此虽然我们写了synchronize,但是在编译的时候jvm会进行锁消除,不会加锁。
#禁用锁消除
-XX:-EliminateLocks
5.wait/notify
(1)原理
- 锁对象调用wait方法(obj.wait),就会使当前线程进入 WaitSet 中,变为 WAITING 状态。
- 处于BLOCKED和 WAITING 状态的线程都为阻塞状态,CPU 都不会分给他们时间片。但是有所区别:
- BLOCKED 状态的线程是在竞争对象时,发现 Monitor 的 Owner 已经是别的线程了,此时就会进入 EntryList 中,并处于 BLOCKED 状态
- WAITING 状态的线程是获得了对象的锁,但是自身因为某些原因需要进入阻塞状态时,锁对象调用了 wait 方法而进入了 WaitSet 中,处于 WAITING 状态
- BLOCKED 状态的线程会在锁被释放的时候被唤醒,但是处于 WAITING 状态的线程只有被锁对象调用了 notify 方法(obj.notify/obj.notifyAll),才会被唤醒。
注:只有当对象加锁以后,才能调用 wait 和 notify 方法
(2)API介绍
obj.wait():让进入 object 监视器的线程到 waitSet 等待
obj.notify() :在 object 上正在 waitSet 等待的线程中挑一个唤醒
obj.notifyAll():让 object 上正在 waitSet 等待的线程全部唤醒
wait(long n):有时限的等待, 到 n 毫秒后结束等待,或是被 notify
(3)wait和sleep区别
- sleep 是 Thread 方法,而 wait 是 Object 的方法
-
sleep 不需要强制和 synchronized 配合使用,但 wait 需要 和 synchronized 一起用
-
sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
-
它们 状态 TIMED_WAITING
(4)wait/notify使用的通用模板
synchronized(lock) {
while(条件不成立) {
lock.wait();
}
// 干活
}
//另一个线程
synchronized(lock) {
lock.notifyAll();
}
(5)同步模式之保护性暂停
-
有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
-
如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者 / 消费者)
-
JDK 中, join 的实现、 Future 的实现,采用的就是此模式
-
因为要等待另一方的结果,因此归类到同步模式
实现:
先写出中间对象:
class GuardedObject {
private Object response;
private final Object lock = new Object();
public Object get() {
synchronized (lock) {
// 条件不满足则等待
while (response == null) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
} }
return response; }
}
public void complete(Object response) {
synchronized (lock) {
// 条件满足,通知等待线程
this.response = response;
lock.notifyAll();
}
}
}
调用:需要防止虚假唤醒(notifyAll),如果虚假唤醒,继续wait
class GuardedObjectV2 {
private Object response;
private final Object lock = new Object();
public Object get(long millis) {
synchronized (lock) {
// 1) 记录最初时间
long begin = System.currentTimeMillis();
// 2) 已经经历的时间
long timePassed = 0;
while (response == null) {
// 4) 假设 millis 是 1000,结果在 400 时唤醒了,那么还有 600 要等
long waitTime = millis - timePassed;
log.debug("waitTime: {}", waitTime);
if (waitTime <= 0) {
log.debug("break...");
break; }
try {
lock.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 3) 如果提前被唤醒,这时已经经历的时间假设为 400
timePassed = System.currentTimeMillis() - begin;
log.debug("timePassed: {}, object is null {}",
timePassed, response == null);
}
return response; }
}
public void complete(Object response) {
synchronized (lock) {
// 条件满足,通知等待线程
this.response = response;
log.debug("notify...");
lock.notifyAll();
}
}
}
(6)join原理
join用的就是我们上面的同步模式。
(7)优化案例
上面案例两个线程一一对应,传送一条数据。但是如果有多个数据,那么就需要多个线程一一对应,怎么实现?
class GuardedObject {
// 标识 Guarded Object
private int id;
public GuardedObject(int id) {
this.id = id;
}
public int getId() {
return id;
}
// 结果
private Object response;
// 获取结果
// timeout 表示要等待多久 2000
public Object get(long timeout) {
synchronized (this) {
// 开始时间 15:00:00
long begin = System.currentTimeMillis();
// 经历的时间
long passedTime = 0;
while (response == null) {
// 这一轮循环应该等待的时间
long waitTime = timeout - passedTime;
// 经历的时间超过了最大等待时间时,退出循环
if (timeout - passedTime <= 0) {
break;
}
try {
this.wait(waitTime); // 虚假唤醒 15:00:01
} catch (InterruptedException e) {
e.printStackTrace();
}
// 求得经历时间
passedTime = System.currentTimeMillis() - begin; // 15:00:02 1s
}
return response;
}
}
// 产生结果
public void complete(Object response) {
synchronized (this) {
// 给结果成员变量赋值
this.response = response;
this.notifyAll();
}
}
}
/**
* 邮箱类,即中间解耦类
*/
class Mailboxes {
private static Map<Integer, GuardedObject> boxes = new Hashtable<>();
private static int id = 1;
// 产生唯一 id
private static synchronized int generateId() {
return id++;
}
public static GuardedObject getGuardedObject(int id) {
return boxes.remove(id);
}
public static GuardedObject createGuardedObject() {
GuardedObject go = new GuardedObject(generateId());
boxes.put(go.getId(), go);
return go;
}
public static Set<Integer> getIds() {
return boxes.keySet();
}
}
/**
* 用户类,信息接受者类
*/
class People extends Thread{
@Override
public void run() {
// 收信
GuardedObject guardedObject = Mailboxes.createGuardedObject();
log.debug("开始收信 id:{}", guardedObject.getId());
Object mail = guardedObject.get(5000);
log.debug("收到信 id:{}, 内容:{}", guardedObject.getId(), mail);
}
}
/**
* 快递员类,信息发送者类
*/
class Postman extends Thread {
private int id;
private String mail;
public Postman(int id, String mail) {
this.id = id;
this.mail = mail;
}
@Override
public void run() {
GuardedObject guardedObject = Mailboxes.getGuardedObject(id);
log.debug("送信 id:{}, 内容:{}", id, mail);
guardedObject.complete(mail);
}
}
/**
* 测试
*/
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new People().start();
}
Sleeper.sleep(1);
for (Integer id : Mailboxes.getIds()) {
new Postman(id, "内容" + id).start();
}
}
(8)生产者消费者模式
我们上面同步模式需要一一对应,以上述案例为例,一个住户需要一个快递员,这个很明显不行。我们使用生产者消费者模式,让生产者消费者不用一一对应,一个快递员对应多个住户。
- 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
- 消费队列可以用来平衡生产和消费的线程资源
- 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
- 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
- JDK 中各种阻塞队列,采用的就是这种模式
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;
}
}
class MessageQueue {
private LinkedList<Message> queue;
private int capacity;
public MessageQueue(int capacity) {
this.capacity = capacity;
queue = new LinkedList<>();
}
public Message take() {
synchronized (queue) {
while (queue.isEmpty()) {
log.debug("没货了, wait");
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Message message = queue.removeFirst();
queue.notifyAll();
return message;
}
}
public void put(Message message) {
synchronized (queue) {
while (queue.size() == capacity) {
log.debug("库存已达上限, wait");
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.addLast(message);
queue.notifyAll();
}
}
}
MessageQueue messageQueue = new MessageQueue(2);
// 4 个生产者线程, 下载任务
for (int i = 0; i < 4; i++) {
int id = i;
new Thread(() -> {
try {
log.debug("download...");
List<String> response = Downloader.download();
log.debug("try put message({})", id);
messageQueue.put(new Message(id, response));
} catch (IOException e) {
e.printStackTrace();
}
}, "生产者" + i).start();
}
// 1 个消费者线程, 处理结果
new Thread(() -> {
while (true) {
Message message = messageQueue.take();
List<String> response = (List<String>) message.getMessage();
log.debug("take message({}): [{}] lines", message.getId(), response.size());
}
}, "消费者").start();
}
6.park & unpark
(1)基本使用
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
(2)原理
- 当前线程调用 Unsafe.park() 方法
-
检查 _counter ,本情况为 0 ,这时,获得 _mutex 互斥锁
-
线程进入 _cond 条件变量阻塞
-
设置 _counter = 0(虽然已经是0了,但是还会再设置一遍)
*2)调用unpark方法
- 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
-
唤醒 _cond 条件变量中的 Thread_0
-
Thread_0 恢复运行
-
设置 _counter 为 0
(3)与Object 的 wait & notify 相比
-
wait , notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park , unpark 不必
-
park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程, notifyAll 是唤醒所有等待线程,就不那么【精确】
-
park & unpark 可以先 unpark ,而 wait & notify 不能先 notify
7.重新理解线程状态转换
- 情况一:NEW –> RUNNABLE
- 当调用了 t.start() 方法时,由 NEW –> RUNNABLE
- 情况二: RUNNABLE <–> WAITING
- 当调用了t 线程用 synchronized(obj) 获取了对象锁后,调用 obj.wait() 方法时,t 线程从 RUNNABLE –> WAITING
- 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时,会在 WaitSet 等待队列中出现锁竞争,非公平竞争
- 竞争锁成功,t 线程从 WAITING –> RUNNABLE
- 竞争锁失败,t 线程从 WAITING –> BLOCKED
- 情况三:RUNNABLE <–> WAITING
- 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE –> WAITING
- t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING –> RUNNABLE
- 情况四: RUNNABLE <–> WAITING
- 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE –> WAITING
- 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING –> RUNNABLE
- 情况五: RUNNABLE <–> TIMED_WAITING
- t 线程用 synchronized(obj) 获取了对象锁后,调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE –> TIMED_WAITING
- t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
- 竞争锁成功,t 线程从 TIMED_WAITING –> RUNNABLE
- 竞争锁失败,t 线程从 TIMED_WAITING –> BLOCKED
- 情况六:RUNNABLE <–> TIMED_WAITING
- 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE –> TIMED_WAITING
- 当前线程等待时间超过了 n 毫秒,或 t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING –> RUNNABLE
- 情况七:RUNNABLE <–> TIMED_WAITING
- 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE –> TIMED_WAITING
- 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING –> RUNNABLE
- 情况八:RUNNABLE <–> TIMED_WAITING
- 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线 程从 RUNNABLE –> TIMED_WAITING
- 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING–> RUNNABLE
- 情况九:RUNNABLE <–> BLOCKED
- t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE –> BLOCKED
- 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争 成功,从 BLOCKED –> RUNNABLE ,其它失败的线程仍然 BLOCKED
- 情况十: RUNNABLE <–> TERMINATED
- 当前线程所有代码运行完毕,进入 TERMINATED
六、ReentrantLock
1.线程活跃性
(1)多把锁
如果我们有一个大房间,学习和睡觉都锁这个房间,那么效率很低。
我们在大房间里创建2个对象,学习锁学习对象,睡觉锁睡觉对象,互不干扰,可以有效增强并发度。
缺点:如果一个线程需要同时获得多把锁,容易发生死锁
(2)死锁
这样一个情况:t1 线程获得 A 对象锁, t2 线程获得 B 对象锁。接下来t2想获取 A 对象的锁,t1想获取B对象的锁,是获取不到的,陷入死锁。
死锁经典问题就是哲学家就餐问题,解决方法:在线程使用锁对象时,顺序加锁即可避免死锁
@Slf4j
public class Test {
public static void main(String[] args) {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("lock A");
try {
sleep(2);
} 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 {
sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (A) {
log.debug("lock A");
log.debug("操作...");
}
}
}, "t2");
t1.start();
t2.start();
}
}
(3)定位死锁
方法一:使用jstack
*1)jps:查看所有的进程ID
*2)jstack 进程ID:查看该进程下所有线程信息
我们继续往下拉可以看到更具体的信息:
方法二:使用jconsole
如图:
(4)活锁
活锁出现在两个线程互相改变对方的结束条件,谁也无法结束。
避免活锁的方法:
在线程执行时,中途给予不同的间隔时间即可。
死锁与活锁的区别:
- 死锁是因为线程互相持有对象想要的锁,并且都不释放,最后到时线程阻塞,停止运行的现象。
- 活锁是因为线程间修改了对方的结束条件,而导致代码一直在运行,却一直运行不完的现象。
实例:
两个线程,结束条件都是count=20。一个线程count++,一个线程count--,互相一直改变count,导致谁都结束不了。
(5)饥饿
某些线程因为优先级太低,导致一直无法获得资源的现象。
我们之前说到,解决死锁的方法可以是使用顺序加锁来避免。
一开始的流程是这样:
用了顺序锁后,必须先获取A才能获取B:
但是加了顺序锁会有一个问题:有的线程会一直拿不到A,从而一直阻塞。而有的线程则一直才运行。阻塞的线程就是处于饥饿状态。
2.ReentrantLock基本概念
ReentrantLock就是可重入锁,和 synchronized 相比具有的的特点:
- 可中断
- 可以设置超时时间
- 可以设置为公平锁 (先到先得)
- 支持多个条件变量( 具有多个 WaitSet)
使用方法:
// 获取ReentrantLock对象
private ReentrantLock lock = new ReentrantLock();
// 加锁
lock.lock();
try {
// 需要执行的代码
}finally {
// 释放锁
lock.unlock();
}
3.ReentrantLock特性
(1)可重入
- 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
- 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
(2)可打断
lock.lockInterruptibly():防止一直等,避免死锁
- 如果没有竞争,该方法就会获得lock锁
- 如果有竞争,就会进入阻塞状态。区别在于,如果其他线程使用interrupt(),会让它结束阻塞状态,并且抛一个InterruptedException
package controller;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
import static java.lang.Thread.sleep;
@Slf4j
public class Test {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("启动...");
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("等锁的过程中被打断");
return;
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
sleep(1);
t1.interrupt();
log.debug("执行打断");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
(3)锁超时
可打断需要其他线程来interrupt,属于被动打断。锁超时是主动中断,从而避免死等。
lock.tryLock(1, TimeUnit.SECONDS):
- 锁设置1秒,如果没有找到就返回
- 我们看到,该方法支持InterruptedException,表示也是个可打断的锁
- 如果不加参数,表示立刻返回,返回值是boolean,如果为false表示没拿到锁,就直接返回即可
package controller;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import static java.lang.Thread.sleep;
@Slf4j
public class Test {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("启动...");
try {
if (!lock.tryLock(1, TimeUnit.SECONDS)) {
log.debug("获取等待 1s 后失败,返回");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("获取不到锁");
return;
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("主线程获得了锁");
t1.start();
}
}
4.解决哲学家就餐问题
必须先拿左筷子,再拿右筷子。同时,如果左筷子没拿到,就释放左筷子,右筷子同理。这样就解决了死锁的问题。
class Chopstick extends ReentrantLock {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
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);
}
}
5.公平锁
ReentrantLock默认是不公平锁。
一般不设置公平锁,会降低并发度。
#使之成为公平锁
ReentrantLock lock = new ReentrantLock(true);
上面为什么加了一个true就会变成公平锁,后面会细说。这里先跳过。
6.条件变量
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入waitSet 等待。
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比:
- synchronized 是那些不满足条件的线程都在一间休息室等消息
- 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
使用要点:
- await 前需要获得锁
- await 执行后,会释放锁,进入 conditionObject 等待
- await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
- 竞争 lock 锁成功后,从 await 后继续执
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
lock.lock();
condition1.await();
condition1.await(1,TimeUnit.SECONDS);
condition1.signal();
condition1.signalAll();
实例:
package controller;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import static java.lang.Thread.sleep;
@Slf4j
public class Test {
static ReentrantLock lock = new ReentrantLock();
static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitbreakfastQueue = lock.newCondition();
static volatile boolean hasCigrette = false;
static volatile boolean hasBreakfast = false;
public static void main(String[] args) {
new Thread(() -> {
try {
lock.lock();
while (!hasCigrette) {
try {
waitCigaretteQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("等到了它的烟");
} finally {
lock.unlock();
}
}).start();
new Thread(() -> {
try {
lock.lock();
while (!hasBreakfast) {
try {
waitbreakfastQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("等到了它的早餐");
} finally {
lock.unlock();
}
}).start();
sleep(1);
sendBreakfast();
sleep(1);
sendCigarette();
}
private static void sendCigarette() {
lock.lock();
try {
log.debug("送烟来了");
hasCigrette = true;
waitCigaretteQueue.signal();
} finally {
lock.unlock();
}
}
private static void sendBreakfast() {
lock.lock();
try {
log.debug("送早餐来了");
hasBreakfast = true;
waitbreakfastQueue.signal();
} finally {
lock.unlock();
}
}
}
7.同步模式之顺序控制:固定顺序
对于t1,t2,我们想固定顺序t2,t1输出,怎么做?
(1)wait/notify版
package controller;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class Test {
// 用来同步的对象
static Object obj = new Object();
// t2 运行标记, 代表 t2 是否执行过
static boolean t2runed = false;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (obj) {
// 如果 t2 没有执行过
while (!t2runed) {
try {
// t1 先等一会
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println(1);
});
Thread t2 = new Thread(() -> {
System.out.println(2);
synchronized (obj) {
// 修改运行标记
t2runed = true;
// 通知 obj 上等待的线程(可能有多个,因此需要用 notifyAll)
obj.notifyAll();
}
});
t1.start();
t2.start();
}
}
(2)park/unpark版本
package controller;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.LockSupport;
@Slf4j
public class Test {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
// 当没有『许可』时,当前线程暂停运行;有『许可』时,用掉这个『许可』,当前线程恢复运行
LockSupport.park();
System.out.println("1");
});
Thread t2 = new Thread(() -> {
System.out.println("2");
// 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』)
LockSupport.unpark(t1);
});
t1.start();
t2.start();
}
}
8. 同步模式之顺序控制:交替输出
t1,t2,t3我们想交替输出,怎么办?
(1)wait/notify版
class SyncWaitNotify {
private int flag;
private int loopNumber;
//当前线程对应的值,循环的次数
public SyncWaitNotify(int flag, int loopNumber) {
this.flag = flag;
this.loopNumber = loopNumber;
}
//第一个参数:当前线程的值等于该值时,可以进行操作
//第二个参数:下一个线程需要等待的值为多少,用于保证顺序
//第三个参数:打印
public void print(int waitFlag, int nextFlag, String str) {
for (int i = 0; i < loopNumber; i++) {
synchronized (this) {
while (this.flag != waitFlag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(str);
flag = nextFlag;
this.notifyAll();
}
}
}
}
@Slf4j
public class Test {
public static void main(String[] args) {
SyncWaitNotify syncWaitNotify = new SyncWaitNotify(1, 5);
new Thread(() -> {
syncWaitNotify.print(1, 2, "a");
}).start();
new Thread(() -> {
syncWaitNotify.print(2, 3, "b");
}).start();
new Thread(() -> {
syncWaitNotify.print(3, 1, "c");
}).start();
}
}
(2)ReentrantLock版本
package controller;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
class AwaitSignal extends ReentrantLock {
// 循环次数
private int loopNumber;
public AwaitSignal(int loopNumber) {
this.loopNumber = loopNumber;
}
//唤醒第一个休息室,让程序开始跑
public void start(Condition first) {
this.lock();
try {
log.debug("start");
first.signal();
} finally {
this.unlock();
}
}
//第一个参数:打印
//第二个参数:当前休息室
//第三个参数:下一个休息室
public void print(String str, Condition current, Condition next) {
for (int i = 0; i < loopNumber; i++) {
this.lock();
try {
current.await();
log.debug(str);
next.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
this.unlock();
}
}
}
}
@Slf4j
public class Test {
public static void main(String[] args) {
AwaitSignal as = new AwaitSignal(5);
Condition aWaitSet = as.newCondition();
Condition bWaitSet = as.newCondition();
Condition cWaitSet = as.newCondition();
new Thread(() -> {
as.print("a", aWaitSet, bWaitSet);
}).start();
new Thread(() -> {
as.print("b", bWaitSet, cWaitSet);
}).start();
new Thread(() -> {
as.print("c", cWaitSet, aWaitSet);
}).start();
as.start(aWaitSet);
}
}
(3)park/unpark
package controller;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.LockSupport;
@Slf4j
class SyncPark {
private int loopNumber;
private Thread[] threads;
public SyncPark(int loopNumber) {
this.loopNumber = loopNumber;
}
public void setThreads(Thread... threads) {
this.threads = threads;
}
public void print(String str) {
for (int i = 0; i < loopNumber; i++) {
LockSupport.park();
System.out.print(str);
LockSupport.unpark(nextThread());
}
}
private Thread nextThread() {
Thread current = Thread.currentThread();
int index = 0;
for (int i = 0; i < threads.length; i++) {
if (threads[i] == current) {
index = i;
break;
}
}
if (index < threads.length - 1) {
return threads[index + 1];
} else {
return threads[0];
}
}
public void start() {
for (Thread thread : threads) {
thread.start();
}
LockSupport.unpark(threads[0]);
}
}
@Slf4j
public class Test {
public static void main(String[] args) {
SyncPark syncPark = new SyncPark(5);
Thread t1 = new Thread(() -> {
syncPark.print("a");
});
Thread t2 = new Thread(() -> {
syncPark.print("b");
});
Thread t3 = new Thread(() -> {
syncPark.print("c\n");
});
syncPark.setThreads(t1, t2, t3);
syncPark.start();
}
}