线程锁的原理是啥

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程锁的原理是啥相关的知识,希望对你有一定的参考价值。

我想问问线程锁是怎么实现互斥功能的

线程锁的原理:当对象获取锁时,它首先使自己的高速缓存无效,这样就可以保证直接从主内存中装入变量。 

同样,在对象释放锁之前,它会刷新其高速缓存,强制使已做的任何更改都出现在主内存中。 这样,会保证在同一个锁上同步的两个线程看到在 synchronized 块内修改的变量的相同值。

一般来说,线程以某种不必让其他线程立即可以看到的方式(不管这些线程在寄存器中、在处理器特定的缓存中,还是通过指令重排或者其他编译器优化),不受缓存变量值的约束。

扩展资料:

线程锁在run()函数中使用QMutex实现同步,当多个线程访问共享变量时,使用lock/trylock和unlock将共享变量包裹,以保证同步访问共享变量。

如果不加锁将会在2秒后同时修改num变量,将会导致线程不按照我们的想法执行,当前线程锁定后,其他线程如果遇到共享变量将会等待解锁;

使用QMutex上锁解锁时,当代码提前退出有可能并未执行unlock(),若其他线程采用lock上锁会一直被阻塞,导致内存溢出。

参考技术A 线程锁的使用 本文内容何时该使用线程锁.
线程锁的写法.
以线程锁的例子来理解线程的调度。使用线程锁的场合程序中经常采用多线程处理,这可以充分利用系统资源,缩短程序响应时间,改善用户体验;如果程序中只使用单线程,那么程序的速度和响应无疑会大打折扣。
但是,程序采用了多线程后,你就必须认真考虑线程调度的问题,如果调度不当,要么造成程序出错,要么造成荒谬的结果。一个讽刺僵化体制的笑话前苏联某官员去视察植树造林的情况,现场他看到一个人在远处挖坑,其后不远另一个人在把刚挖出的坑逐个填上,官员很费解于是询问陪同人员,当地管理人员说“负责种树的人今天病了”。
上面这个笑话如果发生在程序中就是线程调度的问题,种树这个任务有三个线程:挖坑线程,种树线程和填坑线程,后面的线程必须等前一个线程完成才能进行,而不是按时间顺序来进行,否则一旦一个线程出错就会出现上面荒谬的结果。用线程锁来处理两个线程先后执行的情况在程序中,和种树一样,很多任务也必须以确定的先后秩序执行,对于两个线程必须以先后秩序执行的情况,我们可以用线程锁来处理。
线程锁的大致思想是:如果线程A和线程B会执行实例的两个函数a和b,如果A必须在B之前运行,那么可以在B进入b函数时让B进入wait set,直到A执行完a函数再把B从wait set中激活。这样就保证了B必定在A之后运行,无论在之前它们的时间先后顺序是怎样的。线程锁的代码如右,SwingComponentLock的实例就是一个线程锁,lock函数用于锁定线程,当完成状态isCompleted为false时进入的线程会进入SwingComponentLock的实例的wait set,已完成则不会;要激活SwingComponentLock的实例的wait set中等待的线程需要执行unlock函数。public class SwingComponentLock
// 是否初始化完毕
boolean isCompleted = false; /**
* 锁定线程
*/
public synchronized void lock()
while (!isCompleted)
try
wait();
catch (Exception e)
e.printStackTrace();
logger.error(e.getMessage());


/**
* 解锁线程
*
*/
public synchronized void unlock()
isCompleted = true;
notifyAll();

线程锁的使用public class TreeViewPanel extends BasePanel
// 表空间和表树
private JTree tree; // 这个是防树还未初始化好就被刷新用的
private SwingComponentLock treeLock; protected void setupComponents()
// 初始化锁
treeLock = new SwingComponentLock(); // 创建根节点
DefaultMutableTreeNode root = new DefaultMutableTreeNode("DB");
tree = new JTree(root); // 设置布局并装入树
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
add(new JScrollPane(tree)); // 设置树节点的图标
setupTreeNodeIcons(); // 解除对树的锁定
treeLock.unlock();
/**
* 刷新树视图
*
* @param schemas
*/
public synchronized void refreshTree(List<SchemaTable> schemas)
treeLock.lock(); DefaultTreeModel model = (DefaultTreeModel) tree.getModel(); DefaultMutableTreeNode root = (DefaultMutableTreeNode) model.getRoot();
root.removeAllChildren(); for (SchemaTable schemaTable : schemas)
DefaultMutableTreeNode schemaNode = new DefaultMutableTreeNode(
schemaTable.getSchema()); for (String table : schemaTable.getTables())
schemaNode.add(new DefaultMutableTreeNode(table));
root.add(schemaNode);
model.reload();
讲解上页中,setupComponents函数是Swing主线程执行的,而refreshTree函数是另外的线程执行(初始化时程序开辟一个线程执行,其后执行由用户操作决定)。 refreshTree函数必须要等setupComponents函数把tree初始化完毕后才能执行,而tree初始化的时间较长,可能在初始化的过程中执行refreshTree的线程就进入了,这就会造成问题。
程序使用了一个SwingComponentLock来解决这个问题,setupComponents一开始就创建SwingComponentLock的实例treeLock,然后执行refreshTree的线程以来就会进入treeLock的wait set,变成等待状态,不会往下执行,这是不管tree是否初始化完毕都不会出错;而setupComponents执行到底部会激活treeLock的wait set中等待的线程,这时再执行refreshTree剩下的代码就不会有任何问题,因为setupComponents执行完毕tree已经初始化好了。
让线程等待和激活线程的代码都在SwingComponentLock类中,这样的封装对复用很有好处,如果其它复杂组件如table也要依此办理直接创建SwingComponentLock类的实例就可以了。如果把wait和notifyAll写在TreeViewPanel类中就不会这样方便了。总结线程锁用于必须以固定顺序执行的多个线程的调度。
线程锁的思想是先锁定后序线程,然后让线序线程完成任务再解除对后序线程的锁定。
线程锁的写法和使用一定要理解记忆下来。

JAVA中REENTRANT LOCK中公平参数的目的是啥?

【中文标题】JAVA中REENTRANT LOCK中公平参数的目的是啥?【英文标题】:What is the purpose of fairness parameter in REENTRANT LOCK in JAVA?JAVA中REENTRANT LOCK中公平参数的目的是什么? 【发布时间】:2022-01-09 22:29:24 【问题描述】:

我在浏览可重入锁的Java文档时发现了以下文字:

锁的公平性并不能保证线程调度的公平性。因此,使用公平锁的许多线程之一可能会连续多次获得它,而其他活动线程没有进展并且当前没有持有锁。

根据我的理解,这意味着,如果操作系统调度程序调度相同的线程(之前获取锁)并尝试再次获取相同的锁,Java 将允许它获取并且不会遵守公平参数值.有人可以告诉我们公平参数的目的是什么以及应该在什么情况下使用它。 我只是在想它是否只是一个优先级值,这可能会影响调度程序但不能保证线程执行顺序。

【问题讨论】:

线程调度的不同之处在于,您可以拥有比系统拥有的内核多很多倍的活动线程,因此有些线程正在等待获得处理时间。另一方面,如果多个线程正在积极尝试获取锁,则锁的公平性参数将影响它们中的哪一个获得它。文档所说的是,您可能有许多线程可能想要锁定,但如果线程调度程序只执行其中一个线程,即使它之前已经持有它也会获得锁定。 @Thomas 所以它就像对操作系统的提示。与优先级值相同。 不,不是,锁自己处理公平性。 Thead 调度也将由 JVM 处理,但也会受到操作系统的影响,即 JVM 进程本身的调度方式。对于锁,请考虑以下情况:线程 A 获得锁并释放它,现在线程 A 和 B 会再次想要锁,但由于 B 的调度晚于 A,A 再次获得锁,因为没有人在等待它(B 没有'不要尝试获取锁)。 @Thomas 你知道线程 B 不会被考虑,因为当 A 再次尝试获取锁时,它处于阻塞状态而不是可运行状态? 不,B 可能处于活动状态且可运行,但调度程序可能只是不运行它或出于任何原因足够早地运行它。这两个概念只是松散相关 - 检查所罗门和霍尔格的答案,他们正在更详细地描述这一点。 【参考方案1】:

在幼稚的观点中,使用公平锁的线程的行为会像

Thread 1 Thread 2 Thread 3
Acquire Do something Do something
Critical Section Try Acquire Do something
Critical Section Blocked Try Acquire
Release Acquire Blocked
Do something Critical Section Blocked
Try Acquire Release Acquire
Blocked Do something Critical Section
Acquire Do something Release

“Try Acquire”是指对lock() 的调用,它不会立即成功,因为另一个线程拥有锁。它不是指tryLock(),这通常是不公平的。

在这种幼稚的观点中,线程按“线程 1”、“线程 2”、“线程 3”的顺序获取锁,因为这是获取尝试的顺序。尤其是当“线程 1”试图在“线程 2”释放它的同时立即获取锁时,它不会像不公平锁那样超过,而是“线程 3”得到它,因为它等待的时间更长。

但是,正如文档所说,线程调度是不公平的。所以可能会发生以下情况。

Thread 1 Thread 2 Thread 3
Acquire Do something Do something
Critical Section Do something
Critical Section
Release
Do something
Acquire Try Acquire Try Acquire
Critical Section Blocked Blocked
Critical Section Blocked Blocked

空单元格表示线程根本没有获得任何 CPU 时间的阶段。线程可能比 CPU 内核多,其中包括其他进程的线程。操作系统甚至可能更愿意让“线程 1”在内核上继续运行,而不是切换到其他线程,这仅仅是因为该线程已经运行并且切换需要时间。

一般来说,尝试预测到达某个点的相对时间(如前面的工作负载获取锁)并不是一个好主意。在具有优化 JIT 编译器的环境中,即使是两个线程在完全相同的输入下执行完全相同的代码也可能具有完全不同的执行时间。

因此,当我们无法预测 lock() 尝试的时间时,坚持以不可预测的未知顺序获取锁并不是很有用。开发人员仍然希望公平的一种解释是,即使结果顺序不可预测,它也应该确保每个线程都取得进展,而不是在其他线程反复超车时无限等待锁。但这让我们回到了不公平的线程调度;即使根本没有锁,也不能保证所有线程都会进行。

那么为什么公平选项仍然存在?因为有时,人们对它在大多数情况下的工作方式感到满意,即使没有强有力的保证它会一直以这种方式工作。或者简单地说,因为如果它不存在,开发人员会反复要求它。支持公平的成本并不高,也不会影响不公平锁的性能。

【讨论】:

首先非常感谢您付出了这么多努力使其变得简单易懂。我假设 1) 当 Thread2,3 不在 CPU 上运行时,Thread1 发出锁获取请求(上表中缺少)。 2) 来自 JAVA 文档的“未进行”注释是指线程没有获得任何 CPU 时间而不是处于“阻塞状态”的时间。 没有 CPU 时间是“没有进展”的最常见原因。其他原因可能是“运行异常缓慢”,因为 JIT 还没有启动,或者“因为堆栈代码替换而停止”,因为 JIT 确实启动或“等待垃圾收集器”分配尝试。 阻塞状态和等待 CPU 在这里实际上是相同的,因为根据文档“如果一个线程正在等待其他线程获取的锁,它会被禁用以用于线程调度目的”。在阻塞状态下,它没有给任何 cpu 时间。 阻塞状态是公平锁考虑的lock()尝试的结果。在lock() 尝试之前没有从调度程序获取 CPU 时间是锁不知道的。在这两种情况下,线程都不会获得 CPU 时间,但与锁有关的区别就是这一切。【参考方案2】:

锁的公平性并不能保证线程调度的公平性。因此,使用公平锁的许多线程之一可能会连续多次获得它,而其他活动线程没有进展并且当前没有持有锁。

我将“没有进展”解释为“没有进展的原因与所讨论的锁定无关。”我认为他们试图告诉你“公平”仅在以下情况下才有意义锁的竞争非常激烈,以至于经常有一个或多个线程在等待轮到它们来锁定它。

如果线程 T 释放当前没有其他线程正在等待的“公平”锁,那么“公平”不会影响下一个线程将获得它。这只是线程之间的直接竞赛,由 OS 调度程序主持。

只有当多个线程在等待时,一个公平的锁才应该“支持”等待时间最长的那个。特别是,我希望如果某个线程 T 释放了其他线程正在等待的“公平”锁,然后线程 T 立即尝试再次锁定它,那么lock() 函数会注意到其他正在等待的线程,并将 T 发送到队列的后面。

但是,我实际上并不知道它是如何在任何特定的 JVM 中实现的。


P.S.,IMO,“公平”就像绷带,可以止血复合骨折。如果您的程序有一个竞争激烈的锁,以至于“公平”会产生任何影响,那么这是一个严重的设计缺陷。

The same Javadoc 还说,

使用由许多线程访问的公平锁的程序可能会显示出比使用默认设置的程序更低的整体吞吐量(即,速度较慢;通常要慢得多)。

【讨论】:

您好,那么当fairness设置为true时,是否会破坏可重入锁的属性? @Turtle 不,线程可以重新获取锁,如果它已经持有。这里的场景是线程在释放锁后尝试获取锁。 “如果某个线程 T 释放了其他线程正在等待的“公平”锁,”。这里其他线程指的是当时处于运行状态并试图获取锁的线程,对吧?让我们说例如共有 3 个线程 T1、T2、T3 按到达时间排序。在T释放锁并再次尝试获取它的时候,只有T2处于运行状态,其他处于等待状态。 T2 会获取锁吗?请纠正我在这里有点迷路。感谢您的帮助 @Tarun,Java 中没有RUNNING 状态。只有RUNNABLE,这意味着JVM 不知道为什么不允许线程运行的任何原因。 (即,实际上正在运行的线程仍然被JVM称为RUNNABLE。等待监视器锁的线程的状态是BLOCKED。我不知道顶部在我的脑海中,JVM 分配给等待ReentrantLock 的线程的状态是什么,但如果我负责,那也将是BLOCKED 一个实际的例子是项目 Loom 的虚拟线程,它在当前实现中没有抢占式切换,也许永远不会有。因此,一个成功获得锁且永远没有理由放弃 CPU 的线程将继续运行,而当没有其他本机线程可用时,其他虚拟线程将无法继续运行。【参考方案3】:

ReentrantLock 是基于 AbstractQueuedSynchronizer 实现的,这是一个先进先出 (FIFO) 等待队列。

假设A、B、C三个线程依次尝试获取锁,A获取了锁,那么B、C会转化为AbstractQueuedSynchronizer#Node进入队列。这两个线程将被挂起。

当A线程释放锁时,会唤醒其后继节点(AbstractQueuedSynchronizer#unparkSuccessor),即线程B。线程B被唤醒后会再次尝试获取锁。

假设当B线程被唤醒时,突然有一个D线程来尝试获取这个锁。对于公平锁,D线程看到队列中还有其他节点在等待获取锁(AbstractQueuedSynchronizer#hasQueuedPredecessors),直接挂掉。

而且对于不公平锁,D线程会立即尝试获取这个锁,这意味着它可以尝试“跳队列”一次。如果这次“队列跳转”成功,那么可以立即获取锁(这意味着节点B将再次被挂起:它在与D线程的竞争中输了,它被切线了)。如果失败则挂起,作为Node进入队列。

为什么不公平锁表现更好以及何时使用公平锁?

这是来自Java-Concurrency-Practice:

在激烈争用的情况下,插入锁比公平锁性能好得多的一个原因是,在暂停的线程恢复和实际运行之间可能存在显着延迟。假设线程 A 持有一个锁,而线程 B 请求该锁。由于锁忙,B被挂起。当 A 释放锁时,B 会恢复,以便再次尝试。同时,如果线程 C 请求锁,C 很有可能在 B 完成唤醒之前获取锁、使用它并释放它。在这种情况下,每个人都会获胜:B 获得锁的时间不会晚于其他情况,C 获得锁的时间要早​​得多,并且吞吐量得到了提高。

公平的锁在持有时间相对较长时往往效果最佳 时间或锁定请求之间的平均时间相对较长时。 在这些情况下,驳船提供 吞吐量优势 - 当锁未被持有但线程被持有时 目前醒来声称它 - 不太可能持有。

【讨论】:

确实,正如this answer 中所解释的,公平锁和不公平锁之间唯一的实现区别是当锁刚刚释放时对到达线程的处理。 和·when the lock just have been released.有关系吗?根据我对源码的理解,unfair锁无论如何都会尝试获取一次锁,如果恰好此时释放,那么线程就有可能获得锁。但是fair lock 会先调用hasQueuedPredecessors 来查看当前是否有其他线程在等待锁。 @霍尔格 是的,当当前所有者释放锁时,它会从队列中取消一个线程,如果存在的话。然后,未停放的线程将尝试获取锁。到目前为止,如果锁不公平或使用的线程tryLock() 通常忽略公平性,则另一个线程(包括刚刚释放锁的线程)可能会超越并(重新)绕过队列获取锁政策。为先前排队的线程提供新的 CPU 时间所花费的时间越多,其他人超车的机会就越大,收益也越高。 我明白你的意思。如果锁没有被释放,那么公平锁和不公平锁实际上没有区别。我想表达的是,如果锁没有被释放,那么两个锁的行为(代码级别)还是不一样的……但最终的结果其实是一样的。我的发言没有说明这一点。谢谢@Holger

以上是关于线程锁的原理是啥的主要内容,如果未能解决你的问题,请参考以下文章

Java Synchronized 锁的实现原理详解及偏向锁-轻量锁-重量锁

java 锁的本质是啥

synchronize偏向锁底层实现原理

synchronize偏向锁底层实现原理

java多线程,对象锁是啥概念?

自定义自旋锁