Java Web 实战 13 - 多线程进阶之 synchronized 原理以及 JUC 问题
文章目录
- 一 . synchronized 原理
- 1.1 synchronized 使用的锁策略
- 1.2 synchronized 是怎样自适应的? (锁膨胀 / 升级 的过程)
- 1.3 synchronized 其他的优化操作
- 锁消除
- 锁粗化
- 1.4 常见面试题
- 二 . JUC (java.util.concurrent)
- 2.1 Callable 接口
- 2.2 ReentrantLock
- 2.3 原子类
- 2.4 线程池
- ExecutorService 和 Executors
大家好 , 这篇文章给大家分享多线程中 synchronized 的原理以及 JUC 相关问题
注意这块的 synchronized 是小写的 , 一定要注意拼写
推荐大家跳转到 此链接 查看效果更佳~
上一篇文章的链接我也给大家贴到这里了
点击即可跳转到文章专栏~
一 . synchronized 原理
注意这块的 synchronized 是小写的 , 一定要注意拼写
1.1 synchronized 使用的锁策略
- 既是悲观锁 , 也是乐观锁 (自适应锁)
- 既是轻量级锁 , 也是重量级锁 (自适应锁)
- 轻量级锁部分基于自旋锁实现 , 重量级锁部分基于挂起等待锁来实现
- 不是读写锁
- 是非公平锁
- 是可重入锁
1.2 synchronized 是怎样自适应的? (锁膨胀 / 升级 的过程)
synchronized 在加锁的时候要经历几个阶段 :
- 无锁 (没加锁)
- 偏向锁 (刚开始加锁 , 未产生竞争的时候)
- 轻量级锁 (产生锁竞争了)
- 重量级锁 (锁竞争的更激烈了)
其中 , 我们再分析一下什么是偏向锁
偏向锁 , 不是"真正加锁" , 只是用个标记表示 “这个锁是我的了” , 在遇到其他线程来竞争锁之前 , 都始终保持这个状态 .
直到真的有人来竞争了,此时才真的加锁
这个过程类似于单例模式中的"懒汉模式" , 必要的时候再加锁 , 节省开销
举个栗子 :
我是一个漂亮的妹子 , 遇到了一个小哥哥 , 对他各个方面都很满意 , 我们的感情就很快升温
但是我就不和他确定关系 , 造成若即若离的感觉 , 这样的话后面如果我腻歪了 , 随时伸腿就踹了 , 成本很低
这就是偏向锁状态
突然 , 我又发现另外一个妹子也在接近小哥哥 , 这个时候我趁着他们俩刚认识 , 我就赶紧和小哥哥确立男女朋友关系 , 并且发朋友圈官宣 , 另外的这个妹子就上一边等着去
这就是偏向锁在遇到锁竞争的时候 , 再真正进行加锁
如果没有额外的妹子(线程)过来竞争 , 从始至终都是在偏向锁的状态 , 也就省去了加锁以及解锁的开销了 , 这就更加的轻量
1.3 synchronized 其他的优化操作
锁消除
锁消除.编译器自动判定 , 如果认为这个代码没必要加锁 , 就不加了 .
这个操作不是所有情况下都会触发 , 大部分情况下不能触发
比如 :
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");
此处的这几个 append 方法 , 内部都是带有 synchronized 的
如果上述代码都是在同一个线程中运行的 , 此时就没必要再去加锁了
JVM 就悄悄地把锁去掉了
锁粗化
先了解锁的粒度 : synchronized 包含的代码范围是大还是小 , 范围越大 , 粒度越粗 ; 范围越小 , 粒度越细
锁的粒度细了 , 能够更好的提高线程的并发 , 但会也会增加 “加锁解锁” 的次数


1.4 常见面试题
- 能够理解 synchronized 基本执行过程 , 理解锁对象 , 理解锁竞争
- 能够知道 synchronized 的基本策略
- 能够理解 synchronized 内部的一些锁优化的过程 ( 锁升级 , 锁消除 , 锁粗化 )
- 什么是偏向锁
二 . JUC (java.util.concurrent)
concurrent 中文叫做并发
java.util.concurrent 这个包里就存放了很多和多线程开发相关的类
2.1 Callable 接口
和我们之前学习过得 Runnable 非常类似 , 都是可以在创建线程的时候 , 来指定一个 “具体的任务”
而 Callable 指定的任务是带有返回值的 , Runnable 是不带返回值的
Callable 里面会提供一个 call 方法 , call 方法是带有返回值的 , 我们可以借助它很容易的获得到任务的执行结果
举个栗子 : 创建线程计算 1 + 2 + 3 + … + 1000, 不使用 Callable 版本
static class Result {public int sum = 0;public Object lock = new Object();
}public static void main(String[] args) throws InterruptedException {Result result = new Result();// 创建一个线程去计算 1~100 之间的值// 但是我们通过 run 方法没办法返回值// 就需要把结果写入到 Result 类当中的 sum Thread t = new Thread() {@Overridepublic void run() {int sum = 0;for (int i = 1; i <= 1000; i++) {sum += i;}// 赋值操作需要加锁synchronized (result.lock) {result.sum = sum;result.lock.notify();}}};t.start();// 在主线程这里,再去针对 result 结果进行等待// 上面的 result 结果计算好之后,上面的 notify 就会唤醒下面的 wait// 打印 sum 的值synchronized (result.lock) {while (result.sum == 0) {result.lock.wait();}System.out.println(result.sum);}
}
上述代码需要一个辅助类 Result , 还需要使用一系列的加锁和 wait notify 操作 , 代码复杂 , 容易出错 .
我们可以使用 Callable 接口
import java.util.concurrent.Callable;public class Demo28 {public static void main(String[] args) {// 创建 Callable 接口,它是带有泛型参数的// 这个泛型参数实际就是 call 方法的返回值// new 一个匿名内部类Callable<Integer> callable = new Callable<Integer>() {// 这里的 Object 要改成 Integer@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 0; i <= 1000; i++) {sum += i;}return sum;}};}
}
接下来 , 我们就可以新建线程执行这个任务了

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class Demo28 {public static void main(String[] args) throws ExecutionException, InterruptedException {// 创建 Callable 接口,它是带有泛型参数的// 这个泛型参数实际就是 call 方法的返回值// new 一个匿名内部类Callable<Integer> callable = new Callable<Integer>() {// 这里的 Object 要改成 Integer@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 0; i <= 1000; i++) {sum += i;}return sum;}};// 套上一层,目的是为了获取到后续的结果FutureTask<Integer> task = new FutureTask<>(callable);Thread t = new Thread(task);t.start();// 在线程 t 执行结束之前,get 会阻塞等待,直到 t 执行完了,结果算完了// get 才能返回.返回值就是 call 方法 return 的内容System.out.println(task.get());}
}
这里的 FutureTask 就好比 :
我们去餐馆吃饭 , 人很多的时候 , 老板会给你个小票 , 后续就可以凭小票来取餐
到目前为止 , 我们已经学习过好几种创建线程的方式了
- 继承 Thread
- 使用 Runnable
- 使用 lambda
- 使用 Callable
- 使用线程池
2.2 ReentrantLock
ReentrantLock 代表可重入锁
synchronized 已经是可重入锁了 , 为什么还要再弄一个 ReentrantLock 呢 ?
- synchronized 是单纯的关键字 , 以代码块为单位进行加锁解锁 .
ReentrantLock则是一个类 , 提供 lock 方法加锁 , unlock 方法解锁
import java.util.concurrent.locks.ReentrantLock;public class Demo29 {public static void main(String[] args) {ReentrantLock locker = new ReentrantLock();// 加锁locker.lock();// 其他代码逻辑// 解锁locker.unlock();}
}
但这种方式还存在一些问题
假如中间的其他代码逻辑出现了问题 , 抛出了异常 , 后面的 unlock() 就执行不到了
所以我们一般把加锁解锁操作放到 try catch finally 中
import java.util.concurrent.locks.ReentrantLock;public class Demo29 {public static void main(String[] args) {ReentrantLock locker = new ReentrantLock();try {// 加锁locker.lock();// 其他代码逻辑} finally {// 解锁locker.unlock();}}
}
- ReentrantLock 会提供一个"公平锁"版本 , 在构造实例的时候 , 可以通过构造方法指定一个参数 , 切换到公平锁模式
ReentrantLock locker = new ReentrantLock(true);
synchronized 只是一个非公平锁
- ReentrantLock 还提供了一个特殊的加锁操作 : tryLock()
默认的 lock 是加锁失败 , 就阻塞
而 tryLock 加锁失败 , 则不阻塞 , 直接往下执行 , 并且返回 false
除了立即失败之外 , tryLock 还能设定一定的等待时间 (等一会再失败)
- ReentrantLock 提供了更强大的 等待/唤醒 机制
synchronized 搭配的是 Object.wait / notify , 唤醒的时候 , 随机唤醒其中一个
ReentrantLock 搭配了 Condition 类来实现等待唤醒 , 可以做到能随机唤醒一个 , 也能指定线程唤醒
大部分情况下 , 使用锁还是 synchronized 为主 .
特殊场景下 , 才使用 ReentrantLock
2.3 原子类
原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多
我们常用的是 AtomicInteger
他的常用方法有
addAndGet(int delta); i += delta;
decrementAndGet(); --i;
getAndDecrement(); i--;
incrementAndGet(); ++i;
getAndIncrement(); i++;
2.4 线程池
虽然创建销毁线程比创建销毁进程更轻量 , 但是在频繁创建销毁线程的时候还是会比较低效.
线程池就是为了解决这个问题 . 如果某个线程不再使用了 , 并不是真正把线程释放 , 而是放到一个 "池子"中 , 下次如果需要用到线程就直接从池子中取 , 不必通过系统来创建了.
ExecutorService 和 Executors
ExecutorService 是一个线程实例 , Executors 是一个工厂类
Executors 创建线程池的几种方式
- newFixedThreadPool : 创建固定线程数的线程池
- newCachedThreadPool : 创建线程数目动态增长的线程池.
- newSingleThreadExecutor : 创建只包含单个线程的线程池.
- newScheduledThreadPool : 设定 延迟时间后执行命令 , 或者定期执行命令 . 是进阶版的 Timer.
Executors 本质上是 ThreadPoolExecutor 类的封装 , 这个类是标准库中最核心的线池类
打开我们的 Java 文档

我们来看第四个构造方法

实际工作中 , 一般建议大家 , 使用线程池的时候 , 尽量还是用 ThreadPoolExecutor 复杂版本的 , 这里的参数都显式的手动传参 , 这样就可以更好的掌控代码
当我们使用线程池的时候 , 线程数目设置成多少合适 ?
只要你回答出具体的数字 , 一定都是错的 .
不同的场景 , 不同的程序 , 不同的主机配置 , 都会有差异
面试中我们回答不了具体设置几个线程 , 但是可以回答 : 找到合适线程数的方法 -> 压测(性能测试)
针对当前的程序进行性能测试 , 分别设置不同的线程数目 , 分别进行测试
在测试过程中 , 会记录程序的时间、CPU占用、内存占用…
根据压测结果 , 来选择咱们觉得最适合当前场景的数目
关于 JUC , 我们后续还会再增加一些内容 , 大家敬请期待~
如果对你有帮助的话 , 请一键三连嗷~

相关文章:
Java Web 实战 13 - 多线程进阶之 synchronized 原理以及 JUC 问题
文章目录一 . synchronized 原理1.1 synchronized 使用的锁策略1.2 synchronized 是怎样自适应的? (锁膨胀 / 升级 的过程)1.3 synchronized 其他的优化操作锁消除锁粗化1.4 常见面试题二 . JUC (java.util.concurrent)2.1 Callable 接口2.2 ReentrantLock2.3 原子类2.4 线程池…...
【解决】elementui ——tooltip提示在循环中点击一个,同时显示多个的问题!
同时显示多个tooltip——效果图: 点击第一个二维码把循环el-card中所有的tooltip都触发了 解决后效果图: 只显示点击的当前tooltip 解决办法: 通过循环item中定义字段,进行控制tooltip显示隐藏 代码: 页面代码&am…...
SpringBoot-核心技术篇
技术掌握导图 六个大标题↓ 配置文件web开发数据访问单元测试指标指控原理解析 配置文件 1.文件类型 1.1、properties 同以前的properties用法 1.2、yaml 1.2.1、简介 YAML是 “YAML Aint Markup Language”(YAML不是一种标记语言)的递归缩写。在…...
2023还有人不知道kubernetes?| 初步理解kubernetes
文章目录Kubernetes(K8s)一、Openstack&VM1、**认识虚拟化****1.1**、什么是虚拟化**1.2、虚拟化分类**2、OpenStack与KVM、VMWare2.1、OpenStack2.2、KVM2.3、VMWare二、容器&编排技术1、容器发展史1.1、Chroot1.2、FreeBSD Jails1.3、Solaris Zones1.4、LXC1.5、Dock…...
Docker 环境搭建
RabbitMq 安装与启动安装:运行命令:docker pull rabbitmq 默认版本是:latest启动rabbitmq:运行命令:docker run \ # 运行-e RABBITMQ_DETAULT_USERroot \ # 设置用户名-e RABBITMQ_DETAULT_PASS123456 \ # 设置 密码--…...
css实现炫酷充电动画
先绘制一个电池,电池头部和电池的身体 这里其实就是两个div,使用z-index改变层级,电池的身体盖住头部,圆角使用border-radius完成 html部分,完整的css部分在最后 <div class"chargerBox"><div class"ch…...
【Effective C++详细总结】第二章 构造/析构/赋值运算
✍个人博客:https://blog.csdn.net/Newin2020?spm1011.2415.3001.5343 📚专栏地址:C/C知识点 📣专栏定位:整理一下 C 相关的知识点,供大家学习参考~ ❤️如果有收获的话,欢迎点赞👍…...
webpack基础
webpack基础 webpack基础目录webpack基础前言Webpack 是什么?Webpack 有什么用?一、webpack的基本使用webpack如何使用文件和文件夹创建创建文件下载依赖二、基本配置5 大核心概念准备 Webpack 配置文件修改配置文件处理样式资源处理图片资源修改输出资源…...
jQuery《一篇搞定》
今日内容 一、JQuery 零、 复习昨日 1 写出至少15个标签 2 写出至少7个css属性font-size,color,font-familytext-algin,background-color,background-image,background-sizewidth,heighttop,bottom ,left ,rightpositionfloatbordermarginpadding 3 写出input标签的type的不…...
Spring Cloud学习笔记【负载均衡-Ribbon】
文章目录什么是Spring Cloud RibbonLB(负载均衡)是什么Ribbon本地负载均衡客户端 VS Nginx服务端负载均衡区别?Ribbon架构工作流程Ribbon Demo搭建IRule规则Ribbon负载均衡轮询算法的原理配置自定义IRule新建MyRuleConfig配置类启动类添加Rib…...
第九章:C语言数据结构与算法初阶之堆
系列文章目录 文章目录系列文章目录前言一、堆的定义二、堆的实现三、堆的接口函数1、初始化2、销毁3、插入4、删除5、判空6、元素个数四、堆排序1、建堆2、排序五、堆的应用——TOPK1、什么是TOPK问题?2、解决方法总结前言 堆就是完全二叉树。 一、堆的定义 我们…...
Mysql架构初识
🥲 🥸 🤌 🫀 🫁 🥷 🐻❄️🦤 🪶 🦭 🪲 🪳 🪰 🪱 🪴 🫐 🫒 🫑…...
字符串函数和内存函数
🍕博客主页:️自信不孤单 🍬文章专栏:C语言 🍚代码仓库:破浪晓梦 🍭欢迎关注:欢迎大家点赞收藏关注 字符串函数和内存函数 文章目录字符串函数和内存函数前言1. 字符串函数介绍1.1 s…...
Web3中文|GPT-4超越GPT-3.5的五大看点
A Beautiful CinderellaDwelling EagerlyFinally Gains HappinessInspiring Jealous KinLove Magically Nurtures Opulent PrinceQuietly RescuesSlipper TriumphsUniting Very WondrouslyXenial Youth Zealously这是一段描述童话故事《灰姑娘》的内容,它出自GPT-4之…...
动态矢量瓦片缓存库方案
目录 前言 二、实现步骤 1.将数据写入postgis数据库 2.将矢量瓦片数据写入缓存库 3.瓦片接口实现 4.瓦片局部更新接口实现 总结 前言 矢量瓦片作为webgis目前最优秀的数据格式,其主要特点就是解决了大批量数据在前端渲染时出现加载缓慢、卡顿的问题࿰…...
628.三个数的最大乘积
给你一个整型数组 nums ,在数组中找出由三个数组成的最大乘积,并输出这个乘积。 示例 1: 输入:nums [1,2,3] 输出:6 示例 2: 输入:nums [1,2,3,4] 输出:24 示例 3: …...
【数据结构】堆和集合笔记
自己写一个堆首先,明确一下,为什么需要堆?>考虑插入,删除,查找的效率。数组,查找,最快是二分查找O(lgN)。但查找完如果要做什么操作,比如删除,就要挪动元素了。所以合…...
java LinkedList 源码分析(通俗易懂)
目录 一、前言 二、LinkedList类简介 三、LinkedList类的底层实现 四、LinkedList类的源码解读 1.add方法解读 : 〇准备工作 。 ①跳入无参构造。 ②跳入add方法。 ③跳入linkList方法。 ④增加第一个元素成功。 ⑤向链表中添加第二个元素。 2.remove方法解读 : 〇准备工…...
Vue中实现路由跳转的三种方式详细分解
vue中实现路由跳转的三种方式 目录 vue中实现路由跳转的三种方式 一、使用vue-router 1.下载vue-router模块到当前工程 2.在main.js中引入VueRouter函数 3.添加到Vue.use()身上 – 注册全局RouterLink和RouterView组件 4.创建路由规则数组 – 路径和组件名对应关系 5…...
全国自学考试03708《中国近现代史纲要》重点复习精要
1. 西方列强的殖民扩张和鸦片战争的影响。(两面性) :反面—破坏了了中国的小农经济,是中国由封建社会转变为两半社会。 --一系列不公平条约,破坏了中国主权领土完整。 --压迫中国人民,给中国人民带来了巨大…...
Xshell远程连接Kali(默认 | 私钥)Note版
前言:xshell远程连接,私钥连接和常规默认连接 任务一 开启ssh服务 service ssh status //查看ssh服务状态 service ssh start //开启ssh服务 update-rc.d ssh enable //开启自启动ssh服务 任务二 修改配置文件 vi /etc/ssh/ssh_config //第一…...
遍历 Map 类型集合的方法汇总
1 方法一 先用方法 keySet() 获取集合中的所有键。再通过 gey(key) 方法用对应键获取值 import java.util.HashMap; import java.util.Set;public class Test {public static void main(String[] args) {HashMap hashMap new HashMap();hashMap.put("语文",99);has…...
基于uniapp+WebSocket实现聊天对话、消息监听、消息推送、聊天室等功能,多端兼容
基于 UniApp + WebSocket实现多端兼容的实时通讯系统,涵盖WebSocket连接建立、消息收发机制、多端兼容性配置、消息实时监听等功能,适配微信小程序、H5、Android、iOS等终端 目录 技术选型分析WebSocket协议优势UniApp跨平台特性WebSocket 基础实现连接管理消息收发连接…...
ESP32读取DHT11温湿度数据
芯片:ESP32 环境:Arduino 一、安装DHT11传感器库 红框的库,别安装错了 二、代码 注意,DATA口要连接在D15上 #include "DHT.h" // 包含DHT库#define DHTPIN 15 // 定义DHT11数据引脚连接到ESP32的GPIO15 #define D…...
React Native在HarmonyOS 5.0阅读类应用开发中的实践
一、技术选型背景 随着HarmonyOS 5.0对Web兼容层的增强,React Native作为跨平台框架可通过重新编译ArkTS组件实现85%以上的代码复用率。阅读类应用具有UI复杂度低、数据流清晰的特点。 二、核心实现方案 1. 环境配置 (1)使用React Native…...
工程地质软件市场:发展现状、趋势与策略建议
一、引言 在工程建设领域,准确把握地质条件是确保项目顺利推进和安全运营的关键。工程地质软件作为处理、分析、模拟和展示工程地质数据的重要工具,正发挥着日益重要的作用。它凭借强大的数据处理能力、三维建模功能、空间分析工具和可视化展示手段&…...
Xen Server服务器释放磁盘空间
disk.sh #!/bin/bashcd /run/sr-mount/e54f0646-ae11-0457-b64f-eba4673b824c # 全部虚拟机物理磁盘文件存储 a$(ls -l | awk {print $NF} | cut -d. -f1) # 使用中的虚拟机物理磁盘文件 b$(xe vm-disk-list --multiple | grep uuid | awk {print $NF})printf "%s\n"…...
AGain DB和倍数增益的关系
我在设置一款索尼CMOS芯片时,Again增益0db变化为6DB,画面的变化只有2倍DN的增益,比如10变为20。 这与dB和线性增益的关系以及传感器处理流程有关。以下是具体原因分析: 1. dB与线性增益的换算关系 6dB对应的理论线性增益应为&…...
Kubernetes 网络模型深度解析:Pod IP 与 Service 的负载均衡机制,Service到底是什么?
Pod IP 的本质与特性 Pod IP 的定位 纯端点地址:Pod IP 是分配给 Pod 网络命名空间的真实 IP 地址(如 10.244.1.2)无特殊名称:在 Kubernetes 中,它通常被称为 “Pod IP” 或 “容器 IP”生命周期:与 Pod …...
深入浅出Diffusion模型:从原理到实践的全方位教程
I. 引言:生成式AI的黎明 – Diffusion模型是什么? 近年来,生成式人工智能(Generative AI)领域取得了爆炸性的进展,模型能够根据简单的文本提示创作出逼真的图像、连贯的文本,乃至更多令人惊叹的…...

