JUC并发编程
文章目录
- JUC并发编程
- 一、进程与线程
- 1 进程
- 2 线程
- 3 进程与线程的对比
- 4 并行与并发
- 5 同步与异步
- 二、Java线程
- 1 创建和运行线程
- 2 查看进程线程的方法
- 3 栈与栈帧
- 4 线程中常见方法
- 5 start()与run()
- 6 sleep()与yield()
- 7 线程优先级
- 8 join()方法
- 9 interrupt()方法
- 10 使用到interrupt()的多线程设计模——两阶段终止模式
- 11 过时不推荐的方法
- 12 主线程与守护线程
- 13 线程状态-五种
- 14 线程状态-六种
- 三、共享模型之管程
- 1 线程安全问题
- 2 临界区与竞态条件
- 3 synchronized 解决方案
- 4 对上述案例改造为面向对象方法编写
- 5 练习题之线程八锁——帮助理解 synchronized
- 6 变量的线程安全问题分析
- 7 Monitor-对象头
- 8 常用方法介绍
- 9 wait(long n) 和 sleep(long n) 的区别
- 10 ReentrantLock
一、进程与线程
1 进程
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的。
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程 (例如网易云音乐、360 安全卫士等)。
2 线程
一个进程之内可以分为一到多个线程,一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行。
在Java 中,线程作为最小的调度单位,进程作为资源分配的最小单位。在 windows 中进程是不活动的,只是作为线程的容器。
3 进程与线程的对比
- 进程基本上是相互独立的,而线程存在于进程中,是进程的一个子集;
- 进程拥有共享的资源,如内存空间等,可以供它内部的线程共享;
- 线程通信相对简单,它们可以共享进程内的内存,进程间通信较为复杂,同一台计算机的进程通信称为 IPC ,不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP ;
4 并行与并发
单核 cpu 下,线程实际上是串行执行的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows 下时间片最小约为 15 毫秒)分给不同的线程使用,只是由于 cpu 在线程间(时间片很短)的切换速度非常快,给人一种同时运行的错觉 。
- 并发:线程轮流使用 CPU 的情况称为并发(concurrent)。
- 并行:由于我们的个人电脑大多都是多核cpu,多个cpu同时运行多个线程的情况叫做并行。
举例说明:
- 并发:家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人依次做这三件事,这就是并发。
- 并行:家庭主妇雇了3个保姆,一个专门做饭、一个专门打扫卫生、一个专门喂奶,互不干扰,这就是并行。
- 既有并发又有并行:家庭主妇只雇了一个保姆,她们一起做这些事,保姆扫地、家庭主妇给孩子喂奶(这时属于并行),当保姆去做饭时,家庭主妇也想要做饭就只能等待(这时属于并发)。
5 同步与异步
从方法调用的角度,如果:
- 需要等待结果返回,才能继续往下执行,就是同步;
- 不需要等待结果返回,直接往下执行,就是异步;
设计:
- 多线程可以实现方法执行变为异步!!!
结论:
- 比如在项目中,某一个业务操作花费时间较长,这时可以开一个新的线程去处理这个耗时的业务,避免阻塞主线程的执行;
- tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程;
- ui程序中,开线程进行其他操作,避免阻塞ui线程
二、Java线程
1 创建和运行线程
- 方法一:直接使用Thread,创建Thread对象,然后重写run()方法,这个run()方法就是线程的执行体,最后调用Thread对象的start()启动线程。
// 创建线程对象
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...");
- 方法二:使用Runnable配合Thread,创建Runnable对象,并重写run()方法,然后再创建Thread对象,调用Thread对象的start()方法启动线程。
把线程和任务(要执行的代码)分开
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();
2 查看进程线程的方法
-
windows
- tasklist:查看进程
- taskskill杀死进程
-
linux
- ps -ef 查看所有进程
- ps -ft -p 查看某个进程
- kill 杀死进程
3 栈与栈帧
我们知道 JVM 是由堆、栈、方法区所组成,其中栈内存是给谁用的呢? 其实就是线程,每个线程启动虚拟机就会为其分配一块栈内存。
每个栈由多个栈帧(Frame) 组成,对应着每次方法调用时所占用的内存;
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法;
线程上下文切换:
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
- 线程的 cpu 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当上下文切换发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器,它的作用是记录下一条 jvm 指令的执行地址,是线程私有的。
4 线程中常见方法
- start():启动一个新线程,在新的线程运行run方法中的代码;
- run():新线程启动后调用的方法;
- join():等待线程结束:用在线程间通信使用;
- join(long n):最多等待n毫秒;
- getId():获取线程长整型的id,唯一id;
- getName():获取线程名;
- setName(String str):修改线程名;
- getPriority():获取线程优先级;
- setPriority(int n):修改线程优先级,java中规定线程的优先级是1-10的整数,最大优先级是10,最小是1。默认优先级是5,公平竞争。
- getState():获取线程状态,java中线程状态用6个enum表示:NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED;
- isInterrupted():判断是否被打断;
- interrupt():打断线程:如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记;如果打断正在运行的线程,则会设置打断标记;park 的线程被打断,也会设置打断标记。
- 静态方法:
- interrupted():判断当前线程是否被打断,会清除 打断标记;
- currentThread():获取当前正在执行的线程;
- sleep(long n):让当前执行的线程休眠n毫秒,休眠时让出cpu 的时间片给其它线程;
5 start()与run()
直接上测试案例演示:
- run()方法
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("执行完毕!");
}
结果:
- start()方法
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线程的名称,证明线程执行了。
6 sleep()与yield()
-
sleep()
- 调用sleep会让当前线程从 RUNNABLE (可运行状态) 进入TIMED_WAITING(停止状态);
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出InterruptedException;
- 睡眠结束后的线程未必会立刻得到执行;
- 建议用 Timelnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性;
-
yield()
- 调用 yield() 会让当前线程从 RUNNABLE (可运行状态) 进入TERMINATED(停止状态),然后调度器执行其它同优先级的线程。如果这时没有同优先级的线程,那么不能保证让当前线程暂停的效果;
- 具体的实现依赖于操作系统的任务调度器;
7 线程优先级
线程优先级会提示调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它。如果 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短时间占满!
取消注释后,就恢复了正常:
8 join()方法
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左右。
9 interrupt()方法
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();
}
}
结果:
10 使用到interrupt()的多线程设计模——两阶段终止模式
在一个线程 t1 中如何“优雅”终止线程 t2? 这里的优雅指的是给 t2 一个料理后事的机会。
错误思路:
- 使用线程对象的 stop() 方法停止线程;
- stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁。
- 使用 System.exit(int) 方法停止线程;
- 这种做法会让整个程序都停止。
11 过时不推荐的方法
以下这些方法不推荐使用,这些方法已过时,容易破坏同步代码块,造成线程死锁。
- stop() :停止线程运行
- suspend():挂起线程运行
- resume():恢复线程运行
12 主线程与守护线程
默认情况下,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("主线程,运行结束...");
}
}
结果:
没等守护线程执行完毕,主线程就已经结束了。
注意:
- 垃圾回收器线程就是一种守护线程。
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求。
13 线程状态-五种
网络上对于线程状态众说纷纭,有说5种的,有说6种的。现对于这两种不同的说法做出解释。
从操作系统层面-五种。
- 初始状态:仅是在语言层面创建了线程,还未与操作系统线程关联。
- 可运行状态:指该线程已经被创建,与操作系统线程关联,可以由CPU调度执行。
- 运行状态:指获取了cpu时间片运行中的状态,当cpu时间片用完,会从运行状态转移至可运行状态。
- 阻塞状态:如果运行中的线程调用了阻塞api,比如读写一些文件,这时该线程实际不会用到cpu,会导致进入阻塞状态;等读写完毕,操作系统会唤醒阻塞的线程,转移至可运行状态。
- 终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转移为其它状态。
14 线程状态-六种
根据 Thread.State 枚举,分为六种状态。
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
- NEW:线程刚被创建,但是还没有调用 start() 方法。
- RUNNABLE:当调用了 start() 方法之后进入运行状态,注意,JavaAPI层面的 RUNNABLE 状态涵盖了操作系统层面的[可运行状态]、[运行状态]和[阻塞状态] (由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)。
- BLOCKED:
- WAITING:
- TIMED_WAITING:
- TERMINATED:
三、共享模型之管程
1 线程安全问题
案例:两个线程对初始值为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 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
如果以单线程按顺序执行,不会出现任何问题,如果是多个线程,有可能会出现问题。
2 临界区与竞态条件
一个程序运行多个线程本身是没有问题的,问题出在多个线程访问共享资源。
- 多个线程读共享资源其实也没有问题;
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题;
临界区:一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为 临界区。
例如,下面代码中的临界区
static int counter = 0;
static void increment() {
// 临界区
counter ++;
}
static void decrement() {
// 临界区
counter --;
}
竞态条件:多个线程在临界区内执行,由于代码的执行顺序不同而导致结果无法预测,称为发生了竞态条件。
3 synchronized 解决方案
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
- 阻塞式的解决方案:synchronized、Lock
- 非阻塞式的解决方案:原子变量
本次使用阻塞式的解决方案: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 锁过后,得到解决:
思考:synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
为了加深理解,思考以下问题:
-
1、如果把 synchronized(obj) 放在 for 循环的外面,如何理解?
- 加了 synchronized 锁后,实际上是保证了以下4条指令的原子操作。
getstatic i // 获取静态变量i的值 iconst_1 // 准备常量1 iadd // 白增 putstatic i // 将修改后的值存入静态变量i
- 如果把 synchronized 加在for循环外面,也就保证了 4* 5000=20000条指令的原子操作,也就是完成5000次累加后再去完成累减。
-
2、如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会发生什么?
- 得不到解决,要想保证最终的结果,synchronized 必须传入同一个对象。
-
3、如果 t1 synchronized(obj) 而 t2 没有加锁发生什么? 如何理解?
- 也得不到解决,对于临界区的代码,多个线程都需要加锁。
4 对上述案例改造为面向对象方法编写
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;
}
}
5 练习题之线程八锁——帮助理解 synchronized
问题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。
问题3:在问题2的基础上,再加入方法c。
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.
问题8:
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。
6 变量的线程安全问题分析
-
1、成员变量和静态变量是否线程安全?
- 如果它们没有共享,则线程安全;
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况。如果只有读操作,则线程安全,如果有读写操作,则这段代码是临界区,需要考虑线程安全。
-
2、局部变量是否线程安全?
- 局部变量是线程安全的,但局部变量引用的对象则未必线程安全,如果该对象没有逃离方法的作用范围,它是线程安全的。如果该对象逃离方法的作用范围,需要考虑线程安全。
- 局部变量是线程安全的,但局部变量引用的对象则未必线程安全,如果该对象没有逃离方法的作用范围,它是线程安全的。如果该对象逃离方法的作用范围,需要考虑线程安全。
7 Monitor-对象头
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时间片。
8 常用方法介绍
-
obj.wait() 让进入 object 监视器的线程到 WaitSet 等待;
-
obj.wait(long timeout) 让进入 object 监视器的线程到 WaitSet 等待,时间为timeout;
-
obj.notify() 在 object 上正在 WaitSet 等待的线程中挑一个唤醒;
-
obj.notifyAll() 让 object上正在 WaitSet 等待的线程全部唤醒;
9 wait(long n) 和 sleep(long n) 的区别
- sleep 是Thread 方法,而 wait 是 Object 的方法;
- sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起使用;
- sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁;
10 ReentrantLock
相对于 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();
}
}
成功获取了3次锁。
二、可打断特性:
在获取锁时,如果长时间未获取到锁,可以进行打断,但是加锁方式要改变,不能再使用 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