Java并发编程(三)---synchonized解决原子性问题
前言
上一篇我们介绍Java内存模型来处理有序性,可见性的问题。但是,还有一个原子性的问题,没有处理,那么针对原子性的问题我们该怎么处理呢?我们知道在并发编程中的原子性问题主要原因就是,一条高级语句可能会被分成多个CPU指令,在指令执行完之后发生了线程切换,中间状态被暴露造成原子性问题。
锁
现实生活中,我们用自己的锁来保护自己的财产,买门票来锁定演唱会的座位。
同理,在并发编程的世界里我们同样可以引入锁的概念来锁住需要保护的资源。只有获得了锁的线程才能操作资源。
synchronized
Java自带的锁工具是synchronized,用synchronized修饰的代码就相当于上了锁。上了锁就需要互斥执行。即:同一时刻只能有一个线程执行。
我们将一段需要互斥执行的代码称之为临界区。
例如:
synchronized (this) {
this.a = 100;
System.out.println("*******test2执行");
}
如上程序:代码System.out.println("*******test2执行");被synchronized修饰,故此代码被称之为临界区。而a这是受保护的资源。其关系图如下:
synchronized的运用
synchronized 可以修改方法,修饰代码块。使用如下:
class SynchronizedTest {
public synchronized void test1() {
System.out.println("*******test1执行");
}
public void test2() {
synchronized (this) {
System.out.println("*******test2执行");
}
}
public synchronized static void test3() {
System.out.println("*******test3执行");
}
}
当synchronized修饰实例方法时,锁定的就是当前实例对象this。如方法test1所示。
当synchronized修饰代码块块时,锁定的就是括号里的对象。如方法test2所示。
当synchronized修饰静态方法时,锁定的就是当前类的class对象,如方法test3所示。
锁与受保护资源的关系
在现实生活中,我们可以通过通过一把锁保护多个东西,例如,用一把大门的锁,保护你家里面的所有东西。同样的,你一个给一个东西加上两把锁。但是,在并发编程中,同一个资源只能由一把锁保护,一把锁可以保护多个资源。故,并发编程中,锁与受保护资源的关系是1:N。例如:
public class SynchronizedTest3 {
int a = 0;
int b = 0;
static int c = 0;
/**
* 锁定的是this对象,保护了,a,b两个资源
*/
synchronized void setValue() {
a = 100;
b = 20;
}
/**
* 锁定的是SynchronizedTest3的class对象,
* 保护了资源c
* @return
*/
synchronized static int getValue() {
return SynchronizedTest3.c;
}
}
如上程序,synchronized修饰的setValue方法中有a,b两个资源,因为这两个资源都所属与this对象,所以都可以受到synchronized的保护。
而synchronized修饰的getValue方法中只有资源c,而这个c是一个静态变量,属于SynchronizedTest3类,所以它也可以受到保护。
锁如何保护多个资源
多个资源没关联
如果多个资源没有关联的话,我们可以用多个不同的锁来保护,例如:张三的东西用张三的锁,李四的东西用李四的锁。井水不犯河水。例如:
public class Account {
//取款保护锁
private final Object balLock = new Object();
//密码保护锁
private final Object pwdLock = new Object();
private Integer balance = 100;
private String password = null;
/**
* 取款
*/
public void withdrow(Integer amt) {
synchronized (balLock) {
if (balance > amt) {
balance -= amt;
System.out.println("*******扣除后的余额是="+balance);
}
}
}
/**
* 查看余额
* @return
*/
public int getSBalance() {
synchronized (balLock) {
System.out.println("******读取到的余额是="+balance);
return balance;
}
}
/**
* 更改密码
* @param newPwd
*/
public void updatePwd(String newPwd) {
synchronized (pwdLock) {
password = newPwd;
}
}
/**
* 查看密码
* @return
*/
public String getNewPwd() {
synchronized (pwdLock) {
return password;
}
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
}
如上程序,Account类下面有余额和密码两个资源,我们分别创建了取款的保护锁balLock和密码修改的保护锁pwdLock。同时我们可以需要注意的是
相同的资源要用相同的锁,例如取款用的balLock锁,那么查看余额也需要用balLock锁。修改密码用的pwdLock锁,那么查看密码也需要用pwdLock锁。只要这样才能保证数据的正确性。在本例中,我们也可以用this锁来保护,但是这样的话,修改密码和取款就不能分开。用两个不同的锁,可以使得取款和修改密码可以并行。
2. 多个资源有关联
现实中,有很多资源是有关联关系的,例如:转账操作:张三给李四转账100元,那么张三的账户余额和李四的账户余额就是有关联关系的两个资源。那么:我们该如何加锁保护这两个资源使得转账没问题呢?
我们先直接用synchronized修饰转账方法 如下:
synchronized void transfer(Account target, int amt) {
if (balance > amt) {
this.balance = balance - amt;
target.setBalance(target.getBalance() + amt);
}
}
通过前面的描述,我们知道synchronized锁定的对象是this,那么该锁肯定能保护资源this.balance。但是对于资源target.balance却不能保护,因为该资源属于target对象。
故此种方法不可行。
我们来具体分析下:
假如有账户A(有100元),
账户B(有100元),账户C(有100元),三个账户,账户A给账户B转50元,账户B给账户C转50元。所以我们期望的结果是账户A
剩下50元,账户B剩下100元,账户C剩下150元。但是按照我们上述加锁方式,一定会是在这样么?
我们假设线程1执行账户A转账户B50元,线程2执行账户B给账户C50元,那么这两个线程运行在CPU1和CPU2,显然CPU1和CPU2是不互斥的。
线程1获取到的锁是A.this。线程2获取到锁是B.this。如果线程1和线程2同时进入临界区,那么读取到的账户B余额都是100元,如果线程1先于线程2执行完,那么账户B的余额是50。如果线程2先于线程1执行完那么账户B的余额是150元。就是不可能是100元。
上面的原因就是因为锁没有覆盖到所有的应受保护资源。那么该如何处理呢?我们可以通过一个共享对象来处理保护资源。 例如:Accout.class对象,这个对象是所有的余额所共享的,所以能够覆盖this.balance资源和
target.balance资源。
void transfer3(Account target, int amt) {
synchronized (Account.class) {
if (balance > amt) {
this.balance = balance - amt;
target.setBalance(target.getBalance() + amt);
}
}
}
不过在实际项目中,我们都是通过数据库事务+数据库乐观锁来处理转账逻辑的。
总结
本文简单的介绍了锁模型以及synchronized的用法和,锁与资源的关系,最后介绍了锁如何保护多个资源,总结一下就是 锁与资源的关系是 1:N,多个没有关联的资源用多个不同的锁进行保护,有关联关系的资源,用共享锁进行保护。
作者:码农飞哥
微信公众号:码农飞哥