3-多线程安全问题,线程同步

Java
336
0
0
2022-12-13
标签   Java多线程

线程安全问题

指的是多个线程操作同一个共享资源的时候可能会出现的线程安全问题

银行取钱问题

多个用户同时操作一个银行账户进行取钱操作,账户内余额为1000元,每个账户同时取出1000元,由于线程执行的随机性就会导致线程安全问题的产生

银行账户类

定义账户内容,定义取钱方法

package ThreadSafety;


//定义账户类
public class Account {
    private int cardID;
    private double Money;

    //定义取钱过程,所有操作这个账户的人都会取走账户中的m元钱 
    public void DrawMoney(int m){
        //判断取钱的用户
        String name =Thread.currentThread().getName();
        //判断余额是否充足 
        if(Money>=m){
            //输出取钱结果
            System.out.println(name+"用户执行取钱操作,余额充足,支付"+m+"元成功!");
            //更新账户余额
            Money-=m;
        }else{
            //余额不足,取钱失败
            System.out.println(name+"用户执行取钱操作,余额不足,支付失败!");
        }
        System.out.println(name+"用户结束操作,余额"+Money+"元");
    }


    public Account() {
    }

    public Account(int cardID, double money) {
        this.cardID = cardID;
        Money = money;
    }

    public int getCardID() {
        return cardID;
    }

    public void setCardID(int cardID) {
        this.cardID = cardID;
    }

    public double getMoney() {
        return Money;
    }

    public void setMoney(double money) {
        Money = money;
    }
}

线程类

(执行取钱操作)

package ThreadSafety;


//线程类:将取钱行为看作是一条单独的线程创建
public class DrawThread extends Thread{

    //定义一个成员变量,接收账户对象 
    private Account acc;

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

    @Override 
    public void run() {
        //执行取钱操作,每个用户取1000元
        acc.DrawMoney(1000);
    }
}

开始执行

多用户共同取钱

package ThreadSafety;

public class Demo1 {


    public static void main(String[] args) {

        //创建一个允许共享的银行账户(可能会有多人同时使用) 
        Account acc = new Account(111, 1000);
        //创建多个线程去银行账户中取钱(多个线程操作一个资源) 
        Thread user1 = new DrawThread(acc,"user1");
        user1.start();
        Thread user2 = new DrawThread(acc,"user2");
        user2.start();

    }

}

运行结果

违反了正常取钱过程,存在严重安全问题

user1用户执行取钱操作,余额充足,支付1000元成功!
user2用户执行取钱操作,余额充足,支付1000元成功!
user1用户结束操作,余额0.0元
user2用户结束操作,余额-1000.0元

线程同步

也称同步代码块

作用

为了更好的解决线程安全问题的方案

线程同步解决线程安全问题的核心思想

让多个线程实现先后依次访问共享资源,这样就解决了安全问题

做法

将共享资源上锁,每次只能一个线程进入访问完毕之后,其他线程才能进来

三种同步(上锁)方式

  1. 同步代码块
  2. 同步方法
  3. Lock显式锁

同步代码块

作用

将出现线程安全问题的核心代码上锁,每次只允许一个线程进入,执行完毕之后自动解锁,其他线程才能进来执行

格式

synchronized(锁对象){
    // 访问共享资源的核心代码
}

锁对象

理论上可以是任意的“唯一”对象即可

原则上

锁对象建议使用共享资源

- 在实例方法中建议使用this作为锁对象,此时this正好是共享资源(使用前提代码必须高度面向对象)
- 在静态方法中建议使用**类名.class**字节码作为锁对象

将上文中的取款行为进行上锁操作

public void DrawMoney(int m){
    //判断取钱的用户
    String name =Thread.currentThread().getName();
    //判断余额是否充足
    
    //将取款行为上锁(使用this表示锁住的对象是当前账户)
    synchronized (this) {
        if (Money >= m) {
            //输出取钱结果
            System.out.println(name + "用户执行取钱操作,余额充足,支付" + m + "元成功!");
            //更新账户余额
            Money -= m;
        } else {
            //余额不足,取钱失败
            System.out.println(name + "用户执行取钱操作,余额不足,支付失败!");
        }
        System.out.println(name + "用户结束操作,余额" + Money + "元");
    }
}

上锁后的运行结果

user1用户执行取钱操作,余额充足,支付1000元成功!
user1用户结束操作,余额0.0元
user2用户执行取钱操作,余额不足,支付失败!
user2用户结束操作,余额0.0元

同步方法

作用

把出现线程安全问题的和新方法锁起来,每次只允许一个线程进入访问,其他线程必须在方法外面等待

使用方法

直接给方法加上修饰符synchronized

原理

同步方法的原理和同步代码块的底层原理其实是完全一样的,只是同步方法是把整个方法的代码都锁起来的。

同步方法的底层也是有锁对象的:

  • 在实例方法中默认使用this作为锁对象
  • 在静态方法中默认使用类名.class字节码作为锁对象

Lock显式锁

java.util.current.locks.lock机制提供了比synchronized代码块和synchronized同步方法更广泛的锁定操作

二者具有的功能Lock都有,除此之外功能更加强大

Lock锁也称同步锁

其将加锁与释放锁的过程方法化了,如下:

  • 加锁:public void lock()
  • 释放锁:public void unlock()

代码实现

package ThreadSafety;


import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

//定义账户类
public class Account {
    private int cardID;
    private double Money;
    //在账户中创建锁对象 
    //由于账户对象对于某个调用者是唯一的,所以锁对象对于某个调用者也是唯一的 
    private final Lock lock=new ReentrantLock();  //加上final关键字是防止锁的唯一性被破坏,不加一般也可以正常运行


    public void DrawMoney(int m){
        String name =Thread.currentThread().getName();

        lock.lock();  //上锁 
        try {
            if (Money >= m) {
                System.out.println(name + "用户执行取钱操作,余额充足,支付" + m + "元成功!");
                Money -= m;
            } else {
                System.out.println(name + "用户执行取钱操作,余额不足,支付失败!");
            }
            System.out.println(name + "用户结束操作,余额" + Money + "元");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();   //解锁    
        }
        /*
        * 在这里将整个执行代码放到try...catch结构里的原因是避免锁死的现象产生
        * 如果不用try...catch直接将解锁放到顺序执行的代码中,一旦代码报错程序中止运行
        * 就会导致解锁代码无法执行,对象被锁死
        * 所以采用try...catch并将解锁代码放到一定执行的finally中就可以避免这种情况
        * 即使报错,解锁行为也会正常运行
        * */
    }

    public Account() {
    }

    public Account(int cardID, double money) {
        this.cardID = cardID;
        Money = money;
    }

    public int getCardID() {
        return cardID;
    }

    public void setCardID(int cardID) {
        this.cardID = cardID;
    }

    public double getMoney() {
        return Money;
    }

    public void setMoney(double money) {
        Money = money;
    }
}

总结:

  • 线程安全的程序,性能差
  • 线程不安全的程序,性能较好。在开发中假如并不会存在多线程安全问题,一般采用线程不安全的设计