JUC学习
一级目录
二级目录
三级目录
学几个快捷键 可能用到
记住几个快捷键,
抽取代码来包围:ctrl+alt+t
抽取代码为方法:Ctrl + Alt + M
volatile
是轻量级的同步机制,他有三个特征:保证可见性、禁止指令重排+不保证原子性
第二问:深入问
可见性:主内存的值修改,线程都知道了这件事
1. 对可见性的理解+代码
JMM–Java内存模型
1.本身根本不存在,是一种约定/规范,规定了程序中变量的访问方式
2.同步的内容:
线程解锁前,要把共享变量的值刷新回主内存
线程加锁前,要从主内存读取最新的值到自己的工作内存
加锁解锁用的是同一把锁
代码于idea
保证可见性,某线程对主存数据的修改,其他线程也知道
加volatile可以保证可见性
2. 不保证原子性
不保证
例子就是用 多个线程,每个线程是调用addNum方法多次
产生不一致结果
原因:出现了丢失写值的情况,el:写值的时候,线程一写回停住,线程二的修改完成,线程1继续写回,出现重复写
分析:
对num++命令进行java - p
查看jvm指令,可以知道,可能出现这么一个情况,在底层
putfield的时候,写回值时某几个线程执行过快,后面的值把前面的值覆盖,就是写丢了
这里的重复put可以用两个线程num++的例子
num++三步走:取值 ,++,写回
重复的写回,写丢了
https://blog.csdn.net/xdzhouxin/article/details/81236356
以上是理论,下面解释如何解决原子性
解决1:
用synchronized
解决二:
JUC 下面的atomic原子
使用包装的整型类
默认是0
atomic不加synchronized,实现原子性,原理是CAS,乐观锁(自旋锁)
3. 禁止指令重排
在保证数据依赖性的前提下,编译器、cpu对指令优化,重新排列底层指令的执行顺序 导致结果不一致,一般在多线程的环境下
计算机为了提高性能
注意,考虑数据依赖性,不可能源代码没问题,但编译器和处理器让还未定义的变量,优先执行
重排,有些时候需要有些时候不需要,加了volatile,他来决定要不要重排
其次,
总结:
原理:volatile静止指令重排,在加了这个关键字后,就会插入一个内存屏障,在这个内存屏障的前后,不允许cpu和编译器对代码进行重排优化
拓展,volatile与单例设计模式
我们知道,懒汉式的单例模式,可能存在线程安全问题(饿汉不会)
1.出现原因:
在这种并发的情况下,第一个进入的线程进行实例化,在其他线程中可能也刚好进入实例化。
2.解决:加synchronized关键字
无疑可以解决,但是太重了,锁住了整个方法,严重影响效率
3.优化:双端锁版本
public static SingletonDemo getInstance() {
if (instance == null) {
synchronized (SingletonDemo.class) {
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
在加锁的前后都进行判断,有了保证
是可以,但是不保险,还是可能出现指令重排的可能
具体原因就是:
实例化这个代码可以大致分三步,1.分配空间;2.创建对象;3.指向对象引用;
2-3步之间没有依赖关系,有可能颠倒顺序,出现这样的情况;
线程1执行了由于指令重排,第1步和第3步,2还未执行,指向了一个未分配的内存空间
线程2线程调用的时候,发现不为空,return却取不到
3.再优化:加入了volatile,会插入一个内存屏障,在这个内存屏障的前后,不允许cpu和编译器对代码进行重排优化
private static volatile SingletonDemo instance = null;
CAS
compare
比较交换
不保证原子性,用AtomicInterger,原理就是CAS
unsafe类里面很多native方法,unsafe类在启动类加载器rt.jar上,他有很多是调用系统原语,这种调用原语是执行连续,不会造成数据不一致问题
[
var1是原子类对象的本身
var2内存地址偏移量
var4是需要变动的数字
var5是从var1+var2中得到的
读时候不加锁
修改的时候,是一个dowhile循环,
先从内存中读取一个值,作为原来的值,然后在要正确修改的时候,再从内存中读一次,看看这个新的值和原来的值一样不一样,一样就修改,不一样就出现读,直到成功为止
更加底层,靠的就是底层汇编
1.缺点:
不同于synchronized,没加锁,循环时间长开销大
只能保证一个共享变量的原子操作
ABA问题(重点)
卡里10万块,你取5万,此时公司发工资5万到卡里,你老婆又取走5万,如果你不知道发工资了,你还以为凭空多5万呢
2.原子引用
AtomicReference
一个user类
new AtomicReference();
3.ABA的解决
改为用 AtomicStampedReference
1.线程不安全问题
List
new ArrayList<>();
出现的异常叫:java.util.ConcurrentModification
并发修改异常
Vector
Collections.synchronized(new ArrayList<>());
JUC有
CopyOnWriteArrayList<>();
写时复制,读写分离
具体见自己的笔记
Set
Collections.synchronizedSet();
JUC有
CopyOnWriteArraySet
实际上底层还是CopyOnWriteArrayList
面试题:
HashSet底层是什么?
HashMap
为什么add 只加一个 ,放在key,因为他的value是没有很大作用的,是一个叫PRESENT的恒定Object
Map
Hashtable
synchronizedHashmap
ConcurrentHashMap
各种锁
公平锁与非公平锁
公平与不公平——>排序策略
公平锁是队列,每个线程在获取锁,就看看维护的等待队列,
如果为空,获取锁,如果不是空,加入等待队列的队尾,按照FIFO的规则从队列中取出自己
不公平所是可以插队的,
上来直接就尝试获取锁,如果失败了,在采用公平锁的机制
优点是吞吐量大(相对于公平锁)
synchronized而言,是非公平锁
new ReentrantLock();//可重入锁,默认非公平
如果为new ReentrantLock(true);公平锁
可重入锁与递归锁(同一个)
一个线程可以进入任何一个已经拥有锁的所同步着的代码块
可重入锁最大作用(优点)是避免死锁。缺点:必须手动开启和释放锁。
见代码
自旋锁
CAS自旋锁前面有伏笔了
CAS,用AtomicInterger,原理就是CAS,就是自旋锁,
每次都尝试去获取锁,不阻塞,而是循环获取锁
见代码
读写锁(读是共享,写是独自)
类似于前面的CopyOnWrite*
读读 可以共存
读写 不能共存
写写 可以
读写分离的思想
同步器
同步器是一些使线程能够等待另一个线程的对象,允许它们协调动作。最常用的同步器是CountDownLatch和Semaphore,不常用的是Barrier 和Exchanger
A. semaphore:信号量。用于表示共享资源数量。用acquire()获取资源,用release()释放资源。
B. CyclicBarrier 线程到达屏障后等待,当一组线程都到达屏障后才一起恢复执行
C. CountDownLatch 初始时给定一个值,每次调用countDown值减1,当值为0时阻塞的线程恢复执行
CountDownLatch
倒计数发生器 同步计数器
使用方法如下:
1.新建一个CountDownLatch有一个默认数值(如6)
2.每次调用countDownLatch.countDown()都会减一
3.countDownLatch.await()一般阻塞着,countDownLatch.countDown()执行满6次(举例)。就会继续执行。
下拉选项可以用这个
具体实习见代码
CyclicBarrier
可循环屏障(当障碍器的屏障被打破后,会重置计数器,因此叫做循环屏障)
设置了一个内存屏障,内部也有一个count,每次调用await方法都去调用dowait方法,这个方法会判断,当count==0,就会调用“Condition变量trip的signalAll” (还不知道是啥 忽略先)唤醒其他线程正阻塞的线程。同时自己(最后一个到达临界点的线程)也继续执行
https://blog.csdn.net/zy1994hyq/article/details/83587640
见代码
Semaphore
信号灯
(抢车位)一个等一个进入,当写车位(资源)唯一,退化为synchronozed
多个资源互斥使用,另一个用于并发线程数控制
见代码
不同于前面两个,他更灵活,可以伸缩
通过
semaphore.acquire();
+
semaphore.release();
循环利用资源
(题外话:线程池——>高并发系统
dubbo底层——>RPC底层——>NIO)
阻塞队列
为什么要用,什么好处?
某些情况,不得不阻塞,它的存在就是为了实现阻塞时的管理。
多线程领域,某些情况需要挂起线程(比如生产者生产满了,消费者没的消费了)。阻塞队列管理,减去烦躁操作。
程序员就不需要控制什么时候阻塞 什么时候唤醒,BlockingQueue一手包办
体系结构
种类
BlockingQueue是Queue的子接口,是Collection下的子接口
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vbVSimPe-1586435941977)(C:\Users\user\Desktop\Course\学无止境\有可能的面试点\阻塞队列种类.png)]
synchronousQueue,就是生产一个消费一个,一方不消费,另一方不生产
操作
synchronousQueue代码
线程 操作 资源类
判断 干活 通知(在加锁的时候操作)
要使用while 不能用if
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1QXIzhmB-1586435941991)(C:\Users\user\Desktop\Course\学无止境\有可能的面试点\线程通信old.png)]
public void increment() throws Exception{
lock.lock();//最外层 上锁
try{
/** 这是生产的,所以当资源不是为0,叫醒其他线程操作*/
while(number != 0){//改为if,就少了一次确实,可能导致错误唤醒,在2个以上的线程就可能出错,只有两个线程一个生产员工消费就不会
condition.await();
}
number++;
sout();
condition.signAll();//生产完毕,唤醒其他线程去消费
}catch(){
}finally{
lock.unlock();//最外层 解锁
}
}
疯狂讲义753线程通信
判断
几个问题
lock与synchronized区别
synchronized底层是靠monitor实现,每次判断monitor进入数为0,那就设置为一,自己进入,
占有有只能重入,再加一,
其他对象就处于阻塞状态了,直到退出就减一,其他对象才能尝试重新获得这个锁
synchronized是属于jvm层面,
1.底层是monitor,进1退2(第2是异常)——》保证不会死锁
2.不用自己释放
lock
1.lock是api级别的
2.手动释放
三角形替代后有什么好处
代码
一把锁,多个condition
体会第5点
生产者消费者(new)
相比旧版本的 使用volatile+cas+atomic+blockqueue
更加方便,我们不需要手动释放/加锁,还要兼顾性能和效率
这一切由组设队列实现
volatile/cas/atomic/block queue
(代码我下次打)
Callable接口
线程池
抛弃策略
AbortPolicy 从头到尾,达到最多数(队列+最大运行线程数)抛出异常
CallerRunsPolicy 达到最多数,交给调用者执行
DiscardOldestPolicy 等得最久的,抛弃它
DiscardPolicy 直接抛弃