Java锁机制1.0
Posted 364.99°
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java锁机制1.0相关的知识,希望对你有一定的参考价值。
1.锁的简介
1.什么是锁?
锁: 在并发环境下,多个线程会对同一个资源进行争抢,可能会导致数据不一致的问题,即线程不安全问题。为解决这类问题而引入锁机制对这些资源进行锁定。
线程不安全与线程安全:
- 线程安全:多个线程在执行同一段代码的时候采用加锁机制,使每次的执行结果和单线程执行的结果都是一样的。
- 线程不安全:不提供加锁机制保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
哪些元素决定了线程不安全?
线程安全问题都是由全局变量及静态变量引起的。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
JVM运行时内存结构:
线程共享区域: 当多个线程竞争共享区域中的一些数据时,就有可能造成一些意料之外的情况。
- java堆:存放着所有对象
- 方法区:存放着类信息、常量、静态变量等数据
2.锁是如何实现的?
在Java中,每个Object都有一把锁,这把锁存放在对象头中,锁中记录了当前对象被哪个线程所占用。
填充字节: 为了满足java对象的大小必须是8字节的倍数这一要求
实例数据: 属性、方法
对象头: 存放对象的运行时信息,相比于实例数据属于一些额外的存储开销,所以被设计得极小来提高效率(64bit)
- class point:一个指针,指向了当前对象类型所在的方法区中的类型数据。
- mark word:存储了很多和当前对象运行时状态有关的数据。
2.synchronized的同步机制
1.synchronized同步流程
synchronized
经过javac
编译后会生成monitorenter
和monitorexit
两个字节码来进行线程同步,可通过javap
反编译进行查看
Monitor: 管程/监视器。当一个线程进入了monitor,其他线程就只能等待。
synchronized的同步机制:
解释一下流程:
- 首先entry set中聚集了一些要进入monitor的线程(The Owner),处于waiting状态
- 当某个线程A成功进入了monitor,就会处于active状态
- 当线程A遇到一个判断条件,需要暂时让出执行权,A就会进入wait set,状态变为waiting,此时entry set中的线程就有机会进入monitor
- 此时,线程B成功进入monitor并完成了任务。它可以通过
notify
唤醒A,让线程A再次进入monitor执行任务,执行完后退出
2.synchronized存在的问题
通过上述流程我们可以发现synchronized可能存在的问题:影响程序性能
java线程实际上是对操作系统线程的映射
因为synchronized被编译之后实质上是monitorenter
和monitorexit
两个字节码指令,而monitor依赖于操作系统的mutex lock
来实现的。
每当挂起或唤醒一个线程,都要切换操作系统内核态,这种操作是比较重量级的,在一些情况下,甚至切换时间将会超出线程的执行时间,这样使用synchronized将会对程序性能造成很严重的后果。
因此,从java 6开始,synchronized进行了优化,引入了偏向锁、轻量级锁。
接下来介绍锁的四种状态(对应了mark word中的四种状态)。
3.锁的四种状态
锁的四种状态(从低到高): 无锁 → 偏向锁 → 轻量级锁 → 重量级锁
锁只能升级,不能降级
1.无锁
无锁: 没有对资源进行锁定,所有线程都能访问到同一资源。
因此,对于一个无锁的资源,可能存在两种情况:
- 对象不会出现在多线程环境下,或多线程情况下不会竞争此资源。所以无须保护资源,交给线程随意调用就OK。
- 资源会被竞争,但不想资源被锁定,但仍想通过一些机制来控制多线程。
比如:多个线程修改一个值,只让一个线程去修改成功,其他修改失败的线程将会不断尝试再次修改,直到修改成功。(CAS算法,,通过操作系统中通过一条指令来实现,所以能保证线程原子性→无锁编程(效率高))
2.偏向锁
偏向锁: 当有一个线程会来获取锁,那么最理想的情况就是不通过线程切换,也不通过CAS就能获得锁(资源),希望对象能认识这个线程,只要线程过来,对象就直接把锁交出去。
如何实现的?
- 在Markword中,当锁标志位为01时,就判断倒数第三个bit是不是1(偏向锁状态)
1 → 偏向锁
0 → 无锁 - 若为偏向锁,再去读markword的前23bit的值(线程ID),通过此ID确认想要获得对象锁的这个线程是不是老顾客。
- 当锁还是偏向锁时,是通过mark word中的线程ID来找到这个占有锁的线程。
- 假如情况发生了变化,对象发现目前不止有一个线程正在竞争锁,偏向锁就会升级为轻量级锁。
3.轻量级锁
轻量级锁: 不再使用线程ID来判断线程,而是将mark word中的前30bit变成指向线程栈中的锁记录的指针。
如何实现的?
- 当一个线程要获取某个对象的锁时,假如看到锁标志位为00,那么它就是轻量级锁,这时线程会在自己的虚拟机栈中开辟一块空间(Lock Record:存放对象头中的mark word副本+owner指针)。
- 线程通过CAS去获取锁,一旦获得,将会赋值对象头中的mark word到Lock Record中,并且将Lock Record中的owner指针指向该对象。
- 另一方面,mark word中的前30bit将会生成一个指针,指向线程虚拟机中的Lock Record。这样一来就实现了线程和对象锁的绑定。这时一个对象就已经被锁定了。
- 此时,其他线程想要获取这个对象,就需要自旋等待。
自旋: 可以理解为一种轮询,线程在不断地循环,尝试着去看一下目标对象的锁是否被释放,如果释放了就去获取,没有释放就进行下一轮循环。
自旋与操作系统被挂起阻塞的区别: 当对象的锁要是很快被释放,自旋就不需要进行系统终中断和现场恢复,所以它的效率更高。
自旋相当于CPU在空转 → 长时间循环会浪费CPU资源 → 适应性自旋(自旋时间不再固定,而是由上一次在同一个锁上的自旋时间和锁的状态来决定)
举个例子:比如说在同一个锁上,当前正在自旋等待的线程,刚刚已经成功获得过锁,但是锁目前是被其他线程所占用,JVM就会认为这次自旋也很有可能会再次成功,进而允许它进行更长的自旋时间。
当自旋等待的线程超过一个,轻量级线程就会转化成重量级线程
4.重量级锁
重量级锁: 通过Monitor来对线程进行控制
之前synchronized那块儿已经讲过了。
以上是关于Java锁机制1.0的主要内容,如果未能解决你的问题,请参考以下文章