JUC 进阶成神系列03—线程安全之 synchronized

本文详细探讨了Java中的线程安全问题,重点关注synchronized关键字的原理和应用,以及轻量级锁、重量级锁、偏向锁和锁消除等优化策略。讨论了变量线程安全的分析和常见线程安全类,揭示了锁膨胀和自旋优化的过程。
摘要由CSDN通过智能技术生成

synchronized 解决方案

应用之互斥
为避免竞态条件产生,有多种手段。阻塞式解决方案:synchronized,Lock。非阻塞式解决方案:原子变量。

synchronized 解决方案即俗称的对象锁,采用互斥的方式让同一时刻最多只有一个线程能持有对象锁,其他线程再想获取这个对象锁就会阻塞,保证拥有锁的线程可以安全执行临界区代码,不用担心上下文切换。

虽然 java 中互斥和同步都可以用 sychronized 关键字来完成,但有区别:互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码。同步是由于线程执行的先后、顺序不同,需要一个线程等待其他线程运行到某个点。

sychronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。

面向对象改进:把需要保护的共享变量放入一个类即把互斥的逻辑放在方法内部,实现共享资源的保护,对外只需要直接调用方法。

方法上的 synchronized (只能锁对象,this对象或类对象)
加在成员方法上锁住的是 this 对象;加在静态方法上锁住的是类对象。
不加 sychronized 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)。
所谓的“线程八锁”其实就是考察锁住的是那个对象。

变量线程安全分析

成员变量和静态变量是否线程安全?
如果没有被共享,安全。如果被共享了,根据状态是否能改变分两种情况:只有读操作,安全。有读写操作-临界区,需要考虑线程安全问题。

局部变量是否线程安全?
局部变量是线程安全的。但局部变量引用的则未必(引用的是堆中的对象),如果该对象没有逃离方法的作用访问,安全;如果逃离了方法的作用范围,需要考虑线程安全问题。

局部变量的线程安全分析
局部变量每个线程调用 test() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享。局部变量
不同于成员变量,局部变量的 i++ 指令反编译成二进制字节码指令只有一条。各个线程使用时是不共享的各作各的自增,不存在线程安全问题。在这里插入图片描述

局部变量的引用(情况不同)
两个方法访问堆中同一对象,会有线程安全问题。在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
解决办法:将 list (方法所访问的对象)修改为局部变量。
分析:list 是局部变量时,每个线程调用时会创建不同实例,没有共享。method2 的参数从 method1 传递过来,与 1 引用同一个对象。method3 同理。在这里插入图片描述
情况 1:有其他线程调用 method2,3,如果把 method2 或 3 方法由 private 改成 public 也不会有线程安全。method1 调用 list 方法,method2 给 1 传参传的是另一个 list 对象。
情况 2:如果在情况 1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法。内部有新线程访问 list 对象,与原来的线程是共享的。子类方法另起线程访问共享资源是不可控制的,会把内部暴露给外部。体会开闭原则中的闭,private 或 final 提供的安全保护,不让子类修改的意义。

常见线程安全类:String、Integer(包装类)、StringBuffer(字符串拼接)、Random、Vector(线程安全的 list 实现)、Hashtable(线程安全的 Map 实现)、java.until.concurrent 包下的类。这里的线程安全指的是,多个线程调用它们同一个实例的某个方法时,是线程安全的。它们的每个方法是原子的,但多个方法的组合不是原子的。String 的 toString 内部方法,并不改变原有字符串,而是创建了新字符串对象包含了截取后的结果。字符串是不可变类,线程安全;有可以发生修改的属性痘是线程不安全的,比如 Date()。

sychronized 原理

1.轻量级锁
*轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁时间是错开的(即没有竞争)。轻量级锁对使用者是透明的,加锁是针对线程而言的,即语法仍然是 sychronized。

比如,线程 1 先来加锁解锁,线程 2 再来,但加锁的时间是错开的,没有竞争,加轻量级锁优化。如果有竞争,加重量级锁。

一创建锁记录(Lock Record)对象(JVM 层面),每个线程的栈帧都包含一个锁记录的结构,内部可以存储锁定对象的 MarkWord。
二让锁记录中 Object reference 指向锁记录,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录。锁记录里的数据和对象 Mark Word 数据进行交换,加锁。在这里插入图片描述

锁记录里临时记录对象的分代年龄、哈希码等数据,解锁时会恢复回来。这是成功的情况,假如对象已经被其他线程修改为 00,该次锁记录失败。

三如果 cas 替换成功,对象头存储了锁记录地址和状态:00 表示该线程给对象加锁。在这里插入图片描述
四如果 cas 失败,有两种情况:如果是其他线程已经持有了该 object 的轻量级锁,表明有竞争,进入锁膨胀过程。如果是自己执行了 synchronized 锁重入,那么在添加 Lock Record 作为重入的计数,加了几次锁就看 Lock Record 的数量。在这里插入图片描述
五解锁。当退出 synchronized 代码块解锁时如果有 null 的锁记录,表示有锁重入,这时重置锁记录,重入计数减一。当退出 synchronized 代码块解锁时锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头:成功则解锁成功;失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。

2.锁膨胀
锁膨胀:在尝试加轻量级锁过程中 cas 操作无法成功的一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级变为重量级。
当 Thread-1进行轻量级加锁时,Thread-0 已经为该对象加了轻量级锁。这时Thread-1 加轻量级锁失败,进入锁膨胀过程:即为 Object 对象申请 Monitor 锁,让Object 指向重量级锁地址,后两位变成 10;然后自己进入 Monitor 的 EntryList BLOCKED。当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败,这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程。

3.自旋优化
重量级锁竞争时,可以使用自旋进行优化,如果当前线程自旋成功(即这时候持线程已经退出了同步块,释放了锁),这时当前线程可以避免阻塞(阻塞要进入上下文切换,比较耗费性能)。
线程 1 访问同步块,获取 monitor,成功加锁后;假如此时线程2访问同步块,获取monitor,会发现 owner 不是 null,无法成为监控器主人,进行自旋重试。

自旋会占用 cpu,单核下已经有线程占用了,无可用 cpu,多核 cpu 自旋才能发挥优势。Java 6 以后自旋锁是自适应的,比如对象刚刚自旋操作成功,那会认为本次成功概率高,就会多自旋几次;反之,就少自旋甚至不自旋。Java 7 以后不能控制是否开启自旋功能。

4. 偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 cas 操作(cas 查看对象头改成新的锁地址,即使是自己这个线程加的每次也需要查看,影响性能)。Java 6 中引入偏向锁来做进一步优化,只有第一次使用 cas 需要将线程 ID 设置到对象的 Mark Word 头,以后发现这个线程 ID 是自己的表示没有竞争,不需要重新 cas,以后只要不发生竞争,这个对象就归该线程所有。

轻量级锁好比在门上挂书包,每次都要查看是谁持有的锁。进一步优化方式,可以直接把对象名字刻在门口,每次来看也能知道谁是锁的持有者,这是偏向锁的思路。在对象不发生改变情况下,偏向锁操作更少,提升性能。在这里插入图片描述

5.锁消除
加锁与不加锁得分相近的原因:JIT即时编译器会对字节码进行进一步优化,对 Java 代码是用解释的方法执行,但是对热点代码反复调用的方法会进行优化,手段是分析看局部变量是不是能优化,根本不可能逃离方法作用范围,不可能被共享,因此加锁没有意义,就会把锁优化掉,加锁会损耗性能,因此真正执行的时候是没有锁的。

锁粗化:对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化来优化,不同于细分锁的粒度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值