1. JVM
面试常见:
- 请你谈谈你对 JVM 的理解?
- java 8 虚拟机和之前的变化更新?
- 什么是 OOM,什么是栈溢出 StackOverFlowError? 怎么分析?
- JVM的常用调优参数有哪些?
- 内存快照如何抓取,怎么分析 Dump 文件?
- 谈谈JVM中,类加载器你的认识
1. JVM的位置
JRE:java 开发环境,包含了 JVM( C++ 语言编写的)
一个个(.class)类文件 |
---|
JRE–JVM |
操作系统(Windows,Linux,Mac) |
硬件体系(Intel,Spac…) |
2. JVM的体系结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-56dSQAjb-1648561802064)(link-picture\image-20220107161935856.png)]
Java栈、本地方法栈、程序计数器不会有垃圾回收
,否则程序会死掉
99% JVM 调优都是在方法区和堆中调优,Java 栈、本地方法栈、程序计数器是不会有垃圾存在的
2.1 类加载器(Class Loader)
类是模板,是抽象的,类实例化得到的对象是具体的。所有的对象反射回去得到的是同一个类模板
作用:加载 .class 文件,得到 Class
类加载器的种类
-
虚拟机自带的
加载器 -
启动类(根)
加载器:BootstrapClassLoader -
扩展类
加载器:ExtClassLoader -
应用程序类
加载器:AppClassLoader -
双亲委派机制:保证安全,逐级查找:AppCL–>ExtCL–>BootstrapCL
双亲委派机制
类加载器收到类加载
的请求,将这个请求向上委托
给父类加载器
去完成,一 直向上委托,直到启动类加载器
,启动加载器检查是否能够加载当前这个类,有就加载就结束, 使用当前的加载器;没有抛出异常,通知子加载器
进行加载
java通过 native 调用操作系统
的方法
native
- 凡是带了 native 关键字的,说明 java 的作用范围达不到了,会去调用
底层c语言
的库 - 会进入
本地方法栈
,调用本地方法
本地接口 JNI ( Java Native Interface )
JNI作用:拓展 Java 的使用,融合不同的编程语言为 Java 所用(最初: C、C++),它在内存区域
中专门开辟了一块标记区域
:本地方法栈(Native Method Stack),登记 native
方法,在最终执行的时候,加载本地方法库
中的方法
通过 JNI,如Java程序驱动打印机或者Java 系统管理设备
本地方法栈(Native Method Stack)
它的具体做法是:本地方法栈
中标记为native方法,在执行引擎
( Execution Engine ) 执行的时候加载本地库
(Native Libraies)
沙箱安全机制
Java 安全模型的核心就是 Java 沙箱
。沙箱是一个限制
程序运行的环境
。
沙箱机制就是将 Java 代码限定
在虚拟机 ( JVM ) 特定的运行范围中,并且严格限制
代码对本地系统资源
访问,通过这样的措施来保证对代码的有效隔离
,防止对本地系统
造成破坏。沙箱主要限制系统资源访问,系统资源
包括:CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
在 Java 中将执行程序分成:本地代码
、远程代码
。本地代码默认视为可信任
的,而远程代码则被看作是不受信
的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱机制。
在Java1.2版本中,改进了安全机制,增加了代码签名。不论本地代码
或是远程代码
,都会按照用户的安全策略
设定,由类加载器
加载到虚拟机中权限不同的运行空间
,来实现差异化的代码执行权限控制
。
组成沙箱的基本组件:
- 字节码校验器( bytecode verifier ):确保 Java 类文件遵循 Java 语言规范。这样可以帮助 Java 程序实现
内存保护
。但并不是所有的类文件都会经过字节码校验,比如核心类 - 类装载器( class loader ) :其中类装载器在3个方面对 Java 沙箱起作用
- 它防止恶意代码去干涉善意的代码(
双亲委派机制
) - 它守护了被信任的类库边界
- 它将代码归入保护域,确定了代码可以进行哪些操作
- 它防止恶意代码去干涉善意的代码(
2.2 方法区(Method Area)
方法区
是被所有线程共享,所有字段
和方法字节码
,以及一些特殊方法
(如:构造函数,接口代码也在此定义)。简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间。
静态变量(static)、常量(final)、类信息(构造方法、接口定义)、运行时的常量池
都存在方法区中,但是实例变量存在堆内存中,和方法区无关
方法区储存的是:static
、final
、Class
、常量池
2.3 PC寄存器
每个线程都有一个程序计数器,是线程私有
的,就是一个指针,指向方法区
中的方法字节码
(用来存储指向像一条指令的地址
, 也即是将要执行的指令代码
),在执行引擎读取下一条指令, 是一个非常小的内存空间,几乎可以忽略不计
2.4 栈(Stack)
栈(数据结构):先进后出、后进先出
队列:先进先出( FIFO : First Input First Output )
栈内存
- 主管程序的运行,
生命周期
和线程同步
- 线程结束,栈内存也就释放
- 对栈来说,不存在垃圾回收的问题
栈存储的是:8大基本类型
、对象引用
、实例方法
栈帧:局部变量表 + 操作数栈
每执行一个方法,就会产生一个栈帧。程序正在运行
的方法永远都会在栈顶
栈满了,就会报错 StackOverflowError
栈、堆、方法区的交互关系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i3d1K6dq-1648561802067)(link-picture\image-20220107165620842.png)]
2.5 堆(Heap)
一个 JVM 只有一个
堆内存,堆内存的大小是可调节的
。
类加载器
读取了类文件
(.class)后,一般会把类、方法、常量、变量放到堆
中,保存所有引用类型
的真实对象
堆内存
堆内存分为三个区域:
新生区
Young:类诞生成长或死亡的地方- 伊甸园区(Eden Space):所有的对象都是在这里
new
出来的 - 幸存0区
- 幸存1区
- 伊甸园区(Eden Space):所有的对象都是在这里
养老区
old永久区
Perm(在 JDK8 以后,永久存储区改名为元空间
)- 永久区
常驻内存
的,用来存放 JDK 自身携带的Class对象
、Interface接口
元数据,存储的是 Java运行时
的一些环境或类信息 - 永久区不存在垃圾回收,关闭 JVM 虚拟机就会释放这个区域的内存
- 永久区
注意
:
- jdk1.6之前︰有永久代,常量池是在
方法区
; - jdk1.7:有永久代,但是慢慢的退化了,去永久代,常量池在
堆
中 - jdk1.8之后∶无永久代,常量池在
元空间
GC 垃圾回收:主要在新生区和养老区
轻GC:轻量级垃圾回收,主要是在新生区
重GC:重量级垃圾回收,主要是在养老区
,重 GC 就说明内存都要爆了(如:OOM
(Out Of memory))
堆内存调优
Java 虚拟机默认情况下:分配的总内存是电脑内存的 1/4,而初始化的内存是电脑内存的 1/64
可以通过调整(Edit Configuration—>VM options
)这个参数控制 Java 虚拟机初始内存
和分配的总内存
的大小
# 设置虚拟机的总内存和初始占用内存为:1G,并打印日志
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
# 设置虚拟机的总内存和初始占用内存为:1G,并假如堆内存heap出现了OOM则dump出这个异常
-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryErro
当新生代、老年代、元空间内存都满了之后才会报 OOM
内存快照分析工具
内存快照分析工具有:MAT,Jprofiler
- MAT 最早集成于
Eclipse
中 IDEA
中可以使用 Jprofiles 插件,在 Settings—>Plugins 中搜索 Jprofiles,安装改插件即可使用
工具作用
:
- 分析 Dump
内存
文件,快速定位
内存泄露;(dump出的文件应该在src目录
下) - 获得
堆中
的数据 - 获得
大的对象
2.6 垃圾回收(GC)
JVM 在进行 GC 时,并不是对这三个区域统一回收。大部分时候,回收都是新生代
新生代
幸存区
(form , to):幸存0区 和 幸存1区 两者是会交替的,from和to的关系会交替变化
老年区
GC 两种类:轻 GC (普通的 GC ),重 GC (全局 GC )
GC常用算法
-
标记清除法
扫描对象,对对象进行
标记
;清除:对没有标记
的对象进行清除
优缺点
:- 优点:不需要额外的空间!
- 缺点:两次扫描,严重
浪费时间
,会产生内存碎片
-
标记压缩
改良
:标记清除再压缩(压缩:防止内存碎片产生,再扫描,向一端移动存活的对象)再改进
:先标记清除几次之后,再压缩1次 -
复制算法
新生区主要是用复制算法,to 永远是干净的,空的
每次 GC 都会将
Eden 区
活的对象移到幸存区
中,一旦 Eden 区被 GC 后,就会是空的当一个对象经历了15次 GC 后,都还没死,可通过
-XX:MaxTenuringThreshold=9999
这个参数设定进入老年代的时间
复制算法最佳使用场景:1. 对象
存活度较低
的时候 2.新生区
优缺点
:- 优点:
没有内存碎片
- 缺点:
浪费内存
空间(一个幸存区的空间永远是空:to)
- 优点:
-
引用计数器法(不常用)
GC算法总结
- 内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)
- 内存整齐度:复制算法=标记压缩算法>标记清除算法
- 内存利用率:标记压缩算法=标记清除算法>复制算法
年轻代:存活率低,复制算法
老年代:区域大,存活率高,标记清除
(内存碎片不是太多) + 标记压缩
混合实现
2. JUC
JUC(java.util.concurrent下面的类包),专门用于多线程
的开发
1. 线程和进程
线程和进程
进程:是操作系统中的应用程序、是资源分配的基本单位
线程:是用来执行具体的任务和功能,是CPU调度和分派的最小单位
- 一个进程可以包含多个线程,至少包含一个线程;
- Java 默认2个线程 :main 线程、GC 线程
对于Java而言:Thread、Runable、Callable 进行开启线程的
Java是没有权限
去开启线程、操作硬件的,这是一个 native
本地方法,它底层调用的 C++ 代码
并行和并发
并发
:多线程操作同一个资源
- CPU 只有一核,使用CPU快速交替,来模拟多线程。
- 并发编程的本质:
充分利用CPU的资源
并行
: 多个人一起行走
- CPU多核,多个线程可以同时执行。 我们可以使用**
线程池
**!
Runtime.getRuntime().availableProcessors(); // 获取 cpu 的核数
线程的状态
- NEW:新建
- RUNNABLE:运行
- BLOCKED:阻塞
- WAITING:等待
- TIMED_WAITING:超时等待
- TERMINATED:终止
wait/sleep 的区别
-
来自
不同的类
wait => Object
sleep => Thread
TimeUnit.DAYS.sleep(1); //休眠1天 TimeUnit.SECONDS.sleep(1); //休眠1s
-
关于锁的
释放
wait:会释放锁
sleep:不会释放锁
-
使用的
范围是不同
的wait:必须在同步代码块中
sleep:可以在任何地方
-
是否需要
捕获
异常wait:是不需捕获异常
sleep:必须要捕获异常
2. 线程并发
synchronized 与 Lock
-
传统的
synchronized
线程就是一份单独的
资源类
(包含属性
、方法
),没有任何的附属操作
public class Demo { public static void main(String[] args) { // 并发:多线程操作同一资源,把资源放入线程 final Ticket ticket = new Ticket(); new Thread( ()->{ for (int i = 0; i < 40; i++) { ticket.sale(); }},"A").start(); new Thread( ()->{ for (int i = 0; i < 40; i++) { ticket.sale(); }},"B").start(); } } // 线程就是一份单独的资源类,没有任何的附属操作,因此一般用实现 Runnable 的方式 class Ticket { private int number = 30; public synchronized void sale() { if (number > 0) { System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "张票剩余" + number + "张票"); } } }
-
Lock
ReentrantLock
:可重入锁(非公平锁)ReentrantReadWriteLock.ReadLock
:可重入读锁ReentrantReadWriteLock.WriteLock
:可重入写锁公平锁: 十分公平,必须先来后到
非公平锁:十分不公平,可以插队
// 主方法与上面相同 class Ticket2 { private int number = 30; Lock lock = new ReentrantLock(); // 1.创建锁 public synchronized void sale() { lock.lock(); // 2.加锁 try { if (number > 0) { System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "张票剩余" + number + "张票"); } }finally { lock.unlock(); // 3.解锁 } } }
-
synchronized 与 Lock 的区别
- Synchronized 内置的 Java
关键字
,Lock 是一个Java 类
- Synchronized 无法获取
锁的状态
,Lock 可以判断锁的状态 - Synchronized 会
自动释放锁
,lock 必须要手动加锁
和手动释放锁
,如果不释放可能会死锁 - Synchronized 线程1(获得锁->阻塞)、线程2(
等待
);lock 就不一定会一直等待下去,lock 会有一个 trylock 去尝试获取锁
,不会造成长久的等待 - Synchronized 是可重入锁,
不中断的
,非公平的
(可插队);Lock 是可重入锁,可以判断锁
,可以设置公平锁和非公平锁 - Synchronized 适合锁
少量代码
同步问题,Lock 适合锁大量代码
同步问题
- Synchronized 内置的 Java
3. 线程通信
线程之间的通信问题:生产者和消费者问题(等待唤醒、通知唤醒)
线程交替执行
-
synchronized 实现
if 判断
会出现虚假唤醒(解决:等待应该总是放在循环
中(不能使用 if 判断))// public class ConsumeAndProduct { public static void main(String[] args) { Data data = new Data(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { data.increment(); // 加1 } catch (InterruptedException e) { e.printStackTrace(); } } }, "A").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { data.decrement(); // 减1 } catch (InterruptedException e) { e.printStackTrace(); } } }, "B").start(); } } // 等待 业务 通知 class Data { private int num = 0; public synchronized void increment() throws InterruptedException { // +1 while (num != 0) { // 判断等待:if 判断会出现虚假唤醒 this.wait(); } num++; System.out.println(Thread.currentThread().getName() + "=>" + num); this.notifyAll(); // 通知其他线程 +1 执行完毕 } public synchronized void decrement() throws InterruptedException { // -1 while (num == 0) { // 判断等待:if 判断会出现虚假唤醒 this.wait(); } num--; System.out.println(Thread.currentThread().getName() + "=>" + num); this.notifyAll(); // 通知其他线程 -1 执行完毕 } }
-
Lock 实现
// 主方法与上面的相同 // 等待 业务 通知 class Data { private int num = 0; Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); public void increment() throws InterruptedException { // +1 lock.lock(); // 加锁 try{ while (num != 0) { // 判断等待:if 判断会出现虚假唤醒 condition.wait(); } num++; System.out.println(Thread.currentThread().getName() + "=>" + num); condition.signalAll(); // 通知其他线程 +1 执行完毕 }finally{ lock.unlock(); //解锁 } } public void decrement() throws InterruptedException { // -1 lock.lock(); // 加锁 try{ while (num == 0) { // 判断等待:if 判断会出现虚假唤醒 condition.wait(); } num--; System.out.println(Thread.currentThread().getName() + "=>" + num); condition.signalAll(); // 通知其他线程 +1 执行完毕 }finally{ lock.unlock(); //解锁 } } }
-
Condition 的优势:精准的
通知
和唤醒
的线程用 Condition 来
指定通知
下一个进行顺序(按顺序执行)public class ConditionDemo { public static void main(String[] args) { Data3 data = new Data(); new Thread(() -> { for (int i = 0; i < 10; i++) { data3.printA(); } },"A").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { data3.printB(); } },"B").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { data3.printC(); } },"C").start(); } } // 业务代码 判断 -> 执行 -> 通知 class Data { private Lock lock = new ReentrantLock(); private Condition condition1 = lock.newCondition(); private Condition condition2 = lock.newCondition(); private Condition condition3 = lock.newCondition(); private int num = 1; // 1A 2B 3C public void printA() { lock.lock(); try { while (num != 1) { condition1.await(); } System.out.println(Thread.currentThread().getName() + "==> AAAA" ); num = 2; condition2.signal(); // 唤醒 2 }catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } public void printB() { lock.lock(); try { while (num != 2) { condition2.await(); } System.out.println(Thread.currentThread().getName() + "==> BBBB" ); num = 3; condition3.signal(); // 唤醒 3 }catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } public void printC() { lock.lock(); try { while (num != 3) { condition1.await(); // 唤醒 1 } System.out.println(Thread.currentThread().getName() + "==> CCCC" ); num = 1; condition1.signal(); }catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } }
4. 锁现象
锁是谁? 锁的是谁?(对象
、Class
)
-
两个同步方法
,先执行发短信还是打电话?结果:---->发短信先 (锁的是调用的对象)public class dome01 { public static void main(String[] args) { Phone phone = new Phone(); new Thread(() -> { phone.sendMs(); }).start(); TimeUnit.SECONDS.sleep(1); // 睡1秒 new Thread(() -> { phone.call(); }).start(); } } class Phone { public synchronized void sendMs() { System.out.println("发短信"); } public synchronized void call() { System.out.println("打电话"); } }
-
普通方法
,不受锁影响 -
把
synchronized
的方法加上 static 变成静态方法
(锁的是Class类的模板
)
5. 集合不安全
单线程
操作集合是安全的
,多线程
操作集合就是不安全的
-
List 不安全
ArrayList 在并发情况下是不安全的
解决方案
:1. List<String> list = new Vector<>(); //Vector是线程安全的 2. List<String> list = Collections.synchronizedList(new ArrayList<>()); 3. List<String> list = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList:写入时复制! COW 计算机程序设计领域的一种优化策略
CopyOnWriteArrayList
核心思想是:如果有
多个调用者
同时调用相同的资源
(如内存
或者是磁盘上的数据存储
),会共同获取相同的指针
指向相同的资源
,直到某个调用者试图修改
资源内容时,系统才会真正复制
一份专用副本
给该调用者
,而其他调用者仍然保持不变。这过程对其他的调用者都是透明的。此做法主要的优点是如果调用者没有修改
资源,就不会有副本
被创建,因此多个调用者只是读取操作时
可以共享同一份
资源。读的时候不需要加锁,如果读的时候有多个线程正在向 CopyOnWriteArrayList 添加数据,读还是会读到旧的数据
,因为写的时候不会锁住旧的 CopyOnWriteArrayList 。多个线程调用的时候,list,读取的时候,固定的,写入(存在覆盖操作);在写入的时候避免覆盖,造成数据错乱的问题
**CopyOnWriteArrayList 与 Vector **的区别
- Vector底层是使用
synchronized
关键字来实现的,效率特别低下
- **CopyOnWriteArrayList **使用的是
Lock
锁,效率会更加高效
- Vector底层是使用
-
Set 不安全
普通的 Set 集合在并发情况下是不安全的
解决方案
:-
使用
Collections
工具类的 synchronized 包装的 Set 类 -
使用
CopyOnWriteArraySet
写入时复制的 JUC 解决方案1. Set<String> set = Collections.synchronizedSet(new HashSet<>()); 2. Set<String> set = new CopyOnWriteArraySet<>();
hashSet底层
:就是一个HashMap ,所以,HashMap 基础类也存在并发修改异常 -
-
Map 不安全
new HashMap<>; 默认等价 new HashMap<>(16,0.75); // 初始化容量16,加载因子0.75
解决方案
:-
使用
Collections
工具类的 synchronized 包装的 Map 类 -
使用
ConcurrentHashMap
的 JUC 解决方案1. Map<String, String> map = Collections.synchronizedMap(new HashMap<>()); 2. Map<String, String> map = new ConcurrentHashMap<>();
-
6. Callable
Callable 与 Runnable 的区别
- 可以有返回值
- 可以抛出异常
- 方法不同,run()/
call()
public class CallableTest {
public static void main(String[] args)
throws ExecutionException, InterruptedException {
for (int i = 1; i < 10; i++) {
MyThread myThread = new MyThread();
// FutureTask 是 Runnable 实现类,可以接收 Callable
FutureTask<Integer> futureTask = new FutureTask<>(myThread);
// 放入Thread中使用,结果会被缓存
new Thread(futureTask,String.valueOf(i)).start();
// get方法可能会被阻塞,如果在call方法中是一个耗时的方法,
// 所以一般情况会把这个放在最后或者使用异步通信
int a = futureTask.get();
System.out.println("返回值:" + s);
}
}
}
class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("call()");
return 1024;
}
}
7. 常用的辅助类
-
CountDownLatch
:减法计数器主要方法
:- countDown
减一
操作; - await 等待计数器
归零
,归零就唤醒,再继续向下运行
public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(6); // 总数是6 for (int i = 1; i <= 6; i++) { new Thread(() -> { System.out.println( Thread.currentThread().getName() + "==> Go Out"); countDownLatch.countDown(); // 每个线程都数量 -1 },String.valueOf(i)).start(); } countDownLatch.await(); // 等待计数器归零 然后向下执行 System.out.println("close door"); } }
- countDown
-
CyclicBarrier
:加法计数器public class CyclicBarrierDemo { public static void main(String[] args) { // 主线程 CyclicBarrier cyclicBarrier = new CyclicBarrier(7,() -> { System.out.println("召唤神龙"); }); for (int i = 1; i <= 7; i++) { // 子线程 int finalI = i; new Thread(() -> { System.out.println(Thread.currentThread().getName() + "收集了第" + finalI + "颗龙珠"); try { cyclicBarrier.await(); // 加法计数 等待 } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } }).start(); } } }
-
Semaphore
:并发限流作用: 多个共享资源
互斥使用
,并发限流
,控制最大的线程数
原理:
semaphore.
acquire
():获得
资源,如果资源使用完
,就等待资源释放后
再进行使用!semaphore.
release
():释放
资源,会将当前的信号量释放
,然后唤醒
等待的线程!public class SemaphoreDemo { public static void main(String[] args) { Semaphore semaphore = new Semaphore(3); // 线程数量,停车位,限流 for (int i = 0; i <= 6; i++) { new Thread(() -> { try { semaphore.acquire(); // 获得资源 阻塞式等待 System.out.println( Thread.currentThread().getName() + "抢到车位"); TimeUnit.SECONDS.sleep(2); System.out.println( Thread.currentThread().getName() + "离开车位"); }catch (Exception e) { e.printStackTrace(); }finally { semaphore.release(); // 释放资源 } }).start(); } } }
8. 读写锁
ReentrantLock
:可重入锁(默认是非公平锁)
ReentrantReadWriteLock.ReadLock
:可重入读锁
ReentrantReadWriteLock.WriteLock
:可重入写锁
如果不加锁的
情况,多线程的读写
会造成数据不可靠
的问题。
- 可采用 synchronized 这种重量锁和轻量锁 lock 去保证
数据可靠
- 还可采用更细粒度的 ReadWriteLock 读写锁来保证
数据可靠
独占锁(写锁):一次只能被一个线程占有
共享锁(读锁):多个线程可以同时占有
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
int num = 6;
for (int i = 1; i <= num; i++) {
final int finalI = i;
new Thread(() -> {
myCache.write(String.valueOf(finalI), String.valueOf(finalI));
},String.valueOf(i)).start();
}
for (int i = 1; i <= num; i++) {
int finalI = i;
new Thread(() -> {
myCache.read(String.valueOf(finalI));
},String.valueOf(i)).start();
}
}
}
// 加了读写锁后,数据正常
class MyCache2 {
private volatile Map<String, String> map = new HashMap<>();
private ReadWriteLock lock = new ReentrantReadWriteLock(); // 读写锁
public void write(String key, String value) {
lock.writeLock().lock(); // 写锁
try {
System.out.println(Thread.currentThread().getName() + "线程开始写入");
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "线程写入ok");
}finally {
lock.writeLock().unlock(); // 释放写锁
}
}
public void read(String key) {
lock.readLock().lock(); // 读锁
try {
System.out.println(Thread.currentThread().getName() + "线程开始读取");
map.get(key);
System.out.println(Thread.currentThread().getName() + "线程写读取ok");
}finally {
lock.readLock().unlock(); // 释放读锁
}
}
}
// 方法未加锁,导致写的时候被插队
class MyCache {
// volatile 保证共享资源的可见性
private volatile Map<String, String> map = new HashMap<>();
public void write(String key, String value) {
System.out.println(Thread.currentThread().getName() + "线程开始写入");
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "线程写入ok");
}
public void read(String key) {
System.out.println(Thread.currentThread().getName() + "线程开始读取");
map.get(key);
System.out.println(Thread.currentThread().getName() + "线程写读取ok");
}
}
9. 阻塞队列
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0iI3Gd29-1648561802070)(link-picture\image-20220108131237100.png)]
-
BlockingQueue:阻塞队列
BlockingQueue
:是Collection
的一个子类使用阻塞队列的情况:多线程并发处理、线程池
BlockingQueue 有四组 api :
方式 抛出异常 不会抛出异常,有返回值 阻塞等待 超时等待 添加 add offer put offer(timenum.timeUnit) 移除 remove poll take poll(timenum,timeUnit) 判断队首元素 element peek - - // 抛出异常 public static void test1(){ //需要初始化队列的大小 ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(2); System.out.println(blockingQueue.add("a")); System.out.println(blockingQueue.add("b")); //如果多添加一个 抛出异常:java.lang.IllegalStateException: Queue full System.out.println(blockingQueue.add("c")); System.out.println(blockingQueue.remove()); System.out.println(blockingQueue.remove()); //如果多移除一个 抛出异常:java.util.NoSuchElementException System.out.println(blockingQueue.remove()); } // 不抛出异常,有返回值 public static void test2(){ ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(2); System.out.println(blockingQueue.offer("a")); System.out.println(blockingQueue.offer("b")); //如果多添加一个 只会返回 false 不会抛出异常 System.out.println(blockingQueue.offer("c")); System.out.println(blockingQueue.poll()); System.out.println(blockingQueue.poll()); //如果多移除一个 只会返回 null 不会抛出异常 System.out.println(blockingQueue.poll()); } // 等待 一直阻塞 public static void test3() throws InterruptedException { ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(2); //一直阻塞 不会返回 blockingQueue.put("a"); blockingQueue.put("b"); //如果多添加一个 会一直等待这个队列 什么时候有了位置再进去,程序不会停止 //blockingQueue.put("c"); System.out.println(blockingQueue.take()); System.out.println(blockingQueue.take()); //如果多移除一个 也会等待,程序会一直运行 阻塞 System.out.println(blockingQueue.take()); } //等待 超时阻塞 也会等待队列有位置 或者有产品 但是会超时结束 public static void test4() throws InterruptedException { ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(2); blockingQueue.offer("a"); blockingQueue.offer("b"); System.out.println("开始等待"); //超时时间2s 等待如果超过2s就结束等待 blockingQueue.offer("c",2, TimeUnit.SECONDS); System.out.println("结束等待"); System