Java并发编程(三):锁的使用

161 阅读4分钟

这一章的内容主要涉及到锁,和锁会导致的问题。

锁:解决并发编程的万能钥匙

在前面两章中,Java内存模型主要解决了可见性和有序性问题,但还缺一个原子性问题没解决啊?这时候,锁就出现了,锁可以说是能够解决所有的并发编程问题,原子性就主要是靠他解决的,除此之外,也能解决可见性和有序性问题(也主要是靠Happens Before原则)。但锁也不能滥用,下面就讨论下锁的使用。

什么情况下使用锁

并发编程的环境下,很多时候,对一个共享变量同时进行操作往往是不允许的,这种条件称之为互斥。锁就是为了保证互斥性而使用的。
需要互斥访问的代码段称之为临界区,在进入临界区之前,我们需要调用lock()来获取锁,这个时候若是有其他的线程再调用lock(),就会被阻塞,等待持有这个锁的线程调用unlock()之后才会被唤醒,再与其他线程竞争这个锁。

Java语言中的synchronized关键字

Java中的synchronized关键字就是一个最简单的实现对一段代码加锁的方式,将synchronized关键字加在方法的声明上,即可让调用该方法的线程自动获取锁,离开该方法的时候便能自动释放锁。

synchronized void f(){
    ...
}

用锁来保护资源

临界区往往涉及到对一个或多个共享变量的操作,这些变量称为资源,锁所要保护的是资源,而不是代码段

synchronized关键字更加具体的用法

前面提到的synchronized的用法只是加在方法签名上就行了,但其实synchronized的完整语法是这样的:

synchronized (Object){
    临界区代码...
}

Object即要保护的资源对象,像synchronized 加在静态方法上,就是与synchronized (类名.class) {方法内代码...}是等效的,加在非静态方法就是与synchronized (this) {方法内代码...}等效。

锁和受保护资源的关系

锁和受保护资源的关系应是1:N的关系,也就是说一把锁可以保护多个资源,但一个资源不能被多把锁保护(失去保护的能力)。

使用锁会导致的问题:死锁

考虑这样一个代码段:

class X{
    public f(Object other){
        synchronized(this){
            synchronized(other){
                ...
            }
        }
    }
}

当线程A和线程B创建了两个类X的实例A和B,并互相以对方实例为参数调用f时,当它们同时获取到各自的this实例的锁时,会同时开始等待对方实例的锁,就会形成“你等我,我等你”的局面,这种情况称为死锁。
死锁的一个比较专业的定义是:一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。

如何避免死锁呢?

coffman曾总结过,只有以下这四个条件都发生时才会出现死锁:

  1. 互斥,共享资源 X 和 Y 只能被一个线程占用;
  2. 占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
  3. 不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
  4. 循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。 那么我们只要破坏上面四条中任意一个条件就能避免死锁了。互斥是我们使用锁的目的,是不能破坏的,但剩下三个条件是有办法破坏的,下面是一些办法的总结:
  • 破坏占有且等待的条件,我们可以通过一次性获得所有共享资源
  • 破坏不可抢占的条件,我们可以让获得部分共享资源的线程在不能获取到剩下它想要的资源时,主动释放他所拥有的资源。
  • 破坏循环等待条件,我们可以让线程按序获取资源,或是设计一点trick,比如分奇偶讨论获取资源,来破坏循环等待。