ReentrantLock
的使用
ReentrantLock
可以完全替代 synchronized
,提供了一种更灵活的锁.
ReenTrantLock
必须手动释放锁,为防止发生异常,必须将同步代码用 try
包裹起来,在 finally
代码块中释放锁.
public class T { ReentrantLock lock = new ReentrantLock(); // 使用ReentrantLock的写法 private void m1() { // 尝试获得锁 lock.lock(); try { System.out.println(Thread.currentThread().getName()); } finally { // lock.unlock(); } } // 使用synchronized的写法 private synchronized void m2() { System.out.println(Thread.currentThread().getName()); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { T t = new T(); new Thread(t::m1, "t1").start(); new Thread(t::m2, "t2").start(); } }
ReentrantLock
获取锁的方法
尝试锁 tryLock()
使用 tryLock()
方法可以尝试获得锁,返回一个 boolean
值,指示是否获得锁.
可以给 tryLock
方法传入阻塞时长,当超出阻塞时长时,线程退出阻塞状态转而执行其他操作.
public class T{ ReentrantLock lock = new ReentrantLock(); void m1() { lock.lock(); // 相当于 synchronized try { for (int i = 0; i < 5; i++) { TimeUnit.SECONDS.sleep(1); System.out.println(i); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); // 使用完毕后,必须手动释放锁 // 不同于synchronized,抛出异常后,不会自动释放锁,需要我们在finally中释放此锁 } } void m2() { // 尝试获取锁,返回true拿到了 if (lock.tryLock()) { // lock.tryLock(5, TimeUnit.SECONDS) // 等5s内还没拿到就返回false System.out.println("m2..."); lock.unlock(); } else { System.out.println(" m2 没拿到锁"); } } public static void main(String[] args) { T r1 = new T(); new Thread(r1::m1, "t1").start(); // m1 已经执行,被t1占有锁this try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(r1::m2, "t2").start(); // 锁已经被其他线程占用,m1执行完毕后,不会执行 } }
程序运行结果如下:
0 m2 没拿到锁 1 2 3 4
可中断锁 lockInterruptibly
使用 lockInterruptibly
以一种可被中断的方式获取锁.获取不到锁时线程进入阻塞状态,但这种阻塞状态可以被中断.调用被阻塞线程的 interrupt()
方法可以中断该线程的阻塞状态,并抛出 InterruptedException
异常.
interrupt()
方法只能中断线程的阻塞状态
.若某线程已经得到锁或根本没去尝试获得锁,则该线程当前没有处于阻塞状态
,因此不能被interrupt()
方法中断.
public class T{ public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); Thread t1 = new Thread(() -> { lock.lock(); try { System.out.println("t1 start"); TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);// 线程一直占用锁 System.out.println("t1 end"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }, "t1"); t1.start(); // 线程2抢不到lock锁,若不被中断则一直被阻塞 Thread t2 = new Thread(() -> { try { lock.lockInterruptibly(); // t2 尝试获取锁 System.out.println("t2 start"); TimeUnit.SECONDS.sleep(3); System.out.println("t1 end"); } catch (InterruptedException e) { System.out.println("t2 等待中被打断"); } finally { lock.unlock(); // 没有锁定进行unlock就会抛出 IllegalMonitorStateException } }, "t2"); t2.start(); try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } // 打断线程2的等待 t2.interrupt(); } }
程序运行结果如下:
t1 start t2 等待中被打断 Exception in thread "t2" java.lang.IllegalMonitorStateException at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151) at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261) at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457) at c_020.ReentrantLock4.lambda$main$1(ReentrantLock4.java:41) at java.lang.Thread.run(Thread.java:748)
并不是所有处于阻塞状态的线程都可以被 interrupt()方法中断,要看该线程处于具体的哪种阻塞状态.阻塞状态包括普通阻塞,等待队列,锁池队列.
- 普通阻塞: 调用 sleep()方法的线程处于普通阻塞,调用其 interrupt()方法可以中断其阻塞状态并抛出 InterruptedException 异常
- 等待队列: 调用锁的 wait()方法将持有当前锁的线程转入等待队列,这种阻塞状态只能由锁对象的 notify 方法唤醒,而不能被线程的 interrupt()方法中断.
- 锁池队列: 尝试获取锁但没能成功抢到锁的线程会进入锁池队列:
- 争抢 synchronized 锁的线程的阻塞状态不能被中断.
- 使用 ReentrantLock 的 lock()方法争抢锁的线程的阻塞状态不能被中断.
- 使用 ReentrantLock 的 tryLock()和 lockInterruptibly()方法争抢锁的线程的阻塞状态不能被中断.
公平锁
在初始化 ReentrantLock
时给其 fair
参数传入 true
,可以指定该锁为 公平锁
,
synchronized 是不公平锁。
CPU 默认的进程调度是 不公平的
,也就是说,CPU 不能保证等待时间较长的线程先被执行.但 公平锁
可以保证等待时间较长的线程先被执行。
公平锁,先获取锁的人,在锁被释放时,优先获得锁。
不公平锁,无论先后,线程调度器将会随机给某个线程锁,不用计算线程时序,效率较高。
public class T extends Thread { private static ReentrantLock lock = new ReentrantLock(true);// 指定锁为公平锁 @Override public void run() { for (int i = 0; i < 3; i++) { lock.lock(); try { System.out.println(Thread.currentThread().getName() + "获取锁"); sleep(100); }catch (InterruptedException e){ e.printStackTrace(); }finally { lock.unlock(); // 公平锁 t1 unlock 后,等待时间长的一定是 t2 所以下次一定是 t2 执行 } } } public static void main(String[] args) { T t1 = new T(); new Thread(t1).start(); new Thread(t1).start(); } }
程序运行结果如下:
Thread-1获取锁 Thread-2获取锁 Thread-1获取锁 Thread-2获取锁 Thread-1获取锁 Thread-2获取锁
生产者/消费者模式
经典面试题:写一个固定容量的容器,拥有 put 和 get 方法,以及 getCount 方法。能够支持 2 个生产者线程以及 10 个消费者线程的阻塞调用。如果调用 get 方法时,容器为空,get 方法就需要阻塞等待;如果调用 put 方法时,容器满了,put 方法就需要阻塞等待
1、使用 synchronized
的 wait()/notify()
实现
public class MyContainer1<T> { private final LinkedList<T> list = new LinkedList<>(); private final int MAX = 10; private int count = 0; public synchronized void put(T t) { while (list.size() == MAX) { // 如果容量最大,释放锁等待 //【这里为什么使用while,而不是使用if???】 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 否则 put list.add(t); ++count; this.notifyAll(); // 通知消费者线程,可以消费了 // 【这里为什么调用 notifyAll 而不是 notify ?】 } public synchronized T get() { T t = null; while (list.size() == 0) { // 如果容量为空,释放锁等待 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 否则获取 t = list.removeFirst(); count--; this.notifyAll(); // 通知生产者线程生产 return t; } public static void main(String[] args) { MyContainer1<String> c = new MyContainer1<>(); //启动消费者线程 for (int i = 0; i < 10; i++) { new Thread(() -> { for (int j = 0; j < 5; j++) { System.out.println(c.get()); } }, "c" + i).start(); } try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } //启动生产者线程 for (int i = 0; i < 2; i++) { new Thread(() -> { for (int j = 0; j < 10; j++) { c.put(Thread.currentThread().getName() + " " + j); } }, "p" + i).start(); } } }
为什么使用 while 而不是使用 if ?
在与 wait()的配合中,百分之 99 的程序都是与 while 而不是 if 结合使用。
上述代码中,在容器已满的情况下,put 方法会 wait 等待,当容器中的元素被消费者消费了一部分,就会唤醒所有 put 方法,
put 方法会继续向下执行,直接执行 list.add(t),那么多个生产者线程执行 list.add()就有可能出现数据一致性的问题。
如果使用 while 则会循环判断,就避免了这些问题。
不是有锁吗?为什么会需要循环判断?
wait 之后,锁就会失去,再次被唤醒时,并且得到锁之后,是从 list.add()开始执行的,会无判断直接加入到容器中。
为什么调用 notifyAll 而不是 notify ?
因为 notify 有可能再次叫醒一个生产者线程
2、使用 Condition
对象实现
用 Lock 和 Condition 实现,可以精确唤醒某些线程
public class MyContainer2<T> { private final LinkedList<T> list = new LinkedList<>(); private final int MAX = 10; private int count = 0; private Lock lock = new ReentrantLock();// 锁对象 // 绑定在锁上的一个条件,阻塞在该条件上的线程为生产者线程 private Condition producer = lock.newCondition(); // 绑定在锁上的一个条件,阻塞在该条件上的线程为消费者线程 private Condition consumer = lock.newCondition(); public void put(T t) { lock.lock(); try { while (list.size() == MAX) { // 当前线程应该是生产者线程,因此将当前线程阻塞在producerCondition上 producer.await(); } list.add(t); ++count; // 唤醒所有阻塞在consumerCondition的线程,这些被唤醒的线程都应该是消费者线程 consumer.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public T get() { T t=null; lock.lock(); try { while (list.size() == 0) { // 当前线程应该是消费者线程,因此将当前线程阻塞在consumerCondition上 consumer.await(); } t = list.removeFirst(); count--; // 唤醒所有阻塞在producerCondition的线程,这些被唤醒的线程都应该是生产者线程 producer.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } return t; } public static void main(String[] args) { MyContainer2<String> c = new MyContainer2<>(); //启动消费者线程 for (int i = 0; i < 10; i++) { new Thread(() -> { for (int j = 0; j < 5; j++) { System.out.println(c.get()); } }, "c" + i).start(); } try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } //启动生产者线程 for (int i = 0; i < 2; i++) { new Thread(() -> { for (int j = 0; j < 10; j++) { c.put(Thread.currentThread().getName() + " " + j); } }, "p" + i).start(); } } }
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于