标签:基本 调用 编译 system 记录 没有 obj time 同步代码块
首先讲讲锁的分类
线程挂起和线程真正运行之间存在着很长的时间差
多个线程按照申请锁的顺序去获取锁,线程会在一个FIFO队列中排队,队列首个线程才能获取锁
优点:所有线程都能获取锁, 不会出现线程饥饿的情况
缺点:效率较低,吞吐量会下降,因为只有第一个线程才能获取锁,其余线程均处于阻塞状态,cpu唤醒阻塞线程的开销较大,从而导致效率低。
多个线程同时去获取锁,如果没有获取到锁,重新进入等待队列中,等待下一次获取锁,如果获取到了锁,会执行相应线程。
优点:效率较高,充分利用cpu的时间片,cpu的空闲时间减少,利用率提高。
缺点:存在线程饥饿的情况,最坏的可能是线程获取不到锁,出现线程饿死的情况。
可重入锁指的是一个线程获取了一个锁,此线程重新获取这个锁,不会造成死锁。
Synchronized和ReentrantLock都是可重入锁。
可重入锁的实际应用
public class Demo1 {
public synchronized void functionA(){
System.out.println("FunctionA");
functionB();
}
public synchronized void functionB(){
System.out.println("FunctionB");
}
}
如上代码: 当一个带有锁的方法调用另外一个带有锁的方法, 如果不是可重入锁,就会产生死锁,因为无法获取到第二把锁。因为可重入锁可以实现锁的递归,即锁的外层嵌套锁。
具有唯一性和排他性
典型的例子就是Synchronized和ReenTrantLock以及RenentrantReadWriteLock中的写锁
只要有一个线程获取互斥锁,其余的线程只能进入等待状态,无法获取锁
乐观地认为程序的并发程度不深,认为每次去拿数据的时候都不会被其他线程修改,所以不会再处理数据的时候锁定数据。
在拿到数据的时候保存拿到的数据,当要进行修改的时候再次获取这个数据和之前拿到的数据进行对比,如果数据一致,才进行操作。
乐观锁适用于写比较少的情况下(多读场景)
常见的乐观锁操作就是CAS操作,全民Compare And Swap(是一个非阻塞同步操作),主要有三个参数需要读写的内存值V,再次读取的值A,需要修改成的值B。只有当V和A相同时,才用新值B来替换V。如果前后两次读取的值不一样,则通过自旋操作(不断重试)直到修改成功。
CAS操作会带来一个ABA问题,ABA问题的意思如下
如果有三个线程对A值进行操作
第一个线程第一次读取A值
第二个线程将A值修改成了B值
第三个线程将B值修改成了A值
第一个线程第二次读取A值会认为前后两个A值一样,会进行替换操作
但是实际上前后两次的A值并不是同一个A值
为了解决这个问题,加入一个版本号,每次修改值之后可以改变版本号,这样就知道值是否被修改过JUC中的原子类操作都是基于CAS的
分段锁出自jdk1.7中的ConcurrentHashMap,在jdk1.8中去掉了分段锁,但是依然有分段的影子在里面。
? ConcurrentHashMap在1.7中为了提高并发效率,将Map划分成了很多个Segment, 每一个Segment都实现了RenentrantLock,这个锁就叫做分段锁,只有对每一个分段Segment进行操作的时候才对这个分段进行上锁,不影响其他线程对其他的Segment进行操作。
作用:减少在没有实际竞争的情况下,重量级锁带来的性能开销
如何减少性能开销?
具体实现
当一个没有锁的线程(锁标志位为“01”状态,是否为偏向锁为“0”)来申请锁的时候
首先在栈帧上创建LockRecord, 将对象投中的MarkWord复制到锁记录(LockRecord)中。
拷贝成功后,通过CAS操作将MarkWord指向LockRecord,并将所记录中的owner指向Object的LockRecord。
如果这个更新动作成功了,那么这个线程的锁状态会被更改为00,即轻量级锁
如果操作失败,检查对象的MarkWord是否指向当前线程的栈帧。如果是的那么说明当前线程已经拥有了这个对象的锁。直接进入同步代码块继续执行。如果不是,说明有多个线程竞争锁, 轻量级锁会膨胀成重量级锁。锁的状态变成了“10”,MarkWord中存储的是指向重量级锁(互斥量)的指针,后面等待锁的线程也进入阻塞状态
如果存在锁竞争但是不激烈的情况下,可以通过自旋锁优化,自旋失败后再膨胀为重量级锁
重量级锁
减少线程阻塞造成的线程切换
首先,内核态与用户态的切换上不容易优化。但通过自旋锁,可以减少线程阻塞造成的线程切换(包括挂起线程和恢复线程)。
如果锁的粒度小,那么锁的持有时间比较短(尽管具体的持有时间无法得知,但可以认为,通常有一部分锁能满足上述性质)。那么,对于竞争这些锁的而言,因为锁阻塞造成线程切换的时间与锁持有的时间相当,减少线程阻塞造成的线程切换,能得到较大的性能提升。具体如下:
如果在自旋的时间内,锁就被旧owner释放了,那么当前线程就不需要阻塞自己(也不需要在未来锁释放时恢复),减少了一次线程切换。
“锁的持有时间比较短”这一条件可以放宽。实际上,只要锁竞争的时间比较短(比如线程1快释放锁的时候,线程2才会来竞争锁),就能够提高自旋获得锁的概率。这通常发生在锁持有时间长,但竞争不激烈的场景中。
使用-XX:-UseSpinning参数关闭自旋锁优化;-XX:PreBlockSpin参数修改默认的自旋次数。
自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:
自适应自旋解决的是“锁竞争时间不确定”的问题。JVM很难感知到确切的锁竞争时间,而交给用户分析就违反了JVM的设计初衷。自适应自旋假定不同线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定,因此,可以根据上一次自旋的时间与结果调整下一次自旋的时间。
然而,自适应自旋也没能彻底解决该问题,如果默认的自旋次数设置不合理(过高或过低),那么自适应的过程将很难收敛到合适的值。
当一个线程高频地请求, 同步和释放锁,会消耗系统资源。在这种情况下把多次锁请求合并成一次锁请求,减小锁请求,同步,释放带来的性能损耗。
锁粗化的情况:
public void doSomethingMethod(){
synchronized(lock){
//do some thing
}
//这是还有一些代码,做其它不需要同步的工作,但能很快执行完毕
synchronized(lock){
//do other thing
}
}
合并请求后的代码
public void doSomethingMethod(){
//进行锁粗化:整合成一次锁请求、同步、释放
synchronized(lock){
//do some thing
//做其它不需要同步但能很快执行完的工作
//do other thing
}
}
上面做法的前提是, 两个锁请求之间的工作可以迅速做完。如果不能迅速做完,合并之后会导致同步代码块执行需要花费很长时间,极大影响了多线程的工作
合并前代码
for(int i=0;i<size;i++){
synchronized(lock){
}
}
合并后代码
synchronized(lock){
for(int i=0;i<size;i++){
}
}
锁清除是发生在编译器级别的一种锁优化方式。
有时候代码完全不需要加锁,却执行了加锁操作,这个时候编译器就会进行锁清除优化。
例子:StringBuffer的append操作已经是线程安全的了
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
可能在实际使用中并不需要进行加锁处理, 这里StringBuffer作为局部变量使用, 函数执行完, 变量就会被清除,每一个线程都会拥有自己的局部变量, 不会涉及到并发问题, 因此也没必要进行加锁操作;
public class Demo {
public static void main(String[] args) {
long start = System.currentTimeMillis();
int size = 10000;
for (int i = 0; i < size; i++) {
createStringBuffer("Hyes", "为分享技术而生");
}
long timeCost = System.currentTimeMillis() - start;
System.out.println("createStringBuffer:" + timeCost + " ms");
}
public static String createStringBuffer(String str1, String str2) {
StringBuffer sBuf = new StringBuffer();
sBuf.append(str1);// append方法是同步操作
sBuf.append(str2);
return sBuf.toString();
}
}
这个时候可以通过编译器来消除同步锁。
标签:基本 调用 编译 system 记录 没有 obj time 同步代码块
原文地址:https://www.cnblogs.com/jiangblog/p/13216814.html