一文吃透 Java 多线程:六种线程状态流转、并发安全与单例模式实战
一.线程的状态
1.线程所有的状态
- NEW:安排了工作,还未开始行动
- RUNNABLE:可工作的。
- BLOCKED:表示排队等着其他事情(由于加锁产生的阻塞)
- WAITING:表示排队等着其他的事情(无超时时间的阻塞)---->这个阻塞是无join版本
- TIME_WAITING:表示排队等着其他事情(有超时时间的阻塞)---->这个阻塞是有join版本
2.线程状态和线程转移的意义

3.观察线程的状态和转移
观察1:TIME_WAITING、WAITING
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t1.join();
}


观察2:NEW、RUNNABLE、TERMINATED(代表全部工作已经完成)
public static void main(String[] args) {
Thread t=new Thread(()->{
for(int i=0;i<1000_0;i++){
}
},"李四");
System.out.println(t.getName()+":"+t.getState());
t.start();
while(t.isAlive()){
System.out.println(t.getName()+":"+t.getState());
}
System.out.println(t.getName()+":"+t.getState());
}

观察3:关注WAITING 、 BLOCKED 、 TIMED_WAITING 状态的转换
final static Object object=new Object();
public static void main(String[] args) {
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
// throw new UnsupportedOperationException("Not supported yet.");
synchronized(object){
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
System.getLogger(Demo2.class.getName()).log(System.Logger.Level.ERROR, (String) null, ex);
}
}
}
}
},"t1");
t1.start();
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
// throw new UnsupportedOperationException("Not supported yet.");
synchronized(object){
System.out.println("hehe");
}
}
},"t2");
t2.start();
}


观察4:WAITING
final static Object object=new Object();
public static void main(String[] args) {
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
// throw new UnsupportedOperationException("Not supported yet.");
synchronized(object){
while (true) {
try {
object.wait();
} catch (InterruptedException ex) {
System.getLogger(Demo2.class.getName()).log(System.Logger.Level.ERROR, (String) null, ex);
}
}
}
}
},"t1");
t1.start();
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
// throw new UnsupportedOperationException("Not supported yet.");
synchronized(object){
System.out.println("hehe");
}
}
},"t2");
t2.start();
}

二.线程安全问题的原因
1.线程的调度是随机的
2.多个线程同时修改同一个变量
注意:一个线程修改一个变量没事,多个线程修改多个变量没事,多个线程修改不同变量没事
3.修改的操作不是原子的
注:原子性:原子性指一个操作或一系列操作要么全部执行成功,要么全部不执行,不可被中断或部分完成。在多线程或并发编程中,原子性是保证数据一致性的关键特性。
4.内存可见性
5.指令重排序
三.线程安全问题的应对措施
1.解决修改的操作是原子性
如果修改的操作不是原子性,对于下面的代码可以出现任何的情况。
Thread t1=new Thread(()->{
for(int i=0;i<5000;i++){
count++;
}
});
Thread t2=new Thread(()->{
for(int i=0;i<5000;i++){
count++;
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}

为啥么会出现这种情况?
答:线程的调度是唯一的
因此程序员必须保证在任何的执行顺序下都要输出的结果正确。
为了解决这个问题,JAVA引入了锁方式进行解决。通过加锁的方式可以把一段代码打包成一个整体,这样就达到了"原子"操作。
对于锁这样的概念,涉及到两个核心:
1.加锁
2.解锁
java用synchronized来表示

注意:加锁是把若干个操作"打包成一个原子",不是把count++的三个指令变成一个指令了,也就是锁,这三个指令就必须一口气在cpu上执行完,不会发生线程调度。加锁会影响其他的加锁线程,而且是加同一个锁的线程。
如果是两个线程加同一把锁会发生锁竞争,两个线程加不同的锁不会发生锁竞争。
于是乎为了解决这个问题需要对两个线程加同一把锁。相关代码如下:
private static int count=0;
private static Object locker=new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
for(int i=0;i<5000;i++){
synchronized (locker){
count++;
}
}
});
Thread t2=new Thread(()->{
for(int i=0;i<5000;i++){
synchronized (locker){
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
这种加锁方式执行的效率比较高,因为只有count++的操作是串行执行的,这种写法线程的并发执行程度更高。
private static int count=0;
private static Object locker=new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
synchronized (locker){
for(int i=0;i<5000;i++){
count++;
}
}
});
Thread t2=new Thread(()->{
synchronized (locker){
for(int i=0;i<5000;i++){
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
这种执行方式,这两个线程是完全串行的。
总结synchronized使用方法:
1.synchronized(锁对象){}
基础使用常见的方法。
2.修饰一个普通方法的时候,就可以省略锁对象。

3.synchorinzed修饰静态方法

注意:
static修饰的方法,也叫做"类方法"不是针对"实例"的方法,而是针对类的。这在个方法中,没有this。

2.可重入
对于同一个线程连续加锁多次不会发生死锁,这个就叫做可重入。

为啥Java的多线程会有可重入?
由于Java的对象,除了有一个内存区域,保存程序员自定义的成员之外还有一个"隐藏区域",保存"对象头"。
class Student{
int id;
String name;
}

让锁对象本身记录下来拥有者是哪个线程(把线程id给保存下来了)。
jvm如何解决这个问题,如图所示:

注:死锁
1.一个线程一把锁,连续加锁两次
2.两个线程两把锁,每个线程先获取到一把锁,在尝试获取对方的锁
对于2来说有这么一个案例
线程1先获取到锁A,线程2业获取到锁B,然后线程1尝试获取锁B,线程2也尝试获取锁A
private static final Object lock1 = new Object(); private static final Object lock2 = new Object(); public static void main(String[] args) { Thread t1=new Thread(()->{ synchronized(lock1){ System.out.println("t1线程拿到lock1锁"); synchronized(lock2){ System.out.println("t1等待lock2"); } } }); Thread t2=new Thread(()->{ synchronized(lock2){ System.out.println("t2线程拿到lock2锁"); synchronized(lock1){ System.out.println("t2等待lock1"); } } }); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("程序结束"); }
如何避免代码中出现死锁?
关键在于理解死锁的"四个必要条件"
1.锁是互斥的
2.锁不可抢占
(线程1拿到锁之后,线程2也想要这个锁,线程2会阻塞等待,而不是直接把锁抢夺过来)
3.请求和保持
(拿到第一把锁低情况下,不会释放第一把锁,再尝试请求第二把锁)
4.循环等待
(等待锁释放,等待的顺序构成了循环)
对于synchronized来说,条件1和2来说都是synchronized的基本特点。
注意:Java标准库中很多都是线程不安全的。这些类可能涉及到多线程修改共享数据,又没有任何加锁措施。解决线程安全问题是加锁,但是加锁是有代价的,加锁会非常明显的影响到程序的执行效率。加锁意味着可能发生锁竞争,一旦发生锁竞争就会产生阻塞。某个线程一旦因为加锁阻塞,啥时候才能继续执行时间不定,所以写代码的时候要考虑需不需要加锁。
Java标准库中常见的线程不安全的集合类:
- ArrayList
- LinkedList
- HashMap
- TreeMap
- HashSet
- TreeSet
- StringBuilder
这些常见的集合类,大多数是线程不安全的,把加锁角色交给成雪员。
Java标准库中线程安全的集合类:
- Vector(不推荐使用)
- HashTable(不推荐使用)
- ConcurrentHashMap
- StringBuffer
3.内存可见性引起的线程安全问题
private static int flag=0;
public static void main(String[] args) {
Thread t1=new Thread(()->{
while(flag==0){
}
System.out.println("t1结束");
});
Thread t2=new Thread(()->{
System.out.println("请输入一个flag值");
Scanner sc=new Scanner(System.in);
flag=sc.nextInt();
System.out.println("t2结束");
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("程序结束");
}
这段代码会出现线程t1停止不了的问题。

这个问题产生的原因,就是"内存可见性问题"。内存可见性问题是:不同线程对于同一个变量进行修改,其他线程看不见此变量的变化情况。
编译器优化:主流编程语言,编译器的设计者(对于java来说,谈到的编译器包括javac和jvm)考虑到一个问题:实际上写代码的程序员,水平是参差不齐的(差距很大)。虽然有的程序员水平不高,写的代码效率比较低,编译器在编译执行的时候,分析现有代码的意图和效果,自动对这个代码进行调整和优化。在确保执行逻辑不变的前提下,提高程序的效率。
Thread t1=new Thread(()->{
while(flag==0){
}
System.out.println("t1结束");
});
对于这个程序来说,编译器看到的效果是:有一个变量flag会快速的,反复的读取这个内存的值(即反复的执行load,cmp,load,cmp),同时,反复执行的过程中,每次拿到的flag的值还都是一样的。既然load读取到的值都是一样的,而且load开销这么大,于是编译器把从内存读取flag这个操作给优化掉了,此时这个循环的效率大大提升了。
Thread t2=new Thread(()->{
System.out.println("请输入一个flag值");
Scanner sc=new Scanner(System.in);
flag=sc.nextInt();
System.out.println("t2结束");
});
为啥要这么优化?因为编译器不确定这里的flag修改代码到底能不能执行,以及啥时候执行。
为了解决这个问题Java中引入了volatile关键字,通过这个关键字,提醒编译器,某个变量是"易变"的,此时就不要针对这种"易变"的变量进行上述优化。
JMM:Java的内存模型,首先一个Java进程,会有一个"主内存"存储空间,每个Java线程又会有自己的"工作内存"存储空间。形如上面代码,t1进行flag变量的判断,就会把flag值从主内存,先读取到工作内存,用工作内存的值进行判定。同时t2对flag进行修改,修改的则是主内存的值,主内存的变更不会影响到t1的工作内存。
注:主内存就是内存,工作内存是CPU的寄存器和CPU缓存构成的传统说法。
小提示:编译器优化并非100%触发,根据不同的代码结构,可能产生出不同的优化效果
private static int flag=0;
public static void main(String[] args) {
Thread t1=new Thread(()->{
while(flag==0){
try {
Thread.sleep(1);
} catch (InterruptedException ex) {
System.getLogger(Demo07.class.getName()).log(System.Logger.Level.ERROR, (String) null, ex);
}
}
System.out.println("t1结束");
});
Thread t2=new Thread(()->{
System.out.println("请输入一个flag值");
Scanner sc=new Scanner(System.in);
flag=sc.nextInt();
System.out.println("t2结束");
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("程序结束");
}

volatie这个关键字,能够解决内存可见性问题引起的线程安全问题,但是不具备原子性这样的特点。
4.wait/notify
线程执行本省是随机调度(顺序不确定),join控制的线程的结束顺序。如何让线程按照顺序执行呢?
此时就要用到wait和notify关键字。
比如:线程1和线程2希望线程1限制性执行完某个逻辑之后,再让线程2执行。
为了解决这个问题,此时就可以让线程2通过wait主动进行阻塞,让线程1参与调度,等线程1把对应的逻辑执行完了,就可以通过notify唤醒线程2.
注意,wait、notify可以解决"线程饿死"的问题
线程饿死:线程饿死指在多线程环境中,某些线程因长期无法获取所需资源(如CPU时间、锁等)而无法执行任务的现象。通常由于资源分配策略不公平或优先级设置不合理导致。
wait方法做的第一件事就是释放锁。所有先要拿到锁才能释放。
synchronized(lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
注意:wait方法必须放到synchronized中使用。
有一个特别形象的例子,见图:


注:此处的阻塞会一直进行,知道有其他线程调用notify唤醒。
Thread t1=new Thread(()->{
synchronized(lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2=new Thread(()->{
synchronized(lock){
lock.notify();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
这个代码是wait和notify的基本用法。wait进入阻塞之后,需要通过notify唤醒。默认情况下,wait的阻塞是'死等'。
wait做的事情:
- 使当前执行的线程进行等待(把线程放到等待队列中)
- 释放当前的锁
- 满足一定条件时被唤醒,重新尝试获取这个锁
wait这三个操作同时进行。
使用wait的时候,阻塞其实有两个阶段
- WAITING的阻塞。通过wait等待其他线程的通知
- BLOCKED的阻塞。当收到通知之后,就会重新尝试获取锁。重新尝试获取锁的过程中,可能又会遇到锁竞争。
private static Object lock=new Object();
public static void main(String[] args) {
Thread t1=new Thread(()->{
synchronized(lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1唤醒");
}
System.out.println("t1结束");
});
Thread t2=new Thread(()->{
synchronized(lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2唤醒");
}
System.out.println("t2结束");
});
Thread t3=new Thread(()->{
synchronized(lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3唤醒");
}
System.out.println("t3结束");
});
Thread t4=new Thread(()->{
System.out.println("t4开始");
synchronized(lock){
lock.notify();
}
System.out.println("t4结束");
});
t1.start();
t2.start();
t3.start();
t4.start();
try {
t1.join();
t2.join();
t3.join();
t4.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("程序结束");
}
private static Object lock=new Object();
public static void main(String[] args) {
Thread t1=new Thread(()->{
synchronized(lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1唤醒");
}
System.out.println("t1结束");
});
Thread t2=new Thread(()->{
synchronized(lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2唤醒");
}
System.out.println("t2结束");
});
Thread t3=new Thread(()->{
synchronized(lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3唤醒");
}
System.out.println("t3结束");
});
Thread t4=new Thread(()->{
System.out.println("t4开始");
synchronized(lock){
lock.notifyAll();
}
System.out.println("t4结束");
});
t1.start();
t2.start();
t3.start();
t4.start();
try {
t1.join();
t2.join();
t3.join();
t4.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("程序结束");
}
如果有多个线程在wait,此时的notify只能随即唤醒一个线程。此时需要用到notifyAll将其全部唤醒。
如果没任何对象在wait,直接进行notify/notifAll?
答:直接凭空调用notify是没有副作用。
经典面试题
sleep和wait的区别
答:1.wait的设计就是为了提前唤醒的,超时时间只是B计划。sleep的设计就是为了到时间唤醒,虽然也可以通过Interrupt()提前唤醒这样的唤醒会产生异常
2.wait需要搭配锁来使用,wait执行时会优先释放锁。sleep不需要搭配锁使用,当sleep放到synchronized内部时不会释放锁。
5.多线程案例
设计模式的解释:设计模式好⽐象棋中的"棋谱".红⽅当头炮,⿊⽅⻢来跳.针对红⽅的⼀些⾛法,⿊⽅应招的时候有⼀ 些固定的套路.按照套路来⾛局势就不会吃. 软件开发中也有很多常⻅的"问题场景".针对这些问题场景,⼤佬们总结出了⼀些固定的套路.按照这 个套路来实现代码,也不会吃亏.
饿汉模式
类加载的同时,创建实例
class Singleton{
private static Singleton instance=new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
这是线程安全的。
懒汉模式-单线程版
类加载的时候不创建实例。第一次使用的时候才创建实例。
class Singleton{
private static Singleton instance=null;
private Singleton(){}
public static Singleton getInstance(){
if(instance==null){
instance=new Singleton();
}
return instance;
}
}
这个时线程不安全的。
懒汉模式-多线程版
加上synchronized可以解决线程安全的问题。
class Singleton{
private static Singleton instance=null;
private Singleton(){}
public synchronized static Singleton getInstance(){
if(instance==null){
instance=new Singleton();
}
return instance;
}
}
懒汉模式-多线程版(改进)
class Singleton{
private volatile static Singleton instance=null;
private Singleton(){}
public static Singleton getInstance(){
if(instance==null){
synchronized (Singleton.class){
if(instance==null){
instance=new Singleton();
}
}
}
return instance;
}
}
这个代码在加锁的基础上,做出了进⼀步改动:
• 使⽤双重if判定,降低锁竞争的频率.
• 给instance加上了volatile.
理解双重if判定/volatile:
加锁/解锁是⼀件开销⽐较⾼的事情.⽽懒汉模式的线程不安全只是发⽣在⾸次创建实例的时候.因此 后续使⽤的时候,不必再进⾏加锁了.
外层的if就是判定下看当前是否已经把instance实例创建出来了.
同时为了避免"内存可⻅性"导致读取的instance出现偏差,于是补充上volatile.
当多线程⾸次调⽤getInstance,⼤家可能都发现instance为null,于是⼜继续往下执⾏来竞争锁,其 中竞争成功的线程,再完成创建实例的操作.
当这个实例创建完了之后,其他竞争到锁的线程就被⾥层if挡住了.也就不会继续创建其他实例.
6.指令重排序
什么是代码重排序
⼀段代码是这样的:
1. 去前台取下U盘
2. 去教室写10分钟作业
Java多线程编程核心涵盖线程六种状态(NEW至TERMINATED)及转换机制,通过代码示例展示状态变化过程。线程安全问题源于调度随机性、共享变量修改、非原子操作、内存可见性和指令重排序五大因素,可通过synchronized保证原子性、volatile解决可见性、wait/notify实现线程协调。单例模式线程安全实现包括饿汉式的类加载初始化与懒汉式的双重检查锁定优化。典型问题如死锁需避免循环等待条件,注意sleep不释放锁而wait会释放锁的特性差异。文中结合实践代码解析多线程关键技术要点与最佳实现方案。
3. 去前台取下快递
如果是在单线程情况下,JVM、CPU指令集会对其进⾏优化,⽐如,按1->3->2的⽅式执⾏,也是没问 题,可以少跑⼀次前台。这种叫做指令重排序



所有评论(0)