《Java-SE-第二十九章》之Synchronized原理与JUC常用类

前言

在你立足处深挖下去,就会有泉水涌出!别管蒙昧者们叫嚷:“下边永远是地狱!”

博客主页:KC老衲爱尼姑的博客主页

博主的github,平常所写代码皆在于此

共勉:talk is cheap, show me the code

作者是爪哇岛的新手,水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!


文章目录

  • Synchronized原理
    • 偏向锁
    • 自旋锁
    • 重量级锁
    • 其他的优化操作
      • 锁消除
      • 锁粗化
  • Callable接口
    • Callable的用法
  • **JUC(java.util.concurrent)** **的常见类**
    • **ReentrantLock**
    • **信号量** **Semaphore**
    • CountDownLatch
  • 线程安全的集合类
    • 多线程环境使用ArrayList
    • **多线程环境使用哈希表**

Synchronized原理

Synchronized即是轻量级锁又是重量级锁,它会根据实际情况自适应加锁。
在这里插入图片描述

偏向锁

(1)第一次加锁的时候线程,会进入偏向锁 的状态,偏向锁并不是真的加锁,只是给对象头做了一个偏向锁的标记,记录该锁属于哪个线程,如果后续没有其他的线程加锁,就可以不进行加锁操作。如果后续有其他的线程来竞争该锁,那么刚才的锁对象已经记录了当前时锁属于那个线程,很容易知道当前的线程是不是之前记录的线程,那么就取消偏向锁的状态,进入一般的轻量级锁状态,偏向锁是本质是延迟加锁,能不加锁就不加锁,尽量来避免不必要的加锁开销。但是该做的标志还的做,否则无法区分什么时候需要真正加锁。

自旋锁

(2)当其他的线程进入竞争的时候,偏向锁状态消除会进行轻量级锁,也就是自旋锁。

此处的轻量级锁是通过的CAS实现的,具体操作如下

  1. 通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)
  2. 如果更新成功, 则认为加锁成功
  3. 如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU).

自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源.。因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了. 也就是所谓的 “自适应”。

重量级锁

(3)重量级锁

如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁

此处的重量级锁就是指用到内核提供的 mutex .,具体操作如下

  1. 执行加锁操作, 先进入内核态.
  2. 在内核态判定当前锁是否已经被占用
  3. 如果该锁没有占用, 则加锁成功, 并切换回用户态.
  4. 如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒.
  5. 经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒这个线程, 尝试重新获取锁.

其他的优化操作

锁消除

JVM自动判定,发现这个地方的代码,不必加锁,如果你写了Synchronized,就会自动的把锁去掉。比如,只有一个线程,或者多个线程不涉及修改同一个变量,如果代码中写Synchronized,此时Synchronized加锁操作,就会被JVM给干掉。Synchronized加锁是先偏向锁的,只是改 了个标记位,按理说这个操作开销也不大?即是如此,能消除的时候,也不是连这一点开销都不想承担。锁消除也是一种编译器优化的行为,编译器的判定,不一定非常准,因此,如果代码的锁百分之100能消除,就给你消除了。如果这个代码的锁,判断的准,就还是不消除了,锁消除只是在编译器/JVM有十足的把握的时候才进行。

示例代码

StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");

此时每个 append 的调用都会涉及加锁和解锁. 但如果只是在单线程中执行这个代码, 那么这些加锁解锁操作是没有必要的, 白白浪费了一些资源开销。

锁粗化

锁的粒度,Synchronized对应的代码块中包含多少代码,包含的代码少,粒度细,包含的代码多,粒度粗,锁粗化,就是把细粒度的加锁->粗粒度的加锁。粗的前提是保证代码的逻辑不变,细化的时候代码是正确的,粗化之后还是正确的。

举个栗子理解锁粗化,张三给下交代任务,方式一:张三给下属打电话,交代任务1,挂断电话,再打电话,交代任务2,挂断电话,再打电话,交代任务三,方式二:张三大电话,一次性交代了三个任务,再挂断电话。这就是一个锁细化–>锁粗化的过程。

Callable接口

由于Runnable不提供返回值,而时候需要得到返回值,此时就可以使用Callable。

Callable的用法

Callable 是一个 interface ,描述了一个带返回值的任务,相当于把线程封装了一个 “返回值”. 方便程序猿借助多线程的方式计算结果.

代码示例

创建线程计算 1 + 2 + 3 + … + 1000, 不使用 Callable 版本

public class Demo {
    static class Result{
        public int sum  =0;
        private Object lock = new Object();
    }

    public static void main(String[] args) throws InterruptedException {
        Result result = new Result();
        Thread t = new Thread(() -> {
            int sum = 0;
            for (int i = 0; i <=1000;i++) {
                sum+=i;
            }
            synchronized (result.lock) {
                result.sum=sum;
                result.lock.notify();
            }
        });
        t.start();
        synchronized (result.lock) {
            while (result.sum==0) {
                result.lock.wait();
            }
            System.out.println(result.sum);
        }

    }
}

运行结果:
在这里插入图片描述

上述代码需要借助一个辅助类,还需要使用到一系列的加锁和wait/notify,相对而言代码是比较复杂的。

代码示例:创建线程计算 1 + 2 + 3 + … + 1000, 使用 Callable 版本

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = new Callable<Integer>(){
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i=1;i<=1000;i++) {
                    sum+=i;
                }
                return sum;
            }
        };
        FutureTask<Integer> futureTask = new FutureTask<Integer>(callable);
        Thread t = new Thread(futureTask);
        t.start();
        int result = futureTask.get();
        System.out.println(result);
    }
}

运行结果:

在这里插入图片描述

Callable 通常需要搭配 FutureTask 来使用,FutureTask 用来保存 Callable 的返回结果. 因为allable 往往是在另一个线程中执行的, 啥时候执行完并不确定,FutureTask 就可以负责这个等待结果出来的工作。

JUC(java.util.concurrent) 的常见类

ReentrantLock

ReentrantLock是可重入锁和synchronized类似都是实现互斥效果,保证线程安全。

ReentrantLock 的基础使用

  • lock(): 加锁, 如果获取不到锁就死等.
  • trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
  • unlock(): 解锁
ReentrantLock lock = new ReentrantLock(); 
-----------------------------------------
lock.lock();   
try {    
 // working    
} finally {    
 lock.unlock()    
}  

示例代码


import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo3 {
    private static Lock lock = new ReentrantLock();
    private static Condition waitCigaretteQueue = lock.newCondition();
    private static Condition waitbreakfastQueue = lock.newCondition();
    private static volatile boolean hasCigrette = false;
    private static volatile boolean hasBreakFast = false;
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            lock.lock();
            try {
                while (!hasCigrette) {
                    try {
                        waitCigaretteQueue.await();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println("等到了它的烟");
                }

            }finally {
                lock.unlock();
            }
        }).start();

        new Thread(() -> {
            lock.lock();
            try {
                while (!hasBreakFast) {
                    try {
                        waitbreakfastQueue.await();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println("等到了它的早餐");
                }

            }finally {
                lock.unlock();
            }
        }).start();
        Thread.sleep(1000);
        sendBreakFast();
        Thread.sleep(1000);
        sendCigarette();
    }

    private static void sendCigarette() {
        lock.lock();
        try {
            System.out.println("送烟来了");
            hasCigrette = true;
            waitCigaretteQueue.signal();
        }finally {
            lock.unlock();
        }
    }

    private static void sendBreakFast() {
        lock.lock();
        try {
            System.out.println("送早餐来了");
            hasBreakFast = true;
            waitbreakfastQueue.signal();
        }finally {
            lock.unlock();
        }
        
    }
}

运行结果:
在这里插入图片描述

ReentrantLock 和 synchronized 的区别:

  1. synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准库的一个类, 在 JVM 外实现的(基于 Java 实现).

  2. synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock.

  3. synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.

  4. synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式.

  5. 更强大的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程. ReentrantLock搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.

如何选择使用哪个锁?

  • 锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便.
  • 锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等.
  • 如果需要使用公平锁, 使用 ReentrantLock.

信号量 Semaphore

信号量, 用来表示 “可用资源的个数”. 本质上就是一个计数器。举个栗子,可以把信号量想象成是停车场的展示牌: 当前有车位 100 个. 表示有 100 个可用资源。当有车开进去的时候, 就相当于申请一个可用资源, 可用车位就 -1 (这个称为信号量的 P 操作),当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作),如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.。

Semaphore 的 PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用.

代码示例


import java.util.concurrent.Semaphore;

public class SemaphoreDemo {
    //可用资源设置为1
    private static Semaphore semaphore = new Semaphore(1);

    public static void main(String[] args) {
        Runnable runnable = () -> {
            try {
                System.out.println("申请资源");
                semaphore.acquire();
                System.out.println("我获取到资源");
                Thread.sleep(1000);
                System.out.println("我释放资源了");
                semaphore.release();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        };
        for (int i = 0; i <2;i++) {
            Thread t = new Thread(runnable);
            t.start();
        }
    }
}

运行结果:
在这里插入图片描述

CountDownLatch

同时等待 N 个任务执行结束。举个栗子,号称地表最强的下载器IDM,下载文件的时候,会将一个文件分配给多个线程下载,只有当所有的线程下载好了,才是整个文件下载好。

代码示例

假设有十名运动员参加跑步比赛,当所有的运功员通过终点的时候,比赛才结束。


import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {

    public static void main(String[] args) throws InterruptedException {
        //构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成
        CountDownLatch latch = new CountDownLatch(10);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"已经到了");
                latch.countDown();
            }
        };
        for (int i = 0; i <10;i++) {
            new Thread(runnable).start();
        }
        latch.await();
        System.out.println("比赛结束");
    }
}

运行结果:
在这里插入图片描述

线程安全的集合类

多线程环境使用ArrayList

(1)自己使用同步机制 (synchronized 或者 ReentrantLock)

(2)Collections.synchronizedList(new ArrayList);

synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List,synchronizedList 的关键操作上都带有 synchronized。

(3)使用 CopyOnWriteArrayList

CopyOnWrite容器即写时复制的容器。所谓的写时拷贝,就是当我们往一个容器中添加元素的时候,不直接往当前容器添加,而是先将当前的容器进行copy复制出一个新的容器,然后新的容器里添加元素。添加完元素之后,再将 原来容器的引用指向新容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

多线程环境使用哈希表

HashMap 本身不是线程安全的,在多线程环境下使用哈希表可以使用:Hashtable和ConcurrentHashMap

(1)Hashtable

Hashtable只是简单的在一些关键的方法如get/put上加了synchronized。
在这里插入图片描述

在这里插入图片描述

这相当于直接针对 Hashtable 对象本身加锁.

  • 如果多线程访问同一个 Hashtable 就会直接造成锁冲突.
  • size 属性也是通过 synchronized 来控制同步, 也是比较慢的.
  • 一旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到大量的元素拷贝, 效率会非常低.

在这里插入图片描述

一个Hashtable只有一把锁,两个线程访问的Hashtable中的任意数据都会出现锁竞争。

(2) ConcurrentHashMap

相比于 Hashtable 做出了一系列的改进和优化. 以 Java1.8 为例

  • 读操作没有加锁(但是使用了 volatile 保证从内存读取结果), 只对写操作进行加锁. 加锁的方式仍然是用 synchronized, 但是不是锁整个对象, 而是 “锁桶” (用每个链表的头结点作为锁对象), 大大降低了锁冲突的概率.
  • 充分利用 CAS 特性. 比如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况.
  • 优化了扩容方式: 化整为零 , 发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去.,扩容期间, 新老数组同时存在.,后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小部分元素.,搬完最后一个元素再把老数组删掉. 这个期间, 插入只往新数组加,这个期间, 查找需要同时查新数组和老数组。如果是要插入元素,直接在新的数组上添加,如果是删除元素,直接删 了。

currentyHashMap.png


各位看官如果觉得文章写得不错,点赞评论关注走一波!谢谢啦!。

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【ChatGPT 指令大全】怎么利用ChatGPT写报告

目录 选定切入角度 报告开头 大纲生成 草稿撰写 研究报告 提出反对观点 报告总结 研究来源 总结 随着人工智能技术的快速发展&#xff0c;自然语言处理技术在各个领域的应用越来越广泛。其中&#xff0c;ChatGPT作为目前最先进的自然语言处理模型之一&#xff0c;其强…

助力618-Y的混沌实践之路 | 京东云技术团队

一、写在前面 1、混沌是什么&#xff1f; 混沌工程&#xff08;Chaos Engineering&#xff09;的概念由 Netflix 在 2010 年提出&#xff0c;通过主动向系统中引入异常状态&#xff0c;并根据系统在各种压力下的行为表现确定优化策略&#xff0c;是保障系统稳定性的新型手段。…

Go语言time库,时间和日期相关的操作方法

time库 用于处理时间、日期和时区的核心库。在实际开发中&#xff0c;常常需要与时间打交道&#xff0c;例如记录日志、处理时间差、计算时间间隔等等。因此&#xff0c;掌握time库的使用方法对于Go开发者来说非常重要。 在Go语言中&#xff0c;时间表示为time.Time类型&…

基于 yolov8 的人体姿态评估

写在前面 工作中遇到&#xff0c;简单整理博文内容为使用预训练模型的一个预测 Demo测试图片来源与网络,如有侵权请告知理解不足小伙伴帮忙指正 对每个人而言&#xff0c;真正的职责只有一个&#xff1a;找到自我。然后在心中坚守其一生&#xff0c;全心全意&#xff0c;永不停…

嵌入式开发学习(STC51-15-红外遥控)

内容 使用外部中断功能&#xff0c;使按下红外遥控器&#xff0c;将对应键值编码数据解码后通过数码管显示 红外遥控介绍 红外线简介 人的眼睛能看到的可见光按波长从长到短排列&#xff0c;依次为红、橙、黄、绿、青、蓝、紫&#xff1b; 其中红光的波长范围为 0.62&…

Flink Windows(窗口)详解

Windows&#xff08;窗口&#xff09; Windows是流计算的核心。Windows将流分成有限大小的“buckets”&#xff0c;我们可以在其上应用聚合计算&#xff08;ProcessWindowFunction&#xff0c;ReduceFunction&#xff0c;AggregateFunction或FoldFunction&#xff09;等。在Fl…

一、ADAS技术概述

根据《国家车联网产业标准体系建设指南》对智能网联汽车定义&#xff1a;智能网联汽车是指搭载先进的车载传感器、控制器、执行器等装置&#xff0c;并融合现代通信与网络技术&#xff0c;实现车与X&#xff08;人、车、路、云端等&#xff09;智能信息交换、共享&#xff0c;具…

阶段总结(linux基础)

目录 一、初始linux系统 二、基本操作命令 三、目录结构 四、文件及目录管理命令 查看文件内容 创建文件 五、用户与组管理 六、文件权限与压缩管理 七、磁盘管理 八、系统程序与进程管理 管理机制 文件系统损坏 grub引导故障 磁盘资源耗尽 程序与进程的区别 查…

【云原生】Serverless 技术架构分析

一、什么是Serverless? 1、Serverless技术简介 ​ Serverless&#xff08;无服务器架构&#xff09;指的是由开发者实现的服务端逻辑运行在无状态的计算容器中&#xff0c;它由事件触发&#xff0c; 完全被第三方管理&#xff0c;其业务层面的状态则被开发者使用的数据库和存…

交换机Vlan实验

介绍 Vlan表示虚拟局域网。 常见的网络安全技术 VlanACL Vlan的作用 Vlan隔离了广播域&#xff0c;增加了网络的安全性。 知识点 默认vlan vlan1 是默认vlan&#xff0c;主要机器开机了&#xff0c;默认所有的接口都属于Vlan1 交换机的接口模式 Access : 这个模式用来…

【设计模式】-建造者模式

Java建造者模式&#xff1a;创建复杂对象的灵活构建者 在软件开发中&#xff0c;我们经常遇到需要创建一个复杂对象的情况。如果使用传统的构造函数进行对象创建&#xff0c;可能会导致构造函数参数过多&#xff0c;难以管理和维护。建造者模式&#xff08;Builder Pattern&am…

MongoDB 使用总结

&#x1f353; 简介&#xff1a;java系列技术分享(&#x1f449;持续更新中…&#x1f525;) &#x1f353; 初衷:一起学习、一起进步、坚持不懈 &#x1f353; 如果文章内容有误与您的想法不一致,欢迎大家在评论区指正&#x1f64f; &#x1f353; 希望这篇文章对你有所帮助,欢…

无人驾驶实战-第五课(动态环境感知与3D检测算法)

激光雷达的分类&#xff1a; 机械式Lidar&#xff1a;TOF、N个独立激光单元、旋转产生360度视场 MEMS式Lidar&#xff1a;不旋转 激光雷达的输出是点云&#xff0c;点云数据特点&#xff1a; 简单&#xff1a;x y z i &#xff08;i为信号强度&#xff09; 稀疏&#xff1a;7%&…

【肺炎分类数据集】数据量非常充足的新冠肺炎分类数据共享

一、肺炎数据集介绍&#x1f349;&#xff1a; 1.1 格式&#x1f388; 按照标准的格式分为了①训练集train&#xff08;134138575198张&#xff09;&#xff0c;②验证集val&#xff08;8816张&#xff09;&#xff0c;③测试集test&#xff08;234390624张&#xff09;&#…

Windows server上用nginx部署vue3项目

Windows server上用nginx部署vue3项目 一、Node中node_modules文件夹及package.json文件的作用说明二、VUE3项目打包三、Windows Server上的Nginx部署 一、Node中node_modules文件夹及package.json文件的作用说明 node_modules是安装node后用来存放用包管理工具下载安装的包的…

【项目 计网3】Socket介绍 4.9字节序 4.10字节序转换函数

文章目录 4.8 Socket介绍4.9字节序简介字节序举例 4.10字节序转换函数 4.8 Socket介绍 所谓 socket&#xff08;套接字&#xff09;&#xff0c;就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端&#xff0c;提供了应用层进…

VUE之JWT前后端分离认证,学生管理系统

参考资料: SpringBoot搭建教程 SpringCloud搭建教程 JWT视频教程 JWT官网 Vue视频教程 JWT视频参考资料、VUE视频资料,及前后端demo 特别有参考价值的JWT博客1 特别有参考价值的JWT博客2 cookie、localstorage和sessionStorage的区别1 cookie、localstorage和sessi…

总结七大排序!

排序总览 外部排序&#xff1a;依赖硬盘&#xff08;外部存储器&#xff09;进行的排序。对于数据集合的要求特别高&#xff0c;只能在特定场合下使用&#xff08;比如一个省的高考成绩排序&#xff09;。包括桶排序&#xff0c;基数排序&#xff0c;计数排序&#xff0c;都是o…

【数据库】将excel数据导入mysql数据库

环境&#xff1a;Windows10 mysql8以上 将你要导入的excel表另存为txt格式 打开txt格式文件&#xff0c;删除表头行并另存为并更改编码方式&#xff08;由于与数据库的编码不同&#xff0c;会导致导入报错&#xff09; 通过命令行登录数据库 winr cmd进入 进入装mysql的目录位…

软件设计原则

文章目录 一、软件设计原则1. 开闭原则2. 里氏代换原则3. 依赖倒转原则4. 接口隔离原则5. 迪米特法则6. 合成复用原则 一、软件设计原则 在软件开发中&#xff0c;为了提高软件系统的可维护性和可复用性&#xff0c;增加软件的可扩展性和灵活性&#xff0c;程序员要尽量根据软件…
最新文章