什么是锁?
在并发环境中,多个线程争抢同一个公共资源,可能会导致数据不一致的问题,所以引入了锁机制。通过一种抽象的锁对资源进行锁定。
Java内存结构
在Java中每个对象都拥有一把锁,存放在对象头中,锁中记录着当前对象被那个线程占用
对象的结构包括对象头(运行时信息包括Mark Word(当前对象运行时状态有关的数据)和Class Pointer(指向当前对象类型所在方法区中的类型数据)),实例数据(初始化时设置的属性和状态),填充字节(满足Java对象的大小必须是8比特的倍数)
Mark Word非常小,只有32bit。并且是非结构化的,在不同的锁标志位下,不同的的字段可以重用不同的比特位
synchronized关键字
synchronized通过javac编译之后会形成monitorenter和mointorexit两个字节码指令,来使线程同步
monitor
但是monitor是依赖于操作系统的mutex lock。Java线程实际上是对操作系统线程的映射
,每次操作线程,都要切换操作系统的内核态,这种操作时比较重量级的,会对程序性能有严重的损害。
synchronized优化
从Java6开始,引入了偏向锁,轻量级锁。所以锁一共有四种状态,无锁,偏向锁,轻量级锁,重量级锁。(锁只能升级不能降级)
无锁
没有对资源进行锁定
无锁分为两种情况
1、资源不存在竞争
2、资源存在竞争,但是我们不想对资源进行锁定,但是想通过一些机制来控制多线程(例如CAS(compare and swap))
多个线程修改同一个值,只有一个线程可以成功,其他失败的线程会不断重试。
CAS在操作系统中通过一条指令来实现,所以保证了原子性
偏向锁
假如我们给一个对象加锁,但在实际运行时只会有一个线程会获取这个对象锁,最理想的情况就是不通过线程状态的切换和CAS来获得锁。最好就是这个对象认识线程,只要是这个线程就直接把锁交出去,我们认为这个对象偏爱于这个线程,所以称为偏向锁。
实现原理:
当Mark Word中的所标志为01时,查看倒数第三个bit是否是1,如果是1则当前锁状态为偏向锁,然后在查看前23bit(线程ID)来确认是当前想要获得锁的线程是否是老顾客。
如果情况发生了变化,目前不止一个线程在竞争锁,偏向锁就会升级为轻量级锁。
轻量级锁
那么当偏向锁升级为轻量级锁时,又该判断锁和线程的绑定关系呢?
当升级为轻量级锁时,前30个bit就会变成指向栈中锁记录的指针。
当线程想要获得某个对象的锁时,加入看到锁标志位为00时,就知道这个轻量级锁。线程会在自己的虚拟机栈(线程私有)中开辟一块被称为Lock Record的空间。其中存放着对象头中的Mark Word的副本和owner指针。线程通过CAS去获取锁,一旦获得就会复制该对象头中的Mark Word到Lock Record中,并将Lock Record中的owner指针指向该对象,对象中的前30个bit会生产一个指针,指向Lock Record。这时对象就被锁定了,获取了锁的线程则可以进行操作。
如何之后又其他线程想要获得这个对象,那么其他线程就会自旋(线程不断循环去查看对象的锁是否被释放)(CPU空转,长时间的空转会浪费大量资源)等待。
如果对象的锁很快就被释放,自旋不需要系统中断和现场恢复,提高效率。
重量级锁
如果等待自旋的线程超过1个,那么轻量级锁就会升级为重量级锁。那么就会通过monitor来进行控制。