JUC
线程的安全性问题
需要考虑原子性、有序性、可见性
java内存模型
JMM内存规定,每个线程在创建时都有自己单独的工作内存,所有的变量都存储在主内存中,是共享的,线程首先读取主内存的变量到自己工作内存的变量副本上,对数据的修改只能在工作内存上进行,操作完成后再将新的变量写会主内存,各工作内存间不能直接交流。
JMM的三种特性:可见性---线程修改变量及时通知其余线程、原子性、有序性
volatile
volatile关键字、如何保证可见性、是否是强一致性
volatile是轻量级的同步机制,保证可见性、不保证原子性、禁止指令重排
保证可见性:volatile确保线程在操作变量之前,重新读取主内存中的变量值。
不保证原子性:会出现写覆盖的情况
如何保证原子性:1.使用sync 2.使用juc下atmic包下面的类
volatile是如何做的禁止指令重排的
对于volatile修改变量的读写操作,都会加入内存屏障。
每个volatile写操作前面,加StoreStore屏障,禁止上面的普通写和他重排;每个volatile写操作后面,加StoreLoad屏障,禁止跟下面的volatile读/写重排。
每个volatile读操作后面,加LoadLoad屏障,禁止下面的普通读和voaltile读重排;每个volatile读操作后面,加LoadStore屏障,禁止下面的普通写和volatile读重排。
happens-before原则规定了volatile修饰的变量的写一定在读前面
1、程序顺序规则
程序顺序原则,指的是在一个线程内,按照程序代码的顺序,前面的代码运行的结果能被后面的代码可见。(准确的说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。)
2、传递性
传递性,指的是如果A Happens-Before于B,B Happens-Before于C,则A Happens-Before于C。这个是很好理解的。用白话说就是,如果A的操作结果对B可见,B操作结果对C可见,则A的操作结果对C也是可见的。
3、volatile变量规则
指对一个volatile变量的写操作,Happens-Before于后续对这个volatile变量的读操作。如果单单是理解这句话的意思,就是我们熟悉的禁用cpu缓存的意思,使得volatile修饰的变量读到永远是最新的值。
4、锁规则
锁规则,指的是一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
5、线程start()规则
指的是主线程A启动子线程B后,子线程B能看到主线程在启动线程B前的操作。
6、线程join()规则
这个规则跟上一条规则有点类似,只不过这个规则是跟线程等待相关的。指的是主线程A等待子线程B完成(对B线程join()调用),当子线程B操作完成后,主线程A能看到B线程的操作。
7、线程的interrupt()规则
指的是线程A调用线程B的interrupt()方法,Happens-Before 于线程B检测中断事件(也就是Thread.interrupted()方法)。这个也很容易理解
8、finalize()规则
指的是对象的构造函数执行、结束 Happens-Before 于finalize()方法的开始。
volatile应用场景
单体结构中单例模式只需要加sync即可,单分布式架构中,有很多个jvm,该锁对象是不同的。
//单例模式
public class SingletoneDemo {
private static volatile SingletoneDemo instance = null;
//为什么使用volatile 修饰了singleton 引用还用synchronized 锁?
//volatile 只保证了共享变量 singleton 的可见性,但是 singleton = new Singleton();
// 这个操作不是原子的,可以分为三步:
//步骤1:在堆内存申请一块内存空间;
//步骤2:初始化申请好的内存空间;
//步骤3:将内存空间的地址赋值给 singleton;
//私有化构造方法
private SingletoneDemo() {
}
//为什么进行DCL(Double Check)
// A 线程进行判空检查之后开始执行synchronized代码块时发生线程切换(线程切换可能发生在任何时候),
// B 线程也进行判空检查,B线程检查 singleton == null 结果为true,也开始执行synchronized代码块,
// 虽然synchronized 会让二个线程串行执行,如果synchronized代码块内部不进行二次判空检查,singleton 可能会初始化二次。
public static SingletoneDemo getInstance() {
if (instance == null) {
synchronized (SingletoneDemo.class) {
if (instance == null) {
instance = new SingletoneDemo();
}
}
}
return instance;
}
}
破坏单例的方式
反射:发现发射生成的一个新的对象 ,此时单例模式被破坏
序列化:通过先序列化再反序列化的方式,可获取到一个新的单例对象,这就破坏了单例。
克隆
如何防止破坏单例
防止反射 定义一个全局变量,当第二次创建的时候抛出异常
防止序列化 (在单例中加入readResolve方法,因为在反序列化执行过程中会执行到ObjectInputStream#readOrdinaryObject方法,这个方法会判断对象是否包含readResolve方法,如果包含的话会直接调用这个方法获得对象实例)
防止克隆 克隆破坏单例模式需要继承Colneable接口并且重写克隆方法
什么是MESI缓存一致性协议
synchronized
synchronized是如何保证可见性、原子性及有序性的
保证可见性是线程在释放锁之前,必须把工作内存中的值刷新到主内存中(fresh),获得锁之后,把工作内存中的变量清除,重新从主内存读取(refresh)。
保证原子性是线程在执行同步代码块时首先获得锁monitorenter,执行完释放锁monitorexit,有两次释放锁,是为了避免死锁。
有序性不代表禁止指令重排,sync锁住当前线程,每个线程单独执行,就可以保证有序性
synchronized可重入原理
可重入锁:线程可以进入任意一个它已经拥有锁的可重复代码块
作用是避免死锁
每个对象关联一个monitor,monitor有一个计数器,刚开始为0,对这个对象加锁,每次加锁之后指数都加1,释放锁则减1,当减到0的时候,其余线程可以抢占这个对象。
Synchronized和监视器(monitor)有什么关系?为什么Synchronized可以使用任意对象?
首先,每个对象都可以被认为是一个“监视器monitor”,对象里面有个指针指向monitor,这个监视器由三部分组成:独占锁、入口队列,等待队列。
注意:一个对象只能有一个独占锁,但是任意线程都可以拥有这个独占锁(说白了,独占锁就是一个标记)。
Synchronized需要获取对象锁,实际上就是获取的是对象中的独占锁,通过这个标记来判断是否已有线程进入占用(所以synchronized无论使用什么对象都可以,每个对象在堆中都有独占锁)。
而入口队列中放的则是要竞争锁资源的其他线程,如果线程使用了wait方法,则进入对象的等待列队中。
Synchronized中优化
锁消除
jit编译器在编译的时候发现,只有一个线程来抢占资源的时候,就可以消除这个锁了,提升效率。
锁粗化
jit编译器在编译过程中发现,代码里连续多次加锁释放锁,就合并为一个锁。
偏向锁
如果可能只有一个线程来竞争锁,但也有其他可能来竞争锁,但是其他线程概率小,那么就会给这个锁维护一个偏好(Bias),如果这个偏好指向这个线程,就不需要moniterenter和moniterexit这种底层操作,而是基于Bias,如果有其他线程来占用这把锁,则把之前的Bias收回。
轻量级锁
就是说在偏向锁没有成功实现,会尝试采用轻量级锁的方式来加锁,会把对象头里面的一个轻量级锁指针尝试指向持有锁的线程,判断在moniter是否是自己持有锁,如果是自己持有锁就不用moniterenter和moniterexit。失败则用重量锁。
自旋锁
是指在尝试获取锁的时候不会立即阻塞,而是采用循环的方式去获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
自适应自旋锁
自选的时间不是固定的,而是基于上次在同一个锁上的自旋时间和锁的持有状态来决定。
公平锁和非公平锁
*可以在ReentrantLock构造方法中设置,
*空参构造为非公平:默认使用的是非公平锁,减少一定的上下文切换,保证系统更大的吞吐量,一上来就抢占锁,抢不到则排序。
*参数为true为公平锁:先来后到(FIFO)
*synchronized是非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
synchronized 锁升级
无锁-->偏向锁-->轻量锁-->重量锁
synchronized和Lock区别
-
synchronized是关键字,可以加在方法,代码块上,Lock是一个接口,需要使用其实现的类。
-
synchronized可以自动上锁释放锁,Lock手动上锁释放锁。
-
Lock可以根据Condition精准唤醒线程,synchronized只能随机唤醒一个或者全部唤醒。
-
synchronized默认非公平锁,Lock可以选择公平锁和非公平锁。
-
synchronized不可中断,Lock可中断。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UiFwvhOt-1660891177244)(img\synchronized和Lock区别.png)]
CAS
CAS自旋及自旋存在的问题
CompanyAndSwap比较然后替换
AtomicIntegter底层依靠cas算法和unsafe方法unsafe方法用native调用的是系统的底层原理
CAS主要依靠三个值,主内存值、工作内存读取的旧值、工作内存修改的新值,每次工作内存修改完之后、将旧值和主内存中的值比较,若一样则将新值赋值到主内存,若不一样则不赋值,重新读取主内存值到变量副本,做do while循环。
CAS存在的问题:
1.循环时间开销大
2.ABA问题
3.只能保证一个共享变量的原子性,多个共享变量时候还是得用锁。
如何解决ABA问题
使用AtomicStampReference版本号原子引用,每次修改值版本号也会修改,版本号不一样也不会swap。
自旋锁demo
public class SpinLockDemo {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(() -> {
spinLockDemo.myLock();
try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}
spinLockDemo.myUnLock();}, "AA").start();
try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
new Thread(() -> {
spinLockDemo.myLock();
spinLockDemo.myUnLock();}, "BB").start();
}
public void myLock() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "come in");
while (!atomicReference.compareAndSet(null, thread)) {
}
}
public void myUnLock() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + " out");
atomicReference.compareAndSet(thread, null);
}
}
Lock
AQS机制(Abstract Queued Synchronizer)
AQS的核心原理是,如果资源空间空闲,就设请求线程为有效的工作线程,并锁定该线程;
如果资源已被占用,AQS就把当前线程以及等待状态信息构造成一个Node(节点)到同步队列中,同时再阻塞该线程。
当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。
ReetrantReadWriteLock读写锁和RenntrantLock有什么区别?
ReetrantReadWriteLock读共享锁写独占锁
RenntrantLock读写都是独占锁
其他
CountDownLatch
CountDownLatch是基于AQS实现的,通过构造函数给AQS的state设置一个参数,当线程调用await方法会被阻塞,state!=0时会进入阻塞队列,每次调用CountDown方法state-1,当state=0时候,因调用await方法被阻塞的线程会被唤醒,继续执行。
CyclicBarrier
又叫做回环屏障,作用是让一组线程全部达到一个状态之后在全部同时执行,而且他可以重用,
CyclicBarrier也是基于AQS实现的,会维护一个parties记录总线程数,count用于计时,最开始count=parties,当调用await()方法count-1,当count=0,再次将parties赋值为count,达到复用。
![CyclicBarrier](https://img-blog.csdnimg.cn/8721cff5672e41ea97774d1c9751f33d.png)
Semaphore
semaphore(int,boolean)默认为false,非公平锁。
有线程进来int-1,释放资源则+1。
信号量主要用于两个目的,一个是共享资源互斥,一个是并发资源的控制。
如何查看死锁
- 在当前目录下 jps -l 查看当前进程
- 在当前目录下jstack 线程编号