锁
1、什么是锁
在并发环境下,多个线程会对同一资源进行争抢竞争,可能会导致数据不一致的问题出现。为了解决此问题,编程语言引用的锁机制,来对资源进行锁定。可以理解为解决了资源竞争问题的方式就是锁。
当不同的线程竞争灰色区域内的共享数据时,就有可能出现问题。
2、对象
Java中每个对象object都拥有一把锁,锁存放在对象头中,锁记录了当前对象被哪个线程占用。
其中对象头记录了一些运行时信息(markWord、classPoint等)。
其中markWord中记录了一些很重要的锁的信息(64位虚拟机与32位虚拟机不同):
锁状态 | 25bit | 4bit | 1bit | 2bit | |
23bit | 2bit | 是否偏向锁 | 锁标志位 | ||
无锁 | 对象的hashCode | 分代年龄 | 0 | 01 | |
偏向锁 | 线程id | Epoch | 分代年龄 | 1 | 01 |
轻量级锁 | 指向栈中所记录的指针 | 00 | |||
重量级锁 | 指向重量级锁的指针 | 10 | |||
GC标记 | 空 | 11 |
由这32bit的信息,就可以了解到对象锁的信息。
3、Synchronized
看一个简单的demo。可以看到是由MONITORENTER和MONITOREXIT两个字节码指令将业务代码进行包裹。但是因为monitor是依赖于操作系统的mutex lock实现的,所以每次挂起或者唤醒线程时都要切换操作系统的内核态,操作是比较重量级的,甚至切换的时间消耗就大于执行任务的时间(所以其实并不是线程数越多越好)。
Java6引用了偏向锁与轻量级锁的概念。所以也就出现了锁升级的概念。无锁->偏向锁->轻量级锁->重量级锁。(有锁升级,那么有锁降级吗?)
4、锁的4种状态
4.1无锁
无锁没有对资源进行锁定,所有线程均可以对资源进行访问。有两种情况:1、无竞争2、存在竞争但是不使用锁的方式同步线程(经典CAS)。同时CAS在操作系统中是通过一条指令来实现,所以可以保证原子性。
4.2偏向锁
如果需要给对象加锁,同时希望不通过线程切换也不使用CAS来获取锁,而是让对象“认识”某个线程,只要后续是此线程访问此对象,就直接获取锁,这就是偏向锁。偏向锁可以通过前23bit中的线程id来判断,如果相同则直接获取锁,如果不同则升级为轻量级锁。
4.3轻量级锁
最后两个bit位为00时为轻量级锁,此时线程会在自己的虚拟机栈中中开辟出一块Lock Record的空间,虚拟机栈为线程私有的一块空间。Lock Record中存放的是对象头中的markWork的副本以及owner指针,线程通过CAS去尝试获取锁,获取锁之后将会复制对象头中的markWork信息到Lock Record中,同时将owner指针指向该对象,同时对象头中的markWork的30bit将生成一个指针指向虚拟机栈中的Lock Record,这样就实现了线程与对象头的绑定,此时改对象被此线程占有。
此时若有其他的线程想要获取对象,则需要自旋等待。线程不断自旋尝试看目标对象的锁有没有被释放,如果释放了就获取。此方式区别于被操作系统挂起阻塞,若对象很快被释放,自旋不需要进行系统中断和恢复,所以效率更高。
当然为了解决一直不停的自旋,CPU空转问题,优化出现了适应性自旋,自旋时间由上一次在同一个锁上的自旋时间以及锁状态共同决定。如果自旋等待的线程超过1个,轻量级锁将升级为重量级锁。
4.4重量级锁
重量级锁完全锁定资源,通过monitor来进行管控,例如synchronized。