多线程-安全性问题的解决

Posted TGB-Earnest

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程-安全性问题的解决相关的知识,希望对你有一定的参考价值。

多线程的安全性问题解析

我们在使用多线程的时候,可以进行异步操作提高我们程序执行的效率,比如,我们一个页面需要3个接口,后端返回的时候将这个三个接口统一成一个接口执行,这三个接口在这个大接口中,如果按串行的方式执行,那么时间就是这三个接口所需时间之和,如果我们采用多线程的方式,那么返回的时间取决于这三个接口中所用时间最大的接口。
我们再来看一下他的问题。


package com.broky.multiThread.safeThread;

/**
 * @author zxd
 * @date 2022-04-26 20:39
 */
public class SafeTicketsWindow 
    public static void main(String[] args) 
        WindowThread ticketsThread02 = new WindowThread();
        Thread t1 = new Thread(ticketsThread02);
        Thread t2 = new Thread(ticketsThread02);
        Thread t3 = new Thread(ticketsThread02);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    


class WindowThread implements Runnable 
    private int tiketsNum = 100;

    public void run() 
        while (true) 
            if (tiketsNum > 0) 
                try 
                    //手动让线程进入阻塞,增大错票概率
                    Thread.sleep(100);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
                System.out.println(Thread.currentThread().getName() + ":\\t票号:" + tiketsNum);
                /*try 
                    //手动让线程进入阻塞,增大重票的概率
                    Thread.sleep(100);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                */
                tiketsNum--;
             else 
                break;
            
        
    

打印的结果

窗口3:	票号:10
Thread-2:	票号:10
Thread-1:	票号:8
Thread-2:	票号:7
Thread-1:	票号:7
窗口3:	票号:7
Thread-2:	票号:4
窗口3:	票号:4
Thread-1:	票号:4
Thread-1:	票号:1
窗口3:	票号:1
Thread-2:	票号:1

我们发现有重复消费的现象,一个票销售给了两个人

分析:
我们发现执行结果中有重票的情况,

如果t1在输出票号7和票数进行减一的操作之前被阻塞,这就导致这时候t1卖出了7好票,但是总票数没有减少,在t1被阻塞期间,如果t2运行到输出票号时,
那么t2也会输出和t1相同的票号7

通过以上两种情况可以看出,线程的安全性问题时因为多线程正在执行代码的过程中,并且尚未完成的时候,其他线程参与进来执行代码所导致的。

当然还有可能输出的是负值的情况。

多线程安全性问题的解决

原理:

当一个线程在操作共享数据的时候,其他线程不能参与进来。直到这个线程操作完共享数据的时候,其他线程才可以从操作,即使当这个线程操作共享数据的时候发生了阻塞,依旧无法改变这种情况。

在Java中,我们通过同步机制,来解决线程的安全问题

多线程安全问题的解决方式一: 同步代码块

synchronized(同步监视器)需要被同步的代码

说明:
1、操作共享数据(多个线程共同操作的变量)的代码,即为需要被同步的代码。 不能多包涵代码(效率低,如果包到while前面就变成了单线程了),也不能少包含代码
2、共享数据:多个线程共同操作的变量。
3、同步监视器:俗称,锁。任何一个类的对象都可以充当锁。但是所有的线程都必须共用一把锁,共用一个对象。

锁的选择:
1、自行创建,共用对象,如下面demo中的Object对象。
2、使用this表示当前类的对象继承Thread的方法中的锁不能使用this代替,因为继承thread实现多线程时,会创建多个子类对象来代表多个线程,这个时候this指的时当前这个类的多个对象,不唯一,无法当作锁。实现Runnable接口的方式中,this可以当作锁,因为这种方式只需要创建一个实现类的对象,将实现类的对象传递给多个Thread类对象来当作多个线程,this就是这个一个实现类的对象,是唯一的,被所有线程所共用的对象。
3、使用类当做锁,以下面demo为例,其中锁可以写为WindowThread.class ,从这里可以得出结论,类也是一个对象


package com.broky.multiThread.safeThread;

/**
 * @author zhaoxiaodong
 * @date 2022-04-26 20:39
 */
public class SafeTicketsWindow 
    public static void main(String[] args) 
        WindowThread ticketsThread02 = new WindowThread();
        Thread t1 = new Thread(ticketsThread02);
        Thread t2 = new Thread(ticketsThread02);
        Thread t3 = new Thread(ticketsThread02);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    


class WindowThread implements Runnable 
    private int tiketsNum = 100;
    
    //由于,Runnable实现多线程,所有线程共用一个实现类的对象,所以三个线程都共用实现类中的这个Object类的对象。
    Object obj = new Object();
    //如果时继承Thread类实现多线程,那么需要使用到static Object obj = new Object();
    
    public void run() 
        
        //Object obj = new Object();
        //如果Object对象在run()方法中创建,那么每个线程运行都会生成自己的Object类的对象,并不是三个线程的共享对象,所以并没有给加上锁。
        
        while (true) 
            synchronized (obj) 
                if (tiketsNum > 0) 
                    try 
                        //手动让线程进入阻塞,增大安全性发生的概率
                        Thread.sleep(100);
                     catch (InterruptedException e) 
                        e.printStackTrace();
                    
                    System.out.println(Thread.currentThread().getName() + ":\\t票号:" + tiketsNum + "\\t剩余票数:" + --tiketsNum);
                 else 
                    break;
                
            
        
    

打印的结果:


Thread-2:	票号:15
Thread-1:	票号:14
Thread-1:	票号:13
Thread-2:	票号:12
Thread-2:	票号:11
Thread-2:	票号:10
Thread-2:	票号:9
Thread-2:	票号:8
Thread-2:	票号:7
Thread-2:	票号:6
Thread-2:	票号:5
窗口3:	票号:4
窗口3:	票号:3
窗口3:	票号:2
窗口3:	票号:1

多线程安全问题的解决方式二:同步方法

将所要同步的代码放到一个方法中,将方法声明为synchronized同步方法。之后可以在run()方法中调用同步方法。
要点:
1、同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。
2、非静态的同步方法,同步监视器是:this。
3、静态的同步方法,同步监视器是:当前类本身。



package com.broky.multiThread.safeThread;

/**
 * @author zhaoxiaodong
 * @date 2022-04-26 22:39
 */
public class Window02 
    public static void main(String[] args) 
        Window02Thread ticketsThread02 = new Window02Thread();
        Thread t1 = new Thread(ticketsThread02);
        Thread t2 = new Thread(ticketsThread02);
        Thread t3 = new Thread(ticketsThread02);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    


class Window02Thread implements Runnable 
    private int tiketsNum = 100;

    @Override
    public void run() 
        while (tiketsNum > 0) 
            show();
        
    

    private synchronized void show()  //同步监视器:this
        if (tiketsNum > 0) 
            try 
                //手动让线程进入阻塞,增大安全性发生的概率
                Thread.sleep(100);
             catch (InterruptedException e) 
                e.printStackTrace();
            
            System.out.println(Thread.currentThread().getName() + ":\\t票号:" + tiketsNum + "\\t剩余票数:" + --tiketsNum);
        
    


package com.broky.multiThread.safeThread;

/**
 * @author zhaoxiaodong
 * @date 2022-04-26 9:36
 */
public class Window03 
    public static void main(String[] args) 
        Window03Thread t1 = new Window03Thread();
        Window03Thread t2 = new Window03Thread();
        Window03Thread t3 = new Window03Thread();
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.setPriority(Thread.MIN_PRIORITY);
        t3.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        t2.start();
        t3.start();
    


class Window03Thread extends Thread 
    public static int tiketsNum = 100;

    @Override
    public void run() 
        while (tiketsNum > 0) 
            show();
        
    

    public static synchronized void show() //同步监视器:Winddoe03Thread.class  不加static话同步监视器为t1 t2 t3所以错误
        if (tiketsNum > 0) 
            try 
                //手动让线程进入阻塞,增大安全性发生的概率
                Thread.sleep(100);
             catch (InterruptedException e) 
                e.printStackTrace();
            
            System.out.println(Thread.currentThread().getName() + ":\\t票号:" + tiketsNum + "\\t剩余票数:" + --tiketsNum);
        
    

使用同步解决懒汉模式的线程安全问题


package com.broky.multiThread.safeThread;

/**
 * @author zhaoxiaodong
 * @date 2022-04-26 9:36
 */
public class BankTest 


class Bank 
    private Bank() 
    

    private static Bank instance = null;

    public static Bank getInstance() 
        //方式一:效率性差,每个等待线程都会进入同步代码块
        //        synchronized (Bank.class) 
        //            if (instance == null) 
        //                instance = new Bank();
        //            
        //        

        //方式二:在同步代码块外层在判断一次,就防止所有线程进入同步代码块。
        if (instance == null) 
            synchronized (Bank.class) 
                if (instance == null) 
                    instance = new Bank();
                
            
        
        return instance;
    

多线程安全问题的解决方式二:Lock 锁 -JDK5.0新特性
JDK5.0之后,可以通过实例化ReentrantLock对象,在所需要同步的语句前,调用ReentrantLock对象的lock()方法,实现同步锁,在同步语句结束时,调用unlock()方法结束同步锁

建议使用顺序:Lock—》同步代码块(已经进入了方法体,分配了相应的资源)—》同步方法(在方法体之外)


package com.broky.multiThread.safeThread;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author zhaoxiaodong
 * @date 2022-04-26 9:36
 */
public class SafeLock 
    public static void main(String[] args) 
        SafeLockThread safeLockThread = new SafeLockThread();
        Thread t1 = new Thread(safeLockThread);
        Thread t2 = new Thread(safeLockThread);
        Thread t3 = new Thread(safeLockThread);

        t1.start();
        t2.start();
        t3.start();
    


class SafeLockThread implements Runnable
    private int tickets = 100;
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() 
        while (tickets>0) 
            try 
                //在这里锁住,有点类似同步监视器
                lock.lock();
                if (tickets > 0) 
                    Thread.sleep(100);
                    System.out.println(Thread.currentThread().getName() + ":\\t票号:" + tickets + "\\t剩余票数:" + --tickets);
                
             catch (InterruptedException e) 
                e.printStackTrace();
             finally 
                //操作完成共享数据后在这里解锁
                lock.unlock();
            
        
    


持续更新中…

以上是关于多线程-安全性问题的解决的主要内容,如果未能解决你的问题,请参考以下文章

JAVA多线程安全

java-利用多线程Runnable,公用一个参数问题

多线程题

Java之多线程窗口卖票问题(Runnable)

线程安全问题

线程安全问题