从 01 开始 从 01 开始
首页
  • 📚 计算机基础

    • 计算机简史
    • 数字电路
    • 计算机组成原理
    • 操作系统
    • Linux
    • 计算机网络
    • 数据库
    • 编程工具
    • 装机
  • 🎨 前端

    • Node
  • JavaSE
  • Java 高级
  • JavaEE

    • 构建、依赖管理
    • Ant
    • Maven
    • 日志框架
    • Junit
    • JDBC
    • XML-JSON
  • JavaWeb

    • 服务器软件
    • 环境管理和配置管理-科普篇
    • Servlet
  • Spring

    • Spring基础
  • 主流框架

    • Redis
    • Mybatis
    • Lucene
    • Elasticsearch
    • RabbitMQ
    • MyCat
    • Lombok
  • SpringMVC

    • SpringMVC 基础
  • SpringBoot

    • SpringBoot 基础
  • Windows 使用技巧
  • 手机
  • 最全面的输入法教程
  • 最全面的浏览器教程
  • Office
  • 图片类工具
  • 效率类工具
  • RSS
  • 码字工具
  • 各大平台
  • 校招
  • 五险一金等
  • 职场规划
  • 关于离职
  • 杂谈
  • 📖 读书

    • 读书工具
    • 读书笔记
  • 🌍 英语

    • 从零开始学英语
    • 英语兔的相关视频
    • Larry 想做技术大佬的相关视频
  • 🏛️ 政治

    • 反腐
    • GFW
    • 404 内容
    • 审查与自我审查
    • 互联网
    • 战争
  • 💰 经济

    • 关于税
    • 理财
  • 💪 健身

    • 睡眠
    • 皮肤
    • 口腔健康
    • 学会呼吸
    • 健身日志
  • 🏠 其他

    • 驾驶技能
    • 租房与买房
    • 厨艺
  • 电影

    • 电影推荐
  • 电视剧
  • 漫画

    • 漫画软件
    • 漫画推荐
  • 游戏

    • Steam
    • 三国杀
    • 求生之路
  • 小说
  • 关于本站
  • 关于博主
  • 打赏
  • 网站动态
  • 友人帐
  • 从零开始搭建博客
  • 搭建邮件服务器
  • 本站分享
  • 🌈 生活

    • 2022
    • 2023
    • 2024
    • 2025
  • 📇 文章索引

    • 文章分类
    • 文章归档

晓林

程序猿,自由职业者,博主,英语爱好者,健身达人
首页
  • 📚 计算机基础

    • 计算机简史
    • 数字电路
    • 计算机组成原理
    • 操作系统
    • Linux
    • 计算机网络
    • 数据库
    • 编程工具
    • 装机
  • 🎨 前端

    • Node
  • JavaSE
  • Java 高级
  • JavaEE

    • 构建、依赖管理
    • Ant
    • Maven
    • 日志框架
    • Junit
    • JDBC
    • XML-JSON
  • JavaWeb

    • 服务器软件
    • 环境管理和配置管理-科普篇
    • Servlet
  • Spring

    • Spring基础
  • 主流框架

    • Redis
    • Mybatis
    • Lucene
    • Elasticsearch
    • RabbitMQ
    • MyCat
    • Lombok
  • SpringMVC

    • SpringMVC 基础
  • SpringBoot

    • SpringBoot 基础
  • Windows 使用技巧
  • 手机
  • 最全面的输入法教程
  • 最全面的浏览器教程
  • Office
  • 图片类工具
  • 效率类工具
  • RSS
  • 码字工具
  • 各大平台
  • 校招
  • 五险一金等
  • 职场规划
  • 关于离职
  • 杂谈
  • 📖 读书

    • 读书工具
    • 读书笔记
  • 🌍 英语

    • 从零开始学英语
    • 英语兔的相关视频
    • Larry 想做技术大佬的相关视频
  • 🏛️ 政治

    • 反腐
    • GFW
    • 404 内容
    • 审查与自我审查
    • 互联网
    • 战争
  • 💰 经济

    • 关于税
    • 理财
  • 💪 健身

    • 睡眠
    • 皮肤
    • 口腔健康
    • 学会呼吸
    • 健身日志
  • 🏠 其他

    • 驾驶技能
    • 租房与买房
    • 厨艺
  • 电影

    • 电影推荐
  • 电视剧
  • 漫画

    • 漫画软件
    • 漫画推荐
  • 游戏

    • Steam
    • 三国杀
    • 求生之路
  • 小说
  • 关于本站
  • 关于博主
  • 打赏
  • 网站动态
  • 友人帐
  • 从零开始搭建博客
  • 搭建邮件服务器
  • 本站分享
  • 🌈 生活

    • 2022
    • 2023
    • 2024
    • 2025
  • 📇 文章索引

    • 文章分类
    • 文章归档
  • JavaSE

  • JavaSenior

    • 反射

    • 注解

    • 集合类

    • 线程

      • 进程和线程的基本概念
      • Java 中的线程
      • Thread 类的方法
      • Runnable 接口
      • 匿名内部类方式实现线程的创建
      • 线程安全问题
        • 业务场景与演示
        • 线程安全问题产生的原理
        • 解决线程安全问题
      • 线程状态
      • 等待唤醒机制
      • 线程池
  • JavaEE

  • JavaWeb

  • Spring

  • 主流框架

  • SpringMVC

  • SpringBoot

  • Java
  • JavaSenior
  • 线程
2024-01-26
目录

线程安全问题

# 线程安全问题

如果有多个线程在同时运行,而这些线程可能会同时运行同一段代码。如果多线程程序每次的运行结果,和单线程运行的结果是一样的,且其他的变量的值也和预期的是一样的,就是线程安全的。如果不是,则是不安全的。

我们通过一个案例,演示线程的安全问题。

# 业务场景与演示

电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “葫芦娃大战奥特曼”,本次电影的座位共 100 个,本场电影只能卖 100 张票。

我们来模拟电影院的售票窗口,模拟多个窗口同时卖这场电影的 100 张票。

首先,可以明确的是,如果是单线程去卖票,是没有问题的:

即使是多个窗口,如果不涉及共同修改数据,也是没问题的:

如果是同时卖,则可能有问题,例如第一个窗口在卖第 100 号票,而第二个窗口也在卖票,此时就出现了重复的票;还有可能一个窗口卖出最后一张票的同时,另一个窗口也在卖,此时就出现了不存在的票。

先来定义票源,以及卖票操作:

package chapter200Thread;

public class Ticket implements Runnable{

    // 定义一个多个线程共享的票源
    private int ticket = 100;


    // 执行卖票操作
    @Override
    public void run() {
        // 使用死循环,重复卖票
        while (true){
            // 先判断是否还有票
            if ( 0 < ticket){
                System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                ticket--;
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

然后定义一个类,启动多个线程,模拟多个窗口同时卖票:

package chapter200Thread;

public class Demo08Ticket {
    public static void main(String[] args) {
        // 创建 Runnable 接口的实现类对象
        Runnable run = new Ticket();

        // 创建三个窗口对象
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);

        //调用start方法开启多线程,执行run方法
        t0.start();
        t1.start();
        t2.start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

运行结果:存在卖出重复票的行为()

Thread-1正在卖第100张票
Thread-0正在卖第100张票
Thread-2正在卖第100张票
Thread-0正在卖第98张票
Thread-1正在卖第99张票
Thread-0正在卖第96张票
Thread-2正在卖第97张票
Thread-0正在卖第94张票
Thread-1正在卖第95张票
Thread-0正在卖第92张票
.....
1
2
3
4
5
6
7
8
9
10
11

如果没有出现重复卖票或者卖出不存在的票的行为,可以多试几次,或者加个 Thread.sleep 方法,提高出现线程安全问题的概率:

while (true){
    Thread.sleep(10);
    // 先判断是否还有票
    if ( 0 < ticket){
        System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
        ticket--;
    }
}
1
2
3
4
5
6
7
8

# 线程安全问题产生的原理

假设有 3 个线程,分别是 t0,t1,t2

假设这样的场景:

  • 程序处于初始状态,票的数量是 100
  • 当 t0 判断完 ticket > 0 后,输出了“正在卖第 100 张票”,等输出完在这句后,该程序就暂停,CPU 转去执行 t1 线程,此时 ticket -- 还未执行
  • t1 也判断完 ticket > 0 后,输出了“正在卖第 100 张票”,等输出完在这句后,该程序就暂停,CPU 转去执行 t2 线程,此时 ticket -- 还未执行
  • t2 同理,也输出了“正在卖第 100 张票”
  • 此时,就出现了卖出重复票的行为

同理,卖出不存在的票的场景也是一样的,这里就不赘述了。

怎么解决这个问题呢?我们可以让一个线程访问共享数据的时候,无论是否失去了 CPU 的执行权,其他线程都要等待;等到这个线程卖完了一张票,其他线程才能进行卖票。

# 解决线程安全问题

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。

要解决多线程并发访问一个资源的安全性问题,Java 中提供了同步机制(synchronized)。

根据案例简述:

窗口 1 线程进入操作的时候,窗口 2 和窗口 3 线程只能在外等着,窗口 1 操作结束,窗口 1 和窗口 2 和窗口 3 才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺 CPU 资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

为了保证每个线程都能正常执行原子操作,Java 引入了线程同步机制。

那么怎么去使用呢?有三种方式完成同步操作:

  1. 同步代码块。
  2. 同步方法。
  3. 锁机制。

# 同步代码块

同步代码块: synchronized 关键字,可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。格式:

synchronized (同步锁){
  需要同步操作的代码
}
1
2
3

对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁:

  1. 锁对象可以是任意类型。
  2. 多个线程对象要使用同一把锁。
  3. 锁对象的作用:把同步代码块锁住,只让一个线程在同步代码块中执行。
  4. 在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

实践:新建一个类,然后从创建一个锁对象,并将同步代码块放到 synchronized 块中

package chapter200Thread;

public class TicketSynchronized implements Runnable{

    // 定义一个多个线程共享的票源
    private int ticket = 100;

    // 创建一个锁对象,注意要在 run 方法外部创建,否则每个线程都会创建一个锁对象,就无效了。
    Object obj = new Object();

    // 执行卖票操作
    @Override
    public void run() {
        // 使用死循环,重复卖票
        while (true){
           synchronized (obj){
               // 先判断是否还有票
               if ( 0 < ticket){
                   System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                   ticket--;
               }
           }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package chapter200Thread;

public class Demo08TicketSynchronized {
    public static void main(String[] args) {
        // 创建 Runnable 接口的实现类对象
        Runnable run = new TicketSynchronized();

        // 创建三个窗口对象
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);

        //调用start方法开启多线程,执行run方法
        t0.start();
        t1.start();
        t2.start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

运行结果:能正常卖出 100 张票。

# 同步技术的原理

同步技术使用了一个锁对象,这个锁对象叫同步锁,也叫对象锁,对象监视器。

3 个线程一起抢夺 CPU 的执行权,谁抢到了,谁就执行 run 方法进行卖票。

比如,t0 抢到了 CPU 的执行权,执行 run 方法时,会遇到 synchronized 代码块,这时 t0 会检查 synchronized 代码块是否有锁对象,发现有,就会获取到锁对象,进入到同步中执行

而等 t1 抢到了 CPU 的执行权时,执行 run 方法,也遇到 synchronized 代码块,这时 t1 会检查 synchronized 代码块是否有锁对象,发现没有,t1 就会进入到阻塞状态,会一直等待 t0 线程归还锁对象。

一直到 t0 线程执行完同步中的代码,会把锁对象归还给同步代码块,t1 才能获取到锁对象进入到同步中执行。

总结:同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步。同步保证了只能有一个线程在同步中执行共享数据,保证了安全。

但也有一个缺点:线程会频繁地判断,锁获取和释放的情况,程序的效率会降低。

# 同步方法

同步方法:使用 synchronized 修饰的方法,就叫做同步方法,保证 A 线程执行该方法的时候,其他线程只能在方法外等着。格式:

修饰符 synchronized 返回值类型 方法名(参数列表){
    可能会产生线程安全问题的代码
}
1
2
3

演示:

package chapter200Thread;

public class TicketSynchronizedMethod implements Runnable{

    // 定义一个多个线程共享的票源
    private int ticket = 100;

    // 执行卖票操作
    @Override
    public void run() {
        // 使用死循环,重复卖票
        while (true){
           sellTicket();
        }
    }

    public synchronized void sellTicket(){
        // 先判断是否还有票
        if ( 0 < ticket){
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
            ticket--;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package chapter200Thread;

public class Demo08TicketSynchronizedMethod {
    public static void main(String[] args) {
        // 创建 Runnable 接口的实现类对象
        Runnable run = new TicketSynchronizedMethod();

        // 创建三个窗口对象
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);

        //调用start方法开启多线程,执行run方法
        t0.start();
        t1.start();
        t2.start();
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

同步方法的原理:和之前一样,也会把方法内部的代码锁住,只让一个线程执行。同步方法的锁对象,就是线程的实现类对象,也就是 this。我们可以验证下,在 run 方法里输出这个 this:

package chapter200Thread;

public class TicketSynchronizedMethod implements Runnable{

    // 定义一个多个线程共享的票源
    private int ticket = 100;

    // 执行卖票操作
    @Override
    public void run() {
        System.out.println("this: " + this);
        // 使用死循环,重复卖票
        while (true){
           sellTicket();
        }
    }

    public synchronized void sellTicket(){
        // 先判断是否还有票
        if ( 0 < ticket){
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
            ticket--;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

在 main 程序中也输出下 run 对象:

package chapter200Thread;

public class Demo08TicketSynchronizedMethod {
    public static void main(String[] args) {
  
        // 创建 Runnable 接口的实现类对象
        Runnable run = new TicketSynchronizedMethod();
        System.out.println("run: " + run);
        // 创建三个窗口对象
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);

        //调用start方法开启多线程,执行run方法
        t0.start();
        t1.start();
        t2.start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

运行结果:确实是同一个对象

run: chapter200Thread.TicketSynchronizedMethod@6cd8737
this: chapter200Thread.TicketSynchronizedMethod@6cd8737
this: chapter200Thread.TicketSynchronizedMethod@6cd8737
this: chapter200Thread.TicketSynchronizedMethod@6cd8737
Thread-1正在卖第100张票
...........
1
2
3
4
5
6

我们可以这么理解:

public void sellTicket(){
    synchronized(this){
    // 先判断是否还有票
        if ( 0 < ticket){
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
            ticket--;
        }
    }
}
1
2
3
4
5
6
7
8
9

# 静态同步方法

我们可以使用静态的 synchronized 方法:

package chapter200Thread;

public class TicketSynchronizedMethodStatic implements Runnable{

    // 定义一个多个线程共享的票源
    private static int ticket = 100;

    // 执行卖票操作
    @Override
    public void run() {
        System.out.println("this: " + this);
        // 使用死循环,重复卖票
        while (true){
           sellTicket();
        }
    }

    public static synchronized void sellTicket(){
        // 先判断是否还有票
        if ( 0 < ticket){
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
            ticket--;
        }
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package chapter200Thread;

public class Demo08TicketSynchronizedMethodStatic {
    public static void main(String[] args) {

        // 创建 Runnable 接口的实现类对象
        Runnable run = new TicketSynchronizedMethodStatic();
        System.out.println("run: " + run);
        // 创建三个窗口对象
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);

        //调用start方法开启多线程,执行run方法
        t0.start();
        t1.start();
        t2.start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

静态方法的锁对象:就不能是 this 了(因为 this 是创建对象之后产生的),而是本类的 class 属性,也叫 class 文件对象(反射的时候讲过)

该方法了解即可。

# Lock 锁

java.util.concurrent.locks.Lock 提供了比 synchronized 代码块和 synchronized 方法更广泛的锁定操作,同步代码块/同步方法具有的功能,Lock 都有,除此之外更强大,更体现面向对象。

Lock 锁也称同步锁,将加锁与释放锁的操作方法化了,如下:

  • public void lock() :加同步锁
  • public void unlock() :释放同步锁

该功能是 JDK 1.5 之后出现的,是个接口,我们主要会用到其实现类:java.util.concurrent.locks.ReentrantLock;

使用步骤:

  1. 在成员位置创建一个 ReentrantLock 对象
  2. 在可能出现安全问题的代码前,调用 Lock 接口的方法 lock 获取锁
  3. 在可能出现安全问题的代码前,调用 Lock 接口的方法 unlock 释放锁

示例:

package chapter200Thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TicketLock implements Runnable{

    // 定义一个多个线程共享的票源
    private int ticket = 100;

    Lock l = new ReentrantLock();

    // 执行卖票操作
    @Override
    public void run() {
        // 使用死循环,重复卖票
        while (true){
            l.lock();
            // 先判断是否还有票
            if ( 0 < ticket){
                System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                ticket--;
            }
            l.unlock();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package chapter200Thread;

public class Demo08TicketLock {
    public static void main(String[] args) {
        // 创建 Runnable 接口的实现类对象
        Runnable run = new TicketLock();

        // 创建三个窗口对象
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);

        //调用start方法开启多线程,执行run方法
        t0.start();
        t1.start();
        t2.start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

运行结果:也能正常卖出 100 张票。

更好的写法:使用 finally,这样无论是否有异常,都能释放锁。

package chapter200Thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TicketLock implements Runnable{

    // 定义一个多个线程共享的票源
    private int ticket = 100;

    Lock l = new ReentrantLock();

    // 执行卖票操作
    @Override
    public void run() {
        // 使用死循环,重复卖票
        while (true){

            l.lock();

            try {
                // 提高安全问题出现的概率:让程序睡眠一下
                Thread.sleep(10);
                // 先判断是否还有票
                if ( 0 < ticket){
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                    ticket--;
                }
            } catch (Exception e){
                e.printStackTrace();
            } finally {
                l.unlock();
            }

        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
上次更新: 2024/10/1 19:22:42
匿名内部类方式实现线程的创建
线程状态

← 匿名内部类方式实现线程的创建 线程状态→

最近更新
01
吐槽一下《僵尸校园》
05-15
02
2025 年 4 月记
04-30
03
山西大同 “订婚强奸案” 将会给整个社会带来的影响有多严重? - 知乎 转载
04-26
更多文章>
Theme by Vdoing | Copyright © 2022-2025 | 粤 ICP 备 2022067627 号 -1 | 粤公网安备 44011302003646 号 | 点击查看十年之约
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式