马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
抛异常时会释放锁。
当线程在 synchronized 块内部抛出异常时,会自动释放对象锁。- public class ExceptionUnlockDemo {
- private static final Object lock = new Object();
- public static void main(String[] args) {
- Thread t1 = new Thread(() -> {
- synchronized (lock) {
- System.out.println("线程1获取到锁");
- try {
- // 模拟操作
- Thread.sleep(1000);
- // 抛出异常
- throw new RuntimeException("模拟异常");
- } catch (InterruptedException | RuntimeException e) {
- System.out.println("线程1捕获异常: " + e.getMessage());
- }
- // 异常发生后,锁已经被释放
- System.out.println("线程1执行完毕");
- }
- });
- Thread t2 = new Thread(() -> {
- // 短暂等待,确保t1先执行
- try {
- Thread.sleep(500);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("线程2尝试获取锁");
- synchronized (lock) {
- System.out.println("线程2获取到锁");
- }
- });
- t1.start();
- t2.start();
- }
- }
复制代码 效果:
- 线程1获取到锁
- 线程1捕获异常: 模拟异常
- 线程2尝试获取锁
- 线程2获取到锁
- 线程1执行完毕
复制代码 从输出可以或许看出,线程 1 在抛出异常之后,线程 2 就可以或许获取到锁,这表明线程 1 的锁已经由于异常而被释放了。
加锁的区域就是同步代码块
在 Java 中,被锁掩护的代码区域被称为同步代码块(Synchronized Block)。当多个线程访问同一把锁的同步代码块时,会欺压串行执行,从而保证线程安全。同步的意思就是说比如说一个线程在执行时访问了一个别锁的共享资源,在时间片结束后还占着资源,另一个线程也不能访问- public class SynchronizedExample {
- private final Object lock = new Object(); // 锁对象
- // 方法级同步(隐式锁为 this)
- public synchronized void method1() {
- // 同步方法的代码
- }
- // 代码块级同步(显式指定锁对象)
- public void method2() {
- synchronized (lock) {
- // 同步代码块,同一时刻只能被一个线程执行
- }
- }
- }
复制代码 monitor进monitor出
对于我们人来说,一个步伐的执行开始和执行结束是可以理解的,但机器不可。我们计算机内部的指令颠末编译之后是一长串的代码,步伐以数组的情势存到页里,我们加的锁的区域在数组里不答应同时被访问。那么我们需要一些表记指出哪些区域的代码是被锁的不能同时访问。
首先我们要举行范围标记枷锁加锁我们知道这块资源是加锁标记还得。注意我们加锁是这样的。
第一,我们对要操作的资源加上所标记。
第二,我们要操作的资源这块是有这些指令对资源举行操作,这一堆指令要对资源举行操作,其中指令到这些区域。这些区域对它的操作不能是同时举行的,所以我们加锁的话有两个区域,一个是对资源本身加标记,另外一个对操作的资源的指令。也得指定所在的范围同步范围。
monitor进monitor出就是标记这些指令里那一块是不能同时被线程所操作。
JAVA对象头
snchronized用的锁是存在Java对象头里的。假如对象是数组范例,则捏造机用3个字宽(Word)存储对象头,假如对象黑白数组范例,则用2字宽存储对象头。在32位捏造机中,1字宽即是4字节,即32bit.
我们在堆区域创建对象,每一个对象都有锁表记和对象表记,所属范例:类的地址指向方法区。
假如有数组的话有数组长度。
假如一个64位的操作体系可以安装32位或64位的步伐。32位的操作体系只可以安装32位步伐。假如cpu的一个时钟周期可以或许运算32位,那他只能安装32位的操作体系。那假如一个步伐是64位的步伐,它里边一个字宽就是64位。是32位的步伐,那么它就只能说一个字宽是32位。从这个推理下,我们在64位的操作体系下安装了32位的步伐,那么它一个字就是32位。也就是说字宽最终是受步伐的位数影响
锁的升级与对比
在 Java 中,锁的升级是 JVM 为了优化锁性能而引入的机制。随着多线程竞争的加剧,锁会从无锁状态逐步升级为偏向锁、轻量级锁,最终升级为重量级锁。这种升级过程是不可逆的,旨在均衡不同竞争水平下的性能开销。
1. 锁升级的流程
锁的状态会根据竞争情况逐步升级:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
1.1 无锁状态(Normal)
- 特点:对象头的 Mark Word 存储哈希码、分代年事等信息。
- 场景:没有线程访问同步块时。
1.2 偏向锁(Biased Locking)
- 适用场景:只有一个线程频仍访问同步块。
- 原理:
- 首次获取锁时,JVM 会在 Mark Word 中记录线程 ID(通过 CAS 操作)。
- 该线程后续进入同步块时,无需任何同步操作,直接判断 Mark Word 中的线程 ID 是否匹配。
- 优点:无竞争时几乎无额外开销,性能最优。
- 缺点:当其他线程实验竞争锁时,需要撤销偏向锁,消耗 CPU。
1.3 轻量级锁(Lightweight Locking)
- 适用场景:多个线程瓜代访问同步块(无竞争)。
- 原理:
- 线程在栈帧中创建锁记录(Lock Record),并通过 CAS 将 Mark Word 复制到锁记录中。
- 同时,Mark Word 会指向锁记录的地址。
- 假如 CAS 乐成,线程获得轻量级锁;假如失败,表现有竞争,锁会升级为重量级锁。
- 优点:竞争不剧烈时避免线程壅闭,淘汰用户态和内核态的切换。不进壅闭队列,快速循环看看自己哀求的资源有没有解锁。
- 缺点:频仍 CAS 操作大概消耗 CPU。
1.4 重量级锁(Heavyweight Locking)
- 适用场景:多个线程同时竞争锁。
- 原理:
- 依赖操作体系的互斥量(Mutex),线程会被壅闭并进入等候队列。
- 锁释放时,需要唤醒等候的线程,涉及用户态和内核态的切换。
- 缺点:线程壅闭和唤醒的开销大,性能最差。重量解锁的加锁息争锁都挺麻烦,不仅仅是修改对象头标记这么简朴,尚有很多枷锁过程,枷锁竞争过程。
四种锁的对比
特性无锁偏向锁轻量级锁重量级锁适用场景无同步需求单线程访问多线程瓜代访问多线程同时竞争实现方式Mark Word 存储哈希码等信息Mark Word 存储线程 IDMark Word 指向线程栈中的锁记录Mark Word 指向 Monitor 对象线程状态无需壅闭无需壅闭自旋等候壅闭等候性能开销最低低(首次 CAS)中等(CAS 操作)最高(内核态切换)优缺点无同步开销无竞争时最优避免线程切换线程频仍壅闭 / 唤醒释放机制无需释放线程退出同步块时不释放,其他线程竞争时撤销解锁时通过 CAS 恢复 Mark Word解锁时唤醒等候线程
现在是几个线程对资源举行竞争只有一个才能竞争乐成!只有一个竞争乐成加上了锁。其他就会竞争失败。竞争失败的话会竞争失败会进壅闭队列。为什么进入壅闭队列:乐成的线程纵然时间片到期了,下次还是选中他可以立马执行。效率最高。每个加锁资源对应一个壅闭队列
然后第一个乐成占资源的线程已经执行结束后,会给全部在壅闭队列的线程发关照,让他们回到就绪队列。
比较并且交换
比较并交换(Compare-and-Swap, CAS) 是一种实现无锁(Lock-Free)算法的原子操作,用于在多线程情况中实现同步。它答应线程在不使用锁的情况下安全地修改共享数据,从而避免了锁带来的上下文切换和线程壅闭开销。这种方式也是实现了写后读。
- 假如内存地址 V 中的值即是预期旧值 A,则将该位置的值更新为新值 B,并返回 true。
- 假如内存地址 V 中的值不即是预期旧值 A,则不执行更新,返回 false。
但也有缺点,他是分两步的先比较再交换。比如说线程1先比较,发现没题目可以交换,但此时时间片到期。线程2进入也比较发现也没题目,也要交换就有题目了。 他不是原子操作,所以无法实现。只能调用操作体系底层。
流水线技能。他淘汰了电路的切换。提升的团体速度。但是指令顺序会发生变化,会导致后边有个指令重排序导致的并发题目。
java中哪些类是线程安全的?concurrent 开头的是线程安全的聚集。Atomic. 开头的也是线程安全的- private AtomicInteger atomicI = new AtomicInteger(0);
- private int i = 0;
- public static void main(String[] args) {
- final Counter cas = new Counter();
- List<Thread> ts = new ArrayList<Thread>(600);
- long start = System.currentTimeMillis();
- for (int j = 0; j < 100; j++) {
- Thread t = new Thread(new Runnable() {
- @Override
- public void run() {
- for (int i = 0; i < 10000; i++) {
- cas.count();
- cas.safeCount();
- }
- }
- });
- ts.add(t);
- }
- for (Thread t : ts) {
- t.start();
- }
- // 等待所有线程执行完成
- for (Thread t : ts) {
- try {
- t.join();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- System.out.println(cas.i);
- System.out.println(cas.atomicI.get());
- System.out.println(System.currentTimeMillis() - start);
- }
- /** * 使用CAS实现线程安全计数器 */
- private void safeCount() {
- for (;;) {
- int i = atomicI.get();
- boolean suc = atomicI.compareAndSet(i, ++i);
- if (suc) {
- break;
- }
- }
- }
- /**
- * 非线程安全计数器
- */
- private void count() {
- i++;
- }
- }
复制代码 一个使用AtomicInteger实现的线程安全计数器atomicI,另一个是平凡的非线程安全整数i。在多线程情况下运行这两个计数器,会看到不同的效果。
主要分析
- 线程安全题目:
- count()方法使用平凡的i++操作,在多线程情况下会出现竞态条件(Race Condition),导致最终效果小于预期值。
- safeCount()方法使用AtomicInteger和 CAS 操作,保证了原子性,最终效果是正确的。
- CAS 操作的实现:
- for (;;) {
- int i = atomicI.get();
- boolean suc = atomicI.compareAndSet(i, ++i);
- if (suc) {
- break;
- }
- }
复制代码 这段代码通过循环 CAS 操作来保证原子性:
- 获取当前值
- 实验更新为新值
- 假如失败则重试,直到乐成
- 性能考虑:
- 原子操作固然避免了锁的开销,但在高竞争情况下大概会由于频仍重试而低落效率
- 非原子操作固然没有这些开销,但效果是错误的,所以不能只考虑性能
输出效果
对于 100 个线程,每个线程执行 10000 次递增操作:
- atomicI.get()的输出效果应为:100 * 10000 = 1,000,000
- i的输出效果通常会小于 1,000,000,由于非线程安全操作会导致部分递增丢失
- 执行时间取决于体系情况,但通常原子操作会比非原子操作慢一些
- 我们看由于它没有加锁。他100万字肯定有相互覆盖,最终的效果肯定不到100万,就极大概率不到100万,有没有看到100万的一个极小的大概?比较大的大概是到不了100万。由于会有相互覆盖的题目。能理解的加九。对他100万次调用
ABA题目
在多线程编程中,ABA 题目(ABA Problem)是使用 CAS(Compare-and-Swap) 操作时大概遇到的一种特殊题目。它发生在一个值从 A 变为 B,然后再变回 A 的过程中,而 CAS 操作无法感知这个中心变化,误认为值没有被修改过。
1. ABA 题目的本质
CAS 操作的焦点逻辑是:“假如当前值是预期值 A,则将其更新为 B”。但假如在查抄期间,值履历了 A → B → A 的变化,CAS 会认为值 “没有变化” 并乐成更新,而实际上值已经被其他线程修改过,大概导致潜在的错误。
2. ABA 题目示例
假设有一个共享变量 int value = 10,两个线程 T1 和 T2 同时操作它:
- T1 读取 value:T1 准备将 value 从 10 增长到 11,它先读取 value 的当前值为 10。
- T2 修改 value:T2 先将 value 改为 20,然后又改回 10(即 10 → 20 → 10)。
- T1 执行 CAS:T1 执行 CAS(预期值=10, 新值=11),发现当前值确实是 10,认为没有变化,乐成将 value 改为 11。
题目:T1 认为 value 没有被修改过,但实际上它履历了 10 → 20 → 10 的变化,大概导致 T1 的操作基于错误的假设(例如,T1 大概盼望 value 自从读取后没有被其他线程动过)。
3. ABA 题目的危害
- 数据同等性风险:在某些业务逻辑中,中心状态的变化大概影响最终效果。例如:
- 链表操作:删除节点 A,插入新节点 B(内容与 A 雷同),此时 CAS 大概误判节点未变。
- 账户余额:余额从 100 变为 200 再变回 100,CAS 大概答应重复扣款。
- 逻辑错误:某些操作依赖值的一连性变化,而 ABA 大概粉碎这种假设。
4.解决方法
解决 ABA 就加版本号就可以了
线程可见性
一个线程对共享变量做出了修改,其他线程能实时读取到最新的修改,这叫线程可见性
线程间怎样通讯和进程间怎样通讯
进程是由线程构成的。进程全部的功能线程全都有。进程所拥有的功能线程全都有。然后。线程拥有的功能进程不一定有进程的通讯方式线程全都有
里边通讯其实是指交换信息的意思,就把消息传递过去,线程的通讯方式有两种共享内存和消息传递
|