深入学习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

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) {

 }
 }
}
语法
synchronized(对象) // 线程1, 线程2(blocked)
{
 临界区
}
优化, 不加锁,使用面向对象的方式完成原子性的操作

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());
    }
}
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. 线程执行到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就会把所有对象都变为不可偏向的状态

锁消除

在这里插入图片描述

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木 木 水.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值