二、多线程同步问题

一、线程安全问题

  • 同一个账户,假如两个客户同时在不同窗口取钱,若余额1000,A取了800,B也取了800,不同窗口没有及时同步数据,可能就会实际支出了1600,而这是不允许发生的;
  • 这种不安全的问题并不是一定会发生,取决于线程的具体调度,但是一旦发生,就是灾难性的,必须在代码层面解决;

账户类

public class Account {
     
    private double balance;
    private String name;
}

客户线程类

public class DrawThread extends Thread{
     

    private Account account;
    private double drawAccount;
    private String name;
    
    public DrawThread(String name, Account account ,double drawAccount){
     
        // 给线程赋值名字
        super(name);
        this.account = account;
        this.drawAccount = drawAccount;
    }
    
    @Override
    public void run() {
     
        if (drawAccount<account.getBalance()){
     
            
            // 通过该线程休眠,创造可能引发多线程不安全的场景
            try{
     
                Thread.sleep(1000);
            }catch (Exception e){
     
                System.out.println("线程休眠异常");
            }

            account.setBalance(account.getBalance()- drawAccount);

            // 两个人统一扣完钱稍等一下再输出结果
            try{
     
                Thread.sleep(1000);
            }catch (Exception e){
     
                System.out.println("线程休眠异常");
            }

            System.out.println(this.getName()+ "取钱"+ drawAccount + ",余额为" + account.getBalance());
        }else{
     
            System.out.println(this.getName()+ "取钱失败" +"余额为:" + account.getBalance());
        }
    }
}

测试类

public class Test {
     
    public static void main(String[] args) {
     
        // 1 新建一个公共银行卡账号,供几个线程共同使用
        Account account = new Account(1000,"王家");
        DrawThread firstCustomer = new DrawThread("老王",account,800);
        DrawThread secondCustomer = new DrawThread("小王",account,800);
        firstCustomer.start();
        secondCustomer.start();
    }
}
  • 老王进入能够取钱的判断后,在服务员准备的时候,老王突然要上厕所;
  • 小王这个时候也来取钱,也能够进入取钱的业务;
  • 老王上完厕所,取走了800元,小王也取走了800,就导致余额成为-600;

二、同步监视器synchronized

1、同步方法

  • 多个线程同时修改一个共享资源时,就有可能造成线程不安全;
  • 同步监视器:synchronized(Object obj){-------}:要锁定的对象
  • 线程在执行前,必须先获得同步监视器锁定,该线程结束后,会释放该同步监视器,任何时候只有一个线程获得同步监视器的锁定;
  • 当于找了一个门卫替你看着,你上厕所的时候不让别人进来;

客户线程类

  • 只需要锁定Account资源就可以了;
// 取钱的线程
public class DrawThread extends Thread{
     
    private Account account;
    private double drawAccount;
    private String name;

    public DrawThread(String name, Account account ,double drawAccount){
     
        // 给线程赋值名字
        super(name);
        this.account = account;
        this.drawAccount = drawAccount;
    }

    @Override
    public void run() {
     
        // 锁定account对象: 只需要锁定Account资源就可以了;
        synchronized (account){
     
            if (drawAccount<account.getBalance()){
     
                // 正在等待取钱结果,突然想去拉屎
                try{
     
                    Thread.sleep(1000);
                }catch (Exception e){
     
                    System.out.println("线程休眠异常");
                }

                account.setBalance(account.getBalance()- drawAccount);

                // 扣完钱稍等一下再输出结果
                try{
     
                    Thread.sleep(1000);
                }catch (Exception e){
     
                    System.out.println("线程休眠异常");
                }

                System.out.println(this.getName()+ "取钱"+ drawAccount + ",余额为" + account.getBalance());
            }else{
     
                System.out.println(this.getName()+ "取钱失败" +"余额为:" + account.getBalance());
            }
        }

    }
}

2、同步代码块

账户类

public class Account {
     
    private String name;
    private double balance;

   //  setter及getter省略
   
    /**提供一个线程安全的方法,来修改balance的值,类似set方法*/
    public synchronized void drawMoney(double drawMoney,String threadName){
     
        if (drawMoney< this.balance){
     
            try{
     
                Thread.sleep(1000);
            }catch (Exception e){
     
                System.out.println("线程休眠异常");
            }

            this.balance = this.balance - drawMoney;

            try{
     
                Thread.sleep(1000);
            }catch (Exception e){
     
                System.out.println("线程休眠异常");
            }

            System.out.println(threadName+"取钱" + drawMoney + "余额为" + this.balance);
        }else{
     
            System.out.println(threadName+"取钱失败,余额为"+ this.balance);
        }
    }

 
}

客户线程类

public class WangThread extends Thread{
     
    private String name;
    private Account account;
    private double drawMoney;

    public WangThread(String name, Account account, double drawMoney) {
     
        this.name = name;
        this.account = account;
        this.drawMoney = drawMoney;
    }

    @Override
    public void run() {
     
        account.drawMoney(drawMoney,name);
    }
}

测试类

public class Test {
     
    public static void main(String[] args) {
     
        Account account = new Account("王氏家族",10000);
        WangThread little = new WangThread("小王",account,8000);
        WangThread old = new WangThread("老王",account,8000);
        little.start();
        old.start();
    }
}
  • 可变类和不可变类: 若一个类中的属性是不可变的,则为可变类,若属性不可变则为不可变类;
  • 只有可变类可能引发并发异常,即多个线程修改引发的并发问题,该类为线程不安全的类;
  • 只要将该属性的访问方法设置成同步的即可(如账户余额),这种设计也更符合面向对象思想,自己属性的值,就应该由自己的内部方法来修改;
  • 对于可变类,实现多线程环境,是以牺牲效率作为代价换来安全性能的;
  • 如果可变类存在两种运行环境,则可以提供两个版本,一个版本用于单线程环境,一个版本用于多线程环境;
  • StringBuffer:单线程环境; StringBuilder:多线程环境;

监视器的释放

  • 监视器一般用在同步方法或者同步代码块中,以下简称为A代码块

释放

  • 当前线程的A代码块执行完毕;
  • 当前线程的A代码遇到了break,return等终止了该处代码;
  • 当前线程的A代码出现了未处理的Error或者Exception;
  • 当前线程的A代码中出现了同步监视器的wait(),当前线程暂停并释放同步监视器;

不释放

  • 调用sleep,yield方法暂停当前线程,线程并不会释放同步监视器;
  • 其他线程调用了该线程的suspend方法将该线程挂起,该线程不会释放同步监视器;尽量避免用suspend和resume来控制线程;

三、同步锁Lock

  • java 5开始的锁,相比同步监视器,功能更加强大,作用更加灵活;
  • 实现了对共享资源的同步安全性能的保障;
public class Account {
     
    private String name;
    private double balance;

     // setter及getter方法省略
     
    /** 1. 新建锁对象并锁住;
        2. 要同步的代码放在try块中;
        3.try后跟finally,在finally第一行要释放锁; */

     // 新建锁对象
    private final ReentrantLock lock = new ReentrantLock();

   
    /**提供一个线程安全的方法,来修改balance的值,类似set方法*/
    public  void drawMoney(double drawMoney,String threadName){
     // 1 加锁
        lock.lock();
        // 2 try 块
        try{
     
            if (drawMoney< this.balance){
     
                try{
     
                    Thread.sleep(1000);
                }catch (Exception e){
     
                    System.out.println("线程休眠异常");
                }

                this.balance = this.balance - drawMoney;

                try{
     
                    Thread.sleep(1000);
                }catch (Exception e){
     
                    System.out.println("线程休眠异常");
                }

                System.out.println(threadName+"取钱" + drawMoney + "余额为" + this.balance);
            }else{
     
                System.out.println(threadName+"取钱失败,余额为"+ this.balance);
            }
        }
        // 3 finally块
        finally{
     
              //释放锁 
              lock.unlock();
        }
    }

}

你可能感兴趣的:(3.,Java多线程及实战)