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>里面的话:当多个线程访问一个对象,如果不考虑 ...
随机推荐
- 将命令行提示符里的执行结果导出到text文件中
为便于查看和保存命令行提示符里的执行结果, 可以使用 ">" 将执行结果导入到指定.txt文件中. 例如: 在命令行提示符里查看C盘文件,并将结果导入到E盘dir-c-out ...
- Mac下安装appium+python+Android sdk 环境完整流程
安装大纲:1,安装jdk (jdk1.8及以上版本都可以,尽量不要用最新可能会不兼容) 2,安装android-sdk (mac版本的android-sdk) 3,mumu模拟器 (随便找的一个) 4 ...
- java web简单的增删改查
1.主要的文件,运行结果,运行界面,数据库创建的表等图片. 所要创建的文件和要导入的包: 主页面: 显示界面: 数据库的信息: 删除.查找.修改就不一 一列出来,自己可以运行看看.哈哈 2.接下来我将 ...
- oracle 11g调优常用语句
1.查询表的基数及选择性 select a.column_name, b.num_rows, a.num_distinct cardinality, round( ...
- jmeter跨线程使用token
项目的接口测试,今早所有接口都不通了,查看原因是登录接口地址变了..... 原来的方式是每个线程中都写了登陆接口来获取token,但是因为登陆接口地址改变的原因,要改好多个登陆接口,所以就想把toke ...
- 庐山真面目之八微服务架构 NetCore 基于 Dockerfile 文件部署
庐山真面目之八微服务架构 NetCore 基于 Dockerfile 文件部署 一.简介 从今天开始,不出意外的话,以后所写的文章中所介绍项目的部署环境都应该会迁移到Linux环境上,而且是 ...
- 小兔子有颗玻璃心A版【转】
作者:诸君平身链接:https://www.zhihu.com/question/49179166/answer/116926446来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请 ...
- YT Downloader视频下载器
简介 YT Downloader视频下载器是一款非常知名的视频下载器,支持下载YouTube,Facebook,Dailymotion,Vimeo,Metacafe等数百个视频网站的视频 截图介绍 小 ...
- [日常摸鱼]bzoj1036 [ZJOI2008]树的统计Count
听说后天会考x 省选居然还考模板题的么(好吧好像NOI也有考而且也是树剖-) 题意:一棵树,每个点有权值,三种操作:单点修改.求链上最大值.求链上权值和. 直接上模板. 我可能不会写单点修改的线段树了 ...
- webpack配置css-loader
执行 npm init 命令 生成 package.json 文件 在 webstorm 项目中局部安装 webpack(比如安装3.6.0版本) npm install webpack@3.6.0 ...