当前位置: 首页 > news >正文

谈谈线程安全问题及其解决方法

本文讲述一下线程的安全问题及解决方法。

一 线程安全问题举例说明
在电影院售票的场景,假使有100张票待售,有3个窗口在售票,如图所示:
在这里插入图片描述

三个窗口都卖出票1,一个票被卖了3次,多线程访问共享数据“票”,出现了线程安全问题。
问题代码见下:

public class SellTicketImpl1 implements Runnable {
    private int ticket=100;
    @Override
    public void run() {
        while (true){
            if(ticket>0){
                System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
                ticket--;
            }
        }
    }
}
public class ThreadSafeDemo1 {
    public static void main(String[] args) {
        SellTicketImpl1 sellTicket=new SellTicketImpl1();
        Thread t1=new Thread(sellTicket);
        Thread t2=new Thread(sellTicket);
        Thread t3=new Thread(sellTicket);
        t1.start();
        t2.start();
        t3.start();
    }

}

运行后出现下图所示:
在这里插入图片描述

二 解决线程安全问题三种方式

为了保证每个线程都能正常执行原子操作,java引入了线程同步机制。
有三种方式完成同步操作:

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

下面依次讲述三种同步操作:

1 同步代码块

1)说明:
解决线程安全问题的第一种方案是使用同步代码块,同步代码块的格式见下:

synchronized(锁对象){
       可能会出现线程安全问题的代码(访问了共享数据的代码)
   }

注意:

通过代码块中的锁对象,可以使用任意的对象,例如Object类对象;
但是必须保证多个线程使用的锁对象是同一个锁对象:把同步代码块锁住,只让一个线程在同步代码块中执行。
2)代码

public class SellTicketImpl2 implements Runnable {

    private int ticket=100;
    //创建一个Object类对象,当做锁对象
    Object obj=new Object();
    @Override
    public void run() {
        while (true){
            //使用synchronized同步代码块
            synchronized (obj) {
                if (ticket > 0) {
                    try {
                        //让它睡眠1秒
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                    ticket--;
                }
            }
        }
    }
}
public class ThreadSafeDemo2 {
    public static void main(String[] args) {
        SellTicketImpl2 sellTicket=new SellTicketImpl2();
        Thread t1=new Thread(sellTicket);
        Thread t2=new Thread(sellTicket);
        Thread t3=new Thread(sellTicket);
        t1.start();
        t2.start();
        t3.start();
    }

}
  1. 同步方法

1)说明
解决线程安全问题的第二种方案是使用同步方法,同步方法的格式见下:

 修饰符 synchronized 返回值类型 方法名(参数列表){
     可能会出现线程安全问题的代码(访问了共享数据的代码)
 }

2)代码

public class SellTicketImpl3 implements Runnable  {

    //定义一个多个线程共享的票源
    private int ticket=100;
    //设置线程任务:卖票
    @Override
    public void run() {
        while (true) {
            sellTicket();
        }
    }

    //定义同步方法
    public synchronized void sellTicket(){
        //使用死循环,让卖票重复操作执行
        while(true){
            //先判断票是否存在
            if(ticket>0){
                try {
                    //提高安全问题出现的概率,让程序睡眠
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //如果票存在,卖票ticket
                System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
                ticket--;
            }
        }
    }
}
public class ThreadSafeDemo3 {
    public static void main(String[] args) {
        SellTicketImpl3 sellTicket=new SellTicketImpl3();
        Thread t1=new Thread(sellTicket);
        Thread t2=new Thread(sellTicket);
        Thread t3=new Thread(sellTicket);
        t1.start();
        t2.start();
        t3.start();
    }
}

3 Lock锁

1)说明:解决线程安全问题的第三种方案是使用Lock锁,即java.util.concurrent.locks.Lock接口。
Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
Lock 接口中的方法有:
void lock() 获取锁
void unlock() 释放锁

2)使用步骤:

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

3)代码

public class SellTicketImpl4 implements Runnable {

    //定义一个多个线程共享的票源
    private int ticket=100;
    //1.在成员位置创建一个ReentrantLock对象
    //使用多态
    Lock lk=new ReentrantLock();
    //设置线程任务:卖票
    @Override
    public void run() {
        while (true){
            //使用死循环,让卖票重复操作执行
            while(true){
                //2.在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
                lk.lock();
                //先判断票是否存在
                if(ticket>0){
                    try {
                        //提高安全问题出现的概率,让程序睡眠
                        Thread.sleep(10);
                        //如果票存在,卖票ticket
                        System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
                        ticket--;
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        //3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
                        lk.unlock();   //无论是否出现异常,都释放锁
                    }

                }

            }
        }
    }
}
public class ThreadSafeDemo4 {
    public static void main(String[] args) {
        SellTicketImpl4 sellTicket=new SellTicketImpl4();
        Thread t1=new Thread(sellTicket);
        Thread t2=new Thread(sellTicket);
        Thread t3=new Thread(sellTicket);
        t1.start();
        t2.start();
        t3.start();
    }
}

运行后,如下图所示:
在这里插入图片描述

三 同步技术原理:

n个线程t1,t2,t3,…,tn,一起抢夺cpu的执行权,谁抢到了谁执行run方法进行卖票。假设t1抢到了cpu的执行权,执行run方法,遇到synchronized代码块,这时t1会检查synchronized代码块是否有锁对象(上述的obj),如果有,就会获取到锁对象,进入到同步执行;这时候t2也抢到了cpu执行权但是没有检查到锁对象,那么t2就会进入阻塞状态,会一直等待t1线程归还锁对象,一直到t1线程执行完同步中的代码,会把对象归还给synchronized代码块,t2才能获取到锁对象进入到同步中执行。

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

相关文章:

  • 好客租房-12.ES接入java
  • java入门笔记
  • 进阶C语言 第二章-------《进阶指针》 (指针数组、数组指针、函数指针、回调指针)知识点+基本练习题+深入细节+通俗易懂+完整思维导图+建议收藏
  • 【图卷积神经网络】02-谱域图卷积介绍
  • 【JavaWeb】JavaScript基础语法(下)
  • SQL--DDL
  • E2PROM 蓝桥杯 stm32 AT24C02读写函数
  • 前后端登录逻辑讲解-本文使用RSA加密过程-附代码
  • 【操作系统】—— Windows常用快捷键(带你快速了解)
  • client-go实战之九:手写一个kubernetes的controller
  • 使用ebpf 监控mysqld 内核
  • 密码学系列之十:量子密码
  • 【vue2】对路由的理解
  • SOLID原则总结
  • MySQL内外连接
  • [分享]iOS开发-关于在xcode中引用文件夹右边出现问号的解决办法
  • 「面试题」如何实现一个圣杯布局?
  • 【干货分享】SpringCloud微服务架构分布式组件如何共享session对象
  • 2017年终总结、随想
  • AzureCon上微软宣布了哪些容器相关的重磅消息
  • create-react-app项目添加less配置
  • es6--symbol
  • ES学习笔记(10)--ES6中的函数和数组补漏
  • Fundebug计费标准解释:事件数是如何定义的?
  • JavaScript创建对象的四种方式
  • nginx 负载服务器优化
  • React16时代,该用什么姿势写 React ?
  • SpringBoot 实战 (三) | 配置文件详解
  • 老板让我十分钟上手nx-admin
  • 聊聊flink的BlobWriter
  • 那些被忽略的 JavaScript 数组方法细节
  • 前端之React实战:创建跨平台的项目架构
  • 如何在 Tornado 中实现 Middleware
  • 实战:基于Spring Boot快速开发RESTful风格API接口
  • 推荐一个React的管理后台框架
  • 消息队列系列二(IOT中消息队列的应用)
  • ionic异常记录
  • 蚂蚁金服CTO程立:真正的技术革命才刚刚开始
  • ​LeetCode解法汇总518. 零钱兑换 II
  • ​虚拟化系列介绍(十)
  • # Swust 12th acm 邀请赛# [ K ] 三角形判定 [题解]
  • #define与typedef区别
  • #if 1...#endif
  • #Java第九次作业--输入输出流和文件操作
  • (2009.11版)《网络管理员考试 考前冲刺预测卷及考点解析》复习重点
  • (C++17) optional的使用
  • (JS基础)String 类型
  • (办公)springboot配置aop处理请求.
  • (动手学习深度学习)第13章 计算机视觉---微调
  • (二)linux使用docker容器运行mysql
  • (二)七种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB
  • (一)C语言之入门:使用Visual Studio Community 2022运行hello world
  • (状压dp)uva 10817 Headmaster's Headache
  • .Net Core webapi RestFul 统一接口数据返回格式
  • .NET NPOI导出Excel详解