写点什么

多线程安全问题原理和 4 种解决办法

  • 2022-12-14
    中国香港
  • 本文字数:4902 字

    阅读完需:约 16 分钟

多线程安全问题原理和4种解决办法

本文分享自华为云社区《多线程安全问题原理和解决办法Synchronized和ReentrantLock使用与区别》,作者:共饮一杯无。

线程安全问题概述

卖票问题分析


  • 单窗口卖票


一个窗口(单线程)卖 100 张票没有问题


单线程程序是不会出现线程安全问题的


  • 多个窗口卖不同的票



3 个窗口一起卖票,卖的票不同,也不会出现问题


多线程程序,没有访问共享数据,不会产生问题


  • 多个窗口卖相同的票



3 个窗口卖的票是一样的,就会出现安全问题


多线程访问了共享的数据,会产生线程安全问题

线程安全问题代码实现


模拟卖票案例


创建 3 个线程,同时开启,对共享的票进行出售


public class Demo01Ticket {    public static void main(String[] args) {        //创建Runnable接口的实现类对象        RunnableImpl run = new RunnableImpl();        //创建Thread类对象,构造方法中传递Runnable接口的实现类对象        Thread t0 = new Thread(run);        Thread t1 = new Thread(run);        Thread t2 = new Thread(run);        //调用start方法开启多线程        t0.start();        t1.start();        t2.start();    }}public class RunnableImpl implements Runnable{    //定义一个多个线程共享的票源    private  int ticket = 100;    //设置线程任务:卖票    @Override    public void run() {        //使用死循环,让卖票操作重复执行        while(true){            //先判断票是否存在            if(ticket>0){                //提高安全问题出现的概率,让程序睡眠                try {                    Thread.sleep(10);                } catch (InterruptedException e) {                    e.printStackTrace();                }
//票存在,卖票 ticket-- System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票"); ticket--; } } }}
复制代码

线程安全问题原理分析


线程安全问题产生原理图

线程安全问题产生原理图

分析:线程安全问题正常是不允许产生的,我们可以让一个线程在访问共享数据的时候,无论是否失去了 cpu 的执行权;让其他的线程只能等待,等待当前线程卖完票,其他线程在进行卖票。

解决线程安全问题办法 1-synchronized 同步代码块

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

使用 synchronized 同步代码块格式:

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

代码实现如下:

public class Demo01Ticket {    public static void main(String[] args) {        //创建Runnable接口的实现类对象        RunnableImpl run = new RunnableImpl();        //创建Thread类对象,构造方法中传递Runnable接口的实现类对象        Thread t0 = new Thread(run);        Thread t1 = new Thread(run);        Thread t2 = new Thread(run);        //调用start方法开启多线程        t0.start();        t1.start();        t2.start();    }}
public class RunnableImpl implements Runnable{ //定义一个多个线程共享的票源 private int ticket = 100;
//创建一个锁对象 Object obj = new Object();
//设置线程任务:卖票 @Override public void run() { //使用死循环,让卖票操作重复执行 while(true){ //同步代码块 synchronized (obj){ //先判断票是否存在 if(ticket>0){ //提高安全问题出现的概率,让程序睡眠 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
//票存在,卖票 ticket-- System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票"); ticket--; } } } }}
复制代码

注意:

  1. 代码块中的锁对象,可以使用任意的对象。

  2. 但是必须保证多个线程使用的锁对象是同一个。

  3. 锁对象作用:把同步代码块锁住,只让一个线程在同步代码块中执行。

同步技术原理分析


同步技术原理:


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

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


  • t0 抢到了 cpu 的执行权,执行 run 方法,遇到 synchronized 代码块这时 t0 会检查 synchronized 代码块是否有锁对象

发现有,就会获取到锁对象,进入到同步中执行

  • t1 抢到了 cpu 的执行权,执行 run 方法,遇到 synchronized 代码块这时 t1 会检查 synchronized 代码块是否有锁对象

发现没有,t1 就会进入到阻塞状态,会一直等待 t0 线程归还锁对象,t0 线程执行完同步中的代码,会把锁对象归 还给同步代码块 t1 才能获取到锁对象进入到同步中执行

总结:同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步。

解决线程安全问题办法 2-synchronized 普通同步方法

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

格式:

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

代码实现:

    public /**synchronized*/ void payTicket(){        synchronized (this){            //先判断票是否存在            if(ticket>0){                //提高安全问题出现的概率,让程序睡眠                try {                    Thread.sleep(10);                } catch (InterruptedException e) {                    e.printStackTrace();                }
//票存在,卖票 ticket-- System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票"); ticket--; } }
}
复制代码

分析:定义一个同步方法,同步方法也会把方法内部的代码锁住,只让一个线程执行。

同步方法的锁对象是谁?

就是实现类对象 new RunnableImpl(),也是就是 this,所以同步方法是锁定的 this 对象。

解决线程安全问题办法 3-synchronized 静态同步方法

同步方法:使用 synchronized 修饰的方法,就叫做同步方法,保证 A 线程执行该方法的时候,其他线程只能在方法外等着。对于 static 方法,我们使用当前方法所在类的字节码对象(类名.class)。

格式:

public static synchronized void payTicket(){可能会出现线程安全问题的代码(访问了共享数据的代码)}

代码实现:

    public static /**synchronized*/ void payTicketStatic(){        synchronized (RunnableImpl.class){            //先判断票是否存在            if(ticket>0){                //提高安全问题出现的概率,让程序睡眠                try {                    Thread.sleep(10);                } catch (InterruptedException e) {                    e.printStackTrace();                }
//票存在,卖票 ticket-- System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票"); ticket--; } }
}
复制代码

分析:静态的同步方法锁对象是谁?

不能是 this,this 是创建对象之后产生的,静态方法优先于对象

静态方法的锁对象是本类的 class 属性–>class 文件对象(反射)。

解决线程安全问题办法 4-Lock 锁

Lock 接口中的方法:

  • public void lock() :加同步锁。

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

使用步骤:

  1. 在成员位置创建一个 ReentrantLock 对象

  2. 在可能会出现安全问题的代码前调用 Lock 接口中的方法 lock 获取锁

  3. 在可能会出现安全问题的代码后调用 Lock 接口中的方法 unlock 释放锁

代码实现:

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

分析:java.util.concurrent.locks.Lock 接口

Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。相比 Synchronized,ReentrantLock 类提供了一些高级功能,主要有以下 3 项:

  1. 等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于 Synchronized 来说可以避免出现死锁的情况。通过 lock.lockInterruptibly()来实现这个机制。

  2. 公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized 锁非公平锁,ReentrantLock 默认的构造函数是创建的非公平锁,可以通过参数 true 设为公平锁,但公平锁表现的性能不是很好。

公平锁、非公平锁的创建方式:

//创建一个非公平锁,默认是非公平锁Lock lock = new ReentrantLock();Lock lock = new ReentrantLock(false); //创建一个公平锁,构造传参trueLock lock = new ReentrantLock(true);
复制代码
  1. 锁绑定多个条件,一个 ReentrantLock 对象可以同时绑定多个对象。ReenTrantLock 提供了一个 Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像 synchronized 要么随机唤醒一个线程要么唤醒全部线程。

ReentrantLock 和 Synchronized 的区别

相同点:

它们都是加锁方式同步;

都是重入锁;

阻塞式的同步;也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善); ** **


不同点 | SynChronized | ReentrantLock(实现了 Lock 接口) |


| — | — | — |


| 原始构成 | 它是 java 语言的关键字,是原生语法层面的互斥,需要 jvm 实现 | 它是 JDK 1.5 之后提供的 API 层面的互斥锁类 |


| 实现 | 通过 JVM 加锁解锁 | api 层面的加锁解锁,需要手动释放锁。 |

| 代码编写 | 采用 synchronized 不需要用户去手动释放锁,当 synchronized 方法或者 synchronized 代码块执行完之后,系统会自动让线程释放对锁的占用,更安全, | 而 ReentrantLock 则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。需要 lock()和 unlock()方法配合 try/finally 语句块来完成, |

| 灵活性 | 锁的范围是整个方法或 synchronized 块部分 | Lock 因为是方法调用,可以跨方法,灵活性更大 |


| 等待可中断 | 不可中断,除非抛出异常(释放锁方式:


1.代码执行完,正常释放锁;


2.抛出异常,由 JVM 退出等待) | 持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,(方法:

1.设置超时方法 tryLock(long timeout, TimeUnit unit),时间过了就放弃等待;


2.lockInterruptibly()放代码块中,调用 interrupt()方法可中断,而 synchronized 不行)


点击关注,第一时间了解华为云新鲜技术~

发布于: 刚刚阅读数: 2
用户头像

提供全面深入的云计算技术干货 2020-07-14 加入

生于云,长于云,让开发者成为决定性力量

评论

发布
暂无评论
多线程安全问题原理和4种解决办法_开发_华为云开发者联盟_InfoQ写作社区