• 深入学习JUC,深入了解Java线程中的锁,及锁的实现原理,底层的知识又增加了!!!



    在这里插入图片描述

    如何停止一个线程

    1. stop方法, 非常不安全, 不应该使用
      此方法会立即释放此线程拥有的所有的锁, 并且停止run方法中所有正在工作的线程,可能导致操作一些数据还没有完全同步就关闭了停止了,其他线程就会拿到不安全的数据.

    2. 使用interrupt两阶段终止模式停止线程
      其他线程里面interrupt需要停止的线程, 对这个线程打一个中断标记
      **这个线程的run方法里面会由一个判断打断标记是否为true的判断, 如果为真, 不要抛出, 就在判断语句中处理需要执行的善后工作.**

    i++的线程安全问题

    i++的流程, 此时i为静态变量, 才能多线程共享 , i++与i–需要在主存与工作内存中进行数据切换

    static int i;
    i++;
    
    i++的流程
    etstatic i // 获取静态变量i的值
    -----------以下是工作线程-----------
    iconst_1 // 准备常量1
    iadd // 自增
    -----------以上是工作线程,以下是写入主内存-----------
    putstatic i // 将修改后的值存入静态变量i
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    i++为临界区,就是一个代码块包含多线程的读与写操作
    多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

    这四步多线程情况下按照顺序执行没有问题
    但是如果在将工作线程中的数据写入到主内存之前,cpu时间片发送切换,上下文切换, 那么此时读取到的数据就不是实时的数据,之后再进行数据写入,就会操作数据覆盖

    在这里插入图片描述

    在这里插入图片描述

    共享变量线程安全的解决问题

    方法1 :使用synchronized,lock锁的方法阻塞式解决
    synchroniezd可以完成互斥和同步
    互斥就是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
    同步就是由于线程执行的先后顺序不同,需要一个线程待定其他线程运行到某个点
    方法2 :使用原子变量

    synchronized

    基础概念

    synchronized实际上是用**对象锁保证了临界区内代码的原子性。**
    需要锁住的临界区必须是对同一个对象加锁,同时多线程操作临界区时,不能一个线程加锁,一个不加,不然无法实现,临界区内的代码对外是不可分割的,不会被线程切换打断。
    synchronized只能锁对象,如果加在方法上, 锁的就是this对象
    加在静态方法上,锁住的是当前类的对象

    class Test{
     public synchronized void test() {
    
     }
    }
    等价于
    class Test{
     public void test() {
     synchronized(this) {
    
     }
     }
    }
    class Test{
     public synchronized static void test() {
     }
    }
    等价于
    class Test{
     public static void test() {
     synchronized(Test.class) {
    
     }
     }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    语法
    synchronized(对象) // 线程1, 线程2(blocked)
    {
     临界区
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    优化, 不加锁,使用面向对象的方式完成原子性的操作
    
    package org.example.multiThread;
    
    import lombok.extern.slf4j.Slf4j;
    
    class Room {
        int value = 0;
    
        public void increment() {
            synchronized (this) {
                value++;
            }
        }
    
        public void decrement() {
            synchronized (this) {
                value--;
            }
        }
    
        public int get() {
            synchronized (this) {
                return value;
            }
        }
    }
    
    @Slf4j
    public class Test1 {
    
        public static void main(String[] args) throws InterruptedException {
            Room room = new Room();
            Thread t1 = new Thread(() -> {
                for (int j = 0; j < 5000; j++) {
                    room.increment();  //对象的操作是原子性的
                }
            }, "t1");
            Thread t2 = new Thread(() -> {
                for (int j = 0; j < 5000; j++) {
                    room.decrement();
                }
            }, "t2");
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            log.debug("count: {}", room.get());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    java对象头

    一个64位,8个字节的普通对象有32bit,就是4个字节的Mark Word32bit ,4个字节的Klass Word .
    Mark Word包含了很多的信息,包括了hashcode,GC的年龄,加锁的情况等等信息。
    通过Klass World找到对象的Class,就是是一个什么类型的对象。

    数据对象一个96bit, 12个字节,多了32位的数据长度

    一个int的基本类型,占4个字节
    **一个Integer对象,对象头8个字节+int的值4个字节=12个字节**

    在这里插入图片描述

    在这里插入图片描述

    Monitor

    Monitor是操作系统提供的,每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁之后,该对象头的MarkWord就会指向Monitor对象的指针,Monitor里面包含WaitSet, EntryLsit,Owner

    1. 刚开始monitor中的Owner为null。
    2. 当线程1执行synchronized(obj)时,这时候根据这个obj对象找到对应的Monitor,就会将Monitor的Owner置为线程1,一个Monitor只 能有一个Owner.
    3. 当线程1获取锁后,其他线程也进入synchronized(obj),根据obj找到对应的,就会进入EntryList,就是阻塞队列
    4. 线程1执行完同步代码块中的内容后, 唤醒EntryList中等待的线程来竞争锁,竞争是非公平的
    5. WaitSet是之前获得过锁,但是条件不满足,进入了WAITING状态的线程。

    在这里插入图片描述

    优化
    轻量级锁

    线程栈中锁记录作为的轻量级锁。

    轻量级锁是为了优化锁的性能的,虽然一个对象有多线程访问, 但是访问时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化,减少了传统的重量级锁使用操作系统的互斥量产生的性能消耗。

    static final Object obj = new Object();
    public static void method1() {
     synchronized( obj ) {
     // 同步块 A
     method2();
     }
    }
    public static void method2() {
     synchronized( obj ) {
     // 同步块 B
     }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    轻量级锁的流程

    1. 线程执行到synchronized时,先在线程中创建锁记录对象(Lock Record).
      Lock Record有一个两位数的地址和一个对象指针组成。
      对象指针指向锁对象
      两位数的地址用于交换锁对象内的Mark Word

    在这里插入图片描述

    1. 让锁记录中的对象地址指向锁对象, 并且尝试使用cas替换锁对象的Mark Word, 将Mark Word的值存入锁记录。

    正常无锁状态的两位数字为 01,轻量级锁为 00
    cas交换后, 栈帧中的锁记录地址为 01 , 锁对象的Mark Word的锁就为00,表示加上了轻量级锁

    在这里插入图片描述

    1. cas也可能失败
      (1)比如其他线程已经持有了该锁对象的轻量级锁, 表示有了锁的竞争, 进入锁膨胀过程
      (2)==如果自己执行了synchroniezd锁重入,那么再添加一条Lock Record作为重入的计数,==这个新加的Lock Record里面会记录一个null值,这话null值只是一个标记,方便自己锁重入计数

    在这里插入图片描述

    1. 当退出synchroniezd代码块时,就是解锁时,如果有取值为null的锁记录,表示有锁重入,就把重入计数-1
      当解锁时,锁的记录不为null值,使用cas将Mark Word的值恢复给对象头
      此时可能失败,就是锁对象的Mark Word不是00 了,说明锁膨胀了或者升级为重量级锁了,这时就要进入重量级锁的解锁流程
    锁膨胀

    如果尝试加上轻量级锁的过程中, CAS操作无法成功, 这时就是有其他线程为此锁对象加上了轻量级锁,说明存在了竞争了,这时需要进行锁膨胀, 把轻量级锁升级为重量级锁。

    流程

    1. 当线程1尝试对锁对象加锁时,发现锁对象的Mark Word已经为00了,就是轻量级锁,此时进入锁膨胀过程

    在这里插入图片描述

    1. 线程1先为锁对象申请一个Monitor锁, 让锁对象指向Monitor的锁地址
      然后自己进入Monitor的EntryList,成为Blocked状态

    在这里插入图片描述

    1. 当已经获取锁的线程退出同步代码块解锁时,使用cas将Mark Word的值恢复给锁的对象的Mark Word, 失败,这时进入重量级锁的解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList的阻塞线程
    自旋优化

    重量级锁竞争时,还可以使用自旋来进行优化,如果当前线程自旋成功,就是持锁线程退出了synchronized代码块,释放了锁,这时候可以避免线程阻塞,防止因为阻塞带来的上下文切换

    加粗样式
    **

    偏向锁

    轻量级锁在没有竞争时,每次重入仍然需要执行CAS操作。

    所以引入了偏向级锁进行优化。

    只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不需要重新CAS。
    其他线程竞争时,就会撤销轻量级锁

    在这里插入图片描述

    1. 默认是开启偏量级锁的,对象创建后,markword的最后三位为101。此时它的thread, epoch, age 都为0.
    2. 偏量级锁默认是延迟的,不会在程序启动后立即生效,需要等个两三秒生效,如果想要避免延迟,可以加VM参数
      -XX:BiasedLockingStartupDelay=0 来禁止延迟
    3. 如果没有开启偏向锁,那么对象创建后,markword的最后三位值为001,这时他的hashcode, age都为0.第一次用到hashcode是才会赋值, 且偏量级锁会失效.

    以下64位的操作系统

    在这里插入图片描述

    在这里插入图片描述

    线程结束后, 偏量级锁的线程id仍然会保留
    前54位就是操作系统分配的线程ID, 后10位就是对象的信息,thread,epoch,age,锁

    在这里插入图片描述

    偏向锁适合在没有多线程竞争的情况下,多次重入一个锁,优化轻量级锁.
    多个线程会竞争锁的情况下, 这时需要关闭偏向锁已提升性能.
    -XX:-UseBiasedLocking 禁用参数
    UseBiasedLocking前面的 “-” 号会禁用

    在这里插入图片描述

    即使开启了偏量级锁,调用hashcode的时候,也会变成一个normal对象.因为hashcode只有第一次调用才会生成,生成之后就会导致markword的的其他空间被hashcode占用,变为nornaml对象.

    重量级锁的hashcode和线程ID都存在monitor中,不存在markword被占用的情况

    在这里插入图片描述

    偏量级锁的撤销
    1. 调用hashcode方法,hashcode会占用markword的其他空间,导致可偏量变成不可偏向对象,锁也会升级为轻量级锁

    2. 其他线程使用偏量锁对象时,会将偏量锁升级位轻量级锁。
      偏量级锁升级为轻量级锁必须要在线程错开执行的情况下,就是一个线程解锁之后,另一个线程再去获取锁。
      如果在持锁过程中竞争锁, 就会升级为重量级锁。

    在这里插入图片描述

    偏量级锁的批量重定向

    多线程情况下,没有竞争的获取锁,而是错开时间获取锁,一旦偏量级锁被撤销成为不可偏向锁时,就会导致接下来一直使用线程栈中的锁记录作为轻量级锁,性能低下。

    当偏向锁的撤销超过20次之后, 就可以重新把不可偏向的锁以当前线程id重定向为偏量级锁,而且使当前线程接下来的所有锁都进行重定向。

    偏量级锁的批量撤销

    当偏向锁的撤销超过40次之后,JVM就会把所有对象都变为不可偏向的状态

    锁消除

    在这里插入图片描述

  • 相关阅读:
    GEE土地分类——Property ‘B1‘ of feature ‘LE07_066018_20220603‘ is missing.错误
    面试:“索引背后的数据结构是什么样的?”,五分钟带你了解“B树,B+树”
    MySQL主从复制与读写分离
    Qt QSS中 background-image,border-image,以及image属性差别
    关联线探究,如何连接流程图的两个节点
    Python 全栈安全(一)
    外贸干货/与非洲客户打交道要知道的几点
    Spark报错NoSuchMethodError或ClassNotFoundException
    JS前端实现身份证号码合法性校验(校验码校验)
    7-42 子集和问题——组合子集
  • 原文地址:https://blog.csdn.net/tyuiop321/article/details/133186977