java并发编程——锁机制

第一部分:synchronized和volatile

锁机制用来保护对象的一致性以及操作的原子性,是实现线程安全的重要手段。线程安全涉及到对象两个重要的状态:共享性和可变性。如果对象是不可变的、线程私有的那么它一定是线程安全的。所以说,只有在共享的、可变的对象上面进行操作时才需要加锁,以保障线程安全。volatile和synchronized是java 5.0之前最早协调对象共享的机制。下面我们将分别介绍他们:

#synchronized

synchronized用来形容方法或者代码块,是java提供的最早的锁机制,支持重入锁。关于synchronized的详细解析文章有很多,这里列举几个注意事项:

第一,使用synchronized关键字时一定要尽可能的缩小范围,尽可能的在方法块里需要锁的地方使用,而不是直接用来修饰整个方法。这需要我们对方法里面的操作进行分析,哪些需要加锁,哪些不需要加锁,只在需要锁的地方加锁,这样即可以提高程序的效率,同时开放调用方法也减少了线程安全的隐患。

第二,synchronized提供的锁机制是粗粒度的,当有线程访问当前对象的synchronized方法或代码块时,其他线程只能等待当前操作结束才可以访问。这听上去似乎存在一定的性能问题,但java 6.0以后synchronized在并发环境下性能得到了大幅提升,因此建议尽可能的使用synchronized,除非synchronized满足不了业务需求。而且synchronized使用时无需释放锁,而且JVM还提供了专门的优化支持,因此即使synchronized是古老的锁,但是它依然适用于绝大多数场景。

#volatile

volatile用来修饰变量保证其可见性。可见性是一种复杂的属性,volatile变量不会被缓存在寄存器或者其他处理器不可见的地方,在读取volatile变量时返回的一定是最新写入的值。volatile不是线程安全的,他不能替代锁,它只在特定的场景下使用,使用时要非常小心。以下场景可以使用volatile:

第一,对变量的写入不依懒于变量当前的值,或者你能确保只有单个线程更新变量的值;

第二,该变量不会与其它状态变量一起纳入不变性条件中;

第三,在访问变量时不需要加锁。

第二部分:ReentrantLock

ReentrantLock是一个可重入的互斥锁,继承自Lock接口。如果说synchronized是隐式锁的话,那么ReentrentLock就是显式锁。锁的申请、使用、释放都必须显式的申明。Lock接口提供了以下方法:

public interface Lock{
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time,TimeUnit unit) throws InterruptedException;
    void unLock();
    Condition newCondition();
}

ReentrantLock实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性。那既然如此,为什么还要创建一个类似的锁机制呢?内置锁虽然好用,但是缺乏一些灵活性,而ReentrantLock则可以弥补这些不足。

第一,轮询锁与定时锁。tryLock方法实现了可定时的与可轮询的锁实现。与synchronized相比它有更完善的错误恢复机制。内置锁中死锁是一类严重的错误,只能重启程序。而ReentrantLock可以使用可定时或者轮询的锁,它会释放已获得的锁,然后再尝试获得所有的锁。在实现具有时间限制的操作时,定时锁也非常也用。如果操作在给定时间内不能给出结果那么就会使程序提前结束。

第二,可中断锁获取操作。lockInterruptibly方法能够在获得锁的同时保持对中断的响应。而且由于它包含在Lock中,因此无需创建其他类型的不可中断阻塞机制。

第三部分:ReadWriteLock

ReentrantReadWriteLock实现了一种标准的互斥读写锁,继承自ReadWriteLock。ReadWriteLock接口包含以下方法:

public interface ReadWriteLock{
    Lock readLock();
    Lock WriteLock();
}

ReentrantReadWriteLock我们可以这样理解:当执行读操作的时候可以多个线程并发访问;当执行写操作的时候,只可以同时被一个线程访问。所以它使用的场景是读操作多而写操作少的并发场景。此外,ReentrantReadWriteLock还可以设置是否为公平锁,是公平锁的话则可以按照排队的顺序获取锁,非公平锁的话则是随机获得。

第四部分:锁的公平性

上一节讲到了ReentrantReadWriteLock实现了公平锁和非公平锁两种模式。在公平锁模式下线程按照请求的顺序来获得锁,而非公平模式下则可以插队。我们的期望是所有的锁都是公平的,毕竟插队是一种不好的行为。但实际上非公平锁比公平锁有着更高的并发效率。假设线程A持有一个锁,并且线程B也请求这个锁,由于该锁被线程A占有,所以B线程挂起,当A使用结束时释放锁,此时唤醒B,B需要重新申请获得锁。如果同时线程C也请求这个锁,并且C很可能在B获得锁之前已经获得、使用并释放了锁。这样就实现了双赢,B线程获得的锁没有延迟同时线程C也得到了执行,这提高了程序的吞吐率。

总结:与内置锁相比,显式锁提供了一些扩展功能,在处理锁的不可用方面有着更高的灵活性,并且对队列有着更好的控制。但是显式锁无法替换synchronized,只有在synchronized无法满足需求时才会使用它。读写锁允许多个读线程并发的访问被保护对象,当访问以读取操作为主的数据结构时,能提高程序的伸缩性。