https://www.yuque.com/yinhuidong/juc/maxgdb

一、线程安全问题

线程的合理使用能够提升程序的处理性能,主要有两个方面,第一个是能够利用多核CPU以及超线程技术来实现线程的并行执行;第二个是线程的异步化执行相比于同步来说,异步执行能够很好的优化程序的处理性能提升并发吞吐量。

1. 线程安全

一个变量i,假如一个线程去访问这个变量进行修改,这个时候对于数据的修改和访问没有任何问题。但是如果更多的线程对于着同一个变量进行修改,就会存在一个数据安全性问题。
image.png
一个对象是否是线程安全的,取决于他是否会被多个线程访问,以及程序中是如何去使用这个对象的。所以,如果多个线程访问同一个共享对象,在不需要额外的同步以及调用端代码不用做其他协调的情况下,这个共享对象的状态依然是正确的(正确性意味着这个对象的结果与我们预期规定的结果保持一致),那说明这个对象是线程安全的。

2. 如何保证线程并行的数据安全

能够有一种方法使得线程的并行变成串行。加锁
什么是锁?他是处理并发的一种同步手段,如果达到前面的目的,那么这个锁一定需要实现互斥的特性。Java提供加锁的方法就是synchronized关键字。

二、Synchronized实现原理

多线程并发编程中synchronized一直是元老级的,很多人都会称呼为重量级锁。但是随着jdk6对synchronized进行各种优化之后,有些情况下他就并不是那么重量级了。Java6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。

synchronized实现同步的基础:Java中每一个对象都可以作为锁。具体表现为三种情况:

  1. 普通同步方法,锁是当前实例对象
  2. 静态同步方法,锁是当前类的Class对象
  3. 同步方法块,锁是sync括号里面配置的对象

当一个线程试图访问同步代码块时,他首先必须获得锁,退出或者抛出异常时必须释放锁。那么锁到底存储在哪里呢?所里面会有什么信息?

从JVM规范中可以看到synchronized在jvm里面的实现原理,JVM基于进入和退出monitor对象来实现方法同步和代码块同步,但是两者实现细节不一样。代码块同步使用monitor enter和monitor exit指令类实现的,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明。但是方法的同步同样可以使用这两个指令来实现。
monitor enter指令是在编译后插入到同步代码块的开始位置,而monitor exit是插入到方法结束和异常处,JVM要保证每个monitor enter必须有对应的monitor exit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitor exit指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

  1. /**
  2. * @author 二十
  3. * @since 2021/8/26 4:33 下午
  4. */
  5. public class SyncTest {
  6. {
  7. synchronized (Object.class){
  8. try {
  9. TimeUnit.SECONDS.sleep(1);
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. }
  14. }
  15. }

执行javap -v SyncTest 命令
image.png

三、锁的对象

分析:要实现多线程的互斥状态,那这把锁需要哪些因素?

  1. 锁需要有一个东西来表示,比如获得锁是什么状态,无锁状态是什么状态?
  2. 这个状态需要对多个线程共享

观察synchronized的整个语法发现,synchronized(lock)是基于lock这个对象的生命周期来控制锁粒度的,那是不是锁的存储和这个lock对象有关系呢?以对象在jvm内存中是如何存储的作为切入点,分析对象里面有什么特性能够实现锁。

1. 对象在内存中的布局

在 Hotspot 虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。
对象在内存中的存储布局.jpg

2. JVM源码实现

当我们在Java代码里面使用new创建一个对象实例的时候,jvm层面上实际会创建一个instanceOopDesc对象。

Hotspot虚拟机采用OOP-Klass用来描述对象实例Java对象实例,OOP指的是普通对象指针,Klass用来描述对象实例的具体类型。Hotspot采用instanceOopDesc和arrayOopDesc对象用来描述数组类型。

instanceOopDesc的定义在Hotspot源码中的instanceOop.hpp文件中,另外,arrayOopDesc的定义对应arrayOop.hpp。

  1. #ifndef SHARE_OOPS_INSTANCEOOP_HPP
  2. #define SHARE_OOPS_INSTANCEOOP_HPP
  3. #include "oops/oop.hpp"
  4. // An instanceOop is an instance of a Java Class
  5. // Evaluating "new HashTable()" will create an instanceOop.
  6. class instanceOopDesc : public oopDesc {
  7. public:
  8. // aligned header size.
  9. static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }
  10. // If compressed, the offset of the fields of the instance may not be aligned.
  11. static int base_offset_in_bytes() {
  12. return (UseCompressedClassPointers) ?
  13. klass_gap_offset_in_bytes() :
  14. sizeof(instanceOopDesc);
  15. }
  16. };
  17. #endif // SHARE_OOPS_INSTANCEOOP_HPP


从 instanceOopDesc 代码中可以看到 instanceOopDesc继承自 oopDesc,oopDesc 的定义在Hotspot 源码中的oop.hpp 文件中。
在普通实例对象中,oopDesc 的定义包含两个成员,分别是 mark 和 metadata 。
_mark表示对象标记、属于 markOop 类型,也就是接下来要分析的 Mark World,它记录了对象和锁有关的信息。

_metadata 表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中 Klass 表示普通指针、_compressed_klass 表示压缩类指针。

4. 为什么任何对象都可以实现锁

  1. 首先,Java中的每个对象都派生自object类,而每个Java Object在JVM内部都有一个native的C++对象oop/oopDesc进行对应。
  2. 线程在获取锁的时候,实际上就是获得了一个监视器对象(monitor),可以认为他就是一个同步对象,所有的Java对象天生携带monitor。在hotspot源码的markWord.hpp文件中,可以看到下面这段代码:
    1. ObjectMonitor* monitor() const {
    2. assert(has_monitor(), "check");
    3. // Use xor instead of &~ to provide one extra tag-bit check.
    4. return (ObjectMonitor*) (value() ^ monitor_value);
    5. }

    四、锁升级

    使用锁能够实现数据的安全性,但是会带来性能的下降。不使用锁能够基于线程并行提升程序性能,但是却不能保证线程安全性。这两者之间似乎是没有办法达到既能满足性能也能满足安全性的要求。

hotspot 虚拟机的作者经过调查发现,大部分情况下,加锁的代码不仅仅不存在多线程竞争,而且总是由同一个线程多次获得。所以基于这样一个概率,是的 synchronized 在JDK1.6 之后做了一些优化,为了减少获得锁和释放锁带来的性能开销,引入了偏向锁、轻量级锁的概念。因此在 synchronized 中,锁存在四种状态分别是:无锁、偏向锁、轻量级锁、重量级锁; 锁的状态根据竞争激烈的程度从低到高不断升级。

1. 偏向锁的基本原理

怎么理解偏向锁?

当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的 ID,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了。