文章目录
- 一、谈谈Volatile
- 二、CAS 底层原理
- 三、ABA问题
- 四、集合类不安全
- 五、Java的几个重要的锁
- 六、JUC常见的几个包
- 七、阻塞队列
- 八、生产者与消费者问题
- 九、Synchronized锁与Lock锁的区别
- 十、创建线程的四种方式
- 十一、Callable接口
- 十二、线程池
- 十三、线程池面试题
- 十四、死锁排查
- 十五、异步回调
- 十六、CompletableFuture的使用
- 十七、其它面试题
-
-
- 池化技术简介
- 现在有三个线程T1,T2,T3,怎么保证线程按照T1,T2,T3的顺序顺序执行?
- 线程安全
- Java线程池中submit() 和 execute()方法有什么区别?
- 说一下线程之间是如何通信的?
- 1.常见的并发容器?
- 2.常见的同步工具类?
- 说说自己是怎么使用 synchronized 关键字
- 构造方法可以使用 synchronized 关键字修饰么?
- 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
- 什么是悲观锁?什么是乐观锁?
- sleep() 方法和 wait() 方法的区别和共同点?
- ThreadLocal 了解么?
- ThreadLocal 内存泄露问题了解不?
- 线程池如何知道一个线程的任务已经执行完成?
- 讲一下 wait 和 notify 这个为什么要在synchronized 代码块中?
- 什么是守护线程,它有什么特点
-
一、谈谈Volatile
1)、JMM 是什么
JMM (Java 内存模型)是一种抽象的概念 并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素,不包括局部变量和方法参数,这是线程私有的,不存在竞争关系)的访问方式。
-
具体的 JMM 规定如下:
- 所有 共享变量 储存于 主内存 中;
- 每条线程拥有自己的工作内存,保存了被线程使用的变量的副本拷贝;
- 线程对变量的所有操作(读,写)都必须在自己的 工作内存 中完成,而不能直接读写 主内存 中的变量;
- 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存中转来完成
-
两个概念:主内存 和 工作内存:
- 主内存:就是计算机的内存,也就是经常提到的 8G 内存,16G 内存
- 工作内存:我们new student,而且令age = 25 ,那么 age = 25 存储在主内存中,当同时有三个线程同时访问 student 中的 age 变量时,那么每个线程都会拷贝一份,到各自的工作内存,从而实现了变量的拷贝 。
即:JMM 内存模型的可见性,指的是当主内存区域中的值被某个线程写入更改后,其它线程会马上知晓更改后的值,并重新得到更改后的值。
2)、三道面试题
- 并发和并行的区别是什么?
- 并发:多个任务交替进行
- 并行:并行则是指真正意义上的“同时进行”
实际上,如果系统内只有一个CPU,使用多线程时,在真实系统环境下不能并行,只能通过切换时间片的方式交替进行,从而并发执行任务。真正的并行只能出现在拥有多个CPU的系统中。
- JUC 下的三个包
- java.util.concurrent
- java.util.concurrent.atomic
- java.util.concurrent.locks
- 并发编程三要素?(重点)
- 1)原子性:原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。
- 2)可见性:可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。
- 3)有序性:有序性,即程序的执行顺序按照代码的先后顺序来执行。
3)、谈谈你对 Volatile 的理解
volatile 在单线程环境是应用不到的
volatile 是 Java 虚拟机提供的 轻量级 【乞丐版 synchronized】的同步机制。
volatile 修饰的变量具有三种特性:
- 保证可见性
- 不保证原子性【原子性:完整性,不可缺性,中间不可以被分割,要么成功,要么失败】
- 禁止指令重排序【计算机底层实现是:会在其前后加内存屏障,禁止内存屏障前后的指令进行重排序优化】
private volatile static AtomicInteger num = new AtomicInteger();
其中volatile保证让多个线程都能看得见num这个变量(保证可见性),AtomicInteger则是为了保证多个线程并发修改时不会数据错乱(保证原子性);
所以说volatile就是为了保证可见性,AtomicInteger才能保证原子性;
主要记住volatile就是保证可见性的,至于volatile有个禁止指令重排的功能这个了解就行
4)、synchronized 关键字和 volatile 关键字的区别
synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!
volatile
关键字是线程同步的轻量级
实现,所以volatile性能比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。在 Java 早期版本中,synchronized 属于重量级锁
- volatile关键字主要用于解决变量在多个线程之间的
可见性
,而 synchronized关键字解决的是多个线程之间访问资源的同步性(synchronized
关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行
)。 - volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
5)、那你能否写一个 Demo 验证一下可见性 ?
class Demo {
public static void main(String[] args) {
//资源类
Date date = new Date();
new Thread(() ->{
System.out.println(Thread.currentThread().getName() + "线程开始执行");
// 线程睡眠3秒
try {
TimeUnit.SECONDS.sleep(3);
date.setNumber();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"A").start();
//模拟线程B:一直在这里等待循环,直到 number 的值不等于零
while (date.number == 0){
}
//只要变量的值被修改,就会执行下面的语句
System.out.println(Thread.currentThread().getName() + "执行结束");
}
}
class Date{
//volatile 保证可见性
volatile int number;
public void setNumber(){
number = 60;
}
}
详细过程就是:
- 线程 a 从主内存读取 共享变量 到对应的工作内存
- 对共享变量进行更改
- 线程 b 读取共享变量的值到对应的工作内存
- 线程 a 将修改后的值刷新到主内存,失效其他线程对 共享变量的副本
- 线程 b 对共享变量进行操作时,发现已经失效,重新从主内存读取最新值,放入到对应工作内存。
6)、你能否写个 Demo 验证一下 不保证原子性?
1.volatile不保证原子性
2.那如何才能保证原子性呢 ?
方式1:使用 synchronized 【大材小用】
方式2:使用 JUC 下的 AtomicInteger 原子类【底层是基于 CAS】
num++在多线程的情况下是不安全的
7)、什么是指令重排序?如果不重排会有什么问题?你能否写一个禁止指令重排序的 Demo ?
1.什么是指令重排?
- 指令重排:在实际运行时,代码指令可能并不是严格按照代码语句顺序执行的。大多数现代微处理器都会采用将指令乱序执行,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待。通过乱序执行的技术,处理器可以大大提高执行效率。而这就是指令重排。
int x=1; ①
int y=2; ②
x=x+5; ③
y=x*x; ④
//我们期望的执行顺序是①②③④,但可能执行的顺序会变成②①③④等
//可不可能是 ④①②③? 不可能的,处理器在进行指令重排的时候,会考虑数据之间的依赖性! 而④依赖了①。
2.指令重拍可能造成的影响
单线程环境里面确保最终执行结果和代码顺序的结果一致 ,你就不用担心指令重排会导致结果部队;
但是,当多线程交替执行时,由于编译器优化重排,两个线程在使用的变量能否保住一致性是无法确定的,结果无法预测 。
3.你能否写一个禁止指令重排序的 Demo ?
class Date4{
private volatile int a; //使用 volatile 禁止指令重排序
private volatile boolean flag;
public void set(){
a = 5;
flag = true;
}
public void print(){
while (flag){
a = a + 1;
System.out.println("打印成功" + a);
}
}
}
//如果允许指令重排,那就可能执行了flag=true(还没执行a=5)接着执行了while(flag)里面的语句,输出的a可能是1
4.volatile 针对指令重排做了啥?
volatile中会加一道内存的屏障,这个内存屏障可以保证在这个屏障中的指令顺序。(volatile写之前和volatile写之后会加一道内存的屏障,这层屏障可以保证不会出现顺序错乱)
也就是过在 volatile 的写 和 读的时候,加入屏障,防止出现指令重排的!!!
5.那么你在什么场景下有使用到 volatile 呢 ?
单例模式中
8)、单例模式在多线程环境下可能存在安全问题
懒汉单例模式
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo () {
System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
}
public static SingletonDemo getInstance() {
if(instance == null) {
instance = new SingletonDemo();
}
return instance;
}
public static void main(String[] args) {
// 这里的 == 是比较内存地址
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
}
}
输出结果:
main 我是构造方法singletonDemo
true
true
true
true
12345
但是,在多线程环境运行上述代码,能保证单例吗?
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo () {
System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
}
public static SingletonDemo getInstance() {
if(instance == null) {
instance = new SingletonDemo();
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
}
}
输出结果:
4 我是构造方法SingletonDemo
2 我是构造方法SingletonDemo
5 我是构造方法SingletonDemo
6 我是构造方法SingletonDemo
0 我是构造方法SingletonDemo
3 我是构造方法SingletonDemo
1 我是构造方法SingletonDemo
显然不能保证单例。
解决方法之一:用synchronized
修饰方法getInstance()
,但它属重量级同步机制,使用时慎重。
//使用synchronized后你把整个方法里面的代码都锁掉了
public synchronized static SingletonDemo getInstance() {
if(instance == null) {
instance = new SingletonDemo();
}
return instance;
}
9)、volatile解决单例模式问题
解决方法之二:DCL(Double Check Lock双端检锁机制)
谷粒商城项目里面用过这个思想,当时{p151—P155 Redis}中首页要三级分类数据时我们先查询缓存,如果缓存里面没有再加锁查询数据库,但是查询数据库的代码里面第一句就是再查询一遍查询缓存,如果缓存里面真没有就查询数据库了。
DCL双端检锁机制
public class Singleton {
private static Singleton INSTANCE;
//私有化构造器
private Singleton(){
}
//DCL双端检锁机制
public static Singleton getInstance(){
//第一重检查:针对很多个线程同时想要创建对象的情况
if(INSTANCE == null){
//同步代码块锁定
synchronized (Singleton.class){
//第二重锁检查
if(INSTANCE == null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
上面的代码中没有涉及到volatile,它虽然解决了单例问题,但是还有什么问题?为什么我们还要加上 volatile 呢?
因为创建对象分为 3 步:
memory = allocate(); //1.分配对象内存空间
instance(memory); //2.初始化对象
instance = memory; //3.设置instance指向刚分配的内存地址,此时instance != null
但是,步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此下面这种重排优化是允许的:
memory = allocate(); //1.分配对象内存空间
instance = memory;//3.设置instance指向刚分配的内存地址,此时instance! =null,但是对象还没有初始化完成!
instance(memory);//2.初始化对象
但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。
所以当另一条线程访问 instance 时 不为null,但是instance的引用对象可能没有完成初始化,也就造成线程安全问题!
最终的解决方案:
public class Singleton {
//加上volatile禁止指令重排
private volatile static Singleton INSTANCE;
//私有化构造器
private Singleton(){
}
//DCL双端检锁机制
public static Singleton getInstance(){
//第一重检查:针对很多个线程同时想要创建对象的情况
if(INSTANCE == null){
//同步代码块锁定
synchronized (Singleton.class){
//第二重锁检查
if(INSTANCE == null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
二、CAS 底层原理
1、什么是CAS
CAS是compare and swap的缩写,即我们所说的比较交换。
cas是一种基于乐观锁的操作。(乐观锁和悲观锁)
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。
java.util.concurrent.atomic 包下的类大多是使用CAS操作来实现的( AtomicInteger,AtomicBoolean,AtomicLong)。
2、atomicInteger.getAndIncrement()
方法
CAS 底层原理之前,我们先谈谈之前学习过的atomicInteger.getAndIncrement()
,它为什么不加 synchronized 也能保证原子性?首先我们先看看它的源码
从这里能够看到,它的底层又调用了一个 unsafe 类的 getAndAddInt 方法
(1)unsafe 类
Unsafe 是 CAS 的核心类,由于 Java 方法无法直接访问底层系统,需要通过本地(Native)方法来访问,Unsafe 相当于一个后门,基于该类可以直接操作特定的内存数据。Unsafe 类的内部方法操作可以像 C 的指针一样直接操作内存,因为 Java 中的 CAS 操作的执行依赖于 Unsafe 类的方法。
注意 Unsafe 类的所有方法都是 native 修饰的,也就是说 unsafe 类中的方法都直接调用操作系统底层资源执行相应的任务!!!
为什么 Atomic 修饰的包装类,能够保证原子性,依靠的就是底层的 unsafe 类
(2)变量 valueOffset
表示该变量值在内存中的偏移地址,因为 Unsafe 就是根据内存偏移地址获取数据的。
要操作的对象有了,它的内存地址也有了,通过内存地址获取到对象的值,然后进行加 1 的操作
(3)变量 value 用 volatile 修饰
保证了多线程之间的内存可见性,也就是说一个某一个线程修改了value,其他线程立刻就能感知到。
(4)atomicInteger.getAndIncrement()
方法
- val1:AtomicInteger对象本身
- var2:该对象值的引用地址
- var4:需要变动的数量
- var5:根据内存地址var2找到当前对象var1的内存中的真实值,用当前的值与var5比较
- 如果相同,更新var5 + var4 并返回true
- 如果不同,就一直在循环里待着,继续取值然后再比较,直到更新完成
这里没有用 synchronized,而用 CAS,这样提高了并发性,也能够实现一致性,是因为每个线程进来后,进入的 do while 循环,然后不断的获取内存中的值,判断是否为最新,然后在进行更新操作 。
假设线程 A 和线程 B 同时执行 getAndInt 操作(分别跑在不同的 CPU 上)
- AtomicInteger 里面的 value 原始值为 3,即主内存中 AtomicInteger 的 value 为 3,根据JMM 模型,线程 A 和线程 B 各自持有一份价值为 3 的副本,分别存储在各自的工作内存
- 线程 A 通过 getIntVolatile(var1 , var2) 拿到 value 值3,这时线程 A 被挂起(该线程失去 CPU 执行权)
- 线程 B 也通过 getIntVolatile(var1, var2) 方法获取到 value 值也是3,此时刚好线程 B 没有被挂起,并执行了compareAndSwapInt 方法,比较内存的值也是 3,成功修改内存值为 4,线程B打完收工,一切OK
- 这时线程 A 恢复,执行 CAS 方法,比较发现自己手里的数字 3 和主内存中的数字 4 不一致,说明该值已经被其它线程抢先一步修改过了,那么 A 线程本次修改失败,只能够重新读取后在来一遍了,也就是在执行 do while
- 线程 A 重新获取 value 值得到4,(因为变量 value 被 volatile 修饰,所以其它线程对它的修改,线程 A 总能够看到),线程 A 继续执行 compareAndSwapInt 进行比较替换,直到成功。
3、CAS 底层原理
Unsafe 类 + 自旋锁
①Unsafe 是 CAS 的核心类,由于 Java 方法无法直接访问底层系统,需要通过本地(Native)方法来访问,Unsafe 相当于一个后门,基于该类可以直接操作特定的内存数据。Unsafe 类的内部方法操作可以像 C 的指针一样直接操作内存,因为 Java 中的 CAS 操作的执行依赖于 Unsafe 类的方法。
②自旋锁:比较当前线程工作内存中的值和主物理内存中的值,如果相同则执行规定操作,否者继续比较直到主内存和工作内存的值一致为止(自旋锁)。
4、CAS 存在的问题
CAS 是一种乐观锁,它避免了悲观锁独占锁对象的情况,同时也提高了并发性能
存在问题如下:
-
①乐观锁只能保证一个共享变量的原子操作。 对于多个共享变量的操作时,循环CAS就无法保证操做的原子性,这个时候就可以用锁来保证原子性。
-
②长时间自旋可能导致开销大。 加入 CAS 长时间操作不成功一直自旋,会给 CPU带来很大的开销。
-
③ABA 问题 。 CAS 的核心思想是通过比较内存值和预期值是否一样而判断内存值是否被更改过,但此判断逻辑不严谨,假如内存值为 A,后来一条线程修改为 B,最后又被另一个线程改成了 A,则 CAS 认为内存值并没有发生过改变,但实际情况是有被其他线程修改,这种情况对依赖过程值的情景的运算结果影响很大。
解决办法:引入版本号,每次变量更新都把版本号【时间戳】加一。
三、ABA问题
1.什么是ABA问题?
我们希望当A的值被修改过后就告诉我线程1
2.解决 ABA 问题——乐观锁
乐观锁:带版本号的原子操作!
代码逻辑:
- 我们设置初始值为1初始的版本号为1,然后现在让两个线程同时修改值;让线程1获取到当前的版本号(此时版本号为1)后休眠一秒,趁线程1休眠1秒的时候让线程2获取到当前的版本号(此时版本号为1),然后让线程2休眠2秒,趁线程2休眠2秒的时候让线程1做改动,线程1先把值由1改为2然后又由2改为1,线程1改完后,此时线程2休眠结束,线程2看到的值虽然还是1但是版本号不再是它以前的1了,所以线程2改动值失败。
atomicStampedReference.compareAndSet(,,,,)的四个参数依次是(期望的值、修改后的值、当前期望的版本号、执行完后版本号的值)
atomicStampedReference.compareAndSet(1, 2,atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
上面代码含义是“期望是1,获取当前的版本号作为期望的版本号,如果两个条件都满足就修改值为2,同时让版本号加1作为修改后的版本号”
public class CASDemo {
//AtomicStampedReference<Integer> 注意,如果泛型是一个Integer包装类,注意Integer范围是-127到128之间
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1,1);//参数依次是是"初始值"、“初始的版本号”
public static void main(String[] args) {
//第一个线程
new Thread(()->{
int stamp = atomicStampedReference.getStamp(); // 获得目前的版本号(此时版本号为1)
System.out.println("a1=>"+stamp);//输出当前的版本号
try {
TimeUnit.SECONDS.sleep(1); //线程1休眠1秒(在线程1休眠的时候,让线程2就获取到当前的版本号)
} catch (InterruptedException e) {
e.printStackTrace();
}
//期望是1,期望的版本号是获取到的版本号,如果值是1、版本号是获取到的版本号那就让值变为2让版本号加1
atomicStampedReference.compareAndSet(1, 2,
atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println("a2=>"+atomicStampedReference.getStamp());//输出当前的版本号
//期望值是2,期望版本号是获取到的版本号,如果值是2、版本号是获取到的版本号那就让值变为1让版本号加1
System.out.println(atomicStampedReference.compareAndSet(2, 1,
atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
System.out.println("a3=>"+atomicStampedReference.getStamp());//输出当前的版本号
},"a").start();
// 线程2
new Thread(()->{
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("b1=>"+stamp);//注意,stamp是1
try {
TimeUnit.SECONDS.sleep(2);//休眠2秒(趁线程2休眠2秒的时候让线程1做改动)
} catch (InterruptedException e) {
e.printStackTrace();
}
//期望值是1、期望的版本号是1,如果满足两个条件就修改值为6、让版本号加1。(因为线程1修改过使得版本号不再是1所以线程2的修改会失败)
System.out.println(atomicStampedReference.compareAndSet(1, 6,
stamp, stamp + 1));
System.out.println("b2=>"+atomicStampedReference.getStamp());//输出当前的版本号
},"b").start();
}
}
四、集合类不安全
1、List不安全
(1)你能否写一个集合类不安全的例子?
public class ListTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
//来十个线程,每个线程都往list里面丢一个随机数
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0, 5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
ArrayList 在并发情况下是不安全的
你会发现上面的代码会报错,会导致java.util.ConcurrentModificationException
并发修改异常!
(2)导致它出现的原因是什么?
并发修改导致:一个人正在写入,另一个人过来抢夺,导致数据不一致异常 !
(3)有哪些解决方案?
list.add()方法是人家写出来的代码,你千万别说“加锁”,显得你非常业余,你能在人家的源码上加个锁吗?
list接口里面有很多实现类,你用它的实现类就行。
/*
* 解决 ArrayList 并发修改异常的方案,用以下三种的任意一种方式创建list
* 1.List<String> list = new Vector<>();
* 2.List<String> list = Collections.synchronizedList(new ArrayList<>());
* 3.List<String> list = new CopyOnWriteArrayList<>();
* */
public class ListTest {
public static void main(String[] args) {
//List<String> list = new Vector<>();
//List<String> list = Collections.synchronizedList(new ArrayList<>());
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0, 5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
1.Vector
2.Collections.synchronizedList(new ArrayList<>())
书封面易损,那就包一层书皮。
在不安全的new ArrayList<>()
外面包一层安全的Collections.synchronizedList()
就安全了。这里是采用 Collections 集合工具类,在 ArrayList 外面包装一层 同步 机制 。
3.CopyOnWriteArrayList:写入时复制!
使用 JUC 工具类中的 CopyOnWriteArrayList 类
CopyOnWriteArrayList:写入时复制!
① CopyOnWrite容器即写时复制的容器。待一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行copy,复制出一个新的容器Object[] newELements,然后新的容器Object[ ] newELements里添加元素,添加完元素之后,再将原容器的引用指向新的容器setArray (newELements)。
②一种读写分离的思想:读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。
③写入时复制的思想就是要写入时我就复制一份出来,写完了我就是最新的了,让别人的引用指向我。这就解决了在写入的时候避免覆盖,造成数据错乱的问题;
2.Set不安全与解决方案
Set 和 List 同理可得:多线程情况下,普通的 Set 集合是线程不安全的;
解决方案有两种:
1.Set<String> set = Collections.synchronizedSet(new HashSet<>()); 使用 Collection 工具类的 synchronized 包装的 Set 类
2.Set<String> set = new CopyOnWriteArraySet<>(); 使用CopyOnWriteArraySet 写入复制的 JUC 解决方案
public class SetTest {
public static void main(String[] args) {
//Set<String> set = Collections.synchronizedSet(new HashSet<>());
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
3.Map不安全
Map也不安全,解决方案:
* 解决方案
* 1. Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
* 2.Map<String, String> map = new ConcurrentHashMap<>();
* */
public class MapTest {
public static void main(String[] args) {
ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<>();
for (int i = 0; i < 100; i++) {
new Thread(()->{
concurrentHashMap.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));
System.out.println(concurrentHashMap);
},String.valueOf(i)).start();
}
}
}
五、Java的几个重要的锁
1.说说你知道的Java的几种锁
1.公平锁 与 非公平锁
2.可重入锁
3.自旋锁
4.读写锁
1.公平锁 与 非公平锁
synchronized 只能是非公平锁 。
ReentrantLock 默认是非公平锁,但可以指定为公平锁。
Lock lock = new ReentrantLock(true);//创建一个可重入锁,true 表示公平锁,false 表示非公平锁
Lock lock = new ReentrantLock();//不加参数就是默认的非公平锁
2.可重入锁
可重入锁的最大作用就是避免死锁
1.synchronized就是典型的可重入锁,请证明
2.ReentrantLock也是典型的可重入锁,请证明
public class Demo02 {
public static void main(String[] args) {
Phone2 phone = new Phone2();
new Thread(()->{
phone.sms();
},"A").start();
new Thread(()->{
phone.sms();
},"B").start();
}
}
class Phone2{
Lock lock = new ReentrantLock();
public void sms(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "sms");
call(); // 这里也有锁
} catch (Exception e) {
e.