- 介绍
- 实现机制原理
- 线程中断(interrupt)
- 清除中断状态
- 方法抛出InterruptedException,并且清除中断状态
- 锁升级
- 锁的升级过程
- Java 对象头
- 全局安全点(safepoint)
- 偏向锁(多线程竞争可以考虑禁用)
- 批量重偏向
- 批量重偏向的原理
- 轻量级锁
- 重量级锁
- 锁的其他优化
线程状态
private volatile int threadStatus = 0; public enum State { NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED; } public State getState() { // get current thread state return sun.misc.VM.toThreadState(threadStatus); }
线程一共有6种状态, 其状态转换关系如下图所示:
实现机制原理synchronized(this) { //由锁保护的代码 }
synchronized是java提供的原⼦性内置锁,这种内置的并且使⽤者看不到的锁也被称为监视器锁,使⽤synchronized之后,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,他依赖 *** 作系统底层互斥锁实现。
他的作⽤主要就是实现原⼦性 *** 作和解决共享变量的内存可⻅性问题。(从内存语义来说,加锁的过程会清除⼯作内存中的共享变量,再从主内存读取,⽽释放锁的过程则是将⼯作内存中的共享变量写回主内存)
synchronized是排它锁,当⼀个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁,⽽且由于Java中的线程和 *** 作系统原⽣线程是⼀⼀对应的,线程被阻塞或者唤醒时时会从⽤户态切换到内核态,这种转换⾮常消耗性能。
在Java中, 我们可以使用
- wait()
阻塞当前线程(阻塞的原因常常是一些必要的条件还没有满足), 让出监视器锁, 不再参与锁竞争, 直到其他线程来通知(告知必要的条件已经满足了) - wait(long timeout)
- wait(long timeout, int nanos)
,阻塞当前线程,直到其他线程来通知(直到设定的超时等待时间到了) - notify()
通知那些调用了wait方法的一个线程, 让它们从wait处返回继续执行. - notifyAll()
通知那些调用了wait方法的所有线程, 让它们从wait处返回继续执行.
这5个方法来实现同步代码块之间的通信, 注意, 我说的是同步代码块之间的通信, 这意味着:
调用该方法的当前线程必须持有对象的监视器锁
(源码注释: The current thread must own this object’s monitor.)
其实, 这句话换个通俗点的说法就是: 只能在同步代码块中使用这些方法.
道理很简单, 因为只有进入了同步代块, 才能获得监视器锁.
中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。
- 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。
- 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出InterruptedException,从而使线程提前结束 TIMED-WATING 状态。
- 许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。
- 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程 thread 的时候,可以调用 thread.interrupt()方法,在线程的 run 方法内部可以根据 thread.isInterrupted()的值来优雅的终止线程。
在java中,每一个线程都有一个中断标志位,表征了当前线程是否处于被中断状态,我们可以把这个标识位理解成一个boolean类型的变量,当我们中断一个线程时,将该标识位设为true,当我们清除中断状态时,将其设置为false, 不过值得一提的是,在我们能使用到的public方法中,interrupted()是我们清除中断的唯一方法。
方法抛出InterruptedException,并且清除中断状态如果线程直接或间接的调用了下面进入阻塞状态的方法,那么当其他线程要调用该线程的interrupt方法后,他们会发生中断,从阻塞状态变为运行状态,抛出InterruptedException,并且清除中断状态。例如:
Object
wait()
wait(long timeout, int nanos)
sleep(long millis, int nanos)
Thread
join()
join(long millis)
join(long millis, int nanos)
这里值得注意的是,虽然这些方法会抛出InterruptedException,但是并不会终止当前线程的执行,当前线程可以选择忽略这个异常。
比如此时不想或者无法传递InterruptedException异常,也不对该异常做任何处理时,我们最好通过再次调用interrupt来恢复中断的状态,以便调用者处理,借鉴AQS源码中用到这种用法。
interrupt
- 如果线程没有因为上面的函数调用而进入阻塞状态的话,那么中断这个线程仅仅会设置它的中断标志位(而不会抛出InterruptedException)
- 中断一个已经终止的线程不会有任何影响。
在jdk1.6之前,只有重量级锁,但是这个是依赖底层 *** 作系统的mutex指令,比较消耗性能,JDK1.6 以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。
“轻量级”是相对于使用 *** 作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。
在我们常⽤的Hotspot虚拟机中,对象在内存中布局实际包含3个部分:
- 对象头
- 实例数据
- 对⻬填充
⽽对象头包含两部分内容,Mark Word中的内容会随着锁标志位⽽发⽣变化,所以只说存储结构就好了。 - 对象⾃身运⾏时所需的数据,也被称为Mark Word,也就是⽤于轻量级锁和偏向锁的关键点。具体的内容包含对象的hashcode、分代年龄、轻量级锁指针、重量级锁指针、GC标记、偏向锁线程ID、偏向锁时间戳。
- 存储类型指针,也就是指向类的元数据的指针,通过这个指针才能确定对象是属于哪个类的实例。如果是数组的话,则还包含了数组的⻓度
在JVM中,对象在内存中除了本身的数据外还会有个对象头,对于普通对象而言,其对象头中有两类信息:mark word和类型指针。另外对于数组而言还会有一份记录数组长度的数据。
在32位系统上mark word长度为32bit,64位系统上长度为64bit。为了能在有限的空间里存储下更多的数据,其存储格式是不固定的,在32位系统上各状态的格式如下:
可以看到锁信息也是存在于对象的mark word中的。当对象状态为偏向锁(biasable)时,mark word存储的是偏向的线程ID;当状态为轻量级锁(lightweight locked)时,mark word存储的是指向线程栈中Lock Record的指针;当状态为重量级锁(inflated)时,为指向堆中的monitor对象的指针。
safepoint这个词我们在GC中经常会提到,简单来说就是其代表了一个状态,在该状态下所有线程都是暂停的。
偏向锁(多线程竞争可以考虑禁用)Hotspot 的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级
锁执行路径,因为轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换ThreadID 的时候依赖一次 CAS 原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销 *** 作的性能损耗必须小于节省下来的 CAS 原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。
偏向锁获取过程:
(1)访问Mark Word中偏向锁标志位是否设置成1,锁标志位是否为01——确认为可偏向状态。
(2)如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。
(3)如果线程ID并未指向当前线程,则通过CAS *** 作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。
(4)如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
(5)执行同步代码。
偏向锁的释放:
偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。
偏向锁的撤销,需要等待全局安全点safepoint,它会首先暂停拥有偏向锁的线程A,然后判断这个线程A,此时有两种情况:
1:A 线程已经退出了同步代码块,或者是已经不在存活了,如果是上面两种情况之一的,此时就会直接撤销偏向锁,变成无锁状态。
2:A 线程还在同步代码块中,此时将 A 线程的偏向锁升级为轻量级锁。具体怎么升级的看下面的偏向锁升级轻量级锁的过程。
为什么有批量重偏向
当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时将偏向锁撤销为无锁状态或升级为轻量级/重量级锁。这个过程是要消耗一定的成本的,所以如果说运行时的场景本身存在多线程竞争的,那偏向锁的存在不仅不能提高性能,而且会导致性能下降。因此,JVM中增加了一种批量重偏向/撤销的机制。
1.首先引入一个概念epoch,其本质是一个时间戳,代表了偏向锁的有效性,epoch存储在可偏向对象的MarkWord中。除了对象中的epoch,对象所属的类class信息中,也会保存一个epoch值。
2.每当遇到一个全局安全点时(这里的意思是说批量重偏向没有完全替代了全局安全点,全局安全点是一直存在的),比如要对class C 进行批量再偏向,则首先对 class C中保存的epoch进行增加 *** 作,得到一个新的epoch_new
3.然后扫描所有持有 class C 实例的线程栈,根据线程栈的信息判断出该线程是否锁定了该对象,仅将epoch_new的值赋给被锁定的对象中,也就是现在偏向锁还在被使用的对象才会被赋值epoch_new。
4.退出安全点后,当有线程需要尝试获取偏向锁时,直接检查 class C 中存储的 epoch 值是否与目标对象中存储的 epoch 值相等, 如果不相等,则说明该对象的偏向锁已经无效了(因为(3)步骤里面已经说了只有偏向锁还在被使用的对象才会有epoch_new,这里不相等的原因是class C里面的epoch值是epoch_new,而当前对象的epoch里面的值还是epoch),此时竞争线程可以尝试对此对象重新进行偏向 *** 作。
轻量级锁轻量级锁的获取过程
(1)在代码进入同步块的时候,如果同步对象锁状态为偏向状态(就是锁标志位为“01”状态,是否为偏向锁标志位为“1”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。官方称之为 Displaced Mark Word(所以这里我们认为Lock Record和 Displaced Mark Word其实是同一个概念)。这时候线程堆栈与对象头的状态如图所示:
(2)拷贝对象头中的Mark Word复制到锁记录中。
(3)拷贝成功后,虚拟机将使用CAS *** 作尝试将对象头的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向对象头的mark word。如果更新成功,则执行步骤(4),否则执行步骤(5)。
(4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如下所示:
(5)如果这个更新 *** 作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,现在是重入状态,那么设置Lock Record第一部分(Displaced Mark Word)为null,起到了一个重入计数器的作用。下图为重入三次时的lock record示意图,左边为锁对象,右边为当前线程的栈帧,重入之后然后结束。接着就可以直接进入同步块继续执行。
如果不是说明这个锁对象已经被其他线程抢占了,说明此时有多个线程竞争锁,那么它就会自旋等待锁,一定次数后仍未获得锁对象,说明发生了竞争,需要膨胀为重量级锁。
轻量级锁的解锁过程
(1)通过CAS *** 作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
(2)如果替换成功,整个同步过程就完成了。
(3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。
重量级锁重量级锁加锁和释放锁机制
1.调用omAlloc分配一个ObjectMonitor对象,把锁对象头的mark word锁标志位变成 “10 ”,然后在mark word存储指向ObjectMonitor对象的指针
2.ObjectMonitor对象中有两个队列,_WaitSet和_EntryList,用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装成ObjectWaiter对象),
3.ObjectMonitor对象中也有一个变量_owner,_owner指向持有ObjectMonitor对象的线程(就是当前正在执行的线程)
可以这样理解
- 当多个线程进⼊同步代码块时,⾸先进⼊entryList
- 有⼀个线程获取到monitor锁后,就赋值给当前线程,并且计数器+1
- 如果线程调⽤wait⽅法,将释放锁,当前线程置为null,计数器-1,同时进⼊waitSet等待被唤醒,调⽤notify或者notifyAll之后⼜会进⼊entryList竞争锁
- 如果线程执⾏完毕,同样释放锁,计数器-1,当前线程置为null
1、适应性自旋(Adaptive Spinning):从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS *** 作失败时,是要通过自旋来获取重量级锁的。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
2、锁粗化(Lock Coarsening):锁粗化的概念应该比较好理解,就是将多次连接在一起的加锁、解锁 *** 作合并为一次,将多个连续的锁扩展成一个范围更大的锁。举个例子:
public void lockCoarsening() {
int i=0;
synchronized (this){
i=i+1;
}
synchronized (this){
i=i+2;
}
}
上面的两个同步代码块可以变成一个
public void lockCoarsening() {
int i=0;
synchronized (this){
i=i+1;
i=i+2;
}
}
3、锁消除(Lock Elimination):锁消除即删除不必要的加锁 *** 作的代码。比如下面的代码,下面的for循环完全可以移出来,这样可以减少加锁代码的执行过程
public void lockElimination() {
int i=0;
synchronized (this){
for(int c=0; c<1000; c++){
System.out.println©;
}
i=i+1;
}
}
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)