4. 线程同步
书接上回,在使用多线程场景中,可能会存在不同的线程操作同一个数据,例如我们小学题目中常见的 一边放水一边蓄水,一边存钱一边花钱,我们就以存钱为例,一个线程存钱,一个线程取钱,如果两个线程同时操作,取钱的时候也在存钱,比如此刻余额为 100,取50,存 50,那么存钱的线程存完钱的时候是 150,而取钱之后是 50,这两边的结果其实都是不对的,需要进行同步。才能让存取之后的余额一致。下面就介绍几种线程同步的方法。 同步的核心就是 加锁 -> 修改->释放锁
4.1 同步代码块
同步代码块其实就是将并行执行的代码,使用 synchronized 关键字进行同步,其使用方式为:
Object obj = new Object(); //监视器
public void foo(){
...
synchronized(obj) { //同步代码块
doSomething();
}
...
}
当线程运行 foo 方法时,运行到同步代码块部分,需要先获取监视器的锁定,如果监视器已经被其他线程获取,那么此时就进入了阻塞状态,等到监视器被释放后,则可以被其他线程继续竞争获取,从而保证了同步代码块中每时每刻只有一个线程调用。
在 Java 中,任何对象都可以被当做监视器,也就是说synchronized(obj)
中的 obj 可以换成其他对象,但是一般我们推荐将可能被多线程并发访问的对象作为监视器。例如上面存钱和取钱的例子中,我们可以用存取的账户对象作为监视器。放水和蓄水可以用水池作为监视器对象,等等。
4.2 同步方法
和同步代码块差不多 也是用到了 synchronized 关键字。与同步代码块不同的是,同步方法是加在方法的修饰上
public synchronized void foo(){
doSomething();
}
通过同步代码块我们知道 synchronized 是获取了一个对象做监视器,在同步方法(非 static )中,这个监视器其实调用此方法的对象,也就是 this。
对于静态同步方法,其锁的就是这个类,对任意该类内静态同步方法,任意时刻只能有一个方法可以竞争到锁。由于同步方法和静态同步方法,锁的不是同一个对象,所以他们之间没有竞争关系。
静态同步方法如下
public synchronized static void bar(){
}
4.3 锁(Lock)
Java 中有很多锁,这里我们主要了解一下如何使用锁去控制同步访问资源。Lock与 synchronized相比,Lock 更加灵活,同时 Lock 还提供了 Condition 对象,可以应对不同情况的同步操作。
4.3.1 同步锁
锁的一般写法如下
class A {
private final Lock lock = new ReentrantLock(); //创建锁对象
public void foo(){
lock.lock(); // 加锁
try{
doSomething();
} catch(Exeception e){
handleException();
}finally{
lock.unlock(); //解锁
}
}
}
同样的从代码可以看出 流程也是加锁 -> 修改->释放锁,这里我们看到最终解锁是在 finally 中调用的,是因为加锁和释放锁在不同的作用范围的时候,一定要确保解锁,所以在 finally 中调用解锁方法。
4.3.2 死锁
死锁的定义起始很简单,就是两个线程都在等待对方释放监视器,导致了死锁。我们举一个不太恰当的例子,两个人在打架斗殴,互相揪住了对方,都在说“你松手我就松手”,都在等着对方松手,导致一只纠缠。死锁的意思大概也就是如此。
那么我们如何避免死锁呢, 第一点我们要注意 在同步方法总避免调用相互调用。还有在使用同步锁的时候,如果使用了不同的 lock 对象,也要注意是否存在相互调用问题。