Synchronized 关键字实现原理

69 阅读4分钟

照顾没有多线程基础的朋友,补一下线程的基本知识,jdk1.8以前不支持lambada表达式写法,举例多线程原始写法

@Slf4j(topic = "a")
public class Test {

    static Lock customerLock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new myThread());
        thread.start();
    }
}
@Slf4j(topic = "a")
class myThread implements Runnable{

    @Override
    public void run() {
        log.debug("这是一个自定义线程任务");
    }
}

synchronized(lock)//锁的实际上是对象,这个lock一定要全局变量,不能是局部变量。 如果一定要用局部变量,编译也能通过。当lock是个局部变量时,jvm会开启锁的优化(锁消除),那么锁局部变量相当于无锁,没用意义。 sync主要优化手段 锁消除和锁粗化 什么是对象?对象的组成部分?
对象就是堆中的一块内存,如图

image.png synchronized 给对象加锁其实是改变了对象头中的某些值,那么对象头是什么?由什么组成呢?
hotspot openjdk文档中明确指明对象头的组成部分

  • Consists of two words 对象头由两个字组成
  • Includes fundamental information about the heap object's layout, type, GC state, synchronization state, and identity hash code
    包含了堆中对象基本信息、类型、GC状态、锁状态、身份散列码 对象类型 --- 通过kclass pointer当中的地址指向类的首地址 image.png 对象头 = markword(8字节) + klass pointer(4字节) jdk1.8默认开启指针压缩因而klass pointer是4字节,没用开启指针压缩是 8 字节,只规定了2部分组成没有明确规定字的大小
    klass pointer 存的是一个地址---------指向当前对象的类在元空间首地址 image.png markword 不是固定的结构,对象初始化时时无锁状态
    -XX:BiasedLockingStartupDelay=0 关闭延迟开启偏向锁
    -XX:-UseBiasedLocking 禁止偏向锁
    -XX:+UseBiasedLocking 启用偏向锁

image.png 无锁状态下25位未使用;31位存储hashcode;ud1代表未使用;age存储的是对象年龄,因此对象年龄最大为2<<4=15 bl1标识偏向锁标识;lock锁状态标识只有两位最多标识4种状态 由于无锁分为可偏向不可偏向 4位无法表示,因而bl专门用于表示偏向锁是否偏向。 因特尔操作系统采用的是小端存储方式,如图

image.png

小端存储方式是低位在前,高位在后 image.png

锁不可偏向的三种情况

  • jvm关闭了偏向锁,或者延迟了偏向(jvm默认,因为偏向有缺点)
  • 对象被计算了hash code 没有地方存储线程信息
  • 对象膨胀之后(如升级为轻量锁),膨胀过程不可逆,极端情况下可逆 偏向锁首先看锁标志位是否为 01 如果是 01 说明是偏向锁。看对象头是否存储了线程ID,如果存储了线程ID说明已偏向

开发过程中尽量不要使用new Thread 的方式创建线程任务,尽量使用线程池。当线程t1 与线程t2 通过join方式顺序执行,t2的线程ID可能与t1相同, 由于线程ID相同 那么 t2执行时它依然是偏向锁而不会膨胀升级为轻量锁。

总结: 当一个对象锁第一次被线程持有时是偏向锁;如果持有线程来加锁还是偏向锁,如果其他线程来加锁但是时顺序执行没有竞争则膨胀为轻量锁。当多个线程竞争加锁不是顺序执行时膨胀升级为重量锁

wait 方法的原理

Object提供了wait方法,该方法要求调用的Object是被sync加锁的对象,否则抛出异常。(没有加锁的状态对象头无法关联ObjectMonitor)

当一个锁对象调用了wait方法会升级为重量锁

wait方法和sleep方法的区别(二者都会让当前线程阻塞)

wait方法会释放锁,适用于做线程之间的交互。sleep不会释放锁。

public static void main(String[] args) throws InterruptedException {
    Object obj = new Object();
    boolean hasGirl = false;
    Thread t1 = new Thread(()->{
        log.debug("加班");
        synchronized (obj){
            if(!hasGirl){
                log.debug("coding ...");
            }else {
                log.debug("no girl ...");
                try {
                //线程睡眠时由于sleep并不会释放锁,下面的循环代码不会执行
                //将这里修改为 marx.wait(),当前线程释放锁其他代码可以继续执行,当前线程阻塞直至被唤醒
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    },"marx");

    marx.start();

    TimeUnit.SECONDS.sleep(1);
    for (int i = 5;i<5; i++){
        new Thread(()->{
            synchronized (obj){
                log.debug("coding -------");
            }
        },"t"+i).start();
    }

    TimeUnit.SECONDS.sleep(4);
}

上面的代码案例中,即使最后marx线程被其他线程唤醒 依然不会进行coding工作 因为 else没有执行结束。 所以 wait一般使用场景是与 while一起 而不是 if else;将if else代码修改为

while (!hasGirl){
    try {
        log.wait();
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    log.debug("marx 被唤醒 coding....");
}

wait方法释放锁后可以通过 notify唤醒线程。但如果同时有多个线程wait时,调用notify方法是随机唤醒一个。
可以调用notifyAll将同时wait的所有线程一起唤醒。但是,无法实现精确唤醒;那么可能会导致其他业务逻辑非正确执行。
sync关键字当中 wait 和 notify|notifyAll 无法实现精确唤醒满足条件部分线程。
需要注意的是,一旦调用wait方法就会膨胀为重量锁,wait调用后会关联指向 ObjectMonitor(62),并将当前线程对象放入ObjectMonitor当中的 WaitSet 队列。

ReentrantLock实现精确唤醒代码案例(await + asignal || asignalAll)

static boolean isw = false;
static boolean ism = false;

static ReentrantLock lock = new ReentrantLock();
static Condition conditionw = lock.newCondition();//解决线程释放锁无法精确唤醒的问题
static Condition conditionm = lock.newCondition();
public static void main(String[] args) throws InterruptedException {

    Thread marx = new Thread(()->{
        lock.lock();
        while (!isw){
            log.debug("条件未满足 wait");
        }

        try {
            conditionw.await();//ReentrantLock允许在当前线程创建一个Condition
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        log.debug("marx醒来了  开始工作");
        lock.unlock();
    },"marx");

    marx.start();

    Thread rose = new Thread(()->{
        lock.lock();
        while (!ism){
            log.debug("没有钱  wait");
        }

        try {
            conditionm.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.debug("rose醒来了 开始工作");
        lock.unlock();
    },"rose");
    rose.start();

    TimeUnit.SECONDS.sleep(1);
    new Thread(()->{
        lock.lock();
        log.debug("发加班费了");
        ism = true;
        conditionm.signalAll();//唤醒指定线程 conditionm 条件的所有线程 signal()只唤醒头部线程
        lock.unlock();
    },"boss").start();

}

ReentrantLock使用尾插法维护了一个entrySet阻塞队列,可以保证线程的启动顺序、执行顺序一致。
image.png

Synchronized 使用的是头插法维护了一个entryList阻塞队列,因此线程启动顺序与执行顺序相反。

image.png

线程状态转换图

image.png