Java并发编程实战 01并发编程的Bug源头
Java并发编程实战 02Java如何解决可见性和有序性问题java
在上一篇文章02Java如何解决可见性和有序性问题当中,咱们解决了可见性和有序性的问题,那么还有一个原子性
问题我们还没解决。在第一篇文章01并发编程的Bug源头当中,讲到了把一个或者多个操做在 CPU 执行的过程当中不被中断的特性称为原子性,那么原子性的问题该如何解决。编程
同一时刻只有一个线程执行这个条件很是重要,咱们称为互斥,若是能保护对共享变量的修改时互斥的,那么就能保住原子性。缓存
咱们把一段须要互斥执行的代码称为临界区,线程进入临界区以前,首先尝试获取加锁,若加锁成功则能够进入临界区执行代码,不然就等待,直到持有锁的线程执行了解锁unlock()
操做。以下图:
微信
可是有两个点要咱们理解清楚:咱们的锁是什么?要保护的又是什么?并发
在并发编程世界中,锁和锁要保护的资源是有对应关系的。
首先咱们须要把临界区要保护的资源R
标记出来,而后须要建立一把该资源的锁LR
,最后针对这把锁,咱们须要在进出临界区时添加加锁lock(LR)
操做和解锁unlock(LR)
操做。以下:
app
synchronized
可修饰方法和代码块。加锁lock()
和解锁unlock()
都会在synchronized
修饰的方法或代码块先后自动加上加锁lock()
和解锁unlock()
操做。这样作的好处就是加锁和解锁操做会成对出现,毕竟忘了执行解锁unlock()
操做但是会让其余线程死等下去。
那咱们怎么去锁住须要保护的资源呢?在下面的代码中,add1()
非静态方法锁定的是this
对象(当前实例对象),add2()
静态方法锁定的是X.class
(当前类的Class对象)性能
public class X { public synchronized void add1() { // 临界区 } public synchronized static void add2() { // 临界区 } }
上面的代码能够理解为这样:this
public class X { public synchronized(this) void add() { // 临界区 } public synchronized(X.class) static void add2() { // 临界区 } }
在01 并发编程的Bug源头文章当中,咱们提到过count += 1 存在的并发问题,如今咱们尝试使用synchronized
解决该问题。线程
public class Calc { private int value = 0; public synchronized int get() { return value; } public synchronized void addOne() { value += 1; } }
addOne()
方法被synchronized
修饰后,只有一个线程能执行,因此必定能保证原子性,那么可见性问题呢?在上一篇文章02 Java如何解决可见性和有序性问题当中,提到了管程中的锁规则,一个锁的解锁 Happens-Before 于后续对这个锁的加锁。管程,在这里就是synchronized
(管程的在后续的文章中介绍)。根据这个规则,前一个线程执行了value += 1
操做是对后续线程可见的。而查看get()
方法也必须加上synchronized
修饰,不然也无法保证其可见性。
上面这个例子以下图:
code
那么可使用多个锁保护一个资源吗,修改一下上面的例子后,get()
方法使用this
对象锁来保护资源value
,addOne()
方法使用Calc.class
类对象来保护资源value
,代码以下:
public class Calc { private static int value = 0; public synchronized int get() { return value; } public static synchronized void addOne() { value += 1; } }
上面的例子用图来表示:
在这个例子当中,get()
方法使用的是this
锁,addOne()
方法使用的是Calc.class
锁,所以这两个临界区(方法)并无互斥性,addOne()
方法的修改对get()
方法是不可见的,因此就会致使并发问题。
结论:不可以使用多把锁保护一个资源,但能使用一把锁保护多个资源(这里没写例子,只写了一把锁保护一个资源)
在银行的业务当中,修改密码和取款是两个再常常不过的操做了,修改密码操做和取款操做是没有关联关系的,没有关联关系的资源咱们可使用不一样的互斥锁来解决并发问题。代码以下:
public class Account { // 保护密码的锁 private final Object pwLock = new Object(); // 密码 private String password; // 保护余额的锁 private final Object moneyLock = new Object(); // 余额 private Long money; public void updatePassword(String password) { synchronized (pwLock) { // 修改密码 } } public void withdrawals(Long money) { synchronized (moneyLock) { // 取款 } } }
分别使用pwLock
和moneyLock
来保护密码和余额,这样修改密码和修改余额就能够并行了。使用不一样的锁对受保护的资源进行进行更细化管理,可以提高性能,这种锁叫作细粒度锁。
在这个例子当中,你可能发现我使用了final Object
来当成一把锁,这里解释一下:使用锁必须是不可变对象,若把可变对象做为锁,当可变对象被修改时至关于换锁,并且使用Long
或Integer
做为锁时,在-128到127
之间时,会使用缓存,详情可查看他们的valueOf()
方法。
在银行业务当中,除了修改密码和取款的操做比较多以外,还有一个操做比较多的功能就是转帐。帐户 A 转帐给 帐户B 100元,帐户A的余额减小100元,帐户B的余额增长100元,那么这两个帐户就是有关联关系的。在没有理解互斥锁以前,写出的代码可能以下:
public class Account { // 余额 private Long money; public synchronized void transfer(Account target, Long money) { this.money -= money; if (this.money < 0) { // throw exception } target.money += money; } }
在转帐transfer
方法当中,锁定的是this
对象(用户A),那么这里的目标用户target
(用户B)的能被锁定吗?固然不能。这两个对象是没有关联关系的。正确的操做应该是获取this
锁和target
锁才能去进行转帐操做,正确的代码以下:
public class Account { // 余额 private Long money; public synchronized void transfer(Account target, Long money) { synchronized(this) { synchronized (target) { this.money -= money; if (this.money < 0) { // throw exception } target.money += money; } } } }
在这个例子当中,咱们须要清晰的明白要保护的资源是什么,只要咱们的锁能覆盖全部受保护的资源就能够了。
可是你觉得这个例子很完美?那就错了,这里面颇有可能会发生死锁。你看出来了吗?下一篇文章我就用这个例子来聊聊死锁。
使用互斥锁最最重要的是:咱们的锁是什么?锁要保护的资源是什么?,要理清楚这两点就好下手了。并且锁必须为不可变对象。使用不一样的锁保护不一样的资源,能够细化管理,提高性能,称为细粒度锁。
参考文章:
极客时间:Java并发编程实战 03互斥锁(上)
极客时间:Java并发编程实战 04互斥锁(下)
我的博客网址: https://colablog.cn/
若是个人文章帮助到您,能够关注个人微信公众号,第一时间分享文章给您