一、同步
要读写共享的数据。
通过锁的机制,某个线程可以获得对数据的独家访问权,其他的线程则被阻塞,无法访问。
线程使用acquire获得锁,release释放。同一时刻一个锁只能被一个线程所有,其他的线程如果要申请只能等正在使用的线程去释放。
二、锁
每个对象都可以作为锁。java中使用synchronized来对某个语句上锁,表示某一时刻只有一个线程可以执行这条语句,执行完自动释放锁:
1、同步语句、代码块
注意,如果要防止竞争,要用同一把锁(相同的对象)进行保护。
例子,下图中所有的lock都是同一把锁,所以不同的线程之间不会有交错:
但如果像这样,就每个线程的锁都不一样的,起不到保护的作用:
Monitor模式:用ADT作为锁,把所有对ADT的访问都用同一把锁(ADT)锁起来:
2、同步方法
在方法前加上synchronized,相当于对同一个对象的每个方法加了相同的锁(当前对象):
由于锁的机制,线程A对某ADT的一个操作和线程B对相同ADT的一个操作会建立happen-before关系,即A的操作必然在B之前完全结束。
下图是一个错误使用锁的例子,由于每个线程的实例都不一样,所以这里每次循环用的都是不同的锁:
像这样每次循环用的就都是同一把锁(因为是同一个对象):
静态同步方法与类关联,这样同一个类的不同对象的锁也相同:
可见,使用静态同步方法可以非常好的保证线程安全,但是对性能的损耗也很大,最好确定哪些地方需要lock再有针对性地进行lock。
例子,sharedList是一个synchronizedList,下面对其的哪些操作应该加锁?调用isEmpty、add不需要,因为这俩都是原子操作;遍历需要加锁,防止遍历过程中有其他的操作对其进行更改;最后一个需要加锁,防止isEmpty和remove操作之间有另一个线程进行remove的操作:
例子,1和5是对的,其他都是错的,list作为锁被占用,但list仍然是可以被访问、修改的(3、4)。
总结:任何对mutable对象的操作(读/写)都应该被锁住;涉及对多个mutable对象的操作时,注意要用同一把锁进行保护。
三、原子性
加入我们通过monitor模式,已经实现了buf的toString、delete、insert方法的原子性,但将这些方法一起调用的时候还有可能会出现问题,会出现方法与方法之间的交错。比如说线程1delete一个字符串的同时,线程2delete相同的字符串,就会出错。
解决方法是如下图所示对代码块进行加锁,使用的锁为被访问的对象(下图的buf),这样对同一对象的访问会加锁、对不同对象的访问不会加锁:
四、死锁
当一个线程请求多把锁的时候,可能会出现死锁的情况。
下图会出现死锁的情况,线程A占用harry请求snape,线程B占用snape请求harry:
总的来说,如果出现了这样的形式,就有可能出现死锁:
五、wait、notify、notifyAll
锁调用wait,wait可以让占用此锁的线程进入等待状态。notify可以随机唤醒一个正在wait的线程,notifyAll唤醒所有在wait的线程。
被唤醒的线程会继续请求锁,获取锁后,从wait的位置继续执行。
例如下图的线程使用wait,就会释放锁并且暂停执行: