在jdk1.6之前,Monitor的实现完全是依靠操作系统内部的互斥锁,需要进行用户态和内核态的切换(运行在用户态下的程序不能直接访问操作系统内核数据结构和程序。当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态。 处于用户态执行时,进程所能访问的内存空间和对象受到限制,其所处于占有的处理机是可被抢占的 ; 而处于核心态执行中的进程,则能访问所有的内存空间和对象,且所占有的处理机是不允许被抢占的。) 在jdk1.6中,引入了新的概念:偏向锁、轻量级锁、重量级锁
- 自旋锁
- 偏向锁
- 轻量级锁
- 重量级锁
实际上我们知道锁的本质就是线程等待,可以分为线程阻塞和线程自旋,关于他们的区别,简单来说 :
阻塞:要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源。 如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间, 如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。
自旋:如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。(线程还是Runnable的,只是在执行空代码。当然一直自旋也会白白消耗计算资源。)
从java锁的类型来说,阻塞对应的就是悲观锁,自旋对应的就是乐观锁。在java中乐观锁主要的实现方式就是CAS操作,我们来简单说一下CAS
CAS:一个CAS方法包含三个参数CAS(V,E,N)。V表示要更新的变量,E表示预期的值,N表示新值。只有当V的值等于E时,才会将V的值修改为N。如果V的值不等于E,说明已经被其他线程修改了,当前线程可以放弃此操作,也可以再次尝试次操作直至修改成功。基于这样的算法,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰(临界区值的修改),并进行恰当的处理。
额外引申技术点:volatile
上面说到当前线程可以发现其他线程对临界区数据的修改,这点可以使用volatile进行保证。volatile实现了JMM中的可见性。使得对临界区资源的修改可以马上被其他线程看到
synchronized用的锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽
(Word)存储对象头(也就是24个字节),如果对象是非数组类型,则用2字宽存储对象头(16个字节)。在64位虚拟机中,1字宽等于8字节,即64bit。
markword数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,它的最后2bit是锁状态标志位,用来标记当前对象的状态,对象的所处的状态,决定了markword存储的内容(它会根据对象的状态复用自己的存储空间)
64位虚拟机在不同状态下markword结构如下图所示:JVM源码中是这么写的
最后2bit是锁状态标志位,用来标记当前对象的状态,对象的所处的状态,决定了markword存储的内容,如下表所示:
下面来详细说一下各种锁:
- 自旋锁
- 自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
但是线程自旋是需要消耗cup的,说白了就是让cup在做无用功,线程不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间。如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。 - 在JDK 1.6中引入了自适应的自旋锁。 自适应意味着自旋的时间不再固定了, 而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。 如果在同一个锁对象上, 自旋等待刚刚成功获得过锁, 并且持有锁的线程正在运行中, 那么虚拟机就会认为这次自旋也很有可能再次成功, 进而它将允许自旋等待持续相对更长的时间, 比如100个循环。 另外, 如果对于某个锁, 自旋很少成功获得过, 那在以后要获取这个锁时将可能省略掉自旋过程, 以避免浪费处理器资源。
- 自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
- 偏向锁(顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁)
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。- 偏向锁的获取
- 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。
- 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。
- 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。
- 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
- 执行同步代码。
以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
- 偏向锁的撤销
当有另外的线程视图锁定某个已经被偏向过得对象,jvm就需要撤销偏向锁。线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
- 偏向锁的获取
- 轻量级锁
轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;
在《深入理解java虚拟机中》是这样说的,如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量, 那偏向锁就是在无竞争的情况下把整个同步都消除掉, 连CAS操作都不做了。
-
重量级锁 (Synchronized)这里就不多做解释了
他们之间的关系如下:
上面几种锁都是JVM自己内部实现,当我们执行synchronized同步块的时候jvm会根据启用的锁和当前线程的争用情况,决定如何执行同步操作;
在所有的锁都启用的情况下线程进入临界区时会先去获取偏向锁,如果已经存在偏向锁了,则会尝试获取轻量级锁,如果以上两种都失败,则启用自旋锁,如果自旋也没有获取到锁,则使用重量级锁,没有获取到锁的线程阻塞挂起,直到持有锁的线程执行完同步块唤醒他们;
偏向锁是在无锁争用的情况下使用的,也就是同步开在当前线程没有执行完之前,没有其它线程会执行该同步快,一旦有了第二个线程的争用,偏向锁就会升级为轻量级锁,一点有两个以上线程争用,就会升级为重量级锁;如果线程争用激烈,那么应该禁用偏向锁。