synchronized修饰方法
synchronized可以修饰代码块(在线程入门2中有例子),也可以修饰普通方法和静态方法。
修饰普通方法
修饰普通方法简化写法:
修饰静态方法
修饰静态方法简化写法:
注意:利用synchronized上锁,锁的对象是什么不重要,重要的的是两个线程中锁的对象是否是同一个对象。
synchronized 的可重入性
所谓的可重入性,指的是,一个线程,连续对一把锁,加锁两次,或者更多次,不会出现死锁,满足这个条件就叫做“可重入性”。
例如下面代码
假设在我们第一次加锁时成功,那么locker就属于锁定状态,紧接着往下继续执行代码,又发现需要对locker进行上锁,但是刚才已经上过锁了,正常来说,对已经在一个线程中上过锁的对象,在另一个线程中又要对相同的对象进行上锁,就会发生“阻塞等待”,需要这个对象被解锁释放后才能进行上锁。
那么对于上面代码这种情况就会产生“死锁”,导致线程卡死,第二个想要加锁成功就需要第一次释放锁,需要执行到程序“1号”位置,但是想要到“1号”位置 就需要 第二次能成功加锁让程序继续往下走,由于第二次加锁处在“阻塞等待“状态,也就执不了代码,最终到不了2号位置,也就无法释放锁,因此线程就直接被卡死了。
因此在Java中synchronized被设计成”可重入锁“,就能够有效解决问题。但C++中std::mutex就是不可重入锁,就会出现死锁。
还需注意:当出现这种重复 上锁的情况时,在释放锁时,无论多少层都必须要在最外层释放锁。
关于死锁
死锁的案例
1.一个线程,针对一把锁,连续加锁两次及以上,如果是不可重入锁,就会出现死锁。
2.两个线程,两把锁。此时无论是不是可重入锁都回死锁
上面这种情况就类似于生活中,家门钥匙被锁在车里了,车钥匙被锁在加里了。
3.N个线程,M把锁 (此时更容易出现死锁)
一个经典的模型就是”哲学家问题“
如图所示:
有五个哲学家,桌子上有五根筷子,哲学家只做两件事,一个是停下来思考人生什么都不做,另一个是拿起左边和右边的筷子吃面条。
哲学家什么时候吃面条,什么时候吃完停下来思考人生都是随机的,如果其中一个哲学家拿起左右两边的筷子吃面条,此时相邻的哲学家就只能等待他吃完放下筷子,就会出现阻塞等待。
本来正常运转都没什么问题,但是有一天突然出现,每个哲学家都先拿起左手边的筷子,结果发现右手边的筷子已经被拿了,此时哲学家也不会放下自己左边的筷子,只会进行等待右边何时放下筷子,这个时候没人能吃上面条,也没人能放下筷子,就导致了出现了死锁。
死锁是比较严重的bug(会导致线程卡住,也就无法在继续执行后序工作了)
死锁涉及到的四个必要条件
1.互斥使用 (锁的基本特征)当一个线程持有一把锁后,另一个线程想获得锁,就要进行阻塞等待。
2.不可抢占 (锁的基本特征)当锁已经被线程1拿到之后,线程2只能等待线程1主动释放,不嫩恶搞强行抢过来。
3.请求保持 (代码结构) 一个线程尝试获取多把锁 (先拿到锁1,尝试获取锁2,获取的时候,锁1不会释放,嵌套结构)
4.循环等待/环路等待 (代码结构)等待的依赖关系形成环了(哲学家问题)
解决死锁
解决死锁的核心就是破坏上述的必要条件,只要破坏一个,死锁就形成了,但一和二是synchronized的自带的特性,无法干预,只能从3,4入手。
对于3来说,我们可以调整代码结构,把嵌套锁的结构,改成并列结构。
对于4来说,可以约定加锁的顺序,就可以避免循环等待 (例如针对锁进行编号,加多把锁的时候,先加编号小的锁,再加编号大的锁,所有线程都要遵循这个规则)