一个或多个操作在CPU执行的过程中不被中断的特性,称为原子性。
线程出现原子性的问题是因为线程切换导致,同一时刻只能有一个线程操作共享对象才能解决原子性问题。自然而然我们想到加锁方式来搞定原子性问题。线程A持有锁之后才能访问加锁后的资源,其他线程只能等待,直到线程A释放锁后,才有机会抢占持有锁。实现互斥的条件。
王宝令老师举了一个很生动的例子来说明锁模型。一般办公室早高峰是蹲坑的黄金时间,大家都抢着上厕所,无奈坑位有限,运气好的话你去的时候有坑位,这时候你蹲坑锁门,享受一泻千里的快感。其他同事只能在门外苦苦等候,直到你打开门出来后,其他人才有机会进入这个坑位。这个例子中,我们锁的是不是就是坑位,保护的是我们正常拉屎的隐私和权力。换到代码中,是不是就是锁的是共享变量的访问,保护的是共享变量,这是我们理解锁的关键。
在上一节中已经提到synchronized,Java 语言提供的 synchronized 关键字,就是锁的一种实现。synchronized 关键字可以用来修饰方法,也可以用来修饰代码块。他的使用方法如下:
public class Synchronized {
private final Object LOCK = new Object();
// 修饰非静态方法
public synchronized void lockMethod() {
// 受锁保护的资源,临界区
}
// 修饰静态方法
public synchronized static void lockStaticMethod() {
// 受锁保护的资源,临界区
}
public void method() {
// 修饰代码块
synchronized (LOCK) {
// 受锁保护的资源,临界区
}
}
}
synchronized 的加锁lock() 和解锁 unlock()是由java编译器在修饰方法或代码块前后自动添加,无需我们手动进行加锁和解锁步骤。在上面代码中synchronized 修饰代码块时可以看到锁的是LOCK对象,而synchronized 修饰静态方法锁的是当前类的Class对象,即我们的类Synchronized;当修饰非静态方法时锁的是当前的实例对象this。
也就是说下面代码中lockMethod方法和lockStaticMethod方法是由两个不同的锁this,Synchronized.class保护资源i,不会存在互斥关系,会导致并发问题。一个资源只能由同一把锁保护,同一把锁可以保护N个资源。
public class Synchronized {
static int i = 0;
// 修饰非静态方法
public synchronized void lockMethod() {
// 受锁保护的资源,临界区
i++;
}
// 修饰静态方法
public synchronized static int lockStaticMethod() {
// 受锁保护的资源,临界区
return i;
}
}
当我们要保护多个资源时,首先要区分这些资源是否存在关联关系。
-
资源间没有关联关系
比如我们银行卡账户余额balance和银行卡密码pwd两个资源没有直接关联关系。那么我们可以通过balanceLock和pwdLock两个锁来分别管理,不同的资源用不同的锁保护,各自管各自的。当然你也可以用同一把锁来管理这两个资源,只不过会造成不必要的性能浪费,因为这会导致操作串行化。用不同的锁对受保护资源进行精细化管理,能够提升性能,尽量使用细粒度锁是保证性能的关键所在。
public class Account {
private Double balance;
private String pwd;
private final Object balanceLOCK = new Object();
private final Object pwdLOCK = new Object();
// 取款
public void withdraw(Double amt) {
synchronized (balanceLOCK) {
if (this.balance > amt) {
this.balance -= amt;
}
}
}
// 查看余额
public Double getBalance() {
synchronized (balanceLOCK) {
return balance;
}
}
// 更新密码
public void updatePassword(String pwd) {
synchronized (pwdLOCK) {
this.pwd = pwd;
}
}
// 查看余额
public String getPwd() {
synchronized (pwdLOCK) {
return pwd;
}
}
}
- 资源间存在关联关系
假设有 A、B、C 三个账户,余额都是 200 元,我们用两个线程分别执行两个转账操作:账户 A 转给账户 B 100 元,账户 B 转给账户 C 100 元,最后我们期望的结果应该是账户 A 的余额是 100 元,账户 B 的余额是 200 元, 账户 C 的余额是 300 元。
public class Account {
private Double balance;
private String name;
public Account(String name, Double balance) {
this.name = name;
this.balance = balance;
}
public void transfer(Account target, Double money) {
synchronized (Account.class) {
if (this.balance > money) {
this.balance -= money;
target.balance += money;
}
}
}
我们通过Account.class作为共享锁来实现,Account.class 是所有 Account 对象共享的,而且这个对象是 Java 虚拟机在加载 Account 类的时候创建的,所以我们不用担心它的唯一性。但是如果使用class对象作为锁的话,会导致所有的操作都变成串行,降低了执行效率。因此,并不是最合适的选择。
前面我们提到了锁尽可能小的范围细粒度锁,对于上面转账操作A账户转账到B账户,分别加锁。效率肯定就提升了。我们用代码来实现一下A,B两个账户分别加锁:
public class Account {
//账号
private String accountName;
// 余额
private int balance;
public Account(String accountName,int balance){
this.accountName = accountName;
this.balance = balance;
}
// 省略get/set方法
}
public class AccountMain implements Runnable {
//转出账户
public Account fromAccount;
//转入账户
public Account toAccount;
//转出金额
public int amount;
public AccountMain(Account fromAccount,Account toAccount,int amount){
this.fromAccount = fromAccount;
this.toAccount = toAccount;
this.amount = amount;
}
@Override
public void run(){
while(true){
//获取fromAccount对象的锁
synchronized(fromAccount){
//获取toAccount对象的锁
synchronized(toAccount){
//转账进行的条件:判断转出账户的余额是否大于0
if(fromAccount.getBalance() <= 0){
System.out.println(fromAccount.getAccountName() + "账户余额不足!");
return;
}else{
//更新转出账户的余额:
fromAccount.setBalance(fromAccount.getBalance() - amount);
//更新转入账户的余额:
toAccount.setBalance(toAccount.getBalance() + amount);
}
}
}
System.out.println("转出用户:" + fromAccount.getAccountName() + "余额:" + fromAccount.getBalance());
System.out.println("转入用户:" +toAccount.getAccountName() + "余额:" + toAccount.getBalance());
}
}
public static void main(String[] args) {
Account fromAccount = new Account("张三",200000);
Account toAccount = new Account("李四",200000);
// 每次转出2元.
Thread a = new Thread(new AccountMain(fromAccount,toAccount,2));
Thread b = new Thread(new AccountMain(toAccount,fromAccount,2));
a.start();
b.start();
}
}
当我们按照思路写完执行发现,等了好久都没有等到程序结束。尴尬的发现死锁了。
我们来分析下这个例子死锁是怎么造成的,线程a在执行转账操作张三->李四的同一时刻,线程b也执行账户 李四 转账户 张三 的操作。两个线程同时执行到了(1)处,此时线程a的fromAccount是不是就是张三,而对于b线程来说fromAccount就是李四了。此时执行到(2),a 试图获取账户 李四 的锁时,发现账户 李四 已经被锁定(被 b线程 锁定),所以 a 开始等待;b 则试图获取账户 张三 的锁时,发现账户 张三 已经被锁定(被 a线程 锁定),所以 b 也开始等待。于是 a和 b 会无期限地等待下去,最终造成了死锁。
synchronized(fromAccount){(1)
//获取toAccount对象的锁
synchronized(toAccount){(2)
}
}
总结
这节我们了解到通过加锁互斥的方式可以解决原子性问题,以及synchronized不同加锁方法,加锁的对象。在通过转账例子使用细粒度锁的时候又碰到了死锁问题。下一节来整理下如何避免死锁。