class X{ //修饰非静态方法 synchronized void foo(){ //临界区 } //修饰静态方法 synchronized static void bar(){ //临界区 } //修饰代码块 Object obj = new Object(); void baz(){ synchronized(obj){ //临界区 } } }
Java编译器会在synchronized修饰的方法或代码块前后自动加上加锁lock()和解锁unlock(),这样做的好处就是加锁lock()和解锁unlock()一定 是成对出现的,毕竟忘记解锁unlock()可是个致命的Bug(意味着其他线程只能死等下去了)。
//修饰静态方法是用当前类的字节码文件作为锁 class X{ //修饰静态方法 synchronized(X.class) static void bar(){ //临界区 } }
//修饰非静态方法是用当前对象作为锁 class X{ //修饰非静态方法 synchronized(this) static void bar(){ //临界区 } }
如何用一把锁保护多个资源
受保护资源和锁之间合理的关联关系应该是N:1的关系,也就是说可以用一把锁来保护多个资源,但是不能用多把锁来保护一个资源,
示例一:
public class Account { /** *锁:保护账⼾余额 */ private final Object balLock = new Object(); /** * 账⼾余额 */ private Integer balance; /** * 错误的做法 * 非静态方法的锁是this, * this这把锁可以保护自己的余额this.balance,保护不了别人的余额 target.balance * */ synchronized void transfer(Account target,int amt){ if (this.balance > amt) { this.balance -= amt; target.balance += amt;//这段代码会出现线程安全,要保证线程安全的话要使用同一个锁 } } }
示例二:
public class Account { /** *锁:保护账⼾余额 */ private final Object balLock = new Object(); /** * 账⼾余额 */ private Integer balance; /** * 正确的做法,但是会导致整个转账系统的串行 * * Account.class是所有Account对象共享的, * 而且这个对象是Java虚拟机在加载Account类的时候创建的, * 所以我们不用担心它的唯一性 * * 这样还有个弊端:所有的转账都是串行了 */ void transfer2(Account target,int amt){ synchronized(Account.class){ if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } }
这样的话转账操作就成了串行的了,正常的逻辑应该只锁转入账号和被转入账户;不影响其他的转账操作。稍作改造:
示例三:
public class Account { /** *锁:保护账⼾余额 */ private final Object lock; /** * 账⼾余额 */ private Integer balance; //私有化无参构造 private Account(){} //设置一个传递lock的有参构造 private Account(Object lock){ this.lock = lock; } /** * 转账 */ void transfer(Account target,int amt){ //此处检查所有对象共享锁 synchronized(lock){ if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } }
这个方法虽然能够解决问题,但是它要求创建Account对象的时候必须传入同一个对象,
还有就是传递对象过于麻烦,写法繁琐缺乏可行性。
示例四:
public class Account { /** * 账⼾余额 */ private Integer balance; /** * 转账 */ void transfer(Account target,int amt){ //此处检查所有对象共享锁 synchronized(Account.class){ if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } }
用Account.class作为共享的锁,锁定的范围太大。 Account.class是所有Account对象共享的,而且这个对象是Java虚拟机在加载Account类的时候创建的,所以我们不用担心它的唯一性。使用Account.class作为共享的锁,我们就无需在创建Account对象时传入了。
这样新的问题就出来了虽然用Account.class作为互斥锁,来解决银行业务里面的转账问题,虽然这个方案不存在 并发问题,但是所有账户的转账操作都是串行的,例如账户A转账户B、账户C转账户D这两个转账操作现实 世界里是可以并行的,但是在这个方案里却被串行化了,这样的话,性能太差。所以如果考虑并发量这种方法也不行的
正确的写法是这样的(使用细粒度锁):
示例五:
public class Account { /** * 账⼾余额 */ private Integer balance; /** * 转账 */ void transfer(Account target,int amt){ //锁定转出账户 synchronized(this){ //锁住转入账户 synchronized(target){ if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } } }
我们试想在古代,没有信息化,账户的存在形式真的就是一个账本,而且每个账户都有一个账本,这些账本 都统一存放在文件架上。银行柜员在给我们做 转账时,要去文件架上把转出账本和转入账本都拿到手,然后做转账。 这个柜员在拿账本的时候可能遇到以下三种情况:
如果有客户找柜员张三做个转账业务:账户 A转账户B 100元,此时另一个客户找柜员李四也做个转账业务:账户B转账户A 100元,于是张三和李四同时都去文件架上拿账本,这时候有可能凑巧张三拿到了账本A,李四拿到了账本B。张三拿到账本A后就等着 账本B(账本B已经被李四拿走),而李四拿到账本B后就等着账本A(账本A已经被张三拿走),他们要等 多久呢?他们会永远等待下去…因为张三不会把账本A送回去,李四也不会把账本B送回去。我们姑且称为死等吧。
只要破坏其中一个就可以避免死锁
用synchronized实现等待-通知机制
wait与sleep区别:
sleep是Object的中的方法,wait是Thread中的方法
wait会释放锁,sleep不会释放锁
wait需要用notif唤醒,sleep设置时间,时间到了唤醒
wait无需捕获异常,而sleep需要
wait(): 当前线程进入阻塞