【Java并发系列04】线程锁synchronized和Lock和volatile和Condition

时间:2024-04-24 23:38:26

一、前言

  多线程怎么防止竞争资源,即防止对同一资源进行并发操作,那就是使用加锁机制。这是Java并发编程中必须要理解的一个知识点。其实使用起来还是比较简单,但是一定要理解。

  有几个概念一定要牢记:

  • 加锁必须要有锁
  • 执行完后必须要释放锁
  • 同一时间、同一个锁,只能有一个线程执行

二、synchronized

  synchronized的特点是自动释放锁作用在方法时自动获取锁,任意对象都可做为锁,它是最常用的加锁机制,锁定几行代码,如下:

//--------同步方法1
public synchronized void test(){
//一段代码
}
//--------同步方法2
private Object lock=new Object();
public void test2(){
synchronized(lock){ }
}

2.1 synchronized获取的锁

  synchronized可以手动指定锁,当作用在方法时会自动获取锁:

  • 作用于普通方法获得当前对象锁,等价于synchronized(this)
  • 作用于静态方法获得类锁,等价于synchronized(类.class)

三、Lock

  Lock的特点是,必须自己创建锁(锁类型已经指定为Lock的实现类,不能使用其它对象),必须自己释放锁。代码结构如下:

Lock l = ...;
l.lock();
try {
// 执行代码
} finally {
l.unlock();
}

  注意一定要在finally中释放锁,保证即便抛出异常也可以释放。

3.1 ReentrantLock详解

【Java并发系列04】线程锁synchronized和Lock和volatile和Condition

  这是一个Lock的一个实例。

3.1.1 构造方法

  ReentrantLock(可重入锁),只有一个属性即是否公平。公平的含义是当有多个线程竞争锁时,按先来后到获得锁,但使用公平策略时,对效率有一定的影响。

  • ReentrantLock() :最常用,获取一个不公平的锁,
  • ReentrantLock(boolean fair):获取指定公平策略的锁。

3.1.2 方法摘要

  加锁与解锁:

  • void lock() :获取锁,这是最常用的方法。
  • void unlock() :释放锁,必须要的方法,使用完一定要释放锁。
  • boolean tryLock() :仅在调用时锁未被另一个线程保持的情况下,才获取该锁。会破坏公平性原则。
  • boolean tryLock(long timeout, TimeUnit unit) :如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。
  • void lockInterruptibly() :如果当前线程未被中断,则获取锁。

  查询当前锁的相关状态:

  • boolean isLocked() :查询此锁是否由任意线程保持。
  • boolean isHeldByCurrentThread() :查询当前线程是否保持此锁。
  • boolean isFair() :如果此锁的公平设置为 true,则返回 true。
  • int getHoldCount() :查询当前线程保持此锁的次数。
  • int getQueueLength() :返回正等待获取此锁的线程估计数。
  • boolean hasQueuedThread(Thread thread) :查询给定线程是否正在等待获取此锁。
  • boolean hasQueuedThreads() :查询是否有些线程正在等待获取此锁。

   Condition相关(见第五章):

  • Condition newCondition() :返回用来与此 Lock 实例一起使用的 Condition 实例。
  • int getWaitQueueLength(Condition condition):返回等待与此锁相关的给定条件的线程估计数。

四、ReadWriteLock

  当有一种情况,一个类中有多个方法需要同步,其中有读有写,如果所有的方法都使用同步,虽然可以保证数据的准确性,但当读取次数远大于写入次数的时候,同步就会对性能产生较大的影响。这时候,就有一种同步策略,读操作和读操作不互斥,读操作和写操作互斥,写操作和写操作互斥,这样可以提供性能。

  虽然解释的很通俗但是使用它们还是要考虑以下情况(全部来自jdk api):

  1. 在 writer 释放写入锁时,reader 和 writer 都处于等待状态,在这时要确定是授予读取锁还是授予写入锁。Writer 优先比较普遍,因为预期写入所需的时间较短并且不那么频繁。Reader 优先不太普遍,因为如果 reader 正如预期的那样频繁和持久,那么它将导致对于写入操作来说较长的时延。公平或者“按次序”实现也是有可能的。
  2. 在 reader 处于活动状态而 writer 处于等待状态时,确定是否向请求读取锁的 reader 授予读取锁。Reader 优先会无限期地延迟 writer,而 writer 优先会减少可能的并发。
  3. 确定是否重新进入锁:可以使用带有写入锁的线程重新获取它吗?可以在保持写入锁的同时获取读取锁吗?可以重新进入写入锁本身吗?
  4. 可以将写入锁在不允许其他 writer 干涉的情况下降级为读取锁吗?可以优先于其他等待的 reader 或 writer 将读取锁升级为写入锁吗?

4.1 创建与获取锁

  创建ReentrantReadWriteLock:

  • ReentrantReadWriteLock():创建默认非公平的锁。
  • ReentrantReadWriteLock(boolean fair):创建指定公平策略的锁。

  获得读或者写锁:

  • readLock() :返回用于读取操作的锁。
  • writeLock() :返回用于写入操作的锁。

4.2 其他方法

  其它方法不怎么常用,若有具体需求可以查看API文档。

4.3 示例

  下面给一个简单的例子,一个并发访问的map:

class RWDictionary {
private final Map<String, Data> m = new TreeMap<String, Data>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock(); public Data get(String key) {
r.lock();
try { return m.get(key); }
finally { r.unlock(); }
}
public String[] allKeys() {
r.lock();
try { return m.keySet().toArray(); }
finally { r.unlock(); }
}
public Data put(String key, Data value) {
w.lock();
try { return m.put(key, value); }
finally { w.unlock(); }
}
public void clear() {
w.lock();
try { m.clear(); }
finally { w.unlock(); }
}
}

  但是还是不建议这么用,因为已经有ConcurrentHashMap了。

4.4 ReentrantReadWriteLock理解

4.4.1 锁顺序是否可以按读锁或者写锁来优先指定

  不可以,要么是随机的,要么是按照公平策略,优先安排等待时间最长的线程获取它想要的锁。

4.4.2 什么是锁重入

  允许 reader 和 writer 按照 ReentrantLock 的样式重新获取读取锁或写入锁。在写入线程保持的所有写入锁都已经释放后,才允许重入 reader 使用它们。

  此外,writer 可以获取读取锁,但反过来则不成立。在其他应用程序中,当在调用或回调那些在读取锁状态下执行读取操作的方法期间保持写入锁时,重入很有用。如果 reader 试图获取写入锁,那么将永远不会获得成功。

4.4.3 什么是锁降级

  重入还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不可能的。

五、volatile

  我觉得这个关键词水比较深,轻易不要把它用在同步上,volatile的中文意思是不稳定的。先找个JDK源码中的例子看一下(jdk1.8大约有130个类使用了volatile),Thread类中有:

private volatile Interruptible blocker;

  这是线程的与中断有关的变量,当一个线程获得它需要中断时会立即抛出异常。下面是HashMap里面的一个变量:

transient volatile int modCount;

  这个用来变量是一个计数器,用在当迭代时若对容器修改,便抛出异常的一个操作。

5.1 小总结

  在什么情况下使用volatile:当一个变量需要做为一个信号,具有各种状态,改变状态将会引发一种操作的时候,就用volatile。

  简单解释一下,当线程读取一个变量时,会对变量进行缓存,所以若对一种信号的变化比较敏感需要使用volatile,那就不能使用缓存,每次都需要读取实际的值。最后说一遍企图对volatile变量进行并发的i++,这样没有什么意义。

六、Condition

  这是由新增Lock类而同时增加的类,毕竟对象的wait和notify方法要在synchronized语句块中,既然现在用Lock了当然要新增一种新的等待唤醒机制了,JDK API已经说得很清楚了:

Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。 

  而示例已经足够说明用法了,所以java的api文档是最好的参考资料:

class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition(); final Object[] items = new Object[100];
int putptr, takeptr, count; public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
} public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}

  这个就是最基本的用法。

6.1 构造方法

   没有具体的构造方法,通过Lock实现对象来获取Condition对象,Lock有下面的方法:newCondition() :返回用来与此 Lock 实例一起使用的 Condition 实例。

6.2 普通方法

  Condition的方法和对象的等待唤醒类似:

  • void await() :造成当前线程在接到信号或被中断之前一直处于等待状态。
  • boolean await(long time, TimeUnit unit) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
  • long awaitNanos(long nanosTimeout) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
  • void awaitUninterruptibly() :造成当前线程在接到信号之前一直处于等待状态。
  • boolean awaitUntil(Date deadline) :造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。
  • void signal() :唤醒一个等待线程。
  • void signalAll() :唤醒所有等待线程。

  等待变成了await方法,唤醒变成了signal方法。