一、线程安全问题
- 同一个账户,假如两个客户同时在不同窗口取钱,若余额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();
}
}
}