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编译后会生成monitorentermonitorexit两个字节码来进行线程同步,可通过javap反编译进行查看

Monitor: 管程/监视器。当一个线程进入了monitor,其他线程就只能等待。

synchronized的同步机制:

解释一下流程:

  1. 首先entry set中聚集了一些要进入monitor的线程(The Owner),处于waiting状态
  2. 当某个线程A成功进入了monitor,就会处于active状态
  3. 当线程A遇到一个判断条件,需要暂时让出执行权,A就会进入wait set,状态变为waiting,此时entry set中的线程就有机会进入monitor
  4. 此时,线程B成功进入monitor并完成了任务。它可以通过notify唤醒A,让线程A再次进入monitor执行任务,执行完后退出

2.synchronized存在的问题

通过上述流程我们可以发现synchronized可能存在的问题:影响程序性能

java线程实际上是对操作系统线程的映射

因为synchronized被编译之后实质上是monitorentermonitorexit两个字节码指令,而monitor依赖于操作系统的mutex lock来实现的。

每当挂起或唤醒一个线程,都要切换操作系统内核态,这种操作是比较重量级的,在一些情况下,甚至切换时间将会超出线程的执行时间,这样使用synchronized将会对程序性能造成很严重的后果。

因此,从java 6开始,synchronized进行了优化,引入了偏向锁、轻量级锁。

接下来介绍锁的四种状态(对应了mark word中的四种状态)。

3.锁的四种状态

锁的四种状态(从低到高): 无锁 → 偏向锁 → 轻量级锁 → 重量级锁
锁只能升级,不能降级

1.无锁

无锁: 没有对资源进行锁定,所有线程都能访问到同一资源。

因此,对于一个无锁的资源,可能存在两种情况:

  1. 对象不会出现在多线程环境下,或多线程情况下不会竞争此资源。所以无须保护资源,交给线程随意调用就OK。
  2. 资源会被竞争,但不想资源被锁定,但仍想通过一些机制来控制多线程。
    比如:多个线程修改一个值,只让一个线程去修改成功,其他修改失败的线程将会不断尝试再次修改,直到修改成功。(CAS算法,,通过操作系统中通过一条指令来实现,所以能保证线程原子性→无锁编程(效率高))

2.偏向锁

偏向锁: 当有一个线程会来获取锁,那么最理想的情况就是不通过线程切换,也不通过CAS就能获得锁(资源),希望对象能认识这个线程,只要线程过来,对象就直接把锁交出去。

如何实现的?

  • 在Markword中,当锁标志位为01时,就判断倒数第三个bit是不是1(偏向锁状态)
    1 → 偏向锁
    0 → 无锁
  • 若为偏向锁,再去读markword的前23bit的值(线程ID),通过此ID确认想要获得对象锁的这个线程是不是老顾客。
  • 当锁还是偏向锁时,是通过mark word中的线程ID来找到这个占有锁的线程。
  • 假如情况发生了变化,对象发现目前不止有一个线程正在竞争锁,偏向锁就会升级为轻量级锁。

3.轻量级锁

轻量级锁: 不再使用线程ID来判断线程,而是将mark word中的前30bit变成指向线程栈中的锁记录的指针。

如何实现的?

  1. 当一个线程要获取某个对象的锁时,假如看到锁标志位为00,那么它就是轻量级锁,这时线程会在自己的虚拟机栈中开辟一块空间(Lock Record:存放对象头中的mark word副本+owner指针)
  2. 线程通过CAS去获取锁,一旦获得,将会赋值对象头中的mark word到Lock Record中,并且将Lock Record中的owner指针指向该对象。
  3. 另一方面,mark word中的前30bit将会生成一个指针,指向线程虚拟机中的Lock Record。这样一来就实现了线程和对象锁的绑定。这时一个对象就已经被锁定了。
  4. 此时,其他线程想要获取这个对象,就需要自旋等待。

自旋: 可以理解为一种轮询,线程在不断地循环,尝试着去看一下目标对象的锁是否被释放,如果释放了就去获取,没有释放就进行下一轮循环。

自旋与操作系统被挂起阻塞的区别: 当对象的锁要是很快被释放,自旋就不需要进行系统终中断和现场恢复,所以它的效率更高。

自旋相当于CPU在空转 → 长时间循环会浪费CPU资源 → 适应性自旋(自旋时间不再固定,而是由上一次在同一个锁上的自旋时间和锁的状态来决定)


举个例子:比如说在同一个锁上,当前正在自旋等待的线程,刚刚已经成功获得过锁,但是锁目前是被其他线程所占用,JVM就会认为这次自旋也很有可能会再次成功,进而允许它进行更长的自旋时间。

当自旋等待的线程超过一个,轻量级线程就会转化成重量级线程

4.重量级锁

重量级锁: 通过Monitor来对线程进行控制

之前synchronized那块儿已经讲过了。

以上是关于Java锁机制1.0的主要内容,如果未能解决你的问题,请参考以下文章

JAVA锁机制

JAVA锁机制

JAVA锁机制

java并发之线程同步(synchronized和锁机制)

Java并发编程:Java中的锁和线程同步机制

Java并发编程:使用synchronized获取互斥锁的几点说明