从synchronized入手看锁

Posted gitzzp

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从synchronized入手看锁相关的知识,希望对你有一定的参考价值。

从synchronized入手看锁

synchronized基础描述

修饰在方法上,锁的是当前对象,创建多个对象,则锁失效
此时可以用单例、静态方法锁等方法避免

对象实例在JVM中的状态

一个java对象在内存中分为对象头、实例数据、对其三个部分
对象头:8个字节,垃圾回收数据(分代年龄、GC标志)、锁数据、hashcode值等,一部分存放对象的类元数据,通过该数据确定对象是哪个类的实例。
实例数据:声明的变量和方法,包括父类的属性信息
对齐:用于尾部补齐字节数,使字节对齐。

锁的不同状态

JDK6之前只有两个状态:无锁、有锁(重量级锁)

JDK6之后对synchronized进行了优化,新增了两种状态,总共就是四个状态:

  1. 无锁状态
  2. 偏向锁
  3. 轻量级锁
  4. 重量级锁

锁膨胀

无锁——>偏向锁——>轻量级锁——>重量级锁,并且膨胀方向不可逆。

偏向锁

偏向锁的作用:减少同一线程获取锁的代价。锁是为了防止多线程竞争的情况发生,但是在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得,那么此时就是偏向锁。

核心思想:如果一个线程获得了锁,那么锁就进入偏向模式,对象头结构变为偏向锁结构,记录了该线程的ID,当该线程再次请求锁时,无需再做任何同步操作,只需检查对象头的锁标记为偏向锁且线程ID相同即可,这样就节省了开销。

注意:偏向锁是被动锁,只有当其他线程竞争时,持有偏向锁的线程才会释放锁,或者等待系统在全局安全点(没有字节码正在执行的时候),暂停又有偏向锁的线程,然后检查持有偏向锁的线程是否还活着,如果没有,则释放锁

轻量级锁

轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象的时候,偏向锁会升级为轻量级锁。注意第二个线程只是申请锁,而不是竞争锁。

重量级锁

重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级为重量级锁,此时器申请锁的开销也就变大,重量级锁一般使用场景会在追求吞吐量,同步课或者同步方法执行时间较长的场景。

锁消除

锁消除是虚拟机对锁的一种优化,在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁

锁粗化

锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁。

自旋锁与自适应自旋锁

自旋锁:许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得,通过让线程执行循环等待锁的释放,不让出CPU。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。但是它也存在缺点:如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销。

自适应自旋锁:这种相当于是对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。

synchronized与Lock

synchronized的缺点

  1. 无法判断锁状态
  2. 不可中断,如果锁被其他线程占用,当前线程迟迟拿不到一把锁,会出现此案城一直等待
  3. synchronized是非公平锁,排队的线程和新进来的线程获取锁的机会是一样的
  4. 通过monitor关键字获取锁和释放锁,中间不可控
  5. 当锁产生竞争时,会升级为重量锁,有系统进行分配,会从用户空间切换到内核空间

volatile

  1. 防止重排序
  2. 实现可见性
  3. 保证原子性

DCL(Double Check Lock)单例的问题

new 一个对象,在寄存器级别,分为3个步骤:

  000b: new-instance v0, Singleton // type@0000
  000d: invoke-direct v0, Singleton.<init>:()V // method@0000
  0010: sput-object v0, Singleton.singleton:LSingleton; // field@0000
  1. new-instance 申请一块内存,创建对象头
  2. invoke-direct 写入对象实例数据
  3. 将创建的对象和引用关联起来

其中 3 依赖于 1,但2和3中间不存在依赖关系,对于不存在依赖关系的指令,在编译器级别执行顺序可能发生变化,这种现象我们称之为指令重排序

指令重排序

计算机执行指令的顺序在经过程序编译器编译之后行程的指令序列
一般而言,指令序列是会输出确定的结果,以确保每一次的执行都有确定的结果
但是,CPU和编译器为了提升程序执行的效率,会按照一定的规则(单线程情况下,改变指令的先后顺序并不会影响最终的结果,那么就允许指令重排序)允许进行指令优化,这种优化就是指令重排序
指令优化的都是互相不依赖的指令,一般在单线程的时候,这种优化并不会发生问题
但是在并发执行的情况下,就可能发生二义性,得到不同的结果。

volatile是如何防止重排序的

可见性:volatile关键字修饰后,所有的操作都是在内存屏障(主内存)中,主内存是被所有线程所共享的,代价就是牺牲了性能,无法利用高速缓冲区。

volatile不能用于并发,只对读写等操作做到原子性

CAS

CAS(Compare and Swap),翻译过来就是比较并交换

CAS机制中使用了三个基本数据:
内存地址V
旧的预期值A
要修改的新值B

更新一个变量的时候,只有当变量的预期值A和内存地址中V中的实际值相同时,才会将V中的值更新为B
如果在更新的之后发现V中的值与预期值不同,说明有其他线程操作了该数据,会从内存中重新读取数据,并重新操作。

CAS机制中的ABA问题

ABA问题:CAS机制中,只判断了预期值是否相等,那么就会存在一个问题,比如一个int值,初始10,预期为10,在比较的时候,被其他线程修改为11后又修改为10了,那么此时判断初始值和预期值就是相等的,这就是ABA问题。
避免:使用AtomicStampedReference包装一下,该类里边增加了一个基本数据,修改时候的时间戳,每次修改的时候判断一下预期值和修改时间,如果有一个不同,就认为是被其他线程修改过了,会重新读取内存进行操作。

ReentrantLock

优点:可重入、可中断、可限时、公平锁

可重入:同一个线程可以反复得到同一把锁,lock和unlock方法需要成对出现。
可中断:与synchronized不同的是,reentrantLock可以通过中断来处理死锁问题
可限时:超时不能获得锁,就返回false,不会永久等待构成死锁。使用lock.tryLock(long timeout, TimeUnit unit)来实现可限时锁,参数为时间和单位。
公平锁:synchronized是非公平锁,ReentrantLock可以通过构造函数初始化一个公平锁,保证先来的可以先得到锁。

隐性锁与显性锁

隐性锁:每个Java对象可以用作实现同步的内置锁,线程在访问同步代码块的时候必须先获取该内置锁,在退出和中断的时候需要释放内置锁。Java内置锁通过synchronized关键字来使用,使用其修饰方法或者代码块,对象锁和类锁(static方法和class上加锁)不冲突,可以并行存在。
显性锁:如ReentrantLock这种,通过手动来加锁和释放锁的方式

乐观锁和悲观锁

悲观锁:是一种独占锁,synchronized就是悲观锁,会导致其他所有未持有锁的线程阻塞,等待持有锁的线程释放锁。
乐观锁:乐观锁就是假设没有冲突而去完成某项操作,如果操作过程中失败了,那么就重试,直到成功为止。例如CAS机制。

并发三大利器

synchronized:应用monitor机制
atomic包:例:atomicBoolean,原理:应用Volatitle关键字和CAS理论实现并发
lock包:应用CAS理论实现并发

以上是关于从synchronized入手看锁的主要内容,如果未能解决你的问题,请参考以下文章

从synchronized入手看锁

杂谈从底层看锁的实现2

杂谈从底层看锁的实现

多线程 Thread 线程同步 synchronized

synchronized学习

从内部入手,浅谈malloc和new的区别