Java并发之四:正确使用synchronized

2018-03-31  本文已影响0人  亨小利霍

synchronized 同步,又称同步锁,以下简称 "sync"。

虽然synchronized的写法有七八种(见下例), 其实只分两类,一个对象实例锁,一个类锁。

对象实例锁

1、synchronized void method() :普通sync方法,锁调用该方法的对象实现
2、synchronized(this) :sync this,锁this
3、synchronized(objRef) :sync指定变量
4、synchronized(memberRef) :sync类成员变量
5、synchronized(staticMemberRef) :sync静态类成员变量

类锁

6、synchronized static void method() :sync静态方法
7、synchronized(Sample.class) :sync类锁
8、synchronized(this.getClass()) :sync类锁

规则

1、对象锁与类锁互相独立,每个实例、每个类都只有一个锁;

2、同为对象锁的sync调用,判断是否同一个对象引用,如同则存在同步竞争;注意Integer/String的缓存坑,两个变量实为一个。

3、同为类锁的sync调用,判断是否同一个Class类,如同则存在同步竞争。

4、非sync方法,任何时候都不受同步锁影响。

5、synchronized关键字是不能继承的,它不是方法签名。

6、sync类成员变量,该类的不同实例持有的普通成员变量,对应的是该成员变量的不同实例,它们之间是不存在锁竞争的。

7、sync静态类成员变量,该类的不同实例持有的是同一个成员变量实例,所以它们之间是存在锁竞争的。但与该静态成员变量所在类的类锁是互相独立的。

实现原理

sync同步锁,是隐式锁,在进入sync临界区时由jvm自动获取,退出时自动释放。

sync同步锁有三个属性:
    依赖的对象实例/类引用;(对象的锁,锁的对象,同义)
    WaitSet,等待池列表,所有调用wait()方法的线程都会进入该列表,等待池列表中的线程不会去竞争sync锁。待其他进入sync临界区的线程调用notify()后或wait(long)的时间到,会退出本区进入EntryList区;
    EntryList,锁池队列,所有尝试获取sync锁,进入该锁的EntryList,待当前持有锁的线程释放后,按非公平方式分配锁;

wait()方法只能sync临界区内使用,因为wait()的语义只有两个:一就是释放sync锁,未持有锁谈何释放;二是进入WaitSet区,等待notify或时间到;

notify() 会从WaitSet列表中随机唤醒一个;

notifyAll()会将WaitSet列表中所有线程都唤醒,整体进入锁池去竞争sync锁;

正确使用

wait() 应配合while循环使用,不应使用if,务必在wait()调用前后都检查条件,如果不满足,必须调用notify()唤醒下一条线程来处理,自己回去继续wait()直至条件满足再往下执行。

notifyAll()将所有WaitSet中的线程从等待池唤醒,全部进入锁池竞争去sync锁,最终也只有一个线程能获取锁去执行,唤醒+竞争锁池本身是线程上下文的重操作,对性能产生不良影响。滥用notifyAll()有可能导致“惊群效应”。

notify() 是对notifyAll()的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致死锁。正确的场景应该是 WaitSet中等待的是相同的条件,唤醒任一个都能正确处理接下来的事项,如果唤醒的线程无法正确处理,务必确保继续notify()下一个线程,并且自身需要重新回到WaitSet中(参见上一条)。导致死锁的原因是notify()随机唤醒了一条线程,但它既无法正确改变条件,也不叫醒另一个兄弟来搞,就会产生一个情况:锁池中的队列空了,等待池中有一堆线程,但不会再被唤醒永远等待。


参考
        《Java编程思想》之《notify()vs.notifyAll()》

上一篇下一篇

猜你喜欢

热点阅读