JDK并发包(concurrent) - 重入锁(ReentrantLock)

    xiaoxiao2021-03-25  162

    重入锁是可以完全替代synchronized关键字的,在jdk 5.0的早期版本中,重入锁的性能远远高于synchronized的,但是从JDK 6.0开始,jdk在synchronized上做了大量优化,使得两者的性能差距并不是很大。 重入锁使用java.util.concurrent.locks.ReentrantLock类来实现的,先看个例子

    package com.example.thread; import java.util.concurrent.locks.ReentrantLock; /** * Created by mazhenhua on 2017/3/8. */ 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 reenterLock = new ReenterLock(); Thread t1 = new Thread(reenterLock); Thread t2 = new Thread(reenterLock); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } }

    上述代码中,在进行i的累加时,使用了重入锁。由此可以看出,重入锁和synchronized锁,有着显示的操作过程,也就是必须手动加锁和手动释放锁。也因为这样,重入锁的灵活性要比synchronized要好。在使用完重入锁时,一定要记得释放,不然就会导致死锁。

    锁就叫锁呗,为啥非要加个”重入“两个字呢,那是因为这种锁,在一个线程中是可以反复进入的,例如

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

    这样也是可以的,但是加锁的数量与释放锁的数量一定要一致,如果加锁多,释放锁少就会出现死锁,锁无法释放,其他线程也无法获得。如果释放的锁多了,就会报一个错,如图:

    重入锁除了,可以重入之外,还提供了一些其他的高级功能。

    1. 中断响应

    对于synchronized来说,如果一个线程在等待锁,那么他的结果只有两种,第一,获得锁进入临界区,第二,继续等待。而重入锁提供了另外一种可能,那就是线程可以被中断,线程可以根据需要取消对锁的请求。比如,你和朋友约好一起去打球,如果你等了半个小时,朋友还没有到。突然接到一个电话,说由于突发情况,不能如约,那么你一定就扫兴打到回府。中断就是提供了一种类似这样的机制,如果一个线程在等待锁,但是突然被告知不用等了。这种情况对死锁有一定的帮助。例如:

    package com.example.thread; import java.util.concurrent.locks.ReentrantLock; /** * Created by mazhenhua on 2017/3/8. */ public class IntLock implements Runnable { public static ReentrantLock lock1 = new ReentrantLock(); public static ReentrantLock lock2 = new ReentrantLock(); int lock; public IntLock(int lock) { this.lock = lock; } @Override public void run() { try { if (lock == 1){ lock1.lockInterruptibly(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } lock2.lockInterruptibly(); } else { lock2.lockInterruptibly(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } lock1.lockInterruptibly(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { if (lock1.isHeldByCurrentThread()){ lock1.unlock(); } if (lock2.isHeldByCurrentThread()){ lock2.unlock(); } System.out.println(Thread.currentThread().getId() + " :线程退出"); } } public static void main(String[] args) throws InterruptedException { IntLock r1 = new IntLock(1); IntLock r2 = new IntLock(2); Thread t1 = new Thread(r1); Thread t2 = new Thread(r2); t1.start(); t2.start(); Thread.sleep(1000); t2.interrupt(); } }

    线程t1,t2启动后,t1先抢占了lock1锁,再占用lock2锁,t2先占用lock2锁,再请求lock1锁。因此很容易形成相互等待,也就是死锁。这里获取锁使用的是lockInterruptibly()方法,这是一个可被中断的锁。在并发包里,如果方法名上看到类似Interruptibly这样的单词,基本上都是表示,可被中断的,如果前面加上Un即表示不可中断的。main方法的最后一行,将t2线程进行了中断,t2放弃了对lock1的锁申请,也释放了lock2,使得t1得到了lock2,顺利的执行完成。

    2. 锁申请等待限时

    除了避免等待外部通知之外,避免死锁还有一种方法,就是限时等待。还是约朋友去打球,如果你到场后等了几个小时,朋友还没有去,那你一定会扫兴的离开。对于线程也是一样,我们无法判断线程为什么迟迟拿不到锁,也许是产生了饥饿,也许是产生了死锁。但是如果给定一个时间,让线程主动放弃,那么对系统来说,是非常有必要的。例如:

    package com.example.thread; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; /** * Created by mazhenhua on 2017/3/8. */ 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 timeLock = new TimeLock(); Thread t1 = new Thread(timeLock); Thread t2 = new Thread(timeLock); t1.start(); t2.start(); } }

    这里加锁用了tryLock()方法,这个方法接收两个参数,一个表示计时时长,一个表示计时单位。这里设置了5秒,表示线程在获取这个锁时,最多等待5s的时间。 tryLock()方法也可以不传递参数,不传递任何参数表示不等待锁,如果线程在获取锁时,发现锁被占用,此时直接返回false,这种情况不会引起线程等待,更不会产生死锁,下面代码演示了这种情况

    package com.example.thread; import java.util.concurrent.locks.ReentrantLock; /** * Created by mazhenhua on 2017/3/8. */ 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){ e.printStackTrace(); } 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) { e.printStackTrace(); } 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(); } }

    代码很简单自己看。。。。。。

    3.公平锁

    大多数情况下,锁的申请都是不公平的,比如,线程A申请了锁,线程B也申请了这个锁,当锁可用时,是A用,还是B用,这个是随机的,并不是先等待先使用。而公平锁则是,谁先申请的,谁先使用。那么就是需要有个队列存储这些先后顺序,所以想要获得公平就要牺牲掉性能。synchronized就是非公平锁。而重入锁允许我们对其进行设置。他提供了个如下的构造函数:

    public ReentrantLock(boolean fair)

    当fair为true时,表示锁是公平锁,上代码:

    package com.example.thread; import java.util.concurrent.locks.ReentrantLock; /** * Created by mazhenhua on 2017/3/8. */ public class FairLock implements Runnable { public static ReentrantLock fairLock = new ReentrantLock(true); @Override public void run() { while (true){ try { fairLock.lock(); System.out.println(Thread.currentThread().getName() + " 获得锁"); } finally { fairLock.unlock(); } } } public static void main(String[] args) throws InterruptedException{ FairLock r1 = new FairLock(); Thread t1 = new Thread(r1, "Thread-1"); Thread t2 = new Thread(r1, "Thread-2"); t1.start(); t2.start(); } }

    对上面ReentrantLock的几个重要方法做个总结:

    lock():获取锁,如果锁被占用则等待。lockInterruptibly():获取锁,但优先响应中断。tryLock():尝试获得锁,成功返回true,不成功返回false,该方法不等待,直接返回tryLock(long time, TimeUnit unit):给定时间内尝试获取锁,超时返回falseunlock():释放锁。

    在重入锁的实现中,主要包含三个要素: 1. 原子态。原子态使用CAS操作来存储当前锁的状态,判断锁是否已经被别的线程所有。 第二,等待队列。所有线程没有请求到锁时,都会进入等待队列中进行等待。待有线程释放锁后,系统就能2. 从等待队列中,唤醒一个线程,继续工作。 3. 是阻塞源于park()和unpark(),用来挂起和恢复线程,没有得到锁的线程将会被挂起。(后面会介绍敬请期待)。

    4. 重入锁的好搭档:Condition

    如果你理解了Object.wait()和Object.notify()方法的话,那么就很容易理解Condition了。不理解的可以翻回去看多线程基础知识

    Condition接口提供的基本方法如下:

    await()方法会使当前线程等待,同时释放当前锁,当其他线程中使用signal()或者signalAll()方法时,线程会重新获取锁,并继续执行。或者当线程被中断时,也能跳出等待。awaitUninterruptibly()方法,与await()方法基本相同,但是他并不会在等待过程中响应中断,signal()方法用于唤醒一个在等待的线程,signalAll()则是唤醒所有等待中的线程。

    上代码:

    package com.example.thread; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; /** * Created by mazhenhua on 2017/3/8. */ public class ReenterLockCondition implements Runnable { public static ReentrantLock lock = new ReentrantLock(); public static Condition condition = lock.newCondition(); @Override public void run() { try { lock.lock(); condition.await(); System.out.println("Thread is going on"); } catch (InterruptedException e){ e.printStackTrace(); } finally { lock.unlock(); } } public static void main(String[] args) throws InterruptedException { ReenterLockCondition reenterLockCondition = new ReenterLockCondition(); Thread t1 = new Thread(reenterLockCondition); t1.start(); Thread.sleep(2000); lock.lock(); condition.signal(); lock.unlock(); } }

    与wait()和notif()方法需要在synchronized锁内一样,condition也要在Lock锁之内才行

    4. 允许多个线程同时访问:信号量(Semaphore)

    信号量为多线程之间的协作提供了更为强大的支持。广义上说是对锁的扩展,无论是内部锁synchronized还是重入锁,ReentrantLock,一次都只允许一个线程访问资源,而信号量却可以指定多个线程,同时访问某个资源 Semaphore的构造方法有两个Semaphore(int permits),Semaphore(int permits, boolean fair),第一个只指定了有多少个线程可以同时访问,第二个还制定了访问是否公平

    Semaphore类的主要方法:

    acquire():尝试获取一个准入许可,若无则等待直到有线程释放,或者被中断。acquireUninterruptibly(int permits):和acquire()一样,只是不响应中断tryAcquire():尝试获取一个准入许可,不等待,直接返回false或者返回truetryAcquire(long timeout, TimeUnit unit):可以设定等待时间和时间的单位release():释放一个许可

    上代码:

    package com.example.thread; import sun.text.resources.FormatData_en_IN; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * Created by mazhenhua on 2017/3/8. */ public class SemapDemo implements Runnable { final Semaphore semp = new Semaphore(5); @Override public void run() { try { semp.acquire(); // 模拟耗时操作 Thread.sleep(2000); System.out.println(Thread.currentThread().getId() + " : done!"); semp.release(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { ExecutorService exec = Executors.newFixedThreadPool(20); final SemapDemo demo = new SemapDemo(); for (int i = 0; i < 20; i++){ exec.submit(demo); } } }

    运行上面的代码,你会发现,输出是5个一组被输出的。

    转载请注明原文地址: https://ju.6miu.com/read-4969.html

    最新回复(0)