一文深入理解多线程安全问题以及解决方案

推荐阅读

线程安全

线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。

  • 当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类时线程安全的。
  • 存在竞争的线程不安全,不存在竞争的线程就是安全的!

线程安全问题模拟

  • 假设,小白和小黑是一对小夫妻。他们两个共用一张银行卡(主副卡),卡里有88888元。某一天,小黑在万达商场准备买一台笔记本电脑,价格刚好是88888元,而小黑的老婆小白,也正好在万象商场看到一个包包,包包的价格也是88888元。然后,两个人各自拿着银行卡去取钱买,两人都取到了钱并且买到了自己的喜欢的东西。回家后,小白向老公炫耀,自己花了88888买的包包,而小黑也向老婆炫耀。这是,小黑突然问道:“老婆,你哪里来的钱买包包?”。小白说:“卡里取的呀”。此时,小黑一惊,卡里只有88888元,已经被自己花了。那老婆的钱哪里来的,难道???老婆外面有人了??为此两人大吵一架。后来,小黑找到了你。你怎么通过一段代码给他演示这种情况的??
  • 代码实现
  • Account .java
package thread.safe;

/**
 * @Auther Carroll
 * @Date 2020/4/5
 * @e-mail [email protected]
 *
 * 账户类
 */
public class Account {
    private String cardId;
    private double moeny;

    public void drawMoney(double moeny) {
        String name = Thread.currentThread().getName();
        if(this.moeny>=moeny){
            System.out.println(name+"来取钱,余额充足,取走:"+moeny);
            this.moeny -= moeny;
            System.out.println("余额剩余:"+this.moeny);
        }else {
            System.out.println(name+"来取钱,余额不足!");
        }
    }

    public Account() {
    }

    public Account(String cardId, double moeny) {
        this.cardId = cardId;
        this.moeny = moeny;
    }

    public String getCardId() {
        return cardId;
    }

    public void setCardId(String cardId) {
        this.cardId = cardId;
    }

    public Double getMoeny() {
        return moeny;
    }

    public void setMoeny(Double moeny) {
        this.moeny = moeny;
    }
}
  • DrawThread .java
package thread.safe;
/**
 * @Auther Carroll
 * @Date 2020/4/5
 * @e-mail [email protected]
 */
public class DrawThread extends Thread {
    private Account acc ;

    public DrawThread(Account account, String name) {
        super(name);
        acc = account;
    }

    @Override
    public void run() {
        acc.drawMoney(88888);
    }
}

  • ThreadSafe .java
package thread.safe;
/**
 * @Auther Carroll
 * @Date 2020/4/5
 * @e-mail [email protected]
 */
public class ThreadSafe {
    public static void main(String[] args) {
        Account account = new Account("ICBC-888", 88888);

        Thread t1 = new DrawThread(account,"小黑");
        t1.start();

        Thread t2 = new DrawThread(account,"小白");
        t2.start();

    }
}
  • 执行结果
    在这里插入图片描述
  • 故事的结尾:小白和小黑和好了。两人经过不懈的努力,很快就有了小猴子…

通过这个故事,我们看到了线程安全带来的严重问题。那么,我们怎么解决呢?下面我们一起看看如何解决线程安全问题。

线程同步

线程同步:当两个或两个以上线程访问同一资源时,需要某种方式来确保资源在某一时刻只被一个线程 使用 。当多个线程访问同一个数据时,容易出现线程安全问题。需要让线程同步,保证数据安全。

  • 线程同步的作用:就是为了解决线程安全问题的方案。
  • 线程同步解决线程安全问题的核心思想:让多个线程实现先后依次访问共享资源,这样就解决了安全问题。
  • 线程同步的做法:加锁,是把共享资源进行上锁,每次只能一个线程进入访问完毕以后,其他线程才能进来。
  • 线程同步的方式有三种:
    • 同步代码块。
    • 同步方法。
    • lock显示锁。

同步代码块

  • 作用:把出现线程安全问题的核心代码给上锁,每次只能一个线程进入执行完毕以后自动解锁,其他线程才可以进来执行。
  • 格式:
    synchronized(锁对象){
        // 访问共享资源的核心代码
       }
    
  • 代码实例:
synchronized (this){
            if(this.moeny>=moeny){
                System.out.println(name+"来取钱,余额充足,取走:"+moeny);
                this.moeny -= moeny;
                System.out.println("余额剩余:"+this.moeny);
            }else {
                System.out.println(name+"来取钱,余额不足!");
            }
        }
  • 锁对象:理论上可以是任意的“唯一”对象即可。
  • 原则上:锁对象建议使用共享资源。
    • 在实例方法中建议用this作为锁对象。此时this正好是共享资源!必须代码高度面向对象
    • 在静态方法中建议用类名.class字节码作为锁对象。

同步方法

  • 作用:把出现线程安全问题的核心方法给锁起来,
    每次只能一个线程进入访问,其他线程必须在方法外面等待。
  • 用法:直接给方法加上一个修饰符 synchronized.
  • 格式:
    private synchronized void makeWithdrawal(int amt) {
    
    	}
    
  • 代码实例
public synchronized void drawMoney(double moeny) {
        String name = Thread.currentThread().getName();
            if(this.moeny>=moeny){
                System.out.println(name+"来取钱,余额充足,取走:"+moeny);
                this.moeny -= moeny;
                System.out.println("余额剩余:"+this.moeny);
            }else {
                System.out.println(name+"来取钱,余额不足!");
            }
    }
  • 原理: 同步方法的原理和同步代码块的底层原理其实是完全一样的,只是同步方法是把整个方法的代码都锁起来的。
  • 同步方法其实底层也是有锁对象的:
    • 如果方法是实例方法:同步方法默认用this作为的锁对象。
    • 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。

Lock显示锁

  • java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大。
  • Lock锁也称同步锁,加锁与释放锁方法化了,如下:
    • public void lock():加同步锁。
    • public void unlock():释放同步锁。
  • 代码实例
 //创建一把锁对象
 private final Lock lock = new ReentrantLock();

 public void drawMoney(double moeny) {
     String name = Thread.currentThread().getName();
     lock.lock();  //上锁
     try{
         if(this.moeny>=moeny){
             System.out.println(name+"来取钱,余额充足,取走:"+moeny);
             this.moeny -= moeny;
             System.out.println("余额剩余:"+this.moeny);
         }else {
             System.out.println(name+"来取钱,余额不足!");
         }
     }catch (Exception e){
         e.printStackTrace();
     }finally {
         lock.unlock(); //解锁
     }
 }
  • 总结:
    • 线程安全,性能差。
    • 线程不安全性能好。假如开发中不会存在多线程安全问题,建议使用线程不安全的设计类。

Lock和synchronized的区别

  • 1.Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁
  • 2.Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 3.使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
  • 优先使用顺序
    • Lock----同步代码块(已经进入了方法体,分配了相应资源)----同步方法(在方法体之外)

你知道的越多,你不知道的越多。
有道无术,术尚可求,有术无道,止于术。
如有其它问题,欢迎大家留言,我们一起讨论,一起学习,一起进步

发布了217 篇原创文章 · 获赞 268 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qq_40722827/article/details/105323579