多线程03

目录

Java 多线程:线程安全、wait/notify 与单例模式详解

一、线程安全问题

1. 什么是线程安全?

2. 线程安全问题产生的原因

(1)线程的随机调度

(2)多个线程修改同一个变量

(3)操作不是原子的

(4)内存可见性问题

(5)指令重排序

二、wait 与 notify

wait 与 join 的区别

join

wait

锁竞争问题

三、线程饥饿(Starvation)

四、wait 方法详解

1. 当前线程进入等待状态

2. 释放当前持有的锁

注意事项

wait 和 synchronized 必须使用同一个锁对象

五、单例设计模式

什么是设计模式

什么是单例模式

六、饿汉模式

七、懒汉模式

7.1 普通懒汉模式(线程不安全)

7.2 同步方法

7.3 双重检查锁(DCL)

7.4 为什么需要 volatile

总结


Java 多线程:线程安全、wait/notify 与单例模式详解

一、线程安全问题

1. 什么是线程安全?

线程安全指的是:

一段代码在多线程环境下运行时,无论线程如何调度,程序最终都能够得到符合预期的结果。

例如:

count++;

假设两个线程同时执行这行代码。

我们的预期是:

count = count + 2;

但最终结果可能只增加了 1。

这说明程序出现了线程安全问题。


2. 线程安全问题产生的原因

(1)线程的随机调度

线程由操作系统负责调度。

现代操作系统普遍采用抢占式调度

  • 当前线程运行过程中可能随时被暂停;

  • CPU 可能切换到其他线程执行;

  • 程序员无法预测线程切换时机。

因此,多线程执行顺序是不确定的。


(2)多个线程修改同一个变量

当多个线程同时读写同一个变量时,就可能产生数据竞争(Race Condition)。

例如:

count++;

表面上看是一条语句,但实际上对应多个CPU指令:

读取 count
count + 1
写回 count

如果两个线程同时执行,就可能出现:

线程A读取 count = 0
线程B读取 count = 0

线程A写回 1
线程B写回 1

最终结果变成:

count = 1

而不是预期的:

count = 2

(3)操作不是原子的

原子性(Atomicity)指:

一个操作要么全部执行成功,要么完全不执行,中间不会被其他线程打断。

例如:

count++;

并不是原子操作。

因为它对应:

读取
计算
写回

三个步骤。

在执行过程中可能被其他线程插入执行,从而导致线程安全问题。


(4)内存可见性问题

为了提高运行效率,CPU 和编译器会进行优化。

线程读取变量时:

boolean flag = false;

可能不会每次都从主内存读取,而是缓存到自己的工作内存中。

此时:

线程A:

while(!flag){
}

线程B:

flag = true;

虽然线程B已经修改了变量,

但线程A可能一直读取自己缓存中的旧值。

导致循环无法结束。

这就是内存可见性问题。

解决方案:

volatile boolean flag;

volatile能够保证变量修改后立即刷新到主内存,并保证其他线程能够及时看到最新值。


(5)指令重排序

为了提高执行效率:

编译器和CPU可能调整指令执行顺序。

例如:

new Singleton();

理论执行过程:

1. 申请内存
2. 初始化对象
3. 引用指向对象

实际可能被优化为:

1. 申请内存
2. 引用指向对象
3. 初始化对象

此时其他线程可能拿到一个尚未初始化完成的对象。

这就是指令重排序问题。


二、wait 与 notify

wait 和 notify 用于协调多个线程之间的执行顺序。

它们无法决定CPU调度哪个线程,

但能够让线程主动进入等待状态。

例如:

线程A先执行
线程B等待

线程A完成后
唤醒线程B

这样就实现了线程之间的协作。


wait 与 join 的区别

join

t1.join();

含义:

当前线程必须等待 t1 完全执行结束。


wait

lock.wait();

含义:

当前线程等待其他线程调用 notify() 或 notifyAll()。

不要求对方线程结束运行。

因此:

join:等待线程结束

wait:等待线程被唤醒

锁竞争问题

多个线程竞争同一把锁时:

synchronized(lock){
}

锁释放后:

哪个线程获得锁是不确定的。

例如:

A获得锁
A释放锁

下一次:
可能B获得锁
也可能A再次获得锁

完全由操作系统调度决定。


三、线程饥饿(Starvation)

线程饥饿指:

某个线程长期得不到CPU时间或锁资源,导致一直无法执行。

可以把线程看成鸟宝宝:

  • CPU是鸟妈妈;

  • 锁资源是食物;

如果某只鸟长期抢不到食物,

就会发生线程饥饿。


四、wait 方法详解

当线程调用:

wait();

会发生两件事:

1. 当前线程进入等待状态

RUNNING
    ↓
WAITING

2. 释放当前持有的锁

这是wait最重要的特性。

例如:

synchronized(lock){
    lock.wait();
}

执行wait后:

释放 lock
进入等待状态

这样其他线程才能获得锁并执行notify。

如果wait不释放锁:

线程A wait

线程B获取不到锁
无法notify

线程A永远等待

程序将陷入死锁状态。


注意事项

wait 和 synchronized 必须使用同一个锁对象

正确:

synchronized(lock){
    lock.wait();
}

错误:

synchronized(lock1){
    lock2.wait();
}

会抛出:

IllegalMonitorStateException

五、单例设计模式

什么是设计模式

设计模式(Design Pattern):

针对软件开发中的常见问题,总结出来的一套成熟解决方案。

可以理解为程序员的“棋谱”。


什么是单例模式

单例模式要求:

一个类在整个程序运行期间只能创建一个对象。

核心思想:

构造方法私有化
↓
外部无法new
↓
类内部自己创建对象
↓
统一对外提供访问入口

六、饿汉模式

饿汉模式:

类加载时立即创建对象。

public class Singleton {

    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

特点:

  • 实现简单

  • 天然线程安全

  • 获取速度快

缺点:

  • 可能提前创建不需要使用的对象


七、懒汉模式

懒汉模式:

第一次使用时才创建对象。


7.1 普通懒汉模式(线程不安全)

public class Singleton {

    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {

        if(instance == null){
            instance = new Singleton();
        }

        return instance;
    }
}

问题:

多个线程同时进入:

if(instance == null)

可能创建多个对象。


7.2 同步方法

public static synchronized Singleton getInstance(){

    if(instance == null){
        instance = new Singleton();
    }

    return instance;
}

优点:

  • 线程安全

缺点:

  • 每次获取对象都要加锁

  • 性能较低


7.3 双重检查锁(DCL)

public class Singleton {

    private static volatile Singleton instance;

    private Singleton(){}

    public static Singleton getInstance(){

        if(instance == null){

            synchronized(Singleton.class){

                if(instance == null){

                    instance = new Singleton();

                }
            }
        }

        return instance;
    }
}

第一次判断:

if(instance == null)

避免频繁加锁。

第二次判断:

if(instance == null)

防止多个线程重复创建对象。


7.4 为什么需要 volatile

对象创建过程可能发生指令重排序:

1. 分配内存
2. 初始化对象
3. 引用指向对象

可能变成:

1. 分配内存
2. 引用指向对象
3. 初始化对象

此时:

线程A刚执行到步骤2,

线程B发现:

instance != null

直接返回对象。

但对象实际上还没有初始化完成。

因此需要:

private static volatile Singleton instance;

volatile能够禁止这部分重排序,从而保证DCL模式正确运行。


总结

线程安全问题主要来源于:

线程随机调度
        ↓
共享资源竞争
        ↓
原子性问题

内存可见性问题

指令重排序问题

而在单例模式中:

饿汉模式
    ↓
简单、安全

懒汉模式
    ↓
节省资源

DCL + volatile
    ↓
兼顾性能与线程安全
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值