一、重入锁ReentrantLock简介

  重入锁可以完全替代synchronized关键字。在JDK 5.0的早期版本中,重入锁的性能远远好于synchronized,但从JDK 6.0开始,JDK在synchronized上做了大量的优化,使得两者的性能差距并不大。

  重入锁使用java.util.concurrent.locks.ReentrantLock类来实现。重入锁使用案例:

 public class ReenterLock implements Runnable{
     public static ReentrantLock lock=new ReentrantLock();
     public static int i=0;
     @Override
     public void run() {
         for(int j=0;j<10000000;j++){
             lock.lock();
             try{
                 i++;
             }finally{
                 lock.unlock();
             }
         }
     }
     public static void main(String[] args) throws InterruptedException {
         ReenterLock tl=new ReenterLock();
         Thread t1=new Thread(tl);
         Thread t2=new Thread(tl);
         t1.start();t2.start();
         t1.join();t2.join();
         System.out.println(i);
     }
 }

重入锁对逻辑控制的灵活性要远远好于synchronized。

重入锁是可以反复进入的。

lock.lock();
lock.lock();
try{
    i++;
}finally{
    lock.unlock();
    lock.unlock();
}

重入锁可以提供中断处理的能力

  对于synchronized来说,如果一个线程在等待锁,那么结果只有两种情况,要么它获得这把锁继续执行,要么它就保持等待。而使用重入锁,则提供另外一种可能,那就是线程可以被中断。也就是在等待锁的过程中,程序可以根据需要取消对锁的请求。有些时候,这么做是非常有必要的。中断响应可帮助处理死锁

lockInterruptibly() 方法的作用:如果当前线程未被中断则获得锁,如果当前线程被中断则出现异常.

public class TestMain {
    static class IntLock implements Runnable {
        // 创建两个ReentrantLock 锁对象
        public static ReentrantLock lock1 = new ReentrantLock();
        public static ReentrantLock lock2 = new ReentrantLock();
        int lockNum; // 定义整数变量,决定使用哪个锁

        public IntLock(int lockNum) {
            this.lockNum = lockNum;
        }

        @Override
        public void run() {
            try {
                if (lockNum % 2 == 1) { // 奇数,先锁1,再锁2
                    lock1.lockInterruptibly();
                    System.out.println(Thread.currentThread().getName()
                            + "获得锁1,还需要获得锁2");
                    Thread.sleep(new Random().nextInt(500));
                    lock2.lockInterruptibly();
                    System.out.println(Thread.currentThread().getName()
                            + "同时获得了锁 与锁2....");
                } else { // 偶数,先锁2,再锁1
                    lock2.lockInterruptibly();
                    System.out.println(Thread.currentThread().getName()
                            + "获得锁2,还需要获得锁1");
                    Thread.sleep(new Random().nextInt(500));
                    lock1.lockInterruptibly();
                    System.out.println(Thread.currentThread().getName()
                            + "同时获得了锁1 与锁2....");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (lock1.isHeldByCurrentThread()) // 判断当前线程是否持有该锁
                    lock1.unlock();
                if (lock2.isHeldByCurrentThread())
                    lock2.unlock();
                System.out.println(Thread.currentThread().getName() + "线程退出");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        IntLock intLock1 = new IntLock(11);
        IntLock intLock2 = new IntLock(22);
        Thread t1 = new Thread(intLock1);
        Thread t2 = new Thread(intLock2);
        t1.start();
        t2.start();
        // 在main 线程,等待3000 秒,如果还有线程没有结束就中断该线程
        Thread.sleep(3000);// 可以中断任何一个线程来解决死锁, t2 线程会放弃对锁1 的申请,同时释放锁2,t1
                            // 线程会完成它的任务
        if (t2.isAlive()) {
            t2.interrupt();
        }
    }
}

打印:

Thread-0获得锁1,还需要获得锁2
Thread-1获得锁2,还需要获得锁1
java.lang.InterruptedException
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
    at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
    at com.suxiaodong.thread.TestMain$IntLock.run(TestMain.java:33)
    at java.lang.Thread.run(Thread.java:748)
Thread-1线程退出
Thread-0同时获得了锁 与锁2....
Thread-0线程退出

  锁申请等待限时

  除了等待外部通知之外,要避免死锁还有另外一种方法,那就是限时等待。依然以约朋友打球为例,如果朋友迟迟不来,又无法联系到他。那么,在等待1~2个小时后,我想大部分人都会扫兴离去。对线程来说也是这样。通常,我们无法判断为什么一个线程迟迟拿不到锁。也许是因为死锁了,也许是因为产生了饥饿。但如果给定一个等待时间,让线程自动放弃,那么对系统来说是有意义的

public class TimeLock implements Runnable {
    public static ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        try {
            if (lock.tryLock(5, TimeUnit.SECONDS)) {
                Thread.sleep(6000);
            } else {
                System.out.println("get lock failed");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (lock.isHeldByCurrentThread())
                lock.unlock();
        }
    }

    public static void main(String[] args) {
        TimeLock tl = new TimeLock();
        Thread t1 = new Thread(tl);
        Thread t2 = new Thread(tl);
        t1.start();
        t2.start();
    }
}

  在这里,tryLock()方法接收两个参数,一个表示等待时长,另外一个表示计时单位。这里的单位设置为秒,时长为5,表示线程在这个锁请求中,最多等待5秒。如果超过5秒还没有得到锁,就会返回false。如果成功获得锁,则返回true

在本例中,由于占用锁的线程会持有锁长达6秒,故另一个线程无法在5秒的等待时间内获得锁,因此,请求锁会失败

  ReentrantLock.tryLock()方法也可以不带参数直接运行。在这种情况下,当前线程会尝试获得锁,如果锁并未被其他线程占用,则申请锁会成功,并立即返回true。如果锁被其他线程占用,则当前线程不会进行等待,而是立即返回false。这种模式不会引起线程等待,因此也不会产生死锁。

public class TryLock implements Runnable {
     public static ReentrantLock lock1 = new ReentrantLock();
     public static ReentrantLock lock2 = new ReentrantLock();
     int lock;

     public TryLock(int lock) {
         this.lock = lock;
     }

     @Override
     public void run() {
         if (lock == 1) {
             while (true) {
                 if (lock1.tryLock()) {
                     try {
                         try {
                            Thread.sleep(500);
                         } catch (InterruptedException e) {
                         }
                         if (lock2.tryLock()) {
                             try {
                                 System.out.println(Thread.currentThread()
                                         .getId() + ":My Job done");
                                 return;
                             } finally {
                                 lock2.unlock();
                             }
                         }
                     } finally {
                         lock1.unlock();
                     }
                 }
             }
         } else {
             while (true) {
                 if (lock2.tryLock()) {
                     try {
                         try {
                             Thread.sleep(500);
                         } catch (InterruptedException e) {
                         }
                         if (lock1.tryLock()) {
                             try {
                                 System.out.println(Thread.currentThread()getId() + ":My Job done");
                                 return;
                             } finally {
                                 lock1.unlock();
                             }
                         }
                     } finally {
                         lock2.unlock();
                     }
                 }
             }
         }
     }

     public static void main(String[] args) throws InterruptedException {
         TryLock r1 = new TryLock(1);
         TryLock r2 = new TryLock(2);
         Thread t1 = new Thread(r1);
         Thread t2 = new Thread(r2);
         t1.start();
         t2.start();
     }
 }

   上述代码中,采用了非常容易死锁的加锁顺序。也就是先让t1获得lock1,再让t2获得lock2,接着做反向请求,让t1申请lock2,t2申请lock1。在一般情况下,这会导致t1和t2相互等待,从而引起死锁。

  但是使用tryLock()后,这种情况就大大改善了。由于线程不会傻傻地等待,而是不停地尝试,因此,只要执行足够长的时间,线程总是会得到所有需要的资源,从而正常执行(这里以线程同时获得lock1和lock2两把锁,作为其可以正常执行的条件)。在同时获得lock1和lock2后,线程就打印出标志着任务完成的信息“My Job done”。

  执行上述代码,等待一会儿(由于线程中包含休眠500毫秒的代码)。最终你还是可以欣喜地看到程序执行完毕,并产生如下输出,表示两个线程双双正常执行。

9:My Job done
8:My Job done

 公平锁

  大多数情况下,锁的申请都是非公平的. 如果线程1 与线程2 都在请求锁A, 当锁A 可用时, 系统只是会从阻塞队列中随机的选择一个线程,不能保证其公平性.

  公平的锁会按照时间先后顺序,保证先到先得, 公平锁的这一特点不会出现线程饥饿现象.

  synchronized 内部锁就是非公平的. ReentrantLock 重入锁提供了一个构造方法:ReentrantLock(boolean fair) ,当在创建锁对象时实参传递true 可以把该锁设置为公平锁. 公平锁看起来很公平,但是要实现公平锁必须要求系统维护一个有序队列,公平锁的实现成本较高,性能也低. 因此默认情况下锁是非公平的. 不是特别的需求,一般不使用公平锁.

public ReentrantLock(boolean fair)

ReentrantLock的几个重要方法整理如下。

  • lock():获得锁,如果锁已经被占用,则等待。
  • lockInterruptibly():获得锁,但优先响应中断。
  • tryLock():尝试获得锁,如果成功,返回true,失败返回false。该方法不等待,立即返回。
  • tryLock(long time, TimeUnit unit):在给定时间内尝试获得锁。
  • unlock():释放锁。

二、 重入锁的好搭档:Condition条件

  关键字synchronized 与wait()/notify()这两个方法一起使用可以实现等待/通知模式. Lock 锁的newContition()方法返回Condition 对象,Condition 类也可以实现等待/通知模式.

  使用notify()通知时, JVM 会随机唤醒某个等待的线程. 使用Condition 类可以进行选择性通知. Condition 比较常用的两个方法:

  await()会使当前线程等待,同时会释放锁,当其他线程调用signal()时,线程会重新获得锁并继续执行.

  signal()用于唤醒一个等待的线程

  注意:在调用Condition 的await()/signal()方法前,也需要线程持有相关的Lock 锁. 调用await()后线程会释放这个锁,在singal()调用后会从当前Condition 对象的等待队列中,唤醒一个线程,唤醒的线程尝试获得锁, 一旦获得锁成功就继续执行.

public class ConditionalTest {
    private static ReentrantLock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();
    private static volatile int i = 0;

    public static class PrintJsThread extends Thread {
        @Override
        public void run() {
            try {
                lock.lock();
                while (i <= 100) {
                    if (i % 2 == 1) {
                        System.out.println(Thread.currentThread().getName() + "print" + i);
                        i++;
                    }
                    condition.signalAll();
                    condition.await();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
    
    public static class PrintOsThread extends Thread {
        @Override
        public void run() {
            try {
                lock.lock();
                while (i <= 100) {
                    if (i % 2 == 0) {
                        System.out.println(Thread.currentThread().getName() + "print" + i);
                        i++;
                    }
                    condition.signalAll();
                    condition.await();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        PrintJsThread js = new PrintJsThread();
        PrintOsThread os = new PrintOsThread();
        js.start();
        os.start();
    }
} 

 三、Lock与synchronized区别: