[Java EE] 多线程 -- 初阶(2)
4 .线程的状态
public class demo13 {public static void main(String[] args) {for (Thread.State s:Thread.State.values()) {System.out.println(s);}}
}
- NEW : 安排了工作 , 还没开始行动 ; new 了 thread 对象 , 还没 start() , 不具备运行条件
- RUNNABLE : 可工作的 , 又分为 ① 正在运行 , 线程在 cpu 上运行 和 ② 就绪状态 , 线程随时可以去 cpu 上执行 ; 调用 start()线程就进入了就绪状态
- BLOCKED : 线程因竞争对象锁失败(进入
synchronized代码块时被其他线程持有锁) , 暂时停止运行 , 进入阻塞状态 ; 当锁被释放后 , 线程会重新进入就绪状态( RUNNABLE )等待调度 - WAITING : 线程通过调用无超时的等待方法 (Object.wait() , Thread.join() , LockSupport.park() ) 进入此状态 ; 线程不会主动唤醒 , 需要等待其他线程显示唤醒 ( 如 Object.notify() ) , 否则一直等待
- TIMED_WAITING : 线程通过调用带有超时的等待方法(Object.wait(long) , Thread.sleep(long) , Thread.join(long) )进入此状态 ; 与 WAITING的区别是 : TIMED_WAITING 超时会自动唤醒 , 重新进入就绪状态
- TERMINATED : 执行完毕或者 ( run()方法结束 ) 或因异常退出 , 进入终止状态 ; 此时线程生命周期结束 , 无法再被启动 ( 再次调用 start() 会抛异常 )
状态转换关系:
- 新建(New)→ 就绪(Runnable):调用
start()方法- 就绪(Runnable)→ 阻塞(Blocked):竞争锁失败
- 阻塞(Blocked)→ 就绪(Runnable):获得锁
- 就绪(Runnable)→ 等待(Waiting):调用无超时等待方法
- 等待(Waiting)→ 就绪(Runnable):被其他线程唤醒或中断
- 就绪(Runnable)→ 超时等待(Timed Waiting):调用带超时等待方法
- 超时等待(Timed Waiting)→ 就绪(Runnable):超时时间到或被唤醒 / 中断
- 就绪(Runnable)→ 终止(Terminated):线程执行完毕或异常终止

NEW
public class demo14 {public static void main(String[] args) {Thread t = new Thread(()->{System.out.println("hello thread");});System.out.println(t.getState());t.start();}
}![]()
RUNNABLE
public class demo14 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while (true) {}});System.out.println(t.getState());//NEWt.start();Thread.sleep(1000);System.out.println(t.getState());//RUNNABLE}
}WAITING
public class demo14 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while (true){System.out.println("helllo thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();t.join();}
}
此时 main 线程处于 waiting
TIMED_WAITING
public class demo14 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while (true){System.out.println("helllo thread");try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();Thread.sleep(1000);System.out.println(t.getState());}
}
5.线程安全
5.1 线程安全的概念
如果多线程环境下代码运行的结果是符合预期的 , 即再单线程环境下应该的结果 , 则说这个程序是线程安全的
线程安全问题是指 : 当多个现场同时访问共享资源(如共享变量,文件,数据库连接等),由于线程回字形顺序的不确定性(CPU 调度的随机性),导致程序出现数据不一致,逻辑错误或异常的情况
5.2 观察线程不安全
public class demo15 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});t1.start();t2.start();//如果此处没有这两个 join ,count为0,原因是main线程先执行打印了//join的作用是: 让主线程等待子线程执行完毕t1.join();t2.join();//预期结果应该是10wSystem.out.println(count);}
}![]()
![]()
发现多次执行不但结果不一样 , 还不符合预期
对于 :
t1.join();
t2.join();这俩线程谁先 join();无所谓
情况 1 : t1 先结束 , t2 后结束
- main 先在 t1.join()阻塞等待
- t1 结束
- main 再在 t2.join()阻塞等待
- t2 结束
- main 继续执行后续打印
情况 2 : t2 先结束 , t1 后结束
- main 先在 t1.join()阻塞等待
- t2 结束 , t1.join()继续阻塞
- t1 结束
- main 执行到 t2.join() ; 但由于 t2 已经结束 , 此处不会阻塞
- main 继续执行后续打印
5.3 线程不安全的原因
1️⃣线程随机调度,抢占式执行(根本)
2️⃣多个线程同时修改同一个变量(修改共享数据)
3️⃣修改操作不是原子的(原子性缺失)(底层)
原子操作 : 不可分割的操作(如读取一个 int 变量),执行过程中不会被其他线程干扰
非原子操作 :由多个步骤组成的操作(如 count++,实际就包括读取-更新-写回),若执行到一半就被其他线程抢占 CPU ,就可能导致数据错误
针对 cout++分两个线程同时执行 5w 次这个操作 , 有以下观点:
一条 JAVA 语句不一定是原子的,也不一定只是一条指令
每一次 count++操作都是由三步操作组成:① 从内存把数据读取到 CPU (load)② 进行数据更新(add)③ 把数据写回 CPU(save)
由于两个线程他们有各自不同的上下文;此时只有当一个线程的 save 完成后,在进行下一个线程的 load 才能线程安全
4️⃣内存可见性(底层)
可见性指 :一个线程对共享变量值的修改,能够及时的被其他线程看到
换句话说 线程修改共享资源后,主内存的数据未能及时同步到其他线程的工作内存,导致其他线程读取到旧值
5️⃣指令重排序(底层)
CPU 指令重排序可能打乱代码执行顺序,多线程环境下引发逻辑错误
5.4 解决线程不安全问题(下文详细讲解)
1️⃣改为串行执行(解决多线程并发性执行的问题)
public class demo15 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});t1.start();t1.join();t2.start();t2.join(); //预期结果应该是10wSystem.out.println(count);}
}2️⃣互斥锁(synchronized 关键字)
3️⃣使用线程安全的数据结构
4️⃣减少资源共享
5️⃣volatile 关键字
6.synchronized 关键字-监视器锁 monitor
6.1 synchronized 的特性
① 互斥
synchronized 会起到互斥的效果,某个线程执行到某个对象的 synchronized 中时,其他线程如果也执行到同一个对象 synchronized 就会阻塞等待
阻塞等待:
针对每一把锁,操作系统内部维护了一个等待队列,当这个锁被某个线程占有时,其他线程尝试进行加锁,就加不上了,就会阻塞等待,一直到之前线程解锁后,有操作系统唤醒一个新的线程,再来获取到这个锁
- 阻塞等到时不占用 CPU 资源,避免空耗
- 需要依赖系统或其他线程唤醒,否则一直阻塞
public class demo16 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Object object = new Object();Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized(object){count++;}}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (object){count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}![]()

- 进入 synchronized 修饰的代码块,相当于加锁
- 退出 synchronized 修饰的代码块,相当于解锁
② 可重入
是锁的核心特性 , 指同一线程可以多次获取同一把锁 , 不会因为自身已持有该锁而陷入死锁 , 简单来说 "线程自己不会锁住自己"

底层实现 :
锁内部维护一个 线程持有计数器 和 当前持有线程引用:
线程首次获取锁 : 计数器设为 1 , 记录持有线程
同一线程再次获取锁 : 计数器+1 , 直接放行
线程释放锁 : 计数器-1 , 当计数器为 0 时 , 才释放锁给其他线程
6.2 synchronized 使用示例
synchronized 本质上要修改指定对象的对象头 , 从使用角度来看 , synchronized 也势必要搭配一个具体对象来使用
注意 :
- 两个线程 针对同一个对象枷锁 , 才会产生互斥效果
- 如果是不同的锁对象 , 此时不会产生互斥效果 , 线程也不安全

① 修饰代码块 : 明确指定锁哪个对象
锁任意对象
public class demo17 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(()->{for (int i = 0; i < 3; i++) {synchronized (locker){System.out.println("test t1");}}});t1.start();t1.join();System.out.println("test main");}
}可以用任意对象来作为锁 ; 这个锁本身的类型并不重要 , 重要的是 : 是否有其他线程尝试 竞争这个锁 ;
实际上 把一个对象作为锁对象 , 并不影响对象本身的使用 ; 但是一般 一个对象只有一个作用
锁当前对象(容易出现问题)
import static java.lang.Thread.sleep;public class demo17 {private static int count = 0;public void method() throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (this) {demo17.count++;}}});t1.start();//return t1;}public static void main(String[] args) throws InterruptedException {demo17 d3 = new demo17();d3.method();
// d3.method(); demo17 d4 = new demo17();d4.method();// demo17 d1 = new demo17();
//
// Thread T1 = d1.method();
// T1.start();
// T1.join();// Thread T2 = d1.method();
// T2.start();
// T2.join();// demo17 d2 = new demo17();
// Thread T3 = d2.method();
// T3.start();
// T3.join();sleep(2000);System.out.println(demo17.count);}
}也可以使用 Thread.currentThread()来替代 cur ; 但是这样写会让其他线程尝试竞争这个锁时 获取不到相同的锁对象
- 若多个线程通过同一个实例对象调用 method() , 则会竞争 同一把锁 (this) , 此处 count++是安全的

- 若多个线程通过不同实例对象调用 method() , 则每个线程的锁对象是不同的实例(this 不同) , 此时 锁不互斥 , count++会出现线程安全问题(因为 count 是静态共享的)

② 直接修饰普通方法 (和上面效果差不多)
import static java.lang.Thread.sleep;public class demo17 {private static int count = 0;public synchronized void method() throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {demo17.count++;}});t1.start();//return t1;}public static void main(String[] args) throws InterruptedException {demo17 d3 = new demo17();d3.method();//d3.method();demo17 d4 = new demo17();d4.method();sleep(2000);System.out.println(demo17.count);// demo17 d1 = new demo17();//// Thread T1 = d1.method();// T1.start();// T1.join();// Thread T2 = d1.method();// T2.start();// T2.join();// demo17 d2 = new demo17();// Thread T3 = d2.method();// T3.start();// T3.join();}
}③ 修饰静态方法
public class StaticSyncDemo {// 静态共享变量(类级资源)private static int staticCount = 0;// 静态同步方法:锁对象是 StaticSyncDemo.classpublic synchronized static void increment() {staticCount++; // 安全修改静态变量}public static void main(String[] args) throws InterruptedException {// 两个线程调用静态同步方法Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {increment();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {increment();}});t1.start();t2.start();t1.join();t2.join();System.out.println("staticCount = " + staticCount); // 一定是100000}
}普通方法的锁和静态方法的锁的区别 :
普通方法的锁 : 仅对同一个实例对象的多线程生效 ; 若多个线程操作不同实例 ,锁不互斥 , 无法保证线程安全
静态方法的锁 : 对所有实例和线程生效 (因为类对象全局唯一) , 无论多少个实例 , 多线程调用静态同步方法时都会竞争同一把锁
6.3 Java 标准库中的线程安全类
Java 标准库中 很多都是线程不安全的 , 这写了可能会涉及到多线程修改共享数据 , 又没有任何加锁措施
- ArrayList
- LinkedList
- HashMap
- TreeMap
- HashSet
- TreeSet
- StringBuilder
还有一些线程不安全 , 使用一些机制锁来控制
- Vector
- HashTable
- ConcurrentHashMap
- StringBuffer
还有没有使用锁 ,但是不涉及修改操作 , 仍然线程安全
- String
