likes
comments
collection
share

九、synchronize实现原理

作者站长头像
站长
· 阅读数 19

概念

使用synchronize同步关键字可以实现线程之间的同步,保证多个线程的同时操作下的数据安全。但是 synchronize是一个重量级操作,比如下面的案例:

public class Main {

    private int i = 0;

    /**
     * 
     */
    public void setValue() {
        synchronized (this) {
            i++;
        }

    }

}

同步代码块中只有++操作,实际上它的资源占用很少,但是如果在多个线程之间进行互斥等待的话,那么CPU就要在多个线程之间来回切换,本来一件很简单的++操作,在线程之间相互等待却占用了比实际的操作更多的时间。

原理

在了解原理之前,首先要知道java对象在内存中的布局,分为3个部分:

  • 对象头
  • 实例数据
  • 对其填充

当我们new一个java对象时,JVM会在堆中创建出一个instanceOopDesc对象,它就包含了对象头以及实例数据。

九、synchronize实现原理

它的主要两个部分为:_metadata 和 _mark。

_metadata主要保存了类的元数据。今天要了解的重点则是 _mrak,我们可以称之为 标记字段。 它其中主要保存了对象的 hashCode , 分代年龄,锁标志位,是否偏向锁。 _mark的默认结构如下:

九、synchronize实现原理 由于默认情况下没有线程占用该对象,所以锁的状态是 无锁。 考虑到JVM的空间效率,它被设计为非固定的数据结构,除了以上的固定结构之外,还有:

九、synchronize实现原理

上图都是在各种情况下_mardk字段的结构.

Java中的锁分为以下几种状态:

九、synchronize实现原理 GC标记为 GC回收算法有关,本文无需关心。 需要注意的是锁的几种类型:

  • 无锁
  • 偏向锁
  • 轻量级锁
  • 重量级锁

重量级锁

我们熟知的synchronize就是重量级锁,它会引起CPU的在用户态和内核态之间来回切换。当一个对象的锁状态为重量级锁(标志位 10)时,

九、synchronize实现原理

_markdatra 会用30bit记录 一个指向互斥锁(monitor)的指针。

monitor的结构

九、synchronize实现原理

monitir可以看理解为一个同步工具,或者描述为一种同步机制。实际上它是保存在对象头中的一个对象。 这里我们只需要知道,java中每一个对象都有一个自己的ObjectMonitor对象,翻译为:对象监视器

这也就是java中的所有Object及其子类对象都可以作为 的原因.

ObjectMonitor的结构

九、synchronize实现原理

注意其中的几个关键字段:

字段名含义
_EntrySet存放等待锁的block状态的线程队列
_owner指向持有锁对象的线程
_count当某个线程竞争到monitor之后,
_recursions锁的重入次数
_WaitSet存放等待锁的wait状态的线程队列

当多个线程同时访问一段同步代码时,首先他们会进入到_entrySet,当某个线程竞争到monitor之后,_owner会变为当前线程,_count会+1,表示线程已经获得当前锁。

如果持有monitor的线程调用wait方法, 它将会释放锁,_owner会变成 null,_count会自减。同时该线程会进入到 _WaitSet等待被唤醒。 如果持有monirot的线程执行任务完毕,同样也会释放锁,_owner会变成 null,_count会自减,以便其他线程进入获取锁对象。

实例演示

九、synchronize实现原理 如果3个线程同时执行 syncMethod方法,模拟情况如下:

执行之前

九、synchronize实现原理

开始竞争锁

九、synchronize实现原理 此时,3个线程都进了 EntrySet

线程2抢到了锁

九、synchronize实现原理

Owner会指向线程2,同时count++

线程2执行过程中调用了wait

九、synchronize实现原理 此时,count--,Owner变成 null,线程2进入到WaitSet队列

线程1获得了锁,并且在 执行过程调用了 notify

九、synchronize实现原理 线程2会被重新添加到 EntrySet,并尝试重新获取锁。但是,线程1调用notify并不会释放锁。 九、synchronize实现原理

ObjectMonitor对象监视器同步机制

它是JVM对系统级别的互斥锁(MutexLock)的管理过程。期间都会转入到系统内核态。 所以,synchronize实现锁,是基于重量级锁的状态下。当多个线程切换上下文时,是一个很重量级的操作。

经典的生产消费者模式案例代码

用wait和notify确保多线程的数据安全。

import java.util.LinkedList;

class ProducerConsumer {
    private LinkedList<Integer> buffer = new LinkedList<>();
    private int capacity = 5;
    
    public void produce() throws InterruptedException {
        int value = 0;
        while (true) {
            synchronized (this) {
                while (buffer.size() == capacity) {
                    wait();
                }
                System.out.println("Producer produced: " + value);
                buffer.add(value++);
                notify();
                Thread.sleep(1000);
            }
        }
    }
    
    public void consume() throws InterruptedException {
        while (true) {
            synchronized (this) {
                while (buffer.isEmpty()) {
                    wait();
                }
                int value = buffer.removeFirst();
                System.out.println("Consumer consumed: " + value);
                notify();
                Thread.sleep(1000);
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        ProducerConsumer pc = new ProducerConsumer();
        
        Thread producerThread = new Thread(() -> {
            try {
                pc.produce();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        
        Thread consumerThread = new Thread(() -> {
            try {
                pc.consume();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        
        producerThread.start();
        consumerThread.start();
    }
}

JVM对Synchronize的优化

从java6开始,JVM开始对synchronize做出优化,主要目的是减少对 ObjectMonitor的访问,减少对重量级锁的使用,最终减少上下文切换的频率。

锁自旋

自旋锁是一种线程同步机制,它在等待共享资源释放的时候,并不会让线程进入睡眠或阻塞状态,而是让线程处于忙等(自旋)状态,即不断地循环检查共享资源是否可用。这样可以减少线程状态的切换和上下文切换的开销,提高程序的执行效率。

那么,JVM引入自旋锁的终极目的是为了在多核CPU中更好地利用硬件特性,提高多线程程序的执行效率。当线程在自旋锁上自旋等待时,它会尽可能地利用空闲的CPU时间执行自旋操作,以期待共享资源的快速释放。这对于一些短时间的等待是非常有效的,因为在这种情况下,睡眠或阻塞线程的开销可能超过了等待的时间。

总而言之,JVM引入自旋锁的终极目的是通过减少线程状态切换和上下文切换的开销,提高多线程程序的性能和响应性,使程序能够更好地利用多核CPU的硬件特性。

缺点就是:需要占用CPU。

轻量级锁

JVM中存在一些极端情况,对于一个同步代码块,不同线程是在不同的时间段去交替请求这把锁,不存在竞争的情况。就像一个很有纪律的食堂,规规矩矩排队打饭,而不是抢着打饭。或者一个公路上合流的入口,所有车子都很有默契地交替同行,而不是抢行。 在这种情况下,锁会保持轻量级锁的状态,从而避免重量级锁的上下文切换。

实现方式如下:

轻量级锁的标志位为: 00, 当一个线程去执行同步代码块时,JVM会在 当前线程的栈帧中创建出一个LockRecord记录,并将锁对象的Mark拷贝到栈帧中。也就是说,锁对象的markWord已经指向了 这个线程。

九、synchronize实现原理

当线程再次执行同步代码块时,判断当前锁的markWord是否指向 当前线程的栈帧,如果是,则直接执行同步代码块。

如果不是,轻量级锁就膨胀为重量级锁, 注意,这仅仅适用于 多个线程交替获得锁,无竞争的情况。

偏向锁

比轻量级锁更加极端的情况为,不仅仅没有多线程同时竞争,反而只有一个线程一直在执行一段同步代码块,此时,为了让线程获得锁的代价更低,

实现方式为:锁对象头中有一个ThreadId字段,当第一次获得锁的时候,将这个字段设置为 该线程的id,下次获取锁的时候,直接检查id是否一致即可,如果一致,则认为已经获得了锁,则不需要再次获得。

这属于很极端的情况,一旦出现锁竞争,偏向锁就会被撤销,这是一个重量级的操作,此时,偏向锁会膨胀为轻量级锁。。

所谓JVM调优,很多都是 在多线程的场景下,根据业务决定是否开启 偏向锁,轻量级锁,调整 JVM参数,来达到最符合当前实际情况的性能。

总结

  • 偏向锁和轻量级锁都是通过自旋来避免真正的加锁
  • 重量级锁 是获得锁和释放锁
  • 重量级锁是通过对象内部的监视器 ObjectMonitor实现