程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的。
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程 (例如网易云音乐、360 安全卫士等)。
一个进程之内可以分为一到多个线程,一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行。
在Java 中,线程作为最小的调度单位,进程作为资源分配的最小单位。在 windows 中进程是不活动的,只是作为线程的容器。
单核 cpu 下,线程实际上是串行执行的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows 下时间片最小约为 15 毫秒)分给不同的线程使用,只是由于 cpu 在线程间(时间片很短)的切换速度非常快,给人一种同时运行的错觉 。
举例说明:
从方法调用的角度,如果:
设计:
结论:
// 创建线程对象
Thread t = new Thread() {
public void run() {
// 要执行的任务
}
};
// 启动线程
t.start();
例如:
Thread t = new Thread(){
@Override
public void run() {
log.info("running...");
}
};
t.setName("线程t1");
t.start();
log.info("main===>running...");
把线程和任务(要执行的代码)分开
Thread代表线程;
Runnable代表可运行的任务;
Runnable runnable = new Runnable() {
@Override
public void run() {
// 要执行的任务
}
};
// 创建线程对象
Thread t = new Thread(runnable);
// 启动线程
t.start();
例如:
Runnable runnable = new Runnable() {
@Override
public void run() {
// 要执行的任务
log.info("running...");
}
};
// 创建线程对象
Thread t = new Thread(runnable,"线程t2");
// 启动线程
t.start();
log.info("main running...");
方法三:使用FutureTask,传递一个Callable参数,然后重写run()方法,这个run()方法也是线程的执行体,最后创建一个Thread
对象,调用Thread对象的start()方法启动线程。
// 创建任务对象
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("running...");
Thread.sleep(2000);
return 100;
}
});
// 参数1是任务对象,参数2是线程名字
Thread t = new Thread(task, "线程3");
t.start();
windows
linux
我们知道 JVM 是由堆、栈、方法区所组成,其中栈内存是给谁用的呢? 其实就是线程,每个线程启动虚拟机就会为其分配一块栈内存。
每个栈由多个栈帧(Frame) 组成,对应着每次方法调用时所占用的内存;
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法;
线程上下文切换:
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
当上下文切换发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器,它的作用是记录下一条 jvm 指令的执行地址,是线程私有的。
直接上测试案例演示:
public static void main(String[] args) {
Thread t1 = new Thread("线程t1_testRun()") {
@Override
public void run() {
log.info("当前线程:" + Thread.currentThread().getName());
try {
FileReader file = new FileReader("D:\\BaiduNetdiskDownload\\壁纸.jpg");
System.out.println("file:" + file);
} catch (Exception e) {
log.error("找不到文件");
e.printStackTrace();
}
}
};
System.out.println("线程状态:" + t1.getState());
t1.run();
System.out.println("线程状态:" + t1.getState());
log.info("执行完毕!");
}
public static void main(String[] args) {
Thread t1 = new Thread("线程t1_testStart()") {
@Override
public void run() {
log.info("当前线程:" + Thread.currentThread().getName());
try {
FileReader file = new FileReader("D:\\BaiduNetdiskDownload\\壁纸.jpg");
System.out.println("file:" + file);
} catch (Exception e) {
log.error("找不到文件");
e.printStackTrace();
}
}
};
System.out.println("线程状态:" + t1.getState());
t1.start();
System.out.println("线程状态:" + t1.getState());
log.info("执行完毕!");
}
结论:执行run()方法发现线程状态是NEW,处于新建的状态,当前线程名称是main,也就是线程并没有执行;执行start()方法,执行前后线程状态不一致,当前线程名称也是t1线程的名称,证明线程执行了。
sleep()
yield()
线程优先级会提示调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它。如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用。
案例:在没有利用 cpu 来进行计算时,不要让 while(true) 空转浪费 cpu,这时可以使用 yield 或 sleep 来让出 cpu 的使用权,让给其它程序。
此处我在我的阿里云服务器进行效果演示,服务器为1核2G,1核容易看出效果:先注释掉Thread.sleep(50)
public static void main(String[] args) {
while (true) {
try {
// Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
使用 top 命令发现运行这个java造成cpu短时间占满!
取消注释后,就恢复了正常:
join方法用于等待线程结束,用在线程间通信使用。
观察如下代码块,猜想执行结果:
@Slf4j
public class test01 {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test();
}
public static void test() throws InterruptedException {
log.info("开始");
Thread t1 = new Thread(() -> {
log.info("开始");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("结束");
r = 10;
});
t1.setName("线程t1");
t1.start();
log.info("结果为:{}",r); // 最终的结果为多少?
log.info("结束");
}
}
发现线程t1的执行体内,休眠了100毫秒,因此当t1.start();启动线程后,不会去等待t1休眠,而是直接往下执行,因此结果为 0。
对代码块做出改造,加入join()方法。
public class test01 {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test();
}
public static void test() throws InterruptedException {
log.info("开始");
Thread t1 = new Thread(() -> {
log.info("开始");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("结束");
r = 10;
});
t1.setName("线程t1");
t1.start();
t1.join(); // 加入join()方法
log.info("结果为:{}",r); // 结果还是0吗?
log.info("结束");
}
}
此时,当t1.start();启动线程过后,t1.join()会先等待t1执行,因此100毫秒后,才会打印结果,此时r应为10。
案例升级,若等待多个线程,会发生什么?
public class test01 {
static int r = 0;
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test();
}
public static void test() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 10;
});
t1.setName("线程t1");
Thread t2 = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r2 = 20;
});
t2.setName("线程t2");
long start = System.currentTimeMillis();
t1.start();
t2.start();
t1.join();
t2.join();
long end = System.currentTimeMillis();
log.info("r1:{}, r2:{}, cost:{}", r1, r2, end-start); // 最后的打印结果是多少?特别是cost
log.info("结束");
}
}
对代码分析,main线程执行-> t1线程启动 -> t2也启动-> t1线程等待 -> t2线程也等待,最后打印结果。在t1线程等待时,t2也在等待,t1等待1000毫秒后,执行了r1 = 10; 此时t2线程也等待了1000毫秒,因此t2此时只需再等待1000毫秒就执行r2 = 20;最后的结果为 r1=10,r2=20,cost=2000毫秒左右(执行其它代码也需要时间,因此不可能等于固定的2000)。
若先执行t2.join(); 再执行t1.join();呢?
此处代码块省略,直接分析,由于先等待t2线程,但此时t1线程也在同步等待,2000毫秒后,t2线程结束,但在1000毫秒时t1线程早就结束了,因此,最终的cost也是2000毫秒左右,不会有什么变化。
再次思考,若join()方法传递参数呢?比如上述代码块,取消t1.join(), t2.join()变为t2.join(1500),也就是只等待1500毫秒,但t2线程的睡眠时间是2000毫秒。这样的最终结果就是,在等待了1500毫秒后,main主线程继续往下执行,打印结果cost为1500左右。
interrupt()方法会打断 sleep、wait(join的底层原理其实就是wait)、join的线程。
public class test01 {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if (interrupted) {
log.info("被打断了,退出循环");
break;
}
}
}
};
Thread t1 = new Thread(runnable);
t1.start();
Thread.sleep(100);
log.info("打断线程");
t1.interrupt();
}
}
在一个线程 t1 中如何“优雅”终止线程 t2? 这里的优雅指的是给 t2 一个料理后事的机会。
错误思路:
以下这些方法不推荐使用,这些方法已过时,容易破坏同步代码块,造成线程死锁。
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
例如:
public class test01 {
public static void main(String[] args) {
log.debug("主线程,开始运行...");
Thread t1 = new Thread(() -> {
log.debug("守护线程,开始运行...");
try {
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("守护线程,运行结束...");
},"守护线程t1");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("主线程,运行结束...");
}
}
注意:
网络上对于线程状态众说纷纭,有说5种的,有说6种的。现对于这两种不同的说法做出解释。
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
案例:两个线程对初始值为0的静态变量一个做自增,一个做自减,各做 5000 次,结果是0吗?
public class test01 {
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.info("counter:{}",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 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
如果以单线程按顺序执行,不会出现任何问题,如果是多个线程,有可能会出现问题。
一个程序运行多个线程本身是没有问题的,问题出在多个线程访问共享资源。
临界区:一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为 临界区。
例如,下面代码中的临界区
static int counter = 0;
static void increment() {
// 临界区
counter ++;
}
static void decrement() {
// 临界区
counter --;
}
竞态条件:多个线程在临界区内执行,由于代码的执行顺序不同而导致结果无法预测,称为发生了竞态条件。
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
本次使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
注意
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
synchronized 语法
synchronized(对象) {
临界区
}
解决上述案例:
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.info("counter:{}",counter);
}
思考:synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
为了加深理解,思考以下问题:
1、如果把 synchronized(obj) 放在 for 循环的外面,如何理解?
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 白增
putstatic i // 将修改后的值存入静态变量i
2、如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会发生什么?
3、如果 t1 synchronized(obj) 而 t2 没有加锁发生什么? 如何理解?
static class Room {
private int counter = 0;
public void incr() {
synchronized (this) {
counter++;
}
}
public void decr() {
synchronized (this) {
counter--;
}
}
public int getCounter() {
synchronized (this) {
return this.counter;
}
}
}
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.incr();
}
},"t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.decr();
}
},"t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.info("counter:{}", room.getCounter());
}
或者直接加在方法上:
static class Room {
private int counter = 0;
public synchronized void incr() {
counter++;
}
public synchronized void decr() {
counter--;
}
public synchronized int getCounter() {
return this.counter;
}
}
问题1:
static class Number {
public synchronized void a() {
log.info("1");
}
public synchronized void b() {
log.info("2");
}
}
public static void main(String[] args) throws InterruptedException {
Number n1 = new Number();
new Thread(() -> {
n1.a();
},"线程t1").start();
new Thread(() -> {
n1.b();
},"线程t2").start();
}
答案:先打印1,再打印2 或 先打印2,再打印1.
分析:只创建了一个Number 对象n1,但是开启了两个线程,线程t1调用a方法,线程t2调用b方法,由于这两个方法都加了 synchronized 锁,因此如果cpu先调度线程 t1,则先打印1,再打印2;如果cpu先调度线程 t2,则先打印2,再打印1。
问题2:在问题1的基础上,对方法a加入了休眠。
static class Number {
public synchronized void a() throws InterruptedException {
sleep(1000);
log.info("1");
}
public synchronized void b() {
log.info("2");
}
}
public static void main(String[] args) throws InterruptedException {
Number n1 = new Number();
new Thread(() -> {
n1.a();
},"线程t1").start();
new Thread(() -> {
n1.b();
},"线程t2").start();
}
答案:先打印1,再打印2 或 先打印2,再打印1.
分析:只创建了一个Number 对象n1,但是开启了两个线程,线程t1调用a方法,线程t2调用b方法,并且这两个方法都加了 synchronized 锁。因此如果cpu先调度线程 t1,就算要先休眠1秒,但由于加了synchronized 互斥锁,因此会等待线程t1执行完a方法后,线程t2才会执行b方法,因此结果是先打印1,再打印2。如果cpu先调度线程 t2,这时方法b没有休眠,因此结果就是直接先打印2,再打印1。
static class Number {
public synchronized void a() {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("1");
}
public synchronized void b() {
log.info("2");
}
public void c() {
log.info("3");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> {
n1.a();
},"线程t1").start();
new Thread(() -> {
n1.b();
},"线程t2").start();
new Thread(() -> {
n1.c();
},"线程t3").start();
}
答案:先打印3,再打印1,2 或 先打印3,再打印2,1 或先打印2,再打印3,1。
分析:由于方法c没有加锁,方法b加了锁但没有休眠,因此cpu会先调度线程t3或线程t2,如果先调度线程t3,率先执行方法c后,对于线程t1和线程t2执行方法a,b,由于都加了锁,因此当cpu先调度线程t1,则最后结果为先打印3,然后打印1,2;如果cpu先调度线程t2,则最后结果为先打印3,然后打印2,1。如果一开始cpu就先调度线程t2,执行完方法b后,由于线程t1执行a需要休眠1秒,因此不会先执行a方法,而是直接执行没有加锁的c方法,因此最后结果为先打印2,再打印3,1。
问题4:
static class Number {
public synchronized void a() {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("1");
}
public synchronized void b() {
log.info("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> {
n1.a();
},"线程t1").start();
new Thread(() -> {
n2.b();
},"线程t2").start();
}
答案:总数先打印2,后打印1.
分析:创建了两个 Number 对象,一个n1,一个n2,由于n1休眠1秒属于阻塞状态,因此总是会优先打印2,再打印1.
问题5:方法a改为静态方法
static class Number {
public static synchronized void a() {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("1");
}
public synchronized void b() {
log.info("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> {
n1.a();
},"线程t1").start();
new Thread(() -> {
n1.b();
},"线程t2").start();
}
答案: 先打印2,再打印1
由于方法a是一个加了 synchronized 的静态锁,因此锁住的是 Number 这个类对象;而方法b不是静态的,因此锁住的还是 this对象,也就是n1对象,这样就导致方法a、b锁住的是不同的对象,因此这两个方法没有互斥条件,由于方法a会休眠1秒,因此先打印2,再打印1。
问题6:方法a、b都是静态方法
static class Number {
public static synchronized void a() {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("1");
}
public static synchronized void b() {
log.info("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> {
n1.a();
},"线程t1").start();
new Thread(() -> {
n1.b();
},"线程t2").start();
}
答案:先打印1,再打印2或先打印2,再打印1.
分析,由于方法a,b都是静态的,都加有 synchronized 锁,因此方法a,b锁住的都是 Number 这个类对象,也就是锁住的是同一个对象,所以存在互斥条件,因此结果为要么先打印1,后打印2或者先打印2,再打印1。
问题7:
static class Number {
public static synchronized void a() {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("1");
}
public synchronized void b() {
log.info("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> {
n1.a();
},"线程t1").start();
new Thread(() -> {
n2.b();
},"线程t2").start();
}
答案:先打印2,再打印1.
分析,由于方法a是静态的,但有两个 Number 对象n1和n2,方法a锁住的是 Number 类对象,而方法b锁住的是 n2 这个对象,锁住的不是同一个对象,因此没有互斥条件,因此会优先打印2,再打印1.
static class Number {
public static synchronized void a() {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("1");
}
public static synchronized void b() {
log.info("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> {
n1.a();
},"线程t1").start();
new Thread(() -> {
n2.b();
},"线程t2").start();
}
答案:先打印2,再打印1或先打印1,再打印2。
分析:虽然方法a和方法b都是静态的,因此方法a和b都是对 Number 这个类对象加锁,因此存在互斥条件。如果cpu先调度线程t1,则先打印1,后打印2;如果cpu先调度线程t2,则先打印2,再打印1。
1、成员变量和静态变量是否线程安全?
2、局部变量是否线程安全?
java对象头:通常一个对象,在内存中由两部分组成:对象头和成员变量。
以32位虚拟机为例,一个Object Header为对象头、Mark Word为成员变量、:Klass Word为类的类型(如Student类、Teacher类)。
普通对象:对象头为64位(8个字节)。
数组对象:对象头为96位(12个字节)。
其中Mark Word结构为:
Monitor工作原理:每个Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针,Monitor 结构如下:
刚开始 代码块的Monitor 中 Owner 为 null,当Thread-1 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-1,Monitor中也只能有一个Owner。在 Thread-1 上锁的过程中,如果 Thread-2,Thread-3,也来执行 synchronized(obj),就会进入EntryList 进入阻塞状态,当Thread-1 执行完同步代码块的内容后,就会唤醒 EntryList 中等待的线程来竞争锁,竞争的时候是非公平的, WaitSet 中是之前获得过锁,但条件不满足进入 WAITING 状态的线程,待条件满足过后,又会重新进入EntryList ,参与下一轮获取锁。
EntryList 和 WaitSet 都是阻塞状态,不占用cpu时间片。
obj.wait() 让进入 object 监视器的线程到 WaitSet 等待;
obj.wait(long timeout) 让进入 object 监视器的线程到 WaitSet 等待,时间为timeout;
obj.notify() 在 object 上正在 WaitSet 等待的线程中挑一个唤醒;
obj.notifyAll() 让 object上正在 WaitSet 等待的线程全部唤醒;
相对于 synchronized 它具备如下特点:
与synchronized一样,都支持可重入。
ReentrantLock基本语法:
ReentrantLock reentrantLock = new ReentrantLock();
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
ReentrantLock条件变量:
synchronized 中也有条件变量,就是上述讲解的 WaitSet 休息室,当条件不满足时进入 WaitSet 等待,ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比 synchronized 那些不满足条件的线程都在一间休息室等消息,而 ReentrantLock 支持多间休息室,唤醒时也是按休息室来唤醒。
一、可重入特性:
可重入是指同一个线程如果首次获得了这把锁,由于它是这把锁的拥有者,因此有权利再次获取这把锁,锁中有个变量记录重入的次数,如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
// 获取锁
reentrantLock.lock();
try {
log.info("进入主方法");
m1();
} finally {
// 释放锁
reentrantLock.unlock();
}
}
public static void m1() {
// 获取锁
reentrantLock.lock();
try {
log.info("进入m1方法");
m2();
} finally {
// 释放锁
reentrantLock.unlock();
}
}
public static void m2() {
// 获取锁
reentrantLock.lock();
try {
log.info("进入m2方法");
} finally {
// 释放锁
reentrantLock.unlock();
}
}
在获取锁时,如果长时间未获取到锁,可以进行打断,但是加锁方式要改变,不能再使用 reentrantLock.lock() 进行加锁,而是使用 reentrantLock.lockInterruptibly() 进行加锁。
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
// 如果没有竞争则会获取对象锁,如果有竞争或进入阻塞队列,可以被其它线程用 interrupt 打断
log.info("尝试获取锁");
reentrantLock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.info("没有获取到锁,返回");
return;
}
try {
log.info("获取到锁");
} finally {
// 释放锁
reentrantLock.unlock();
}
}, "线程t1");
reentrantLock.lock();
t1.start();
sleep(1000);
log.info("打断线程t1");
t1.interrupt();
}
避免无期限的等待,因此采用 reentrantLock.tryLock() 尝试获取锁,获取到就返回 true ,获取不到就返回false。
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.info("尝试获取锁");
boolean isLock = reentrantLock.tryLock();
if (!isLock) {
log.warn("没有获取到锁,返回");
return;
}
try {
log.info("获取到锁");
} finally {
// 释放锁
reentrantLock.unlock();
}
}, "线程t1");
log.info("主线程先获取到锁");
reentrantLock.lock();
t1.start();
t1.interrupt();
}
由于主线程先获取到了锁,因此线程t1会获取失败:
还可以设置超时时间:
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.info("尝试获取锁");
boolean isLock = false;
try {
isLock = reentrantLock.tryLock(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
log.warn("没有获取到锁,发生了异常,返回");
return;
}
if (!isLock) {
log.warn("没有获取到锁,返回");
return;
}
try {
log.info("获取到锁");
} finally {
// 释放锁
reentrantLock.unlock();
}
}, "线程t1");
log.info("主线程先获取到锁");
reentrantLock.lock();
t1.start();
}
结果:此时没有打断锁,而是超时自动返回了。
再次演示,主线程设置2秒后释放锁,而线程t1等待4秒:
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.info("尝试获取锁");
boolean isLock = false;
try {
isLock = reentrantLock.tryLock(4, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
log.warn("没有获取到锁,发生了异常,返回");
return;
}
if (!isLock) {
log.warn("没有获取到锁,返回");
return;
}
try {
log.info("获取到锁");
} finally {
// 释放锁
reentrantLock.unlock();
}
}, "线程t1");
log.info("主线程先获取到锁");
reentrantLock.lock();
t1.start();
sleep(2000);
reentrantLock.unlock();
log.info("主线程先释放了锁");
}
结果:主线程先加锁,2秒后才释放锁,由于线程t1一直在等待,在主线程释放锁的一瞬间,线程t1立马获取锁成功。
四、锁公平性特性:
ReentrantLock 默认是不公平的,即等待锁的线程,并不是按顺序排队,有可能后来的线程会先获取到锁。但是 ReentrantLock 可以通过构造方法设置是否公平。
// ReentrantLock 底层构造方法
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock reentrantLock = new ReentrantLock(true);
由于篇幅过长,因此对本笔记进行了拆分,其余部分随此链接查看:
https://blog.csdn.net/weixin_44780078/article/details/130753056