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

多线程,安全,问题,原理,解决办法 · 浏览次数 : 89

小编点评

**线程安全问题解决方案** **1. Lock锁** 使用`ReentrantLock`类实现线程锁,锁对象是`this`对象。 **2. Sync方法** 使用`synchronized`关键字同步方法,锁对象是`this`对象。 **3. Static同步方法** 使用`static`关键字同步方法,锁对象是`class`对象。 **4. Lock锁接口** 使用`Lock`接口实现锁操作,提供等待可中断等功能。 **注意事项** *锁对象必须是可重入的,否则会导致死锁。 *锁释放时必须调用`unlock()`方法,释放锁对象。 *锁的持有时间必须小于同步块的锁持有时间。

正文

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

本文分享自华为云社区《多线程安全问题原理和解决办法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);
 //创建一个公平锁,构造传参true
Lock lock = new ReentrantLock(true);

3.锁绑定多个条件,一个ReentrantLock对象可以同时绑定多个对象。ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

ReentrantLock和Synchronized的区别

相同点:

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

不同点:

 

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

与多线程安全问题原理和4种解决办法相似的内容:

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

摘要:多线程访问了共享的数据,会产生线程安全问题。 本文分享自华为云社区《多线程安全问题原理和解决办法Synchronized和ReentrantLock使用与区别》,作者:共饮一杯无。 线程安全问题概述 卖票问题分析 单窗口卖票 一个窗口(单线程)卖100张票没有问题单线程程序是不会出现线程安全问

[转帖]多线程安全问题原理和解决办法Synchronized和ReentrantLock使用与区别

https://bbs.huaweicloud.com/blogs/386388?utm_source=oschina&utm_medium=bbs-ex&utm_campaign=other&utm_content=content 【摘要】 线程安全问题概述 卖票问题分析单窗口卖票一个窗口(单线程

学了这么久的高并发编程,连Java中的并发原子类都不知道?

摘要:保证线程安全是 Java 并发编程必须要解决的重要问题,本文和大家聊聊Java中的并发原子类,看它如何确保多线程的数据一致性。 本文分享自华为云社区《学了这么久的高并发编程,连Java中的并发原子类都不知道?这也太Low了吧》,作者:冰 河。 今天我们一起来聊聊Java中的并发原子类。在 ja

京东云开发者|经典同态加密算法Paillier解读 - 原理、实现和应用

随着云计算和人工智能的兴起,如何安全有效地利用数据,对持有大量数字资产的企业来说至关重要。同态加密,是解决云计算和分布式机器学习中数据安全问题的关键技术,也是隐私计算中,横跨多方安全计算,联邦学习和可信执行环境多个技术分支的热门研究方向。 本文对经典同态加密算法Pailier算法及其相关技术进行介绍,重点分析了Paillier的实现原理和性能优化方案,同时对基于公钥的加密算法中的热门算法进行了横向

[转帖]Redis客户端Jedis、Lettuce、Redisson

https://www.jianshu.com/p/90a9e2eccd73 在SpringBoot2.x之后,原来使用的jedis被替换为了lettuce Jedis:采用的直连,BIO网络模型 Jedis有一个问题:多个线程使用一个连接的时候线程不安全。 解决思路是: 使用连接池,为每个请求创建

Java面试题:Spring Bean线程安全?别担心,只要你不写并发代码就好了!

Spring Bean是单例模式,即在整个应用程序上下文中只有一个实例。在多线程环境下,Singleton Scope Bean可能会发生线程安全问题。Spring Bean是否线程安全取决于Bean的作用域和Bean本身的实现。在使用Singleton Scope Bean时需要特别注意线程安全问...

Rust中的并发性:Sync 和 Send Traits

在并发的世界中,最常见的并发安全问题就是数据竞争,也就是两个线程同时对一个变量进行读写操作。但当你在 Safe Rust 中写出有数据竞争的代码时,编译器会直接拒绝编译。那么它是靠什么魔法做到的呢? 这就不得不谈 Send 和 Sync 这两个标记 trait 了,实现 Send 的类型可以在多线程

ThreadLocal 核心源码分析

ThreadLocal 简介 多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证和规避多线程访问出现线程不安全的方

Java并发Map的面试指南:线程安全数据结构的奥秘

简介 在计算机软件开发的世界里,多线程编程是一个重要且令人兴奋的领域。然而,与其引人入胜的潜力相伴而来的是复杂性和挑战,其中之一就是处理共享数据。当多个线程同时访问和修改共享数据时,很容易出现各种问题,如竞态条件和数据不一致性。 本文将探讨如何在Java中有效地应对这些挑战,介绍一种强大的工具——并

JAVA多线程并发编程-避坑指南

本篇旨在基于编码规范、工作中积累的研发经验等,整理在多线程开发的过程中需要注意的部分,比如不考虑线程池参数、线程安全、死锁等问题,将会存在潜在极大的风险。并且对其进行根因分析,避免每天踩一坑,坑坑不一样。