1.进程与线程
1.1 基本概念
1.1.1 进程
进程可以理解为是程序的一个运行示例。一个程序由指令和数据构成,这些指令要运行,数据要读写,就必须将指令加载到CPU,数据加载到内存。在指令运行过程中需要用到磁盘,网络等设备,进程就是用来加载指令,管理内存,管理IO的。 当一个程序运行时,会被从磁盘加载到内存中。这时就开启了一个进程。大部分程序可以同时运行多个进程示例,也有程序只能启动一个进程示例。
1.1.2 线程
线程是操作系统最小的调度单位,一个进程之内可以分为一到多个线程。一个线程就是一个指令流,这个指令流中的一条条指令会以一定的顺序交给CPU执行。进程是操作系统作为资源分配的最小单位,进程是不活动的,只是作为线程的容器。
1.1.3 进程与线程对比
1.进程之间基本上是相互独立的,而线程存在于进程之内,是进程的一个子集
2.进程拥有共享的资源(如内存空间),共其内部的线程共享
3.进程间的通信较为复杂,同一台计算机之间的进程通信称为IPC,不同计算机之间的进程通信需要通过网络协议通信。
4.线程通信相对简单,线程之间共享进程内的内存(如多线程可以访问同一个共享变量)
5.线程更加轻量,线程的上下文切换成本一般比进程的上下文切换低。
1.2 并行与并发
在单核CPU下,线程实际还是串行执行的,而操作系统中有一个组件叫任务调度器,任务调度器将CPU时间片分给不同程序使用,由于CPU时间片非常短,CPU在线程间切换的速度非常快,此时我们感觉多线程之间是同时运行的。这种多线程轮流使用CPU的做法称为并发。
编辑
编辑
多核CPU下,每个核心都可以调度运行线程,这时候称之为线程是可以并行的。
编辑 编辑
引用Rob Pike的一段叙述:
并发是同一时间应对多件事的能力。并行事同一时间动手做多件事的能力。
1.3 应用
1.3.1 异步调用
从调用方式上来讲,需要等待结果返回才能继续运行的方式就是同步,不需要等待结果返回就能继续运行的方式就是异步。
多线程可以让方法变为异步的,比如读取磁盘文件时,假设读取花费了5秒钟,如果没有线程调度机制,这5秒CPU什么也做不了,后续的业务逻辑也无法执行。
再比如再项目中,视频文件需要进行转换格式等费时操作时,这时候开一个新线程处理视频转换,就能避免主线程阻塞。
再比如TomCat中的servlet也是如此,让用户线程来处理耗时的操作,避免阻塞tomcat工作线程。
1.3.2 多核CPU的多线程应用
单核CPU下,多线程并不能提高实际程序的运行效率,只是为了再不同任务之间切换,做到不同线程轮流使用CPU,不至于让一个线程一直占有CPU。多核CPU可以并行跑多个线程,但能否提高程序运行效率还是要看具体情况的。有些任务经过进行设计拆分可以提高运行效率,但并非所有任务都需要拆分,任务的目的不同,谈拆分和效率将毫无意义。IO操作不占用CPU,一般我们使用的是阻塞IO,此时线程虽然不用CPU,但需要一直等待IO结束,这个过程并没有重复利用线程。
2.Java线程
2.1 Java创建运行线程
2.1.1 直接使用Thread类
// 创建线程对象 Thread t = new Thread() { public void run() { // 要执行的任务 } }; // 启动线程 t.start();
Thread t1 = new Thread("t1") { @Override //run 方法内实现了要执行的任务 public void run() { log.debug("hello"); } }; t1.start();
2.1.2 使用Runnable接口配合Thread类
Runnable runnable = new Runnable() { public void run(){ // 要执行的任务 } }; // 创建线程对象 Thread t = new Thread( runnable ); // 启动线程 t.start();
// 创建任务对象 Runnable task2 = new Runnable() { @Override public void run() { log.debug("hello"); } }; // 参数1 是任务对象; 参数2 是线程名字,推荐 Thread t2 = new Thread(task2, "t2"); t2.start();
lambda表达式精简代码:
// 创建任务对象 Runnable task2 = () -> log.debug("hello"); // 参数1 是任务对象; 参数2 是线程名字,推荐 Thread t2 = new Thread(task2, "t2"); t2.start();
使用Runnable更容易与线程池等高级API配合,并且让任务脱离了Thread继承体系,更加灵活。
2.1.3 FutureTask配合Thread
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
// 创建任务对象 FutureTask<Integer> task3 = new FutureTask<>(() -> { log.debug("hello"); return 100; }); // 参数1 是任务对象; 参数2 是线程名字,推荐 new Thread(task3, "t3").start(); // 主线程阻塞,同步等待 task 执行完毕的结果 Integer result = task3.get(); log.debug("结果是:{}", result);
多线程之间交替执行,谁先谁后无法控制。
2.2 查看进程线程的方法
windows:
任务管理器可以查看进程和线程数,也可以用来杀死进程
tasklist 查看进程
taskkill 杀死进程
Linux:
ps -fe 查看所有进程
ps -fT -p <PID> 查看某个进程(PID)的所有线程
kill 杀死进程
top 按大写 H 切换是否显示线程
top -H -p <PID> 查看某个进程(PID)的所有线程
Java:
jps 命令查看所有 Java 进程
jstack <PID> 查看某个 Java 进程(PID)的所有线程状态
jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)
2.3 线程运行
2.3.1 栈与栈帧
JVM由堆,栈,方法区构成,其中栈内存就是给线程使用的,每个线程启动后,虚拟机会为其分配一块栈内存。每个栈由多个栈帧构成,每个栈帧对应着每次方法调用时所占的内存。每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
2.3.2 线程上下文切换
因为下面列举的一些原因导致CPU不再执行当前线程,转而执行另一个线程的代码:
线程的CPU时间片用完,垃圾回收,有更高优先级的线程需要运行,线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法。
当上下文切换发生时,需要操作系统保存当前线程的状态,并恢复另一个线程的状态,状态信息包括程序计数器,每个栈帧的信息,如局部变量,操作数帧,返回地址等。Java中对应的概念就是程序计数器,它的作用是记住下一条JVM指令的执行地址,是线程私有的。频繁的上下文切换会影响性能。
2.4 Java中线程操作常用方法
编辑
编辑
2.4.1 start与run
@Slf4j(topic = "c.TestStart") public class TestStart { public static void main(String[] args) { Thread t1 = new Thread("t1") { @Override public void run() { log.debug(Thread.currentThread().getName()); FileReader.read(Constants.MP4_FULL_PATH); } }; t1.run(); log.debug("do other things ..."); } }
程序仍在 main 线程运行, FileReader.read() 方法调用还是同步的
@Slf4j(topic = "c.TestStart") public class TestStart { public static void main(String[] args) { Thread t1 = new Thread("t1") { @Override public void run() { log.debug(Thread.currentThread().getName()); FileReader.read(Constants.MP4_FULL_PATH); } }; t1.start(); log.debug("do other things ..."); } }
程序在 t1 线程运行, FileReader.read() 方法调用是异步的
直接调用 run 是在主线程中执行了 run,没有启动新的线程 ,使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码。
2.4.2 sleep与yield
Sleep:
1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出
InterruptedException
3. 睡眠结束后的线程未必会立刻得到执行
4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
yield:
1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
2. 具体的实现依赖于操作系统的任务调度器
线程优先级:
线程优先级会提示调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它,如果CPU比较忙,那么优先级高的线程会获取更多时间片,但CPU空闲时,优先级几乎没作用。
@Slf4j(topic = "c.TestYield") public class TestYield { public static void main(String[] args) { 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.setPriority(Thread.MIN_PRIORITY); t2.setPriority(Thread.MAX_PRIORITY); t1.start(); t2.start(); } }
2.4.3 join方法
让当前目标线程执行完业务逻辑后再执行其他线程的业务逻辑。
private static void test1() throws InterruptedException { log.debug("开始"); Thread t1 = new Thread(() -> { log.debug("开始"); sleep(1); log.debug("结束"); r = 10; }); t1.start(); log.debug("结果为:{}", r); log.debug("结束"); }
上面代码执行,因为主线程和t1线程是并行执行的,t1线程需要1秒计算之后才能算出r=10,而主线程一开始就要打印r的结果,所以只能打印出r=0,此时我们可以在start方法之后引入join方法,等待线程t1运行结束。
t1.start(); t1.join(); log.debug("结果为:{}", r); log.debug("结束");
问:下面代码cost大约需要多少秒:
private static void test2() throws InterruptedException { Thread t1 = new Thread(() -> { sleep(1); r1 = 10; }); Thread t2 = new Thread(() -> { sleep(2); r2 = 20; }); t1.start(); t2.start(); long start = System.currentTimeMillis(); t1.start(); t2.start(); t1.join(); t2.join(); long end = System.currentTimeMillis(); log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start); }
第一个join:等待t1时,t2并没有停止,而是正在运行
第二个join:1s后,执行到此,t2也运行了1s,因此也只需要再等待1s。
颠倒两个join最终的结果也是一样的
20:45:43.239 [main] c.TestJoin - r1: 10 r2: 20 cost: 2005
编辑
有时效的join
等够时间:
static int r1 = 0; static int r2 = 0; public static void main(String[] args) throws InterruptedException { test3(); } public static void test3() throws InterruptedException { Thread t1 = new Thread(() -> { sleep(1); r1 = 10; }); long start = System.currentTimeMillis(); t1.start(); // 线程执行结束会导致 join 结束 t1.join(1500); long end = System.currentTimeMillis(); log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start); }
输出
20:48:01.320 [main] c.TestJoin - r1: 10 r2: 0 cost: 1010
没等够时间:
static int r1 = 0; static int r2 = 0; public static void main(String[] args) throws InterruptedException { test3(); } public static void test3() throws InterruptedException { Thread t1 = new Thread(() -> { sleep(2); r1 = 10; }); long start = System.currentTimeMillis(); t1.start(); // 线程执行结束会导致 join 结束 t1.join(1500); long end = System.currentTimeMillis(); log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start); }
输出
20:52:15.623 [main] c.TestJoin - r1: 0 r2: 0 cost: 1502
2.4.4 interrupt方法
打断 sleep,wait,join 的线程
这几个方法都会让线程进入阻塞状态,打断 sleep 的线程, 会清空打断状态,以 sleep 为例
t1线程原本计划休眠0.5秒后继续执行,但被打断后会被强制唤醒并因捕捉异常而终止,不会正常运行后续逻辑
private static void test1() throws InterruptedException { Thread t1 = new Thread(()->{ sleep(1); }, "t1"); t1.start(); sleep(0.5); t1.interrupt(); log.debug(" 打断状态: {}", t1.isInterrupted()); }
21:18:10.374 [main] c.TestInterrupt - 打断状态: false
打断正常运行的线程
打断正常运行的线程, 不会清空打断状态
private static void test2() throws InterruptedException { Thread t2 = new Thread(()->{ while(true) { Thread current = Thread.currentThread(); boolean interrupted = current.isInterrupted(); if(interrupted) { log.debug(" 打断状态: {}", interrupted); break; } } }, "t2"); t2.start(); sleep(0.5); t2.interrupt(); }
20:57:37.964 [t2] c.TestInterrupt - 打断状态: true
原理解释:Java线程中断不是强制杀死线程,而是通过设置中断标志给目标线程发送了一个中断信号。它的核心作用是传递 “你可以停止工作了” 的通知,最终线程是否停止、何时停止,完全由线程自身决定。
打断park线程
打断 park 线程, 不会清空打断状态
private static void test3() { Thread t1 = new Thread(() -> { log.debug("park..."); LockSupport.park(); log.debug("unpark..."); log.debug("打断状态:{}", Thread.currentThread().isInterrupted()); }, "t1"); t1.start(); sleep(0.5); t1.interrupt(); }
输出
21:11:52.795 [t1] c.TestInterrupt - park... 21:11:53.295 [t1] c.TestInterrupt - unpark... 21:11:53.295 [t1] c.TestInterrupt - 打断状态:true
如果打断标记已经是 true, 则 park 会失效
private static void test4() { Thread t1 = new Thread(() -> { for (int i = 0; i < 5; i++) { log.debug("park..."); LockSupport.park(); log.debug("打断状态:{}", Thread.interrupted()); } }); t1.start(); sleep(1); t1.interrupt(); }
输出
21:13:48.783 [Thread-0] c.TestInterrupt - park... 21:13:49.809 [Thread-0] c.TestInterrupt - 打断状态:true 21:13:49.812 [Thread-0] c.TestInterrupt - park... 21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true 21:13:49.813 [Thread-0] c.TestInterrupt - park... 21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true 21:13:49.813 [Thread-0] c.TestInterrupt - park... 21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true 21:13:49.813 [Thread-0] c.TestInterrupt - park... 21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true
2.4.5 不推荐的方法
编辑
2.5 主线程与守护线程
默认情况下,Java进程需要等待所有线程都运行结束才会结束,有一种特殊的线程叫做守护线程,只要其他非守护线程运行结束了,及时守护线程代码没执行完,也会强制结束。
@Slf4j(topic = "c.TestDaemon") public class TestDaemon { public static void main(String[] args) { log.debug("开始运行..."); Thread t1 = new Thread(() -> { log.debug("开始运行..."); sleep(2); log.debug("运行结束..."); }, "daemon"); // 设置该线程为守护线程 t1.setDaemon(true); t1.start(); sleep(1); log.debug("运行结束..."); } }
输出
08:26:38.123 [main] c.TestDaemon - 开始运行... 08:26:38.213 [daemon] c.TestDaemon - 开始运行... 08:26:39.215 [main] c.TestDaemon - 运行结束..
垃圾回收器线程就是一种守护线程。TomCat中的Acceptor和Poller线程都是守护线程,所以TomCat收到shutdown命令后不会等待它们处理完当前请求。
2.6 线程五状态模型
从操作系统的层面来描述:
编辑
初始状态:在语言层面创建了进程,还未与操作系统关联
就绪状态(可运行状态):该线程已经被创建(与操作系统关联),可以由CPU调度执行
运行状态:获取了CPU时间片中的状态,当CPU时间片用完时,会从运行状态转为就绪状态,会导致线程上下文的切换。
阻塞状态:如果调用了阻塞API,如BIO读写文件,这时线程不会用到CPU,会导致线程上下文切换,进入阻塞状态,等BIO等IO操作结束后,会由操作系统唤醒阻塞的线程,转为就绪状态。
终止状态:线程执行完毕,生命周期已经结束,不会转为其他状态。
2.7 Java层面的线程六状态
编辑
New:线程刚被创建,还没有调用start方法
Runnable:调用了start方法之后,此阶段涵盖了操作系统层面的就绪状态,运行状态,阻塞状态
Blocked,Waiting,Timed_waiting:是Java API对阻塞状态的细分
Terminated:线程代码运行结束。
3.共享模型之管程
3.1问题引入
两个线程对初始值为0的静态变量一个做自增,一个做自减,各做5000次,结果为0吗?
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 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
编辑
临界区:
多个线程读共享资源是没问题的,但如果在多个线程对共享资源读写操作时发生指令交错,那就会出现问题。一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区,操作系统定义临界区为同一时间只能有一个线程访问的资源区域。
例如,下面代码中的临界区:
static int counter = 0; static void increment() // 临界区 { counter++; } static void decrement() // 临界区 { counter--; }
多个线程在临界区内执行时,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。
3.2 synchronized
为了避免临界区发生竞态条件,有多种手段可以达到目的。
阻塞式解决方案:synchronized,Lock
非阻塞式方案:原子变量
synchronized,俗称对象锁,采用互斥的方式让同一时刻最多只有一个线程持有对象锁,其他线程再想获取这个对象锁就会被阻塞住,这样就可以保证拥有锁的线程可以安全执行临界区的代码,不用担心中途发生线程上下文的切换。
注意
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
3.2.1 基础语法
synchronized(对象) // 线程1, 线程2(blocked) { 临界区 }
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); }
你可以做这样的类比:
synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人进行计算,线程 t1,t2 想象成两个人,当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行count++ 代码,这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了。
这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦), 这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入 ,当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码。
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
面向对象改进:
class Room { int value = 0; public void increment() { synchronized (this) { value++; } } public void decrement() { synchronized (this) { value--; } } public int get() { synchronized (this) { return value; } } } @Slf4j public class Test1 { 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()); } }
3.2.2 方法上的sychronied
class Test{ public synchronized void test() { } } 等价于 class Test{ public void test() { synchronized(this) { } } }
class Test{ public synchronized static void test() { } } 等价于 class Test{ public static void test() { synchronized(Test.class) { } } }
不加 synchronized 的方法
不加 synchronzied 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)
3.2.3 线程8锁
考察先锁住哪个对象
情况1:12 或 21
@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后12,或 2 1s后 1
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
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
@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(); }
3.3 变量的线程安全分析
3.1.1 引入
成员变量和静态变量是否线程安全?
如果他们没有共享那么就线程安全。如果他们被共享了,根据他们状态是否能够改变,又分为两种情况:如果只有读操作,那线程安全,如果有读写操作,则这段代码式临界区,需要考虑线程安全问题。
局部变量是否线程安全?
局部变量是线程安全的。但局部变量引用的对象未必,如果该对象没有逃离方法的作用范围,他是线程安全的,如果该对象逃离方法的作用范围,则需要考虑线程安全问题。
局部变量的线程安全性分析
public static void test1() { int i = 10; i++; }
每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享
public static void test1(); descriptor: () Vflags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=0 0: bipush 10 2: istore_0 3: iinc 0, 1 6: return LineNumberTable: line 10: 0 line 11: 3 line 12: 6 LocalVariableTable: Start Length Slot Name Signature 3 4 0 i I
编辑
局部变量的引用稍有不同
一个成员变量的示例:
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 还未 add,线程1 remove 就会报错:
Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 at java.util.ArrayList.rangeCheck(ArrayList.java:657) at java.util.ArrayList.remove(ArrayList.java:496) at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35) at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26) at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14) at java.lang.Thread.run(Thread.java:748)
分析:
无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量 ,method3 与 method2 分析相同。
编辑
将 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); } }
此时list是局部变量,每个线程调用时会创建其不同实例,没有共享,而method2的参数是从method1中传递过来的,与method1中引用同一个对象,method3的参数分析与method2相同。
编辑
方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?
情况1:有其它线程调用 method2 和 method3
情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法
class ThreadSafe { public final void method1(int loopNumber) { ArrayList<String> list = new ArrayList<>(); for (int i = 0; i < loopNumber; i++) { method2(list); method3(list); } } public void method2(ArrayList<String> list) { list.add("1"); } private void method3(ArrayList<String> list) { System.out.println(1); list.remove(0); } } class ThreadSafeSubClass extends ThreadSafe{ // @Override public void method3(ArrayList<String> list) { System.out.println(2); new Thread(() -> { list.remove(0); }).start(); } }
- 若将
method2和method3改为public:
- 情况 1(外部线程调用):可能导致
list被多线程共享,引入线程安全问题。 - 情况 2(子类覆盖):子类可通过重写进一步破坏线程安全,风险更高。
原代码的线程安全依赖于 “list 仅被当前线程访问”,而 private 修饰符是保证这一前提的重要手段。扩大方法的访问权限(如改为 public)会破坏这种封装性,增加线程安全风险。
3.1.2 常见线程安全的类
String
Integer
StringBuffer
Random
Vector
Hashtable
java.util.concurrent 包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为它们的每个方法是原子的,但注意它们多个方法的组合不是原子的。
线程安全类方法的组合
分析下面代码是否线程安全?
Hashtable table = new Hashtable(); // 线程1,线程2 if( table.get("key") == null) { table.put("key", value); }
编辑
这段代码不是线程安全的,即使 Hashtable 本身的 get() 和 put() 方法是线程安全的(加了 synchronized 锁),但组合起来的 “判断 - 修改” 操作是非原子的,可能导致线程安全问题。
不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
,或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?
public class Immutable{ private int value = 0; public Immutable(int value){ this.value = value; } public int getValue(){ return this.value; } }
- 不可变性:
value被private修饰,且没有任何修改它的方法(无setter),一旦创建,value无法改变。 - 线程安全:多线程调用
getValue()时,读取的都是初始值,不存在冲突,因此线程安全。
如果想增加一个增加的方法呢?
public class Immutable{ private int value = 0; public Immutable(int value){ this.value = value; } public int getValue(){ return this.value; } public Immutable add(int v){ return new Immutable(this.value + v); } }
- 不可变性:
add方法不会修改当前对象的value,而是返回一个新的Immutable对象(包含计算后的值)。原对象始终保持初始状态。 - 线程安全:
- 多线程调用
add时,各自基于原对象的value创建新对象,互不干扰; - 即使多个线程持有同一个
Immutable对象,也只能读取value,无法修改,因此安全。
总结:
- 不可变类的线程安全核心:对象状态创建后不可修改,多线程访问时只有读操作,无写操作,因此无冲突。
- “修改” 不可变对象的本质:如
String.replace或自定义的add方法,都是通过创建新对象实现 “逻辑上的修改”,原对象不变,因此仍安全。 - 复合操作的线程安全:需通过加锁等机制保证原子性,与不可变类的安全机制不同(不可变类无需处理修改,因此天然安全)。
3.1.3 线程安全性实例分析:
例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) { // 使用上述变量 } }
Servlet 是单实例多线程的 ——Web 容器会创建一个 MyServlet 实例,所有请求都由这个实例的 doGet 方法处理(即多线程共享同一个实例变量)。线程安全与否,取决于 “变量是否被多线程共享” 且 “是否存在修改操作”。
| 变量 | 线程安全? | 核心原因 |
map |
❌ 不安全 | 共享可变对象 HashMap本身是线程不安全的(无同步机制),并发修改会出错。 |
S1 |
✅ 安全 | 共享不可变类 String,仅读无改。 |
S2 |
✅ 安全 | final 限制变量不换指向,String 不可变,双重保障。 |
D1 |
❌ 不安全 | 共享可变对象 Date,可修改内部时间,并发访问会出错。 |
D2 |
❌ 不安全 | final 仅限制变量不换指向,但 Date 内部可修改,共享时仍有风险。 |
例2:
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++; } }
这个案例中,MyServlet 和 UserServiceImpl 的组合是线程不安全的,核心问题出在 UserServiceImpl 中的实例变量 count 被多线程共享且存在修改操作。count 是共享的可变状态,无同步机制保护共享修改。
例3:
@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)); } }
这个 MyAspect 切面类在多线程环境下是线程不安全的,核心问题出在实例变量 start 被多线程共享且存在读写冲突。
例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) { // ... } } }
是线程安全的,因为:不存在多线程共享的可变状态,每个线程的操作都是独立的
- 所有实例变量都是无状态的(无修改操作),或引用不可变。
- 方法内部仅使用局部变量(线程私有),不存在多线程共享的可变状态。
这种 “无状态组件”(如 UserServiceImpl、UserDaoImpl 无实例变量修改)是多线程环境下的理想设计,天然具备线程安全性。
例5:
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(); } }
UserDaoImpl 中的 conn 实例变量被多线程共享且存在修改操作,会导致数据库连接被错误覆盖、重复关闭或状态混乱,因此整个调用链线程不安全。每个线程的数据库连接应该是线程私有的(局部变量),但此处被所有线程共享,且存在并发赋值和状态修改,必然导致资源竞争和操作混乱。
如果要变为线程安全,需要将 conn 改为局部变量,确保每个线程操作自己的连接,互不干扰。
例6:
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 { // 是否安全 private Connection = null; public void update() throws SQLException { String sql = "update user set password = ? where username = ?"; conn = DriverManager.getConnection("","",""); // ... conn.close(); } }
UserServiceImpl 不再将 userDao 作为实例变量,而是在 update() 方法中每次创建新的 UserDaoImpl 实例,这意味着:每个线程调用 userService.update() 时,都会得到一个独立的 UserDaoImpl 对象(线程私有),避免了多个线程共享同一个 UserDaoImpl 实例。虽然 UserDaoImpl 变为线程私有,避免了多线程共享 conn 的问题,但 conn 作为实例变量仍可能导致。
例7:
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(); } }
其中 foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法
public void foo(SimpleDateFormat sdf) { String dateStr = "1999-10-11 00:00:00"; for (int i = 0; i < 20; i++) { new Thread(() -> { try { sdf.parse(dateStr); } catch (ParseException e) { e.printStackTrace(); } }).start(); } }
sdf原本是bar方法的局部变量(每个bar调用创建一个独立实例),但foo方法启动了 20 个线程并发调用sdf.parse(),导致sdf被多线程共享。- 由于
SimpleDateFormat内部有可变状态(如Calendar实例),多线程并发parse会导致状态混乱,出现ParseException或解析出错误的日期(例如年份变成负数、月份错乱)。
例8:
private static Integer i = 0; public static void main(String[] args) throws InterruptedException { List<Thread> list = new ArrayList<>(); for (int j = 0; j < 2; j++) { Thread thread = new Thread(() -> { for (int k = 0; k < 5000; k++) { synchronized (i) { i++; } } }, "" + j); list.add(thread); } list.stream().forEach(t -> t.start()); list.stream().forEach(t -> { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); log.debug("{}", i); }
synchronized 的锁对象必须是引用不变的对象(如 final 修饰的对象)。若锁对象的引用会变化(如本例中的 i),则会导致锁失效,无法保证同步效果。这是使用 synchronized 时的常见陷阱,尤其需要注意 “锁对象是否可变”。
3.4 两个线程安全性实例
3.4.1 卖票练习
import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.Vector; @Slf4j(topic = "c.ExerciseSell") public class ExerciseSell { public static void main(String[] args) throws InterruptedException { // 模拟多人买票 TicketWindow window = new TicketWindow(1000); // 所有线程的集合 List<Thread> threadList = new ArrayList<>(); // 卖出的票数统计 List<Integer> amountList = new Vector<>(); for (int i = 0; i < 2000; i++) { Thread thread = new Thread(() -> { // 买票 int amount = window.sell(random(5)); // 统计买票数 amountList.add(amount); }); threadList.add(thread); thread.start(); } for (Thread thread : threadList) { thread.join(); } // 统计卖出的票数和剩余票数 log.debug("余票:{}",window.getCount()); log.debug("卖出的票数:{}", amountList.stream().mapToInt(i-> i).sum()); } // Random 为线程安全 static Random random = new Random(); // 随机 1~5 public static int random(int amount) { return random.nextInt(amount) + 1; } } // 售票窗口 class TicketWindow { private int count; public TicketWindow(int count) { this.count = count; } // 获取余票数量 public int getCount() { return count; } // 售票 public synchronized int sell(int amount) { if (this.count >= amount) { this.count -= amount; return amount; } else { return 0; } } }
3.4.2 转账练习
import lombok.extern.slf4j.Slf4j; import java.util.Random; @Slf4j(topic = "c.ExerciseTransfer") public class ExerciseTransfer { public static void main(String[] args) throws InterruptedException { Account a = new Account(1000); Account b = new Account(1000); Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { a.transfer(b, randomAmount()); } }, "t1"); Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { b.transfer(a, randomAmount()); } }, "t2"); t1.start(); t2.start(); t1.join(); t2.join(); // 查看转账2000次后的总金额 log.debug("total:{}", (a.getMoney() + b.getMoney())); } // Random 为线程安全 static Random random = new Random(); // 随机 1~100 public static int randomAmount() { return random.nextInt(100) + 1; } } // 账户 class Account { private int money; public Account(int money) { this.money = money; } public int getMoney() { return money; } public void setMoney(int money) { this.money = money; } // 转账 public void transfer(Account target, int amount) { synchronized(Account.class) { if (this.money >= amount) { this.setMoney(this.getMoney() - amount); target.setMoney(target.getMoney() + amount); } } } }
3.5 Monitor与Sychronized原理
3.5.1 Java对象头
以 32 位虚拟机为例
普通对象
|--------------------------------------------------------------| | Object Header (64 bits) | |------------------------------------|-------------------------| | Mark Word (32 bits) | Klass Word (32 bits) | |------------------------------------|-------------------------|
数组对象
|---------------------------------------------------------------------------------| | Object Header (96 bits) | |--------------------------------|-----------------------|------------------------| | Mark Word(32bits) | Klass Word(32bits) | array length(32bits) | |--------------------------------|-----------------------|------------------------|
其中 Mark Word 结构为
|-------------------------------------------------------|--------------------| | Mark Word (32 bits) | State | |-------------------------------------------------------|--------------------| | hashcode:25 | age:4 | biased_lock:0 | 01 | Normal | |-------------------------------------------------------|--------------------| | thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased | |-------------------------------------------------------|--------------------| | ptr_to_lock_record:30 | 00 | Lightweight Locked | |-------------------------------------------------------|--------------------| | ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked | |-------------------------------------------------------|--------------------| | | 11 | Marked for GC | |-------------------------------------------------------|--------------------|
64 位虚拟机 Mark Word
|--------------------------------------------------------------------|--------------------| | Mark Word (64 bits) | State | |--------------------------------------------------------------------|--------------------| | unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal | |--------------------------------------------------------------------|--------------------| | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased | |--------------------------------------------------------------------|--------------------| | ptr_to_lock_record:62 | 00 | Lightweight Locked | |--------------------------------------------------------------------|--------------------| | ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked | |--------------------------------------------------------------------|--------------------| | | 11 | Marked for GC | |--------------------------------------------------------------------|--------------------|
3.5.2 Monitor原理
Monitor称为监视器或管程
每个Java对象都可以关联一个Monitor对象,线程获取synchronized锁需要使用锁对象关联monitor,该对象头的Mark Word中就被设置指向Monitor对象的指针 编辑
monitor有三个属性,owner,entrylist,waitset,其中owner关联获取锁的线程,entrylist关联处于阻塞状态的线程,waitset关联处于waiting状态的线程。
线程获取锁对象后会判断owner是否为空,如果为空,则和owner关联,否则,线程进入entrylist进入阻塞状态,调用wait方法的线程进入waitset。
刚开始Monitor中的Owner为空。当Thread2执行syschronized(obj)时,就会将Monitor所有者Owner置为Thread2,Monitor中只能有一个Owner。在Thread2上锁的过程中,如果其他线程也来执行syschronized(obj),就会进入EntryList BLOCKED。在Thread2执行完同步代码块的内容,然后唤醒EntryList中阻塞的线程。
3.5.3 synchronized原理
Monitor实现的锁属于重量级锁,加锁操作依赖于操作系统级别的mutex指令,涉及到用户态和内核态之间的切换,成本开销较大。在JDK1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决没有多线程竞争场景下传统锁机制带来的性能开销问题。偏向锁对应只有一个线程持有锁的情况,轻量级锁对应不同线程交替持有锁的情况,重量级锁对应多线程竞争锁的情况。锁对象对象头的MarkWord中的lock字段表示表明了是哪种类型的锁。
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁后(重量级锁),该对象头的MarkWord中就被设置了指向Monitor对象的指针。
很多时候,在Java程序运行时,同步块中的代码都是不存在竞争的,不同线程交替指向同步块中的代码,这时候,重量级锁是没有必要的,因此引入了JVM轻量级锁的概念。
轻量级锁加锁解锁流程:
同步代码块中的代码不存在竞争且不同线程交替指向同步块中的代码时,JVM引入轻量级锁的概念。加锁过程如下:
每个线程获取锁时,都会在自己的线程栈中创建一个锁记录(LockRecord)并将其obj字段指向锁对象。然后通过CAS操作将对象头中的MarkWord替换为指向LockRecord地址的指针,同时把原来锁对象MarkWord保存到LockRecord中,完成数据交换。如果CAS成功完成交换数据,则表明此时线程持有了这个对象锁,如果CAS失败,有两种情况:如果此时有多线程竞争,则直接升级为重量级锁,如果当前还是同一个线程持有锁,发生了锁重入,则在当前线程栈中再添加一条LockRecord,
此时线程栈中LockRecord的数量就是锁的重入次数,每新加入一个锁记录,也都要执行一个obj指向锁对象和CAS的两步操作。有重入的锁记录其所锁记录地址为null。
解锁:遍历线程栈,找到所有obj字段等于锁对象的LockRecord,如果锁记录的MarkWord为null,则这个是重入锁的所记录,则清除这个锁记录,如果锁记录的MarkWord不为null,则用CAS指令将对象头中的MarkWord恢复为无锁状态(重写交换一次数据),如果失效则膨胀为重量级锁。
编辑
偏向锁加锁解锁流程:
没有锁竞争,只有一个线程自己持有锁时采用偏向锁。加锁流程如下:
首先线程获取锁时会在自己的线程栈中创建一个LockRecord记录,并将其obj字段指向锁对象。然后通过CAS指令将LockRecord的线程id存到锁对象对象头的MarkWord中,同时设置偏向锁的标识为001,如果成功完成CAS则获取锁成功。如果出现锁重入,则再次创建一个WordRecord,同样将obj字段指向对象锁,然后此时不再执行CAS操作了,直接判断对象头中的线程id是否是自己的即可。一旦锁发生了竞争,就会升级为重量级锁。
3.5.4 wait/notify
obj.wait() 让进入 object 监视器的线程到 waitSet 等待
obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
编辑
3.5.5 Park/Unpark
它们是 LockSupport 类中的方法
// 暂停当前线程 LockSupport.park(); // 恢复某个线程的运行 LockSupport.unpark(暂停线程对象)
与 Object 的 wait & notify 相比:
wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】
park & unpark 可以先 unpark,而 wait & notify 不能先 notify
3.6 重新理解线程状态转化
1.new->runnable:
线程t调用t.start()方法时。
2.Runnable->Waiting:
t 线程用 synchronized(obj) 获取了对象锁后,调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING。调用 obj.notify() , obj.notifyAll() , t.interrupt() 时,竞争锁成功,t 线程从 WAITING --> RUNNABLE,竞争锁失败,t 线程从 WAITING --> BLOCKED。
3.Runnable->Waiting:
当前线程调用t.join()时,当前线程从Runnable->Waiting,当前线程t运行结束或者调用了当前线程的interrupt()时,线程从Waiting->Runnable。
4.Runnable->Waiting:
当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING。
调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING -->
RUNNABLE。
5.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。
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) ,当前线程从 RUNNABLE --> 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。
3.7 死锁
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁。
t1 线程 获得A对象锁,接下来想获取B对象锁, t2 线程 获得 B对象锁,接下来想获取A对象锁。
这个时候t1线程和t2线程都在互相等待对方的锁,就产生了死锁。
package com.itheima.basic; import static java.lang.Thread.sleep; public class Deadlock { public static void main(String[] args) { Object A = new Object(); Object B = new Object(); Thread t1 = new Thread(() -> { synchronized (A) { System.out.println("lock A"); try { sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (B) { System.out.println("lock B"); System.out.println("操作..."); } } }, "t1"); Thread t2 = new Thread(() -> { synchronized (B) { System.out.println("lock B"); try { sleep(500); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (A) { System.out.println("lock A"); System.out.println("操作..."); } } }, "t2"); t1.start(); t2.start(); } }
定位死锁:监测死锁用jps定位进程id,再用jstack定位死锁。也可以使用jconsole工具或VisualVM等故障处理工具。
避免死锁要注意加锁顺序,另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查。
3.8 活锁
两个线程互相改变对方的结束条件,最后谁也无法结束。
public class TestLiveLock { static volatile int count = 10; static final Object lock = new Object(); public static void main(String[] args) { new Thread(() -> { // 期望减到 0 退出循环 while (count > 0) { sleep(0.2); count--; log.debug("count: {}", count); } }, "t1").start(); new Thread(() -> { // 期望超过 20 退出循环 while (count < 20) { sleep(0.2); count++; log.debug("count: {}", count); } }, "t2").start(); } }
3.9 饥饿
一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不
易演示,讲读写锁时会涉及饥饿问题。
下面我讲一下我遇到的一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题
编辑
编辑
3.10 AQS
全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,它是构建锁或者其他同步组件的基础框架。
AQS常见的实现类
- ReentrantLock 阻塞式锁
- Semaphore 信号量
- CountDownLatch 倒计时锁
在AQS中维护了一个使用了volatile修饰的state属性来表示资源的状态,0表示无锁,1表示有锁。
线程0来了后,去尝试修改state属性,如果state属性为0,就修改其为1,标识线程0抢锁成功,线程1和2也会尝试修改state属性,发现此时state=1,有其他线程持有锁了,此时线程1和2会进入FIFO队列中等待,FIFO是一个双向队列,同时维护了标识头节点和尾节点的两个指针。在去修改state状态的时候,使用的cas自旋锁来保证原子性,确保只能有一个线程修改成功,修改失败的线程将会进入FIFO队列中等待
编辑
公平锁与非公平锁:
新的线程与队列的等待线程共同抢资源,是非公平锁。新的线程到队列中等待,只让队列中的head线程获取锁,满足先来先得,是公平锁。
比较典型的AQS实现类ReentrantLock,它默认就是非公平锁,新的线程与队列中的线程共同来抢资源。
3.11 ReentrantLock
ReentrantLock翻译过来是可重入锁,相对于synchronized它具备以下特点:
- 可中断
- 可以设置超时时间
- 可以设置公平锁
- 支持多个条件变量
- 与synchronized一样,都支持重入
// 获取锁 reentrantLock.lock(); try { // 临界区 } finally { // 释放锁 reentrantLock.unlock(); }
ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似
构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。查看ReentrantLock源码中的构造方法:
编辑
提供了两个构造方法,不带参数的默认为非公平
如果使用带参数的构造函数,并且传的值为true,则是公平锁
其中NonfairSync和FairSync这两个类父类都是Sync
而Sync的父类是AQS,所以可以得出ReentrantLock底层主要实现就是基于AQS来实现的。
实现思路:
编辑
- 线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功
- 假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部
- 当exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程
- 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁
3.12 synchronized和Lock有什么区别 ?
第一,语法层面
- synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现,退出同步代码块锁会自动释放
- Lock 是接口,源码由 jdk 提供,用 java 语言实现,需要手动调用 unlock 方法释放锁
第二,功能层面
- 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
- Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量,同时Lock 可以实现不同的场景,如 ReentrantLock, ReentrantReadWriteLock
第三,性能层面
- 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
- 在竞争激烈时,Lock 的实现通常会提供更好的性能
统合来看,需要根据不同的场景来选择不同的锁的使用。