多线程锁及升级

一、对象头

1. 简介

  • 以32位的 JVM 为例,每个对象头都包含了如下信息
# 0 组成
Mark Word:      锁的信息,hashcode,垃圾回收器标志
Klass Word:     指针,包含当前对象的Class对象的地址

# 1. 普通对象,占用8个字节,64位
                  Object Header (64 bits)
 Mark Word(32 bits)        Klass Word(32 bits)

# 2. 数组对象, 占用12个字节, 96位      包含额外的4个字节用来保存数组长度
                  Object Header (96 bits)
 Mark Word(32 bits)       Klass Word(32 bits)      Array Length (32 bits)

2. Mark Word

- 01/00/11:      代表是否加锁
- age:           垃圾回收器标记

在这里插入图片描述

二、 Monitor监视器

1. monitor

- 由 《操作系统》提供,又叫监视器或者管程
- 包含三部分
   1. Owner:      保存当前获取到java锁的,线程指针
   2. EntryList:   保存被java锁的锁阻塞的,线程指针
   3. WaitSet:     保存被java锁等待的,线程标记

- synchronized 的java对象,会被关联到一个monitor监视器,java对象头的Mark Word就被设置为
  monitor监视器的地址
- 重量级锁
- 非公平锁

2. 竞争步骤

  • java对象被synchronized修饰后
  • 当它获取到对象锁的时候,该对象就会被关联到monitor,java对象的Mark Word就会变为 prt_to_heavyweight_monitor, 即是用来保存重量级锁的地址
1. thread-1 通过synchronized获取到一个obj对象
  1.1 将obj对象头中的Mark Word变为prt_to_heavyweight_monitor(30 bit)(monitor指针)
  1.2 在owner中保存obj对象头的hashcode等信息
  1.3 obj对象头的锁状态变为 10(重量级锁)
  1.4 根据monitor指针,将Owner设置为thread-1
   
2. thread-2 过来后,检查obj锁对象头
   2.1 发现该obj对象头的Mark Word的锁状态已经是重量级锁
   2.2 根据Mark Word中锁的地址检查到当Owner已经有其他线程了
   2.3 thread-2进入到EntryList,进行Block
  
3. thread-1 执行完临界区代码后,
      3.1 monitor的Owner进行清空
      3.2 将owner中的当前线程的owner和obj对象头中的monitor地址再次交换
      3.3 monitor唤醒EntryList中其他线程
      3.4 其他在 EntryList 中等待的线程, 再次竞争对象锁,再次设置monitor的Owner

a. synchronized(obj),就会有一个monitor监管该对象
b. 同步代码块如果发生异常时候,也会将锁释放
c. synchronized(obj), 必须关联到同一个obj,不然就不会指向同一个monitor

在这里插入图片描述

三、常见锁

1. 轻量级锁

  • 一个对象虽被多个线程访问,但访问时间错开,不存在竞争
  • 轻量级锁对使用者 是透明的, 语法:syncronized
  • 当存在其他线程竞争的时候,自动升级为重量级锁

2. 锁重入

  • 一个线程在调用一个方法的时候,在调用链中,多次使用同一个对象来加锁
# 加锁
1. 创建锁纪录对象

- 线程在自己的工作内存内,创建栈帧,活动栈帧创建一个  《锁记录》  的结构
- 锁记录对象:是在JVM层面的,对用户无感知
- 锁记录: lock record: 加锁的信息,用来交换对象头的mark word, 00 代表轻量级锁状态
          Object Reference:  锁对象的指针
          
# Object Body: 该锁对象的成员变量

在这里插入图片描述

2.1 cas交换成功(compare and set)

# 01 代表无锁,   00代表轻量级锁
- 将Object reference 指向锁对象的地址
- 尝试cas交换Object中的 Mark Word和栈帧中的锁记录
- Object对象头中,交换后变为00,代表该对象现在是轻量级锁

在这里插入图片描述

2.2 cas失败

- 情况一:锁膨胀,若其他线程持有该obj对象的轻量级锁,表明有竞争,进入锁膨胀过程,加重量级锁
- 情况二:锁重入,若本线程再次synchronized锁,再添加一个Lock Record作为重入计数

- 两种情况区分: 根据obj中保存线程的lock record地址来进行判断


null: 表示重入了几次

在这里插入图片描述

解锁
- 退出synchronized代码块时,若为null的锁记录,表示有重入,这时清除锁记录(null清楚)
- 退出synchronized代码块时,锁记录不为null,cas将Mark Word的值恢复给对象头
  同时obj头变为00无锁状态
- 成功则代表解锁成功; 失败说明轻量级锁进入了锁膨胀

在这里插入图片描述

3. 锁膨胀

  • 在尝试轻量级加锁时,cas无法成功
  • 可能因为:其他线程为此对象加上了轻量级锁(有竞争),这时进行锁膨胀,锁变为重量级锁
  • 轻量级锁没有阻塞机制
加锁
- 当thread-1进行轻量级加锁时,thread-0已经为该对象加了轻量级锁

在这里插入图片描述

进入锁膨胀

- thread-1轻量级加锁失败,进入了锁膨胀流程。要进行阻塞(只有重量级锁才有)

在这里插入图片描述

申请monitor锁

- 为Object对象申请monitor锁,并让Object的mark word 指向重量级锁地址
- 然后自己进入monitor的EntryList 进行 Block

在这里插入图片描述

解锁

- 当Thread-0 退出同步块时,使用cas将Mark Word的值恢复给对象头,失败进入重量级解锁流程
- 按照Monitor地址找到Monitor,设置Owner为null,唤醒EntryList中BLOCKED线程

4. 自旋优化-重量级锁

- 一个线程的锁被其他线程持有时,该线程并不会直接进入阻塞
- 先本身自旋,同时查看锁资源在自旋优化期间是否能够释放   《避免阻塞时候的上下文切换》
- 若当前线程自旋成功(即此时持有锁的线程已经退出了同步块,释放了锁),这时线程就避免了阻塞
- 针对重量级锁而言
- 10: 重量级锁
智能自旋: 
-  自适应的: Java6之后,对象刚刚的一次自旋成功,就认为自旋成功的概率大,就会多自旋几次
   反之,就少自旋几次甚至不自旋
- 自旋会占用cpu时间,单核自旋就是浪费,多核自旋才有意义
- java7之后不能控制是否开启自旋功能
- 不公平的锁

在这里插入图片描述
在这里插入图片描述

5. 偏向锁

- 轻量级锁在没有竞争的时候,发生锁重入的时候,依然需要执行CAS检查,存在性能损耗
- 锁重入时,会产生多条锁记录,作为锁记录

- Java6之后,第一次发生CAS后,将《线程ID》设置到锁对象的Mark Word头上
  而不会将Mark Word和轻量级锁的锁记录进行交换,
- 后续重入,只要发现是该线程的就不会再进行CAS检查
- 只要不发生竞争,这个锁就让该线程一直使用

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 调用锁对象的hashcode,会撤销掉偏向锁
  • hashcode默认是0,第一次调用的时候就会产生对应的hashcode

6. 锁消除

- Java的 JIT, 即时编译器,对热点代码进行优化
- 逃逸分析: JVM  是根据锁对象是否可以发生逃逸分析来判断
- JVM默认开启锁消除机制
- Java中锁消除默认是打开的,会根据代码中锁关联的对象是否能够逃逸决定是否优化
- 关闭锁消除: Java -XX: -EliminateLocks -jar demo.jar

6.1 消除

package com.nike.erick.d01;

public class Demo07 {
    
    public static void main(String[] args) {
        lockMethod();
        nonLockMethod();
    }

    /*虽然此时加了synchronized, 但是代码在执行的时候
     * 1. 并不存在多线程同步访问的场景,所以synchronized 被JIT优化掉了*/
    private static void lockMethod() {
        long startTime = System.currentTimeMillis();
        /*做成包装类,来增加演唱时间*/
        Integer number = 0;
        for (int i = 0; i < 10000000; i++) {
            synchronized (new Object()) {
                number++;
            }
        }
        System.out.println("Lock Method Times: " + (System.currentTimeMillis() - startTime));
    }

    private static void nonLockMethod() {
        long startTime = System.currentTimeMillis();
        Integer number = 0;
        for (int i = 0; i < 10000000; i++) {
            number++;
        }
        System.out.println("Non Lock Method Times: " + (System.currentTimeMillis() - startTime));
    }
}

6.2 逃逸分析

  • 如果锁对象可能逃逸,那么就不会进行锁优化
private static void lockMethod() {
        boolean flag = true;
        Object lock = new Object();
        
        new Thread(() -> {
            synchronized (lock) {
                System.out.println("逃逸代码块");
            }
        }).start();

        long start = System.currentTimeMillis();

        /**
         * 上面锁逃逸,所以并不会进行锁消除
         */
        for (int i = 0; i < 100000000; i++) {
            synchronized (lock) {
                flag = !flag;
            }
        }

        System.out.println("加锁:" + (System.currentTimeMillis() - start));
    }

四、不相干锁

1. 锁粒度细化 – 多把锁

  • 一间屋子两个功能:睡觉,学习,互不影响,
  • 如果用一个屋子(一个对象锁)的话,并发度很低
// 互不影响的功能

package com.dreamer.multithread.day04;

import java.util.concurrent.TimeUnit;

public class Demo02 {
    public static void main(String[] args) throws InterruptedException {

        long start = System.currentTimeMillis();
        BigRoom room = new BigRoom();

        // 睡觉的一类线程
        Thread firstSleep = new Thread(() -> room.sleep());
        Thread secondSleep = new Thread(() -> room.sleep());

        // 工作的一类线程
        Thread firstWork = new Thread(() -> room.work());
        Thread secondWork = new Thread(() -> room.work());

        firstSleep.start();
        secondSleep.start();
        firstWork.start();
        secondWork.start();

        firstSleep.join();
        secondSleep.join();
        firstWork.join();
        secondWork.join();

        // 一共需要 2*2+2*2 = 8s
        System.out.println("total time:" + (System.currentTimeMillis() - start));
    }
}

class BigRoom {

    /*下面两个方法,永远不会在同一个时间调用,因此用同一把锁,浪费资源*/
    public void sleep() {
        synchronized (this) {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void work() {
        synchronized (this) {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 一个对象中,如果不同种类方法只会被同一种线程调用,则可以进行锁粒度细化
  • 如果多把锁,被一个方法同时使用了,可能造成死锁
// 执行上面的方法,只需要4s
class BigRoom {

    private Object sleepLock = new Object();
    private Object workLock = new Object();

    /*下面两个方法,永远不会在同一个时间调用,因此用同一把锁,浪费资源*/
    public void sleep() {
        synchronized (sleepLock) {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void work() {
        synchronized (workLock) {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

2. 锁活跃

2.1 死锁

- 线程一:持有a锁,等待b锁
- 线程二:持有b锁,等待a锁
- 互相等待引发的死锁问题
- 哲学家就餐问题
- 定位死锁: 可以借助jconsole来定位死锁
- 解决方法: 都按照相同顺序加锁就可以,但可能引发饥饿问题
package com.dreamer.multithread.day04;

import java.util.concurrent.TimeUnit;

public class Demo02 {
    public static void main(String[] args) {
        BigRoom room = new BigRoom();
        new Thread(() -> room.sleepAndWork()).start();
        new Thread(() -> room.workAndSleep()).start();
    }
}

class BigRoom {

    private final Object sleepLock = new Object();
    private final Object workLock = new Object();

    // 互相持有对方的锁
    public void sleepAndWork() {
        synchronized (sleepLock) {
            consumeTime();
            synchronized (workLock) {
                System.out.println("睡醒---工作啦");
            }
        }
    }

    public void workAndSleep() {
        synchronized (workLock) {
            consumeTime();
            synchronized (sleepLock) {
                System.out.println("工作后--要睡觉啦¬");
            }
        }
    }

    private void consumeTime() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2.2 饥饿锁

  • 某个线程因为优先级太低,一直得不到cpu的执行

2.3. 活锁

  • 两个线程中互相改变对方结束的条件,导致两个线程一直运行下去
  • 可能会结束,但是二者会交替进行
package com.dreamer.multithread.day04;

public class Demo04 {
    private static int counter = 10;

    public static void main(String[] args) {
        new Thread(() -> {
            while (counter < 20) {
                counter++;
                System.out.println(" ++ 操作:" + counter);
            }

        }).start();

        new Thread(() -> {
            while (counter > 0) {
                counter--;
                System.out.println(" -- 操作:" + counter);
            }
        }).start();
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值