【Java核心知识】JUC包相关知识

news2024/11/28 14:34:05

文章目录

  • JUC包主要内容
  • Java内置锁
    • 为什么会有线程安全问题
    • Synchronize锁
    • Java对象结构
    • Synchronize锁优化
    • 线程间通信
    • Synchronize与wait原理
  • CAS和JUC原子类
    • CAS原理
    • `JUC`原子类
    • `ABA`问题
  • 可见性和有序性
    • 为什么会有可见性
    • 参考链接
  • 显式锁
    • Lock接口常用方法
    • 显式锁分类
    • 显式锁实现原理
    • 参考链接

JUC包主要内容

JUC包是与并发编程相关的包,主要包含四部分原子类并发集合多线程,如下图所示。

其中,

  • 锁可以分为内置锁和显式锁;
  • 原子类主要是一些通过CAS实现的原子类;
  • 并发集合主要就是一些线程安全的集合,比如ConcurrentHashMap,BlockQueue等;
  • 多线程包括callable接口和线程池等;

在这里插入图片描述

Java内置锁

为什么会有线程安全问题

i++线程不安全的原因在于自增操作不是原子性的,可以分为三步:内存取值寄存器加1存值到内存

除了原子性之外,可见性有序性也会导致线程安全问题。可见性是指线程B并不一定能够及时看到线程A对变量的修改。

Synchronize锁

Synchronize关键字可以作用在方法上,也可以作用于代码块上,本质上都是锁住了某个对象,但synchronize作用于方法上是一种粗粒度的锁,会导致其他线程也不能访问该对象的其他方法。

JVM的堆中,每个对象都会有一个对象监视器,synchronize就是锁住了这个对象监视器,从而保证了原子性。

那么如何保证可见性呢?线程加锁时,必须清空工作内存中共享变量的值,从而使用共享变量时需要从主内存重新读取;线程在解锁时,需要把工作内存中最新的共享变量的值写入到主存,以此来保证共享变量的可见性。(这里是个泛指,不是说只有在退出synchronized时才同步变量到主存)

Java对象结构

Java的对象都放在JVM的堆中,每个对象的结构包括:

  • 对象头:

    • Mark Word:记录哈希码,GC标志位、锁状态等信息。不同锁状态下Mark Word是不同的,但最后两位都代表了锁状态。

    • 类对象指针:指向方法区的该类相关信息

    • 数组长度:如果对象是数组才有此结构

  • 对象体:包含对象的实例变量,包含父类的实例变量

  • 对齐字节:为了保证8字节的对齐而填充的数据

Synchronize锁优化

为了优化Synchronize锁的性能,Java提出了逐步升级的四种锁:无锁->偏向锁->轻量级锁->重量级锁。

  • 无锁:
  • 偏向锁:Mark Word中存储持有锁的线程ID,当有线程执行时,先判断对象头的线程ID是否与此线程ID相等,如果相等,直接向下执行;如果不相等,说明存在竞争,锁升级为轻量级锁。
  • 轻量级锁:对象头存储持有锁的线程ID,将对象头原来的哈希码放入线程栈帧中的锁记录中。当别的线程竞争锁时,不会立即阻塞,切换用户态,而是会自旋,然后使用CAS尝试获取锁,降低了阻塞线程的消耗。自旋等待时间和上一个竞争线程等待结果有关:如果上一个竞争线程自旋成功了,那么这次自旋的次数会更多;如果上一个竞争线程自旋失败了,那么这次自旋的次数会减少。自旋不会一直持续下去,如果超过了指定时间,会膨胀为重量级锁!
  • 重量级锁:重量级锁对象头会指向一个监视器对象(每个对象都有一个监视器对象),该监视器通过三个队列(竞争队列、阻塞队列、等待时间片的就绪队列)来登记和管理排队的线程,会涉及到线程的阻塞,切换用户态。

轻量级锁执行过程:

  • 1、判断对象是否加锁,如果没加锁,进行以下操作

  • 2、在自己的栈帧中创建锁记录,用来存放加锁对象的哈希码

  • 3、创建好锁记录后,通过CAS自旋操作,尝试将锁对象头的锁记录指针替换成栈帧中锁记录的地址

  • 4、替换栈帧后会返回锁对象的哈希码,然后填入栈帧的锁记录中

线程间通信

可以使用Objectwait(),notify()方法来进行线程间的通信。

wait()方法的原理

1)当线程调用了lock(某个同步锁对象)的wait()方法后,JVM会将当前线程加入lock监视器的WaitSet(等待集),等待被其他线程唤醒。
2)当前线程会释放lock对象监视器的Owner权利,让其他线程可以抢夺lock对象的监视器。
3)让当前线程等待,其状态变成WAITING

notify()方法的原理

1)当线程调用了lock(某个同步锁对象)的notify()方法后,JVM会唤醒lock监视器WaitSet中的第一个等待线程。
2)当线程调用了locknotifyAll()方法后,JVM会唤醒lock监视器WaitSet中的所有等待线程。
3)等待线程被唤醒后,会从监视器的WaitSet移动到EntryList,线程具备了排队抢夺监视器Owner权利的资格,其状态从WAITING变成BLOCKED
4)EntryList中的线程抢夺到监视器Owner权利之后,线程的状态从BLOCKED变成Runnable,具备重新执行的资格。

缓冲队列

/**
 * 生产者消费者队列
 */
//数据缓冲区,类定义
public class DataBuffer<T> {
    public static final int MAX_AMOUNT = 10; //数据缓冲区最大长度
    //保存数据
    private List<T> dataList = new LinkedList<>();
    //数据缓冲区长度
    private Integer amount = 0;
    // 用来保证只有一个线程存元素或者取元素
    private final Object LOCK_OBJECT = new Object();
    // 当队列满了后,用于阻塞生产者
    private final Object NOT_FULL = new Object();
    // 当队列为空时,用于阻塞消费者
    private final Object NOT_EMPTY = new Object();
    // 向数据区增加一个元素
    public void add(T element) throws Exception
    {
        // 队列已满,不能存元素
        while (amount > MAX_AMOUNT)
        {
            synchronized (NOT_FULL)
            {
                System.out.println("队列已经满了!");
                // 等待未满通知,这里为什么需要wait,是因为需要等待一个条件满足,而不能只用synchronize,某一时刻只有一个线程拥有NOT_FULL是不行的
                NOT_FULL.wait();
            }
        }
        // 保证原子性
        synchronized (LOCK_OBJECT)
        {
            dataList.add(element);
            amount++;
            System.out.println(Thread.currentThread().getName() + "生产了一条消息" + amount);
        }
        synchronized (NOT_EMPTY)
        {
            //发送未空通知
            NOT_EMPTY.notify();
        }
    }
    /**
     * 从数据区取出一个商品
     */
    public T fetch() throws Exception
    {
        // 数量为零,不能取元素
        while (amount <= 0)
        {
            synchronized (NOT_EMPTY)
            {
                System.out.println(Thread.currentThread().getName() + "队列已经空了!");
                //等待未空通知
                NOT_EMPTY.wait();
            }
        }
        T element = null;
        // 保证原子性
        synchronized (LOCK_OBJECT)
        {
            element = dataList.remove(0);
            amount--;
            System.out.println(Thread.currentThread().getName() + "消费了一条消息" + amount);
        }
        synchronized (NOT_FULL)
        {
            //发送未满通知
            NOT_FULL.notify();
        }
        return element;
    }
}

生产者和消费者

@Test
public void testProducerConsumerQueue() throws InterruptedException {
    //共享数据区,实例对象
    DataBuffer<String> dataBuffer = new DataBuffer<>();
    // 同时并发执行的线程数
    final int THREAD_TOTAL = 20;
    //线程池,用于多线程模拟测试
    ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_TOTAL);
    //假定共11条线程,其中有10个消费者,但是只有1个生产者
    final int CONSUMER_TOTAL = 10;
    final int PRODUCE_TOTAL = 1;
    for (int i = 0; i < PRODUCE_TOTAL; i++) {
        //生产者线程每生产一个商品,间隔50毫秒
        threadPool.submit(() -> {
            for (int j = 0; j < 10; j ++) {
                //首先生成一个随机的商品
                String s = "商品";
                //将商品加上共享数据区
                try {
                    dataBuffer.add(s);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }
    for (int i = 0; i < CONSUMER_TOTAL; i++)
    {
        //消费者线程每消费一个商品,间隔100毫秒
        threadPool.submit(() -> {
            for (int j = 0; j < 2; j ++) {
                // 从PetStore获取商品
                String s = null;
                try {
                    s = dataBuffer.fetch();
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }
    Thread.sleep(10000);
}

Synchronize与wait原理

Synchronizewait都会将线程加入到等待队列中,但是两者加入的等待队列并不是同一个,Synchronize加入的是对象监视器的等待队列,当退出Synchronize代码块后会自动唤醒线程,而waitObject的方法,加入的是另一个等待集合,只能通过notify()notifyAll()唤醒。

CAS和JUC原子类

CAS原理

CAS(Compare And Swap),是比较交换的缩写,可以用来实现乐观锁。乐观锁本质上是无锁的,每次更新前都把原来的旧值和要更新的新值一块传入,如果发现传入的旧值和当前内存上的旧值一样,则更新成功;否则更新失败;

乐观锁就是一直调用CAS操作,不断获取旧值,计算新值,然后传入旧值和新值进行更新,线程一直在自旋,直到更新成功为止。

示例

public class CompareAndSwap {
    public volatile int value; //值
    //不安全类
    // private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final Unsafe unsafe = getUnsafe();
    //value 的内存偏移(相对与对象头部的偏移,不是绝对偏移)
    private static final long valueOffset;
    //统计失败的次数
    public static final AtomicLong failure = new AtomicLong(0);
    static
    {
        try
        {
            //取得value属性的内存偏移
            valueOffset = unsafe.objectFieldOffset(CompareAndSwap.class.getDeclaredField("value"));
            System.out.println("valueOffset:=" + valueOffset);
        } catch (Exception ex) {
            throw new Error(ex);
        }
    }
    //通过CAS原子操作,进行“比较并交换”
    public final boolean unSafeCompareAndSet(int oldValue, int newValue)
    {
        //原子操作:使用unsafe的“比较并交换”方法进行value属性的交换
        return unsafe.compareAndSwapInt( this, valueOffset, oldValue, newValue );
    }
    //使用无锁编程实现安全的自增方法
    public void selfPlus()
    {
        int oldValue = value;
        //通过CAS原子操作,如果操作失败就自旋,直到操作成功
        for(;;) {
            oldValue = value;
            failure.incrementAndGet();
            if (unSafeCompareAndSet(oldValue, oldValue + 1)) return;
        }
        // do
        // {
        //     // 获取旧值
        //     oldValue = value;
        //     //统计无效的自旋次数
        //     //记录失败的次数
        //     failure.incrementAndGet();
        // } while (!unSafeCompareAndSet(oldValue, oldValue + 1));
    }

    /**
     * 通过反射获取Unsafe
     * @return
     */
    public static Unsafe getUnsafe()
    {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            return (Unsafe) theUnsafe.get(null);
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    }
}
/**
 * 测试CAS操作
 * @throws InterruptedException
 */
@Test
public void testCAS() throws InterruptedException {
    final CompareAndSwap compareAndSwap = new CompareAndSwap();
    AtomicInteger res = new AtomicInteger(0);
    //倒数闩,需要倒数THREAD_COUNT次
    CountDownLatch latch = new CountDownLatch(10);
    for (int i = 0; i < 10; i++)
    {
        // 提交10个任务
            Executors.newCachedThreadPool().submit(() ->
            {
                //每个任务累加1000次
                for (int j = 0; j < 1000; j++)
                {
                    compareAndSwap.selfPlus();
                    res.getAndIncrement();
                }
                latch.countDown();// 执行完一个任务,倒数闩减少一次
            });
    }
    latch.await();// 主线程等待倒数闩倒数完毕
    System.out.println(res);
    System.out.println("累加之和:" + compareAndSwap.value);
    System.out.println("失败次数:" + CompareAndSwap.failure.get());
}

JUC原子类

JUC包下的原子类可以分为四组:

  • 基本原子类:AtomicInteger,整型;AtomicLong,大整数;AtomicBoolean:布尔型;
  • 数组原子类:AtomicIntegerArray:整型数组原子类;AtomicLongArray:长整型数组原子类;AtomicReferenceArray:引用类型数组原子类。
  • 引用原子类:AtomicReference:引用类型原子类;AtomicMarkableReference:带有更新标记位的原子引用类型;AtomicStampedReference:带有更新版本号的原子引用类型。
  • 字段更新原子类:AtomicIntegerFieldUpdater:原子更新整型字段的更新器;AtomicLongFieldUpdater:原子更新长整型字段的更新器;AtomicReferenceFieldUpdater:原子更新引用类型里的字段。

JUC原子类下的底层实现也是通过不断CAS自旋+volatile(实现可见性)实现的,可以从源码看到。

ABA问题

使用CAS自旋更新虽然没有加锁,降低了线程切换成本,但是容易产生ABA问题。即线程1将值从A到B又到A,此时线程2被唤醒,以为变量没有改变过,从而引起错误的判断。解决办法是添加时间戳,可以借助AtomicStampedReference原子类实现。

可见性和有序性

为什么会有可见性

现代处理器都是多核的,每个核都会有自己独有的高速缓存L1,L2,L3,这些核又共享一个主内存,每次涉及变量更新或读取时,CPU都是先从高级缓存中读取并进行修改,然后随机写入到主存。这样就产生了问题,如果核1对公有变量A进行了修改,但是还没来得及写入主存,那么核2从主存中读取到的值就是未及时更新的脏值。

一般操作系统会使用Lock指令在总线上进行广播,哪些变量的高速缓存已失效,必须从主存中重新读取。Java的volatile关键字会在字节码上加入loadloadloadstorestorestorestoreload内存屏障来保证更改后的变量立即写入主存,且告知其他核的高速缓存该值已失效,必须从主内存重新读取。

volatile并不保证原子性,因为虽然volatile会强制将修改刷回主存,但是修改并刷回主存的指令不是原子性的,可能有中断的可能。比如线程A修改完变量后,准备刷回主存,这时发生了线程调度,线程B知道自己的数据失效了,但是从主存中重新获取的数据不一定是最新的,因为线程A只是在本地修改了数据,但还没有写入主存。

参考链接

内存屏障与JVM指令

如果你知道这灵魂拷问的6连击,面试volitile时就稳了

显式锁

Lock接口常用方法

所有的锁实现类都会实现Lock接口,该接口主要有以下几个方法:

  • lock():阻塞获取锁,如果当前线程不能抢到锁,线程会加入阻塞队列进行等待,直到获取到锁;

  • tryLock(): 非阻塞抢锁,如果当前线程抢不到锁,线程会立刻返回false

  • tryLock(long time, TimeUnit unit): 超时返回,如果当前线程在一段时间内抢不到锁,则会返回false

  • unlock(): 释放锁;

下面是一个使用ReentrantLock的示例,使用三个线程同时对某一个执行加一操作,每个线程操作100次,累计300次。

public class LockTest {
    private int count;

    @Test
    public void testReentrantLock() throws InterruptedException {
        Lock lock = new ReentrantLock();

        ExecutorService executorService = Executors.newCachedThreadPool();

        CountDownLatch countDownLatch = new CountDownLatch(3);

        for (int i = 0; i < 3; i ++) {
            executorService.execute(() -> {
                for (int j = 0; j < 100; j ++) {
                    // 获取锁
                    lock.lock();
                    try {
                        count ++;
                    } finally {
                        // 释放锁
                        lock.unlock();
                    }
                }
                // 每完成一个线程,就更新countDownLatch
                countDownLatch.countDown();
            });
        }

        countDownLatch.await();

        System.out.println(count); 
    }
}

显式锁分类

显式锁的分类有很多种,大致上可以分为下面这些:

在这里插入图片描述

显式锁实现原理

JUC包下的显式锁都是基于AQS实现的,AQS使用一个队列保存想要获取锁的线程,同时在队头使用CAS竞争获取锁,不会阻塞线程,是一种乐观锁

当有新线程加入时,会通过CAS加入队尾,然后监控队列前一个元素的状态,这时不会发生CAS,但线程也不会阻塞,而是会调用yield()主动让出时间片。

参考链接

Java中常见的各种锁

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/968144.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据结构(Java实现)-排序

排序的概念 排序&#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的排列起来的操稳定性&#xff1a;假定在待排序的记录序列中&#xff0c;存在多个具有相同的关键字的记录&#xff0c;若经过排序&#xff…

chatgpt谈论日本排放污水事件

W...Y的主页 &#x1f60a; 代码仓库分享 &#x1f495; 近日&#xff0c;世界发生了让人义愤填膺的时间——日本排放核污水。这件事情是那么的突然且不计后果&#xff0c;海洋是我们全人类共同的财产&#xff0c;而日本却想用自己一己私欲将全人类的安全置之度外&#xff0c…

攻防世界-Caesar

原题 解题思路 没出现什么特殊字符&#xff0c;可能是个移位密码。凯撒密码加密解密。偏移12位就行。

Spring Cloud--从零开始搭建微服务基础环境【三】

&#x1f600;前言 本篇博文是关于Spring Cloud–从零开始搭建微服务基础环境【三】&#xff0c;希望你能够喜欢 &#x1f3e0;个人主页&#xff1a;晨犀主页 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是晨犀&#xff0c;希望我的文章可以帮助到大家&#xff0c;…

全网都在用的nnUNet V2版本改进了啥,怎么安装?(一)

nnUNet&#xff0c;这个医学领域的分割巨无霸!在论文和比赛中随处可见他的身影。大家对于nnUNet v1版本的教程都赞不绝口&#xff0c;因为它简单易懂、详细全面&#xff0c;让很多朋友都轻松掌握了使用方法。 最近&#xff0c;我也抽出时间仔细研究了nnUNet v2&#xff0c;并全…

vue声明周期

1.在created中发送数据 async created(){ const resawait axios.get("url) this.listres.data.data } 2.在mounted中获取焦点 mounted(){ document.querySelector(#inp).focus()

分类预测 | MATLAB实现GRNN广义回归神经网络多特征分类预测

分类预测 | MATLAB实现GRNN广义回归神经网络多特征分类预测 目录 分类预测 | MATLAB实现GRNN广义回归神经网络多特征分类预测分类效果基本介绍模型描述预测过程程序设计参考资料分类效果 基本介绍 MATLAB实现GRNN广义回归神经网络多特

企业架构LNMP学习笔记9

nginx配置文件定义php-fpm服务&#xff1a; 编写测试文件&#xff1a; vim /usr/local/nginx/html/index.php 内容&#xff1a; <?phpphpinfo(); 在nginx的配置文件中配置&#xff1a; 修改配置文件&#xff0c;告知nginx如果收到.php结尾的请求&#xff0c;交由给php-…

「MySQL-02」数据库的操纵、备份、还原和编码规则

目录 一、库操作 1. 创建数据库 2. 查看所有数据库 3. 删除数据库 4. 修改数据库 5. 进入一个数据库 二、查看和设置数据库的编码规则 1. MySQL的两个编码规则&#xff1a;字符集和校验规则 2. 查看MySQL当前使用的字符集以及校验规则 3. 查看MySQL支持的所有字符集 4. 查看MyS…

nnUNet v2数据准备及格式转换 (二)

如果你曾经使用过nnUNet V1&#xff0c;那你一定明白数据集的命名是有严格要求的&#xff0c;必须按照特定的格式来进行命名才能正常使用。 这一节的学习需要有数据&#xff0c;如果你有自己的数据&#xff0c;可以拿自己的数据来实验&#xff0c;如果没有&#xff0c;可以用十…

JVM类的加载过程

加载过程 JVM的类的加载过程分为五个阶段&#xff1a;加载、验证、准备、解析、初始化。 加载   加载阶段就是将编译好的的class文件通过字节流的方式从硬盘或者通过网络加载到JVM虚拟机当中来。&#xff08;我们平时在Idea中书写的代码就是放在磁盘中的&#xff0c;也可以通…

Kubernetes可视化管理工具Kuboard部署使用及k8s常用命令梳理记录

温故知新 &#x1f4da;第一章 前言&#x1f4d7;背景&#x1f4d7;目的&#x1f4d7;总体方向 &#x1f4da;第二章 安装 Kubernetes 多集群管理工具 - Kuboard v3&#x1f4d7;部署方式&#x1f4d7;通过Kuboard v3 - Kubernetes安装&#xff08;在master节点执行)&#x1f4…

大学生攻略:正确的购买和使用你的电脑

笔者是计算机专业在读大学生&#xff0c;从小学开始接触电脑&#xff0c;进行过各种操作(更换硬件维修&#xff0c;换系统&#xff0c;系统命令行&#xff0c;管理员权限&#xff0c;无视风险继续安装&#xff0c;没有这条 )&#xff0c;相对大学生有一定参考价值。 购买 1.买…

【Java并发】聊聊AQS原理机制

什么是AQS AbstractQueuedSynchronizer是一个抽象队列同步器&#xff0c;主要是实现并发工具类的基石。 是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石&#xff0c; 通过内置的FIFO队列来完成资源获取线程的排队工作&#xff0c;并通过一个int类变量表示…

仿京东 项目笔记1

目录 项目代码1. 项目配置2. 前端Vue核心3. 组件的显示与隐藏用v-if和v-show4. 路由传参4.1 路由跳转有几种方式&#xff1f;4.2 路由传参&#xff0c;参数有几种写法&#xff1f;4.3 路由传参相关面试题4.3.1 路由传递参数&#xff08;对象写法&#xff09;path是否可以结合pa…

MyBatis-Plus —— 初窥门径

前言 在前面的文章中荔枝梳理了MyBatis及相关的操作&#xff0c;作为MyBatis的增强工具&#xff0c;MyBatis-Plus无需再在xml中写sql语句&#xff0c;在这篇文章中荔枝将梳理MyBatis-Plus的基础知识并基于SpringBoot梳理MyBatis-Plus给出的两个接口&#xff1a;BaseMapper和ISe…

【微服务部署】三、Jenkins+Maven插件Jib一键打包部署SpringBoot应用Docker镜像步骤详解

前面我们介绍了K8SDockerMaven插件打包部署SpringCloud微服务项目&#xff0c;在实际应用过程中&#xff0c;很多项目没有用到K8S和微服务&#xff0c;但是用到了Docker和SpringBoot&#xff0c;所以&#xff0c;我们这边介绍&#xff0c;如果使用Jenkinsjib-maven-plugin插件打…

第 3 章 栈和队列 (循环队列)

1. 背景说明 和顺序栈相类似&#xff0c;在队列的顺序存储结构中&#xff0c;除了用一组地址连续的存储单元依次存放从队列头到队列尾的元素之外&#xff0c; 尚需附设两个指针 front 和 rear 分别指示队列头元素及队列尾元素的位置。约定&#xff1a;初始化建空队列时&#x…

测试人:“躺平?不可能的“, 盘点测试人在职场的优势

之前有这么一个段子&#xff1a;有人喜欢创造世界&#xff0c;他们做了程序员&#xff1b;有人喜欢拯救世界&#xff0c;他们做了测试员&#xff01;近几年&#xff0c;测试工程师在企业究竟是怎么样的发展&#xff1f;随着企业对于用户体验的满意度越来越重视&#xff0c;更加…

vue 从后端取图片返回发现是两张,但自己只要一张图片 怎么操作

1.用JavaScript里边常用的分隔符去操作 item.original_status.user.profile_image_url.split(,)[0] <van-imagewidth"100"height"100":src"item.original_status.user.photo_domain item.original_status.user.profile_image_url.split(,)[0]&quo…