多线程lock与lockinterruptibly区别
在多线程编程中,锁(lock)是用来确保多个线程在访问共享资源时能够保持一致性和正确性的关键工具。
java 提供了多种实现锁的机制,其中最常用的是 reentrantlock
。reentrantlock
提供了两种获取锁的方法:lock
和 lockinterruptibly
。
这两者在处理线程中断时表现不同,理解这些差异对于编写健壮的多线程程序至关重要。
lock 方法
示例代码:
public static void lock() { lock lock = new reentrantlock(); try { lock.lock(); thread t1 = new thread(() -> { thread currentthread = thread.currentthread(); try { lock.lock(); system.out.println(currentthread.getname() + " lock后的代码"); } finally { lock.unlock(); } system.out.println("currentthread = " + currentthread.getname() + " isinterrupted:" + currentthread.isinterrupted()); }, "t1"); t1.start(); t1.interrupt(); system.out.println("currentthread = " + thread.currentthread().getname()); } finally { lock.unlock(); } }
执行结果:
currentthread = main
t1 lock后的代码
currentthread = t1 isinterrupted:true
解释:
在这个示例中,主线程(main
)首先获取了锁。然后启动一个新线程(t1
),该线程尝试再次获取同一个锁。主线程在启动 t1
后立即调用 t1.interrupt()
方法中断 t1
线程。
尽管 t1
线程被中断,它还是成功获取到了锁并执行了锁后的代码。这是因为 lock
方法在获取锁时不会理会线程的中断状态,只要锁可用,它就会获取锁。换句话说,即使线程被中断,它也会继续等待获取锁,直到成功为止。
lockinterruptibly 方法
示例代码:
public static void lockinterruptiblydemo() { lock lock = new reentrantlock(); try { lock.lock(); thread t1 = new thread(() -> { thread currentthread = thread.currentthread(); try { lock.lockinterruptibly(); system.out.println(currentthread.getname() + " lockinterruptibly后的代码"); } catch (interruptedexception e) { e.printstacktrace(); } finally { lock.unlock(); } system.out.println("currentthread = " + currentthread.getname() + " isinterrupted:" + currentthread.isinterrupted()); }, "t1"); t1.start(); t1.interrupt(); system.out.println("currentthread = " + thread.currentthread().getname()); } finally { lock.unlock(); } }
执行结果:
currentthread = main
java.lang.interruptedexception
at java.util.concurrent.locks.abstractqueuedsynchronizer.acquireinterruptibly(abstractqueuedsynchronizer.java:1220)
at java.util.concurrent.locks.reentrantlock.lockinterruptibly(reentrantlock.java:335)
at xxx.lambda$lockinterruptiblydemo$1(xxx.java:39)
at java.lang.thread.run(thread.java:748)
exception in thread "t1" 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 xxx.lambda$lockinterruptiblydemo$1(xxx.java:44)
at java.lang.thread.run(thread.java:748)
解释:
在这个示例中,t1
线程尝试获取锁时调用了 lockinterruptibly
方法。与 lock
方法不同,lockinterruptibly
在尝试获取锁时会检查线程的中断状态。如果线程被中断,它会立即抛出 interruptedexception
异常,而不会继续等待获取锁。
执行结果显示,t1
线程在尝试获取锁时被中断,并且抛出了 interruptedexception
。由于异常被捕获并打印,t1
线程并没有获取锁,这避免了在某些情况下可能出现的死锁问题。
区别总结
中断响应:
lock
方法:不响应中断,线程会一直尝试获取锁,直到成功为止。lockinterruptibly
方法:响应中断,线程在检测到中断后会抛出interruptedexception
并停止尝试获取锁。
使用场景:
lock
方法适用于不需要考虑中断的场景。lockinterruptibly
方法适用于需要及时响应中断的场景,例如当需要能够中断线程来避免死锁时。
面试题:为什么 aqs 中的队列设计为双向链表?
aqs(abstractqueuedsynchronizer)是 java 并发包中锁和同步器的基础框架。aqs 内部使用了一个 fifo(先进先出)的双向链表来管理等待线程的队列。
原因:
双向遍历:
- 当一个节点(线程)被唤醒时,需要能方便地找到前驱节点以确保线程的唤醒顺序和正确性。
- 双向链表允许从当前节点轻松访问前驱节点,从而能够修改前驱节点的状态。
节点删除:
- 在一些情况下,节点(线程)可能需要从队列中取消。
- 双向链表使得删除节点操作更加高效,因为可以直接通过前驱和后继节点来重新链接,而不需要从头遍历。
状态更新:
- 双向链表结构使得更新节点状态(如将节点从等待状态变为运行状态)更为便捷
- 可以在节点之间高效地传播状态信息
实现复杂性:
- 虽然双向链表在实现上稍微复杂一些
- 但提供的灵活性和操作效率的提升在高并发场景中是值得的
通过这种设计,aqs 能够更高效地管理线程队列,确保锁和同步器的高性能和正确性。
详细分析:aqs 中的双向链表
aqs 是一个基于节点的框架,所有等待获取锁的线程都会被封装成一个节点并加入到等待队列中。该队列的结构和操作直接影响锁的性能和响应能力。
aqs 中的主要节点结构:
每个节点包含以下主要部分:
- 前驱节点:指向前一个等待线程的节点。
- 后继节点:指向后一个等待线程的节点。
- 线程引用:包含正在等待获取锁的线程的引用。
- 状态:表示线程的等待状态(如等待、取消等)。
操作示例:
入队:
- 当一个线程请求获取锁但锁不可用时,它会被封装成一个节点并加入到队列末尾。
- 双向链表结构允许快速定位队尾并将新节点链接上去。
出队:
- 当一个节点被唤醒时(通常是获取到锁),它需要从队列中移除。
- 双向链表结构允许通过前驱和后继节点高效地重新链接,删除节点。
状态传播:
- 当一个节点状态改变时(如从等待到运行),它需要通知前驱或后继节点。
- 双向链表允许在节点之间高效地传播状态,确保队列中的每个线程都能正确响应状态变化。
总结
理解 lock
和 lockinterruptibly
的区别有助于我们在多线程编程中选择适当的锁获取方式,确保程序的健壮性和响应能力。而 aqs 中的双向链表设计则是确保锁和同步器高效管理等待线程队列的重要基础,这种设计使得线程管理更加高效和灵活,在高并发场景中表现尤为突出。通过深入理解这些底层机制,我们可以更好地编写高性能的多线程应用程序。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论