您的位置:首页 > 文旅 > 旅游 > 阿里云服务器价格表包年_网站建设怎么建设_今日深圳新闻最新消息_seo排名哪家有名

阿里云服务器价格表包年_网站建设怎么建设_今日深圳新闻最新消息_seo排名哪家有名

2024/12/31 1:00:11 来源:https://blog.csdn.net/m0_73233932/article/details/142858923  浏览:    关键词:阿里云服务器价格表包年_网站建设怎么建设_今日深圳新闻最新消息_seo排名哪家有名
阿里云服务器价格表包年_网站建设怎么建设_今日深圳新闻最新消息_seo排名哪家有名

目录

(一)锁策略

(1)悲观锁vs乐观锁

(2)重量级锁vs轻量级锁

(3)挂起等待锁vs自旋锁

(4)读写锁

(5)可重入锁与不可重入锁

(6)公平锁vs非公平锁

(二)CAS

(1)CAS实现无锁编程

(2)AtomicInteger

2.1典型的线程安全问题

 2.2AtomicInteger用CAS解决

(3)ABA问题

(三)synchronized原理

 (1)锁膨胀(锁升级)

1.1java对象构成

1.2Monitor

 1.3早期sychronized问题

1.4sychronized的改进

(2)锁消除

(3)锁粗化

(四)总结


(一)锁策略

这里说的锁策略不是说的某一种锁,而是锁的一些特性

(1)悲观锁vs乐观锁

悲观与乐观,顾名思义就是这个锁对可能的锁冲突是持有悲观态度,还是乐观态度

悲观锁:悲观锁会假定要操作的数据会频繁发生锁冲突,所以会在每次操作数据的时候都会进行加锁,知道数据操作完成后才会释放锁。

实现机制:互斥锁,同步块等

乐观锁:乐观锁会假定要操作的数据不会频繁发生锁冲突,并且不会真的加锁,而是在修改数据前检查是否有别的线程修改过数据,如果修改过就重试报错等。

实现机制:版本号机制,CAS等

(2)重量级锁vs轻量级锁

轻与重, 可以看出是与锁的开销大小有关联的,重量级锁开销较大,轻量级锁开销较小。

重量级锁:重量级锁一般会涉及到系统api,一般通过互斥量(Mutex)实现,当一个线程尝试获取另一个线程持有的重量级锁,该线程会被阻塞,直到获取到锁为止。

 轻量级锁:轻量级锁一般是在用户态操作的,减少了线程阻塞的开销,当一个线程尝试获取另一个线程持有的轻量级锁,该线程不会被阻塞,而是尝试通过CAS操作来实现获取到锁,如果成功则线程获取到锁继续执行,如果失败则尝试重新获取锁。

(3)挂起等待锁vs自旋锁

挂起等待和自旋,是一个线程尝试获取锁之前会选择什么策略 

挂起等待锁:当一个线程尝试获取一个被其他线程持有的锁时,该线程会被阻塞,放入等待队列中,直到持有锁的线程释放锁

 自旋锁:当一个线程尝试获取一个被其他线程持有的锁时,该线程不会阻塞,而是在一个循环中不断检查锁是否被释放(忙等)

小结:到这我们可以察觉到,其实上述的几种锁都有或多或少的共性 ,比如

乐观锁和自旋锁通常情况下就是轻量级锁,因为他们一般都不涉及系统api的调用。

挂起等待锁通常可以认为是重量级锁,因为挂起等待会调用系统api,开销较大。

悲观锁如果调用了系统api,一般可以认为是重量级锁。但如果是基于程序语言的一些原子操作(如java中的原子类)来实现的悲观锁,没有导致线程的挂起等待和大量系统资源消耗不属于重量级锁


(4)读写锁

在java中,读写锁通过ReentrantReadWriteLock类来实现,读写锁通常用于解决读多写少的场景。当多个线程同时读取共享数据时,使用读写锁可以允许多个线程同时获取读锁,但当某一线程需要更新数据,需要获取写锁时,此时其他所有读线程和写线程都会被阻塞,保证数据一致性

(5)可重入锁与不可重入锁

重入与不可重入的区别,关键在于一个线程能不能加锁多次

如果一个锁是不可重入锁,那么当同一个线程两次获取同一个锁时就会发生死锁(因为要第一个锁释放后才能获取到第二个锁)

如果一个锁时可重入锁,当一个线程第二次获取锁,但发现这个锁的拥有者恰好是自己,那么就可以加一次锁,将计数器+1,释放一次锁将计数器-1,直到计数器为0才真正释放锁

如下图

不可重入锁

可重入锁

(6)公平锁vs非公平锁

公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列排队,永远是队列中的第一位得到锁

优点:避免线程饥饿

缺点:cpu需要唤醒阻塞线程,资源开销大,吞吐量下降

 

 非公平锁:多个线程去获取锁,都会直接尝试获取锁,获取不到,再去等待队列等候,若能获取到就直接获取到锁

优点:减少cpu线程开销,提升吞吐率

缺点:可能会导致线程饿死

 

(二)CAS

(1)CAS实现无锁编程

CAS全称是compare and swap(比较并交换),那么这里比较和交换的是什么呢,其实比较的是内存和寄存器,下面我来用一个情景来解释CAS的工作过程

现在我们有一个内存M,一个寄存器A和一个寄存器B,写做CAS(M,A,B),

如果寄存器A中的值和内存M中的值相等,则将寄存器B中的值赋值给M,并返回true

如果寄存器A中的值和内存M中的值不相等,则什么都不做,返回false

寄存器B中有什么,我们不关心,我们最需要关注的是内存M和寄存器A中的值是否相同 

如下是CAS操作伪代码,如果address(内存M)与expectValue(寄存器A)中的值相同,则将swapValue(寄存器B)的值赋值到address(内存M)中,并返回true,否则返回false 

boolean CAS(address, expectValue, swapValue) {//CAS操作伪代码if (&address == expectedValue) {&address = swapValue;return true;}return false;}

那么CAS怎么实现无锁编程呢?

 CAS的本质是一个CPU指令,而一个CPU指令是原子的,而我们加锁的目的,也是为了保证代码操作的原子性,那么我们就可以通过CAS来达到线程安全的目的,同时能够避免线程阻塞

(2)AtomicInteger

 在 java.util.concurrent.atomic包下,有许多原子类,AtomicInteger是其中之一,这些类的原子操作都是通过CAS来实现的,接下来我们以AtomicInteger为例,介绍CAS是怎么保证操作原子性的

2.1典型的线程安全问题

我们知道两个线程对同一个变量进行++操作会导致线程安全问题

如下代码中,两个线程同时对count进行++操作,期望结果是20000,但每次的结果总会比20000少,如下图

具体原因我已经在另一篇文章中详细讲过(第三段线程安全,案例解析中)——>进程,线程,线程池

public class Demo3 {static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {for (int i = 0; i < 10000; i++) {count++;}});Thread t2 = new Thread(() -> {for (int i = 0; i < 10000; i++) {count++;}});t.start();t2.start();t.join();t2.join();System.out.println(count);//期望结果是20000}
}

每次结果都不同且比期望结果小 

 2.2AtomicInteger用CAS解决

 如下代码,我们只是将普通int类型,替换成为了原子类,那么结果就会变成我们期望的20000值,如下图

public class Demo3 {private static AtomicInteger count = new AtomicInteger(0);//替换为了原子类public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {for (int i = 0; i < 10000; i++) {count.getAndIncrement();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 10000; i++) {count.getAndIncrement();}});t.start();t2.start();t.join();t2.join();System.out.println(count);//期望结果为20000}
}

 每次结果都为期望值

 我们将getAndIncrement方法点开,最终发现他的底层是用compareAndSwapInt的本地方法实现的,也就是说这个方法调用了系统的CAS指令

具体工作流程我们用一段伪代码来模拟一下

lass AtomicInteger {private int value;public int getAndIncrement() {int oldValue = value;while ( CAS(value, oldValue, oldValue+1) != true) {oldValue = value;}return oldValue;}}

先明确一下对应关系,这样不容易迷

 value(内存M),oldvalue(寄存器A),oldvalue(寄存器B)

现在我们有两个线程进行的getAndIncrement操作,也就是后置++操作,当t1线程刚完成赋值操作后,t2线程进行了穿插操作,等在轮到t1线程操作时,t1线程通过CAS发现oldvalue和value值并不相等,说明++操作已经被别的线程穿插了,于是进入循环,更新oldvalue,再进入循环执行++操作,返回oldvalue,下图是详细过程


(3)ABA问题

通过上面的例子我们已经了解到,CAS是通过“值有没有发生变化”,来作为“线程有没有被穿插”的依据,但是在极端的情况下,如果其他线程将值从A->B->A,那么第一个线程就不会发现值已经被穿插执行了。

本身如果值被其他线程篡改过,也不会发生什么bug,毕竟值还是相同的,进行的操作逻辑还没有变,但是在一些极端情况下还是会发生bug,比如下面这个场景


 我去银行取钱,银行卡中有1000元,我想取500元,但ATM机反应比较慢,我就连续点了两下取钱按钮,恰好在这两次取钱操作的空隙,在第一次取钱操作刚完成后,有人向这个银行卡中打了500元,这时第二次取钱操作可能发现值没有变,就又取了500元,于是我们实际上只取了500元,但实际扣款却扣了1000元

下图为详细过程 


 我们可以发现,出现ABA问题的关键要素,是会使值反复横跳,有增有减,只要我们这是将值朝着一个固定的方向变化,我们就能够避免ABA问题,但是,像存取款这种天生就有增有减的情景,我们可以引入版本号控制来规避,我们约定每次修改值,都会让版本号自增,此时CAS判定不只是判定余额值,而且还判定版本号是否变化,如果版本号没有发生变化,那么线程操作就一定没有被别的线程穿插

(三)synchronized原理

 (1)锁膨胀(锁升级)

1.1java对象构成

在聊锁膨胀之前,我们先聊聊java中的对象,Java中的对象一般可分为三部分对象头,实例数据,对齐填充

对象头:对象头中一般包含两类信息

1:一类是用于存储对象运行时的数据,如哈希码,GC分代年龄,锁标志位信息等,这部分我们称它为"Mark Word"(标记字段)

2:另一类是,对象指向它的类元数据的指针(Klass Point),虚拟机通过这个指针来确定这个对象是哪个类的实例

ps:由于对象要存储的运行时数据很多,已经超出32,64位Bitmap结构所能承受的,但是在对象头中的数据和对象自身定义无关的数据,为提升虚拟机的空间效率,Mark Word被设计称一个可以动态定义的数据结构,以便根据对象状态复用自己的存储空间


关于MarkWord“复用自己的存储空间”这一说法,实际上是指它在锁升级过程中的行为。在JVM中,锁的状态可以分为无锁状态、偏向锁、轻量级锁和重量级锁。当一个对象被用作锁对象时,其MarkWord中的锁状态标志会发生变化。在锁升级的过程中,MarkWord的内容可能会被复制到线程的栈帧中,以便进行轻量级锁的实现。当锁从轻量级锁升级为重量级锁时,MarkWord的内容可能会再次发生变化,以反映新的锁状态

实例数据:这部分主要存放我们定义的各种数据信息,父类信息等。

对齐填充:这个东西不是必要存在的,主要是因为JVM要求对象起始地址必须是8字节的整数倍,所以如果对象不是8字节的整数倍会被这部分自动填充为8字节的整数倍

1.2Monitor

我们先来看一段测试代码,以及它的反编译文件

public class Test{public synchronized void husband(){synchronized(new Demo3()){}}}

 反编译文件

 我用红框框标记了几处地方

1:ACC_SYCHRONIZED标志位,当我们执行同步方法的时候(被sychronized修饰的方法),会先判断这个方法是或否有ACC_SYNCHRONIZED标志位,如果有就去隐式地调用monitorenter和monitorexit。

2:monitorenter和monitorexit

我们上面提到的对象头,它会关联到一个monitor对象

当一个线程使用sychronized关键字获取对象的锁时,JVM会在对象头中设置一个指向Monitor对象的指针

此时,Monitor对象中的owner字段会指向当前持有锁的线程

如果其他线程尝试获取同一个对象的锁,他们会发现Monitor对象中的owner不是自己,就会进入等待队列并阻塞

当我们进入一个同步方法,执行monitorenter,获取当前对象的所有权,monitor的进入数+1

若一个monitor已经被一个线程持有,而这个线程恰好是自己,那么当再次进入时,monitor进入数再+1

若执行完monitorexit,对应的monitor进入数-1,直到减到0时,释放这个锁,才能被其他线程持有

 1.3早期sychronized问题

在jdk1.6之前,sychronized时一个效率比较低下的重量级锁,主要因为在1.6之前,sychronized的实现是基于上述的监视器锁(monitor)来实现的,而上述过程需要涉及到操作系统底层的Mutex Lock来实现,这需要操作系统从用户态到核心态频繁的切换,而这个状态的切换成本较高,也就是说这种重量级锁实现的sychronized效率很低。

1.4sychronized的改进

既然前面说到早期sychronized效率低下,那么这些大佬们就着手对sychronized进行了优化,这个优化就是引入了锁升级策略,在无锁和重量级锁间,加入了其他状态的锁来实现过渡

下图是锁升级的过程,且升级的过程是不可逆的。 

 

 

 偏向锁:偏向锁的意思是这个锁会偏向于第一个获得它的线程,本质上是一个乐观锁,当这个线程再次请求锁的时候不需要进行任何的同步操作,从而提升性能。此时对象头的Mark  Word结构就是偏向锁结构。

轻量级锁:如果此时另外一个线程也要尝试获取这个锁,那么偏向模式就会结束,如果并发数量不大时,会优先使用轻量级锁,此时对象头中的Mark Word结构是轻量级锁结构

自旋锁:自旋锁是从轻量级锁到重量级锁的过渡锁,通过CAS来实现自旋,防止线程挂起导致需要调用操作系统接口使效率变慢,但如果自旋超出阈值(默认大小是10),就会升级成重量级锁

重量级锁:就是上方讨论的monitor争夺的过程。

升级过程简单示例,图片来自——三太子敖丙

(2)锁消除

锁消除是存在于编译器层面的手段,当一个程序,在代码层面要求同步,但是在检测时发现不可能存在共享数据竞争。此时编译器就会将这种不必要的锁进行消除来提升效率。

比如我们使用StringBuffer只是作为某个方法的局部变量使用,此时当每个线程调用包含StringBuffer类的方法时,都会创建一个StringBuffer类的对象,此时根本不会发生线程安全问题,而StringBuffer类中的线程安全的操作就是完全没有必要的,此时编译器就会考虑进行锁消除。

而锁消除的判定依据主要来源于逃逸分析的数据支持,如果判断一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那么就可以把它们当作栈上的数据对待,认为它们是线程私有的,加锁操作就无需进行。

(3)锁粗化

说锁粗化之前,我们先讲讲锁粒度,我们认为,如果sychronized同步代码中,代码越多,锁的粒度越粗,代码越少,锁的粒度越细。

通常情况下,我们为了保证多线程之间的有效并发,会要求每个线程持有的锁时间尽可能的短,来提升效率,但是在有些情况下一个粒度细的锁被反复的进行加锁解锁的操作(涉及反复的锁竞争),可能实际的效果还不如粗的锁


举个例子:假如你想领导汇报工作,一个工作汇报了两三分钟,汇报完了,过了一会,又给领导汇报另外一个工作,汇报完了,又过了几分钟,又给领导汇报另外一个工作,虽然每次汇报的时间短,但你这样频繁的骚扰领导,此时领导想砍你的心都有了,这样分次汇报的效果,还不如一次汇报完的效果好。

(四)总结

1:这篇我们讲到了一些不同特性的锁,如悲观乐观,重量轻量等

2:然后我们又讲到了CAS比较并交换的工作原理,以及它的ABA问题

3:同时我们又谈到了sychronized的底层原理,以及早期sychronized的问题,并进行的锁升级的优化改进等。

4:然后又对锁消除和锁粗化进行了大概的意思阐述。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com