1.java线程状态
可以看博文:java线程状态
2.锁的种类
- 从线程是否需要对资源加锁可以分为 悲观锁 和 乐观锁
- 从资源已被锁定,线程是否阻塞可以分为 自旋锁
- 从多个线程并发访问资源,也就是 Synchronized 可以分为 无锁、偏向锁、 轻量级锁 和 重量级锁
- 从锁的公平性进行区分,可以分为公平锁 和 非公平锁
- 从根据锁是否重复获取可以分为 可重入锁 和 不可重入锁
- 从多个线程能否获取同一把锁分为 共享锁 和 排他锁
3.乐观锁VS悲观锁
悲观锁:
悲观锁在持有数据的时候总会把资源或者数据锁住,此时如果有其他线程想要获取这个资源就会阻塞,直到悲观锁把
资源释放为止。Java 中的 Synchronized 和 ReentrantLock 等独占锁(排他锁)也是一种悲观锁思想的实现,因为
Synchronzied 和 ReetrantLock 不管是否持有资源,它都会尝试去加锁,生怕自己心爱的宝贝被别人拿走。悲观锁
适用于写多读少的情况。
乐观锁:
乐观锁的实现方案一般来说有两种:版本号机制和CAS实现 。乐观锁总认为资源和数据不会被别人所修改,所以读取
不会上锁,但是乐观锁在进行写入操作的时候会判断当前数据是否被修改过。乐观锁适用于读多写少的情况。
4.自旋锁
在多处理器环境中某些资源的有限性,有时需要互斥访问(mutual exclusion),这时候就需要引入锁的概念,只有获
取了锁的线程才能够对资源进行访问,由于多线程的核心是CPU的时间分片,所以同一时刻只能有一个线程获取到锁。
那么就面临一个问题,那么没有获取到锁的线程应该怎么办?通常有两种处理方式:一种是没有获取到锁的线程就一
直循环等待判断该资源是否已经释放锁,这种锁叫做自旋锁,它不用将线程阻塞起来(NON-BLOCKING);还有一种处
理方式就是把自己阻塞起来,等待重新调度请求,这种叫做互斥锁。
当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别的线程占用,那么此线程就无法获取到这把锁,该
线程将会等待,间隔一段时间后会再次尝试获取。这种采用循环加锁 -> 等待的机制被称为自旋锁(spinlock)。
4.1 自旋锁的原理
自旋锁的原理比较简单,如果持有锁的线程能在短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态
和用户态之间的切换而进入阻塞状态,它们只需要等一等(自旋),等到持有锁的线程释放锁之后即可获取,这样就避
免了用户进程和内核切换的消耗。
4.2 自旋锁的优缺点
优点:
自旋锁避免了操作系统进程调度和线程切换,所以自旋锁通常适用在时间比较短的情况下。自旋锁尽可能的减少线程
的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线
程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!
缺点:
如果某个线程长时间拥有锁的话,自旋锁会非常耗费性能,因为虽然避免了线程切换之间的开销,但是它占用了处理
器的时间,白白消耗了处理器资源而不做任何有用的事情。线程持有锁的时间越长,则持有该锁的线程被
OS(Operating System) 调度程序中断的风险越大。如果发生中断情况,那么其他线程将保持旋转状态(反复尝试获
取锁),而持有该锁的线程并不打算释放锁,这样导致的是结果是无限期推迟,直到持有锁的线程可以完成并释放
它为止。
自旋等待的时间一定需要一个限度,如果超过限定次数仍然没有成功获取到锁,就应该利用传统方法去挂起线程,自
旋默认次数是10,可以使用-XX:PreBlockSpin来更改。在JDK1.6之后引入了自适应的自旋锁,意味着自旋的时间不
在固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功
获取了锁,并且持有锁的线程正在运行中,那么虚拟机会认为这次自旋很有可能会成功,进而虚拟机允许自旋等待持
续相对更长的时间;如果对于某个锁,自旋很少成功获取,那在以后要获取这个锁时可能直接忽略掉自旋过程,避免
浪费处理器资源。
5. java 对象头
在HotSpot虚拟机中,对象在内存中存储布局可以分为三块:对象头(Header)、实例数据(Instance Date)
和对齐填充(Padding)。对象头主要包含二部分数据:Mark Word和Class Pointer(类型指针)。
Mark Word:默认存储对象的HashCode、GC分代年龄、锁状态标志、线程持有的锁等。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
class Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
- 无状态:就是无锁的时候,对象头开辟 25bit 的空间用来存储对象的 hashcode ,4bit 用于存放分代年龄,1bit 用来存放是否偏向锁的标识位,2bit 用来存放锁标识位为01。
- 偏向锁 :划分更细,还是开辟25bit 的空间,其中23bit 用来存放线程ID,2bit 用来存放 epoch,4bit 存放分代年龄,1bit 存放是否偏向锁标识, 0表示无锁,1表示偏向锁,锁的标识位还是01。
- 轻量级锁:开辟 30bit 的空间存放指向栈中锁记录的指针,2bit 存放锁的标志位,其标志位为00。
- 重量级锁:和轻量级锁一样,30bit 的空间用来存放指向重量级锁的指针,2bit 存放锁的标识位,为11。
- GC标记:开辟30bit 的内存空间却没有占用,2bit 空间存放锁标志位为11。
Synchronized锁
synchronized用的锁记录是存在Java对象头里的。JVM基于进入和退出 Monitor 对象来实现方法同步和代码块同步
。代码块同步是使用 monitorenter 和 monitorexit 指令实现的,monitorenter 指令是在编译后插入到同步代码块的
开始位置,而 monitorexit 是插入到方法结束处和异常处。任何对象都有一个 monitor 与之关联,当且一个 monitor
被有后,它将处于锁定状态。根据虚拟机规范的要求,在执行 monitorenter 指令时,首先要去尝试获取对象的锁,
如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应地,在执行 monitorexit
指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。如果获取对象锁失败了,那当前线程就要阻塞等待,
直到对象锁被另一个线程释放为止。
Monitor
Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,任何对象都有一个monitor与之关联。监
视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要
从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized
效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为重量级锁。
Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁:锁一共有4种状态,级别从低到
高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但不能降级。等到下一次线程再进
入和退出同步代码块时就不需要进行 CAS 操作进行加锁和解锁,只需要简单判断一下对象头的 Mark Word 中是否
存储着指向当前线程的线程ID,判断的标志当然是根据锁的标志位来判断的。
在代码进入同步代码块的时候,如果此对象没有被锁定,虚拟机首先在当前线程的栈帧中建立一个锁记录空间,用
来存储锁对象目前的Mark Word的拷贝(Displace Mark Word)。
轻量级锁的获取和释放:
引入轻量级锁的主要目的是在多没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下:
获取锁:
step1:判断当前对象是否处于无锁状态(hashcode|age|0|01),若是则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word);否则执行步骤(3)。
step2:JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变成00(表示此对象处于轻量级锁状态),执行同步操作;如果失败则执行步骤(3)。
step3:判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态。
解锁:
轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:
step1:取出在获取轻量级锁时保存在Displaced Mark Word中的数据。
step2:用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功,否则执行(3)。
step3:如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要在释放锁的同时需要唤醒被挂起的线程。
偏向锁:
获取锁 :
step1:检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁标识位为01;
step2: 若为可偏向状态,则检查线程ID是否为当前线程ID,如果是,则执行步骤(5),否则执行步骤(3)。
step3:如果线程ID不为当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行线程(4)。
step4:通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块;。
step5: 执行同步代码块。
释放锁:
偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。
偏向锁的撤销需要等待全局安全点(这个时间点是上没有正在执行的代码)。其步骤如下:
step1:暂停拥有偏向锁的线程,判断锁对象是否还处于被锁定状态。
step2:撤销偏向锁,恢复到无锁状态(01)或者轻量级锁的状态。
锁的优缺点对比:
参考:
不懂什么是锁?看看这篇你就明白了
深入理解java虚拟机-p401
【死磕Java并发】-----深入分析synchronized的实现原理