Java线程安全与锁优化,锁消除,锁粗化,锁升级
线程安全的定义
来自《Java高并发实战》“当多个线程访问一个对象的时候,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法的时候进行任何的协调工作,调用的对象的行为都能获得正确的结果,那这个对象就是线程安全的。”
这句话要求线程安全的代码都必须具备一个特征:代码本身封装了所有的正确的手段(同步或者互斥等),令调用者无需再做任何措施来保证线程的安全。
Java中的线程安全的理解
首先线程安全就限定于多个线程访问共享资源的情况,这是前提。并且线程安全不是一个非真既假的判断题,我们可以按照共享数据的类型划分“安全程度”,划分成5类:不可变,绝对线程安全,相对线程安全,线程兼容和线程对立。
不可变
不可变的对象一定是线程安全的,无论是对象方法的实现还是方法的调用者,都不需要再擦去任何线程安全保障措施。在Java语言中,如果共享数据是一个基本数据类型那直接使用final关键字修饰就是不可变的。如果共享数据是一个对象的话,那就需要保证对象的行为不会对对象的状态产生影响才醒。例如String就是一个不可变的类,调用他的replace(),subString()方法等都会生成一个新的字符串对象,不会改变旧的值。在Java中不可变的类型有多种,枚举也是不可变的以及Number的子类,BigDecimal,BigInteger,Integer,但是AtomicInteger和AtomicLong并非不可变。
绝对线程安全
在Java API中标注的线程安全的类严格来讲,都不是绝对线程安全的。例如Vector中每个方法都用Synchronized修饰可以说调用的时候线程安全啦,但是执行以下代码就会报数组越界异常。
import java.util.Vector;
public class VectorTest {
private static Vector<Integer> vector = new Vector<>();
public static void main(String[] args) {
while (true) {
for (int i = 0; i < 10; i++) {
vector.add(i);
}
Thread removeThread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
});
Thread printThread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
System.out.print(vector.get(i));
}
}
});
removeThread.start();
printThread.start();
while (Thread.activeCount() > 20) {
// return;
}
}
}
}
要是保证上面的代码安全执行需要将removeThread和printThread方法用synchronized修饰。
所以使用线程安全的集合在调用的时候并不意味着不在进行同步了。
相对线程安全
相对线程安全就是平时通常意义上讲的线程安全,我们在调用的过程中不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用的时候才去额外的同步来保证结果的正确性。
线程兼容
线程兼容是指:对象本身不是线程安全的,但是可以通过调用时正确的同步来保证线程安全。Java的大多数API都是线程兼容的。
线程对立
线程对立:无论调用端采不采取同步都无法保证线程安全。例如Thread的suspend方法和resume方法,如果有两个线程同时持有一个线程对象,一个尝试中断线程,一个恢复线程,如果并发进行的话,无论调用是否进行了同步都会产生死锁。
实现线程安全的方式
1、互斥同步
同步是指在多个线程并发访问共享资源时,保证共享数据在同一时刻只能被一个线程使用,而互斥是实现同步的一种方式。互斥可以设置临界区,互斥量和信号量,这些都是实现互斥的一种方式,因此,在4个字里面,互斥是因,同步是果。互斥是方法,同步是目的。
在Java中最基本的互斥同步手段就是使用synchronized关键字,synchronized关键字经过编译会在同步代码块前后形成monitorenter和monitorexit指令,这两个指令都需要一个引用类型的参数作为锁定和解锁的对象,如果程序中明确指定了这个对象,那参数就是这个对象;如果没有明确指定,那就根据synchronized修饰的方法是实例方法是类方法,去取对应的对象实例或Class对象来作为锁对象。
在执行monitorenter指令的时候需要去获取对象的锁,如果已获取就把锁的计数器加一,相应的在执行monitorexit指令时会将锁技术器-1,当计数器为0,所就被释放。如果获取锁失败,那该线程就一直阻塞,直到另一个线程释放锁。
需要注意的是:synchronized是可重入锁,他不会把自己锁死。意思是一个线程可以多次获取同一把锁,又称递归锁,如果之前已经获取到锁,那就可以直接使用获取到的锁进入。用来防止一个线程多次获取锁把自己锁死。
锁优化
自旋锁与自适应自旋
所谓的自旋简单来说就是一个while死循环,从JDK1.6开始就默认开启了自旋,但是让线程自旋等待,在等待时间内什么事都不干一直循环判断也是资源上的浪费,因此设置了自旋次数,默认次数是10没如果自旋尝过了限定的次数仍然没有成功获取锁就直接把这个线程挂起了不再进行自旋。
在JDK1.6又引入了自适应自旋,这个就比较智能了,自旋时间不再固定,由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。如果虚拟机认为这次自旋也很有可能再次成功那就会次序较多的时间,如果自旋很少成功,那以后可能就直接省略掉自旋过程,避免浪费处理器资源。
锁消除
锁消除是指:虚拟机编译器在运行时,检测到了共享数据没有竞争的锁,将这些锁进行消除。
public String concatString(String s1,String s2,String s3){
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
上面的代码中StringBuffer是线程安全的,append方法有个同步块,锁是sb对象。虚拟机观察sb,发现他的作用域限制在这个方法内部,所以sb的引用不会被用在方法外,其他线程无法访问到他,因此这里面append方法虽然有锁但是就被虚拟机消除了。所以直接忽略所有同步就执行了。
锁粗化
这个怎么理解呢?其实就是推荐将同步块的作用范围写的尽可能小,这样是为了是的需要的同步操作数量尽可能少,如果存在锁竞争,那也可以尽快的拿到锁,锁粗化就是将多次上锁解锁的请求合并为一次同步请求。
例如
for(int i=0;i<10;i++){
synchronized(lock){
// do something
}
}
经过锁粗化就变成了下面的代码
synchronized(lock){
for(int i=0;i<10;i++){
// do something
}
}
偏向锁
偏向锁也是JDk1.6中引入的一项锁优化,他的目的是消除数据在无竞争情况下的同步原语,以此来提高性能。偏向锁的作用在于可以在无竞争锁的情况下将整个同步都消除掉,CAS也消除掉。
偏向锁的偏其实就是偏向 第一个获取他的线程,如果在接下来的执行过程中没有其他线程获取锁,那么持有偏向锁的线程
将永远不需要在进行同步。
轻量级锁
在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。
锁升级的过程
无锁->偏向锁->轻量级锁->重量级锁
无锁是对资源没有锁定,所有线程都可以访问并修改同一个资源,但是在同一时刻只能有一个线程能修改成功。其他线程会不断重试直到修改成功。
偏向锁的升级
如果当前虚拟机启用了偏向锁(-XX:+UseBiasedLocking
\),那么当一个锁被线程获取的时候,虚拟机会把这个锁(其实也就是一个对象)中的标志位设置为 01 ,代表偏向模式。同时使用CAS操作把获取到这个锁的线程的ThreadID记录在锁对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进行这个锁相关的同步代码块时,虚拟机都可以不在进行任何同步操作。
当另一个线程尝试获取这个锁的时候,偏向模式宣告结束。根据锁对象目前是否处于被锁定的状态来撤销偏向锁恢复为无锁状态,或者是升级到轻量级锁状态。
偏向锁的取消
-XX:-UseBiasedLocking=false
轻量级锁起作用的依据就是:“对于绝对大部分的锁,在整个同步周期内都是不存在竞争性的”这是经验所得出的结论。
当两条以上的线程争夺使用同一把锁时,轻量级锁不在有效,膨胀为重量级锁。Mark Word中存储的就是指向重量级的指针,后面没有获得锁的线程进入阻塞状态。
Mark Word:32比特空间,25位是HashCode,4比特是GC分代年龄,2比特是用来存储锁标志,1比特固定为0
存储内容 | 标志位 | 状态 |
---|---|---|
对象哈希码、对象分代年龄 | 01 | 未锁定状态 |
偏向线程ID,偏向时间戳,对象分代年龄 | 01 | 偏向锁状态 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 重量级锁定 |
空,不需要记录信息 | 11 | GC标记 |
以上有理解不到位的地方欢迎指出!
参考:https://zhuanlan.zhihu.com/p/71156910
深入理解Java虚拟机 第二版
https://juejin.im/post/5ca766dcf265da30d02fb35c
Java线程安全与锁优化,锁消除,锁粗化,锁升级的更多相关文章
- java线程总结3--synchronized关键字,原理以及相关的锁
在多线程编程中,synchronized关键字非常常见,当我们需要进行"同步"操作时,我们很多时候需要该该关键字对代码块或者方法进行锁定.被synchronized锁定的代码块,只 ...
- java线程总结--synchronized关键字,原理以及相关的锁
在多线程编程中,synchronized关键字非常常见,当我们需要进行“同步”操作时,我们很多时候需要该该关键字对代码块或者方法进行锁定.被synchronized锁定的代码块,只能同时有一条线程访问 ...
- Java并发编程:synchronized和锁优化
1. 使用方法 synchronized 是 java 中最常用的保证线程安全的方式,synchronized 的作用主要有三方面: 确保线程互斥的访问代码块,同一时刻只有一个方法可以进入到临界区 保 ...
- 深入理解Java虚拟机读书笔记9----线程完全与锁优化
九 线程完全与锁优化 1 Java语言中的线程完全 ---线程安全:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用 ...
- Java 虚拟机对锁优化所做的努力
作为一款公用平台,JDK 本身也为并发程序的性能绞尽脑汁,在 JDK 内部也想尽一切办法提供并发时的系统吞吐量.这里,我将向大家简单介绍几种 JDK 内部的 "锁" 优化策略. 1 ...
- jvm层面锁优化+一般锁的优化策略
偏向锁: 首先了解对象头MARK指针(对象头标记,32位): 存储GC标记,对象年龄,对象Hash,锁信息(锁记录的指针,偏向锁线程的ID) 大部分情况是没有竞争的,所以可以通过偏向来提高性能 所谓的 ...
- 并发设计模式和锁优化以及jdk8并发新特性
1 设计模式 (1) 单例模式 保证一个类只能一个对象实现.正常的单例模式分为懒汉式和饿汉式,饿汉式就是把单例声明称static a=new A(),系统第一次调用的时候生成(包括调用该类的其他静态资 ...
- 深入理解java虚拟机(7)---线程安全 & 锁优化
关于线程安全的话题,足可以使用一本书来讲解这些东西.<Java Concurrency in Practice> 就是讲解这些的,在这里 主要还是分析JVM中关于线程安全这块的内容. 1. ...
- Java并发编程学习:线程安全与锁优化
本文参考<深入理解java虚拟机第二版> 一.什么是线程安全? 这里我借<Java Concurrency In Practice>里面的话:当多个线程访问一个对象,如果不考虑 ...
随机推荐
- 【题解】AcWing 193. 算乘方的牛
原题链接 题目描述 约翰的奶牛希望能够非常快速地计算一个数字的整数幂P(1 <= P <= 20,000)是多少,这需要你的帮助. 在它们计算得到最终结果的过程中只能保留两个工作变量用于中 ...
- NOI Online #2 提高组 游戏
没用二项式反演的菜比. 题目链接 Solution 非平局代表的树上祖先关系是比较好统计,(可以在处理一个点时,考虑用他去匹配他的子树中的东西)而平局的关系比较难统计.我们不妨求出至少 \(k\) 个 ...
- docker redis 设置和使用
1 开启docker 拉取redis镜像 1.1 桌面版docker 在镜像所在位置命令行执行 docker load -i redis.tar 1.2 开启redis docker run -p ...
- SQL盲注、SQL注入 - SpringBoot配置SQL注入过滤器
1. SQL盲注.SQL注入 风险:可能会查看.修改或删除数据库条目和表. 原因:未对用户输入正确执行危险字符清理. 固定值:查看危险字符注入的可能解决方案. 2. pom.xml添加依赖 ...
- js 控制输入框保存数字级小数点后一位
$('#Question8').on('keyup', function () { var regVoter = $("#Question8").val(); regVoter = ...
- html 01-认识Web和Web标准
01-认识Web和Web标准 #Web.网页.浏览器 #Web Web(World Wide Web)即全球广域网,也称为万维网. 我们常说的Web端就是网页端. #网页 网页是构成网站的基本元素.网 ...
- [日常摸鱼]bzoj1968 [Ahoi2005]COMMON 约数研究
题意:记$f(n)$为$n$的约数个数,求$\sum_{i=1}^n f(i)$,$n \leq 10^6$. 我也不知道为什么我要来做这个- 直接枚举每个数会是哪些数的约数-复杂度$O(n log ...
- Flink 反压 浅入浅出
前言 微信搜[Java3y]关注这个朴实无华的男人,点赞关注是对我最大的支持! 文本已收录至我的GitHub:https://github.com/ZhongFuCheng3y/3y,有300多篇原创 ...
- 小白数据分析——Python职位全链路分析
最近在做Python职位分析的项目,做这件事的背景是因为接触Python这么久,还没有对Python职位有一个全貌的了解.所以想通过本次分析了解Python相关的职位有哪些.在不同城市的需求量有何差异 ...
- Redis5.0 主从模式和高可用 搭建和测试报告
Redis 单机模式很简单,相关测试水文看这里 Redis5 压力测试结果反馈报告 必须的,今天接着写水文,写一写现在redis 支持的三种集群,主从模式,哨兵模式,Cluster模式,今天先搞主从模 ...