先来一个小例子,假设夫妻俩开了一个共同账户,然后同时去取款,丈夫取了600,妻子取了800,按照之前的思路来写就是
public class TestThread {
public static void main(String[] args) {
Account account = new Account(1000);
GetMoneyThread personA = new GetMoneyThread("丈夫", account, 600);
GetMoneyThread personB = new GetMoneyThread("妻子", account, 800);
personA.start();
personB.start();
}
}
class Account {
private int balance;// 账户余额
public Account(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
}
class GetMoneyThread extends Thread {
private int money;// 每次取钱的金额
private Account account;// 账户
private String name;// 取款人姓名
public GetMoneyThread(String name, Account account, int money) {
this.name = name;
this.account = account;
this.money = money;
}
@Override
public void run() {
this.setName(name);
if (money <= account.getBalance()) {
System.out.println(this.getName() + "成功取出 " + money + " 元 ");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
account.setBalance(account.getBalance() - money);
System.out.println("此时账户内余额为 " + account.getBalance() + " 元");
} else {
System.out.println(this.getName() + " 取款时余额不足,取款失败");
}
}
}
输出结果是
丈夫成功取出 600 元
妻子成功取出 800 元
此时账户内余额为 400 元
此时账户内余额为 -400 元
然而账户里一共才有1000块钱,却取出了1400,账户的余额变成了-400,银行岂不是亏死了,究其原因,是因为夫妻(两个线程)俩同时对账户做出操作,而卡内余额没有及时更新造成的。
一个程序中存在多个线程并没有关系,每个线程挨个儿执行,依次去占用 CPU 去执行自己的代码,你走你的阳关道我走我的独木桥,相互不冲突,这本没有问题,但是现实编程往往需要多个线程去访问同一个资源:譬如同一块内存、同一个数据库、同一个文件。
当多个线程同时访问一个资源时,因为线程的执行过程不可控(占用 CPU 的顺序由线程调度器决定以及 I/O 的延时特性),所以很容易导致运算结果和实际结果不符的情况(譬如文章一开始举的例子)。
这个就是线程安全问题
,多个线程访问同一个资源,该资源就被成为共享资源
,也有人叫它临界资源
。而修改这个资源的代码块被成为临界区
。
为了避免解决这个线程安全问题,就需要引入同步互斥
概念了,同步互斥就是在多个线程访问共享资源时,保证共享数据在同一时刻只能被一个线程使用,互斥是因,同步是果。当线程访问资源时,会给资源加一道“锁”,已经被“锁住”的资源别的线程无法访问,当该线程访问完资源之,“取消锁”,别的线程方可继续访问。
我们可以使用两种方法给资源“加锁”:
synchronized
Lock
synchronized 有两个重要含义:它确保了一次直有一个线程可以执行代码的受保护部分,而且它确保了一个线程更改的数据对于其他线程是可见的。
同步代码块的格式如下:
synchronized(obj){
...
// 此处的代码就是同步代码块
}
其中 obj 可以看作是一个监视器也可以看作是一个“锁”,任何 Java 对象都可以充当,在线程执行到同步代码块的时候,会先锁定“监视器”,再执行代码块中的内容,执行完成之后会释放对监视器的锁定,在监视器处于锁定状态的时候,其余线程无法访问代码块中的内容。
在上面取钱的那个例子中,账户就可以当作是一个监视器,在一个线程操作该账户的时候,其他线程无法同时访问。修改一下 GetMoneyThread 类:
class GetMoneyThread extends Thread {
private Account account;// 账户
private int money;// 取的钱数
private String name;// 取款人姓名
public GetMoneyThread(Account account, int money, String name) {
this.account = account;
this.money = money;
this.name = name;
}
@Override
public void run() {
this.setName(name);
synchronized (account) {// 账户对象作为同步监视器,线程执行前先锁定该对象,使得其他线程无法访问,待同步代码执行完释放锁,其他线程开始锁定同步监视器
if (account.getBalance() >= money) {
System.out.println(name + " 取了 " + money + " 元");
account.setBalance(account.getBalance() - money);
System.out.println(name + " 取款后,卡内余额为 " + account.getBalance());
} else {
System.out.println("卡内余额不足," + name + " 取款失败");
}
}
}
}
执行结果
丈夫 取了 600 元
丈夫 取款后,卡内余额为 400
卡内余额不足,妻子 取款失败
也可以直接将方法设置为 synchronized,对于普通的 synchronized 方法,他的同步监视器就是调用该方法的对象,对于静态 synchronized 方法来说,同步监视器是与 Class 对象相关的监控器。
重新修改一下之前的取款代码:
public class TestThread {
public static void main(String[] args) {
Account account = new Account(1000);
GetMoneyThread personA = new GetMoneyThread(account, 600, "丈夫");
GetMoneyThread personB = new GetMoneyThread(account, 800, "妻子");
personA.start();
personB.start();
}
}
class Account {
private int balance;// 账户余额
public Account(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
public synchronized void getMoney(String name, int money) {
if (balance >= money) {
System.out.println(name + " 取了 " + money + " 元");
balance = balance - money;
System.out.println("卡内余额为 " + balance);
} else {
System.out.println("卡内余额不足," + name + " 取款失败");
}
}
}
class GetMoneyThread extends Thread {
private Account account;// 账户
private int money;// 取的钱数
private String name;// 取款人姓名
public GetMoneyThread(Account account, int money, String name) {
this.account = account;
this.money = money;
this.name = name;
}
@Override
public void run() {
this.setName(name);
account.getMoney(name, money);
}
}
执行结果
妻子 取了 800 元
卡内余额为 200
卡内余额不足,丈夫 取款失败
注意: synchronized 只能修饰代码块或方法,但不能修饰构造方法或者成员变量。
不管是同步代码块还是同步方法,执行逻辑都是 加锁 → 修改 → 释放锁 → 其他线程加锁 →...,在同步代码块中,可以将同步监视器设置为this,这表示该代码块将与这个类中的 synchronized 方法使用同一个锁。
任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?程序无法显式释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定。
当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。
当前线程在同步代码块、同步方法中遇到 break、return 终止了该代码块、该方法的继续执行,当前线程也会释放同步监视器。
当前线程在同步代码块、同步方法中出现了未处理的 Error 或者 Exception,导致了该代码块、该方法异常结束时,当前线程会释放同步监视器。
当前线程执行同步代码或同步方法时,程序执行了同步监视器对象的 wait() 方法,则当前线程进入暂停状态,并释放同步监视器。
当编写同步代码时,有几个简单的准则可以遵循,这些准则在避免死锁和性能危险的风险方面大有帮助:
Synchronized 代码块应尽可能的简短
不要在 Synchronized 块中调用可能引起阻塞的方法,譬如I/O
在持有锁的时候,尽量不要调用其他对象的方法。
《深入理解 Java 虚拟机》
《疯狂 Java 讲义》