深入底层带你理解Synchronized

由于今天主要是围绕Synchronized,所以这里就主要讲一下它的底层原理以及扩展内容。

乐观锁

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为
别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数
据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),
如果失败则要重复读-比较-写的操作。
java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入
值是否一样,一样则更新,否则失败。


悲观锁


悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人
会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。
java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,
才会转换为悲观锁,如 RetreenLock。

乐观锁中CAS算法原理实现:

CAS的全成是: Compare And Swap(比较相同再交换)。是现代CPU广泛支持的一种对内存中的共享数
据进行操作的一种特殊指令。
CAS的作用:CAS可以将比较和交换转换为原子操作,这个原子操作直接由CPU保证。CAS可以保证共
享变量赋值时的原子操作。CAS操作依赖3个值:内存中的值V,旧的预估值X,要修改的新值B,如果旧的预估值X等于内存中的值V,就将新的值B保存到内存中.


接下来主要讲Synchronized


三种加锁方式:方法锁,对象锁,类锁
Synchronized作用范围

  1. 作用于方法时,锁住的是对象的实例(this);
  2. 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen
    (jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,
    会锁所有调用该方法的线程;
  3. synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,
    当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
    在这里插入图片描述
    这是其中的一种方式,加同步代码块锁,能够实现线程安全。
    接下来我们探究一下它的底层实现原理:
    我们先反编译一下这个java文件

在这里插入图片描述
其中的两个指令mointorenter和monitorexit是关键,我们就来了解一下这两个指令在虚拟机规范里面的说明

monitor监视器锁

可以看出无论是synchronized代码块还是synchronized方法,其线程安全的语义实现最终依赖一个叫
monitor的东西,那么这个神秘的东西是什么呢?
关于这些指令的作用,我们直接参考JVM规范中描述:
monitorenter :
每个对象有一个监视器锁(monitor),当monitor被占用时该对象就会处于锁定状态。线程执行monitorenter指令时尝试获取monitor的所有权,如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
如果线程已经占有该monitor,只是重新进入,则将monitor的进入数加1.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit:
  执行monitorexit的线程必须是monitor对应的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
  通过这两段描述,我们应该能很清楚的看出Synchronized同步块的实现原理,不过还有两点需要我们注意下,首先synchronized同步块对同一条线程来说是可重入的,不会出现自己将自己锁死的问题,但同步块在已进入程序执行完之前,是会阻塞后面其他线程的进入。通过上图我们也可知道Synchronized同步块的语义底层其实就是通过一个monitor的对象来完成,而我们前面学习的wait/notify等方法的调用也依赖于monitor对象,这也就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
至于为什么Synchronized同步块要使用两个monitorexit指令?因为如果只使用一个,当线程在运行的过程中发生异常而无法释放锁时,就会造成死锁现象,因此另一个monitorexit指令的作用就是在线程发生异常时释放锁的。
Synchronized的优化
**

自旋锁原理

**
前面我们讨论monitor实现锁的时候,知道monitor会阻塞和唤醒线程,线程的阻塞和唤醒需要CPU从
用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,这些操作给系统的并发性能
带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很
短的一段时间,为了这段时间阻塞和唤醒线程并不值得。如果物理机器有一个以上的处理器,能让两个
或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行
时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自
旋) , 这项技术就是所谓的自旋锁。
自旋锁在JDK 1.4.2中就已经引入 ,只不过默认是关闭的,可以使用-XX:+UseSpinning参数来开启,在
JDK 6中 就已经改为默认开启了。自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本
身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等
待的效果就会非常好,反之,如果锁被占用的时间很长。那么自旋的线程只会白白消耗处理器资源,而
不会做任何有用的工作,反而会带来性 能上的浪费。因此,自旋等待的时间必须要有一定的限度,如果
在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。
synchronized (Demo01.class) {

System.out.println(“aaa”);
}自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值
是10次,用户可以使用参数-XX : PreBlockSpin来更改。


适应性自旋锁


在JDK 6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上
的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持
有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持
续相对更长的时间,比如100次循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取
这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控
信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虛拟机就会变得越来越“聪明”了。


自旋锁的优缺点


自旋锁尽可能的减少了线程的阻塞,这对于锁竞争不激烈,且占用锁时间非常短的代码块来说性能大幅度的提升了,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!但是如果锁的竞
争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,会白白浪费CPU资源。同时如果有大量线程在竞争一个锁,会导致获取锁的时间很长,这时
候线程自旋的消耗就大于线程阻塞挂起操作的消耗,同时其它需要CPU的线程也因为不能获取到CPU,而造成CPU的浪费,这种情况下也不适合使用自旋锁;
自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!
**

接下来上代码,这里需要找到hotspot虚拟机的实现规范,下面一些注释是自己的理解

在这里插入图片描述
在这里插入图片描述

**

在这里插入图片描述

平时写代码如何对synchronized优化

减少synchronized的范围
同步代码块中尽量短,减少同步代码块中代码的执行时间,减少锁的竞争。
synchronized (Demo01.class) {
System.out.println(“aaa”);
}
降低synchronized锁的粒度
将一个锁拆分为多个锁提高并发度
Hashtable hs = new Hashtable();
hs.put(“aa”, “bb”);
hs.put(“xx”, “yy”)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

ReentantLock

ReentantLock 继承接口 Lock 并实现了接口中定义的方法,他是一种可重入锁,除了能完
成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等
避免多线程死锁的方法。
Lock 接口的主要方法

  1. void lock(): 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经
    被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁.
  2. boolean tryLock():如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false. 该方法和
    lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用,
    当前线程仍然继续往下执行代码. 而 lock()方法则是一定要获取到锁, 如果锁不可用, 就一
    直等待, 在未获得锁之前,当前线程并不继续向下执行.
  3. void unlock():执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程
    并不持有锁, 却执行该方法, 可能导致异常的发生.
  4. Condition newCondition():条件对象,获取等待通知组件。该组件和当前的锁绑定,
    当前线程只有获取了锁,才能调用该组件的 await()方法,而调用后,当前线程将缩放锁。
  5. getHoldCount() :查询当前线程保持此锁的次数,也就是执行此线程执行 lock 方法的次
    数。
  6. getQueueLength():返回正等待获取此锁的线程估计数,比如启动 10 个线程,1 个
    线程获得锁,此时返回的是 9
  7. getWaitQueueLength:(Condition condition)返回等待与此锁相关的给定条件的线
    程估计数。比如 10 个线程,用同一个 condition 对象,并且此时这 10 个线程都执行了
    condition 对象的 await 方法,那么此时执行此方法返回 10
  8. hasWaiters(Condition condition):查询是否有线程等待与此锁有关的给定条件
    (condition),对于指定 contidion 对象,有多少线程执行了 condition.await 方法
  9. hasQueuedThread(Thread thread):查询给定线程是否等待获取此锁
  10. hasQueuedThreads():是否有线程等待此锁
  11. isFair():该锁是否公平锁
  12. isHeldByCurrentThread(): 当前线程是否保持锁锁定,线程的执行 lock 方法的前后分
    别是 false 和 true
  13. isLock():此锁是否有任意线程占用
  14. lockInterruptibly():如果当前线程未被中断,获取锁
  15. tryLock():尝试获得锁,仅在调用时锁未被线程占用,获得锁
  16. tryLock(long timeout TimeUnit unit):如果锁在给定等待时间内没有被另一个线程保持,
    则获取该锁。

面试题:synchronized与Lock的区别


  1. synchronized是关键字,而Lock是一个接口。
  2. synchronized会自动释放锁,而Lock必须手动释放锁。
  3. synchronized是不可中断的,Lock可以中断也可以不中断。
  4. 通过Lock可以知道线程有没有拿到锁,而synchronized不能。
  5. synchronized能锁住方法和代码块,而Lock只能锁住代码块。
  6. Lock可以使用读锁提高多线程读效率。
  7. synchronized是非公平锁,ReentrantLock可以控制是否是公平锁
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值