线程同步
系统的线程调度有一定的随机性,当多个线程访问同一个数据时,不作处理的话,很容易出现线程安全问题,我们可以通过线程同步来解决这个问题。
我们用银行取钱的场景的来模拟这种情况:
银行类,可以设置账户上的余额,提供一个取钱的方法,只要没有超过账户上的钱就扣除对应的钱数:
import com.sun.scenario.effect.impl.prism.PrImage;
/**
* Created by liujiawei on 2018/7/7.
*/
public class Bank{
private int balance; //账户余额
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
public void getMoney(String name, int money){
balance = balance - money;
if(balance > 0){
System.out.println(name + "取了" + money + ",还剩" + balance);
setBalance(balance);
}else{
System.out.println(name + "想取" + money +",余额不足");
}
}
}
操作类,用来模拟多个人取钱的操作,sleep(500)模拟取钱的耗时:
package com.ljw.Thread.BankGetMoney;
/**
* Created by liujiawei on 2018/7/8.
*/
public class GetMoney implements Runnable{
Bank bank;
int count;
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public Bank getBank() {
return bank;
}
public void setBank(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
try {
bank.getMoney(Thread.currentThread().getName(),count);
Thread.currentThread().sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试类:
package com.ljw.Thread.BankGetMoney;
/**
* Created by liujiawei on 2018/7/7.
*/
public class TestBank {
public static void main(String[] args) {
Bank bank = new Bank();
bank.setBalance(1000);
GetMoney getMoney = new GetMoney();
getMoney.setBank(bank);
getMoney.setCount(600);
GetMoney getMoney2 = new GetMoney();
getMoney2.setBank(bank);
getMoney2.setCount(600);
Thread thread = new Thread(getMoney,"甲");
Thread thread2 = new Thread(getMoney2,"乙");
thread.start();
thread2.start();
}
}
账户余额设置为了1000,两个线程每次都是取600,正常情况下应该是一个取完钱,另一个就会提示余额不足,我们看下运行结果是不是也是这样:
看起来好像没有问题,但是涉及到金钱都是需要比较谨慎的,我们再多运行几次:
可以看到,其中有一次提示两次都取钱成功了,账户上只剩了-200元,这就是我们上面所说到的线程安全问题。
同步代码块:
为了解决多个线程操作同一个数据可能会带来的线程安全问题,java中引入了同步监视器。使用同步监视器的通用方法是同步代码块:
synchronized (obj){
//对obj的操作
}
对可能会被多个线程同时访问的资源加上synchronized关键字,当线程访问被synchronized修饰的资源时,它会获取到对应的同步监视器,此时其他线程访问该资源时,就会因为无法获取同步监视器而操作不了数据。
到了这里,我们对上面取钱会出现的那种情况已经有了解决方法,只要对访问的资源加上synchronized就行了,因为synchronized用来修饰对象,所以我们稍微修改下getMoney() 方法:
public void getMoney(String name, int money) {
synchronized (this) {
this.balance = this.balance - money;
if (this.balance > 0) {
System.out.println(name + "取了" + money + ",还剩" + this.balance);
setBalance(this.balance);
} else {
System.out.println(name + "想取" + money + ",余额不足");
}
}
}
我们对当前银行这个对象做同步监视,通过this.balance的方式获取账户余额,重新运行下我们的测试类:
从运行结果中可以看到,再也没有出现过余额为负的情况的出现。
通过同步代码块,线程访问指定资源时,会按照加锁-操作数据-释放锁这样的顺序来进行。
同步方法:
除了同步代码块以外,java还提供了同步方法用来保证线程安全。使用方法就是用synchronized关键字来修饰方法(非static方法),同步方法不需要显示指定同步监视器,她的同步监视器是this对象。使用同步方法来对银行取钱的代码进行改造,修改getMoney()方法:
public synchronized void getMoney(String name, int money) {
this.balance = this.balance - money;
if (this.balance > 0) {
System.out.println(name + "取了" + money + ",还剩" + this.balance);
setBalance(this.balance);
} else {
System.out.println(name + "想取" + money + ",余额不足");
}
}
重新运行我们的测试类:
可以看到,使用同步方法一样解决了线程安全的问题。
同步监视器的释放
线程在进入同步代码块或者同步方法之前,都需要获取对同步监视器的锁定,但是程序不会显示释放同步监视器的锁定,线程会在以下几种情况释放同步监视器的锁定:(1) 同步方法/同步代码块内的方法正常执行完毕;
(2)同步方法/同步代码块内有break/return等可以结束方法;
(3)同步方法/同步代码块内有未捕获的Exception或Error;
(4)同步方法/同步代码块内调用了同步监视器的wait()方法;
但是以下情况不会释放同步监视器的锁定:
(1) 调用线程的sleep()和yield()方法不会释放同步监视器的锁定;
(2)其他线程调用该线程的suspend()方法也不会释放同步监视器的锁定;
同步锁
除了同步监视器,java还有一种机制可以保证线程安全,通过同步锁对象(Lock)显式的加锁/释放锁来实现。Lock比synchronized更加灵活,可以支持属性差别很大的数据,还可以支持多个相关的condition对象(有关condition的介绍在后面)。
通常使用最多的锁是重入锁(ReentrantLock),我们看下如果用ReentrantLock对象来修改之前的代码是怎么做的:
ReentrantLock lock = new ReentrantLock();
public void getMoney(String name, int money) {
lock.lock();
try {
this.balance = this.balance - money;
if (this.balance > 0) {
System.out.println(name + "取了" + money + ",还剩" + this.balance);
setBalance(this.balance);
} else {
System.out.println(name + "想取" + money + ",余额不足");
}
}
finally {
lock.unlock();
}
}
首先需要声明一个ReentrantLock对象,在需要实现线程安全的地方通过lock()方法加上锁,在结束的地方通过unlock() 方法释放锁,重入锁通常会和tryfinally代码块结合使用,保证同步锁的释放。
重入锁支持多个锁同时存在,只要全部显式释放就可以,如下:
ReentrantLock lock = new ReentrantLock();
public void getMoney(String name, int money) {
lock.lock();
try {
lock.lock();
this.balance = this.balance - money;
if (this.balance > 0) {
System.out.println(name + "取了" + money + ",还剩" + this.balance);
setBalance(this.balance);
} else {
System.out.println(name + "想取" + money + ",余额不足");
}
}
finally {
lock.unlock();
lock.unlock();
}
}
死锁
当两个线程互相等待对方释放同步监视器的时候,这个时候就会发生死锁,没有报错,没有异常,但是所有的线程都进入了阻塞状态,我们编写代码时,要尽量编码死锁的情况。Thread的suspend()将线程挂起,就很容易导致死锁,所以不建议使用suspend()。