Java多线程——加锁机制

Java的锁分为内置锁和显式锁。内置锁在我们平时使用synchronized关键字的时候获取。而本文所提到的显式锁则是通过获取java.util.concurrent.locks包下面的ReentrantLock类或者ReentrantReadWriteLock类的实例来获取的。

一、内置锁(synchronized)

通过synchronized关键字可以获取到内置锁,synchronized定义的同步代码块包括两个部分: 1. 对象的锁;2. 锁的范围。

synchronized用法主要分为三种情况:

1. 代码块——获取对象lock的锁,直到执行完代码块,释放该锁:

synchronized (lock) {
// Do something
}

2. 实例方法——获取this的锁,直到执行完方法,释放该锁:

public synchronized void f() {
// Do something
}

3. 静态方法——获取类的锁(XXX.class),直到执行完方法,释放该锁:

public synchronized static void f() {
// Do something
}

一些特性:

1. 无需显示释放锁,锁的释放是自动执行的;

2. 锁的重入:Java中锁的粒度是线程级别的,也就是说当线程持有某一个对象的锁的时候,该线程可以再次获取该对象的锁,从而进入同步代码块。JVM会为每一个锁维持一个计数器。每当被获取时计数器加1,退出代码块时-1,当计数器为0时,锁将被释放。

3. 死锁。还记得“哲学家的筷子”吗?当线程需求多个锁的时候就有可能导致死锁问题。只有大家遵循同一个顺序来请求锁才可以避免这种情况的发生。

二、ReentrantLock

类ReentrantLock实现了接口Lock,相对于内置锁而言它提供了更多的选择,所以也更加灵活。下面是获取的一种方法

class X {
   private final Lock lock = new ReentrantLock();
   // ...

   public void m() { 
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }

一些特性:

1. 避免死锁

ReentrantLock实现的tryLock() 方法会立即返回获取锁成功与否,而不是在获取锁失败的时候使线程阻塞,这避免了死锁的发生。下面代码实现了定时重试的功能,并在10次之后失败之后退出:

public void testTryLock(SomeObject a, SomeObject b, long timeout) throws InterruptedException{
  int count = 0;
  long fixedDelay = timeout;

  while (true){
    if (a.lock.tryLock()){
      try{
        if(b.lock.tryLock()){
          try {
            // Do Something
            return;
          }finally{
            b.lock.unlock();
          }
        }    
      }finally{
        a.lock.unlock();
      }
    }

    ++count;

    if (count > 10)
      return;
    
    NANOSECONDS.sleep(fixedDelay+rnd.nextLong()%timeout);
  }
}

2. 可中断

tryLock()以及lockInterruptibly()方法能够使在获取锁的同时保持线程响应中断,但是需要注意的是,如果是使用lock()则不会有此功能。

3. 公平性

通过构造函数public ReentrantLock(boolean fair)可以指定公平性机制。

三、ReentrantReadWriteLock

类ReentrantReadWriteLock提供了读写锁的机制。

在用标准的互斥锁(内置锁和ReentrantLock都是互斥锁)时,只有一个线程可以获取到锁,其它获取锁的线程都会阻塞。然而这种一竿子打死的严苛协议难免会伤及“无辜”,比如说当两个线程对于某一个对象都操作都是读的时候我们就没有必要去阻止某一个对象获取锁(在这种情况下,对象就相当于一个不可变对象,而不可变对象一定是线程安全的)。而读写锁就放宽了限制,允许多个读操作的线程同时获取读取锁。在读多写少的情况之下,这可以提高并发性,从而提高性能。然而,读写锁更加地复杂,如果读多写少并不是很突出的话,性能亦或会相较于传统的互斥锁有所下降。

一个ReentrantReadWriteLock的实例,可以通过readLock()和writeLock()可以分别获取到读取锁和写入锁。

由于有两种不同的锁,在使用ReentrantReadWriteLock时,我们应该考虑到以下几点:

1. 锁的升级和降级

当线程持有写入锁的时候能不能不释放该锁而直接获取读取锁?当线程持有读取锁的时候获取写入锁是不是有优先权(会不会导致Deadlock)?

2. 重入

是否允许读取锁重入,而写入锁又如何?

3. 优先级

当即将释放锁的是写入锁的时候,队列里面哪种线程(读线程和写线程)获取锁的优先级会高一点?同样的,如果线程持有的锁是读取锁的时候,新来了一个读线程(而队列里面已经有了写线程),是立刻执行读线程(写线程可能会面临Starvation)还是让其在写线程后面等待?

总结

synchronized关键字给我们提供了获取对象内置锁的方式。而显示锁则可以提供更多的功能和灵活性。选择内置锁还是显式锁的方式,取决于具体的需求。最后需要注意的是内置锁也有很多优点,它无需手动释放,而且是JVM的内置特性(因此JVM会对其做很多优化),同时已经被大量的代码采用,效率上相对于ReentrantLock的劣势也已然不再明显……

Reference:

1. “Java Concurrency In Practice” by Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes and Doug Lea

2. http://docs.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/locks/package-summary.html

    原文作者:java锁
    原文地址: https://blog.csdn.net/sakurairo_maukoro/article/details/44135697
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞