多线程安全问题的解决方案(多线程怎么保证线程安全)

本文分享自华为云社区《多线程安全问题原理和解决办法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);
  1. 锁绑定多个条件,一个ReentrantLock对象可以同时绑定多个对象。ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

ReentrantLock和Synchronized的区别

相同点:

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

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

华为云博客_大数据博客_AI博客_云计算博客_开发者中心-华为云

(0)

相关推荐

  • java如何多线程并发访问解决方案

    synchronized关键字主要解决多线程共享数据同步问题. ThreadLocal使用场合主要解决多线程中数据因并发产生不一致问题. ThreadLocal和Synchonized都用于解决多线程 ...

  • linux系统下openmp多线程编程示例

    关键语法: 复制代码 代码如下: #inlcude <omp.h> #pragma omp parallel for #pragma omp for reduction(+: 变量) #p ...

  • 在Linux系统上进行openmp多线程编程的方法

    OpenMP是一种多处理器多线程的编程语言,能够支持多个平台,包括Linux系统,那么Linux下要如何进行openmp多线程编程呢?一起来了解下吧。 关键语法: 代码如下: #inlcude 《om ...

  • java实现多线程编程

    操作方法 01 本文详细讲述一下Java实现多线程编程的三种方法,多线程领域里实现多线程编程的总体思路就是重构run()方法以实现自己所需的功能,然后创建线程对象调用start()方法来启动线程,然后 ...

  • JAVA线程安全:[4]volatile关键字

    volatile是java提供的一种同步手段,只不过它是轻量级的同步,为什么这么说,因为volatile只能保证多线程的内存可见性,不能保证多线程的执行有序性.而最彻底的同步要保证有序性和可见性,例如 ...

  • Java创建线程的两个方法

    操作方法 01 Java提供了线程类Thread来创建多线程的程序.其实,创建线程与创建普通的类的对象的操作是一样的,而线程就是Thread类或其子类的实例对象.每个Thread对象描述了一个单独的线 ...

  • JAVA线程安全:[1]浅谈JAVA内存模型

    关于java线程安全,网上有很多资料,我只想从自己的角度总结对这方面的考虑,有时候写东西是很痛苦的,知道一些东西,但想用文字说清楚,却不是那么容易.我认为要认识java线程安全,必须了解两个主要的点: ...

  • 线程间同步的方法

    想了解线程同步的方法就得先知道什么是线程,然后线程怎么创建的,再然后就是去实现线程的同步. 什么是线程? 线程是电脑程序运行时的最小单元,一个电脑程序运行会有一个或多个线程存在. 下面我会以java代 ...

  • Visual Basic构建线程安全的Singleton

    大抵而言,Singleton 模式应该是设计模式中相当常用的一种了.它能够节省宝贵的 CPU 或内存资源,避免不必要的创建对象开销.然而,在多线程应用中,对于那些非线程安全的数据库组件,传统的 Sin ...