原创声明:本文转载自公众号【胖滚猪学编程】,转载务必注明出处!
在 并发编程BUG源头 文章中,我们初识了并发编程的三个bug源头:可见性、原子性、有序性。在 如何解决可见性和原子性 文章中我们大致了解了可见性和有序性的解决思路,今天轮到最后一个大bug,那就是原子性。
锁是一种通用的技术方案,Java 语言提供的 synchronized 关键字,就是锁的一种实现。
理论说完了,来点实际的吧!首先我们用synchronized 修饰非静态方法来改写第一章中原子性问题的那段代码:
private long count = 0; // 修饰非静态方法 当修饰非静态方法的时候,锁定的是当前实例对象 this。 // 当该类中有多个普通方法被Synchronized修饰(同步),那么这些方法的锁都是这个类的一个对象this。多个线程访问这些方法时,如果这些线程调用方法时使用的是同一个该类的对象,虽然他们访问不同方法,但是他们使用同一个对象来调用,那么这些方法的锁就是一样的,就是这个对象,那么会造成阻塞。如果多个线程通过不同的对象来调用方法,那么他们的锁就是不一样的,不会造成阻塞。 private synchronized void add10K(){ int start = 0; while (start ++ < 10000){ this.count ++; } } public static void main(String[] args) throws InterruptedException { TestSynchronized2 test = new TestSynchronized2(); // 创建两个线程,执行 add() 操作 Thread th1 = new Thread(()->{ test.add10K(); }); Thread th2 = new Thread(()->{ test.add10K(); }); // 启动两个线程 th1.start();th2.start(); // 等待两个线程执行结束 th1.join();th2.join(); System.out.println(test.count); }
运行一下吧!你会发现永远都可以达到我们想要的效果了~
除了上面代码中修饰非静态方法,还可以修饰静态方法和代码块
// 修饰静态方法 当修饰静态方法的时候,锁定的是当前类的 Class 对象,即TestSynchronized2.class 。这个范围就比对象锁大。这里就算是不同对象,但是只要是该类的对象,就使用的是同一把锁。 synchronized static void bar() { // 临界区 } // 修饰代码块 java中经典的双重锁检查机制 private volatile static TestSynchronized2 instance; public static TestSynchronized2 getInstance() { if (instance == null) { synchronized (TestSynchronized2.class) { if (instance == null) { instance = new TestSynchronized2(); } } } return instance; }
深入分析锁定的对象和受保护资源的关系,综合考虑受保护资源的访问路径,多方面考量才能用好互斥锁。受保护资源和锁之间的关联关系是 N:1 的关系。如果一个资源用N个锁,那肯定出问题的,就好像一个厕所坑位,你有10把钥匙,那不是可以10个人同时进了?
现在给出两段错误代码,想一想到底为啥错了吧?
static long value1 = 0L; synchronized long get1() { return value1; } synchronized static void addOne1() { value1 += 1; }
long value = 0L; long get() { synchronized (new Object()) { return value; } }
第一段错误原因:
因为我们说过synchronized修饰普通方法 锁定的是当前实例对象 this 而修饰静态方法 锁定的是当前类的 Class 对象
所以这里有两把锁 分别是 this 和 TestSynchronized3.class
由于临界区 get() 和 addOne() 是用两个锁保护的,因此这两个临界区没有互斥关系,临界区 addOne() 对 value 的修改对临界区 get() 也没有可见性保证,这就导致并发问题了。
第二段错误原因:
加锁本质就是在锁对象的对象头中写入当前线程id,但是synchronized (new Object())每次在内存中都是新对象,所以加锁无效。
问:刚刚的例子都是多个锁保护一个资源,这样百分百是不行的。那么一个锁保护多个资源,就一定可以了吗?
答:如果多个资源彼此之间是没有关联的,那可以用一个锁来保护。如果有关联的话,那是不行的。比如说银行转账操作,你给我转账,我账户多100,你账户少100,我不能用我的锁来保护你,就像现实生活中我的锁是不能保护你的财产的。
划重点!要区分多个资源是否有关联!但是一个锁保护多个没关联的资源,未免性能太差了哦,比如我听歌和玩游戏可以同时进行,你非得让我做完一个再做另一个,岂不是要双倍时间。所以即使一个锁可以保护多个没关联的资源,但是一般而已,会各自用不同的锁,能够提升性能。这种锁还有个名字,叫细粒度锁。
问:刚刚说到银行转账的案例,那么假如某天在某银行同时发生这样一个事,柜员小王需要完成A账户给B账户转账100元,柜员小李需要完成B账户给A账户转账100元,请问如何实现呢?
答:其实用两把锁就实现了,转出一把,转入另一把。只有当两者都成功时,才执行转账操作。
public static void main(String[] args) throws InterruptedException { Account a = new Account(200); //A的初始账户余额200 Account b = new Account(300); //B的初始账户余额200 Thread threadA = new Thread(()->{ try { transfer(a,b,100); } catch (InterruptedException e) { e.printStackTrace(); } }); Thread threadB = new Thread(()->{ try { transfer(b,a,100); } catch (InterruptedException e) { e.printStackTrace(); } }); threadA.start(); threadB.start(); } static void transfer(Account source,Account target, int amt) throws InterruptedException { synchronized (source) { log.info("持有锁{} 等待锁{}",source,target); synchronized (target) { if (source.getBalance() > amt) { source.setBalance(source.getBalance() - amt); target.setBalance(target.getBalance() + amt); } } } }
至此,恭喜你,一波问题解决了,可是遗憾的告诉你:又导致了另一个bug。这段代码是有可能发生死锁的!并发编程中要注意的东西可真是多哟。咱们先把死锁这个名词记住!持续关注【胖滚猪学编程】公众号!在我们后面的文章中找答案!
现在我们已经知道互斥锁可以保证原子性,也知道了如何使用synchronized来保证原子性。但synchronized 并不是JAVA中唯一能保证原子性的方案。
如果你粗略的看一下J.U.C(java.util.concurrent包),那么你可以很显眼的发现它俩:
一个是lock包,一个是atomic包,只要你英语过了四级。。我相信你都可以马上断定,它们可以解决原子性问题。
由于这两个包比较重要,所以会放在后面的模块单独说,持续关注【胖滚猪学编程】公众号吧!
本文转载自公众号【胖滚猪学编程】 用漫画让编程so easy and interesting!