设计上解决线程安全的一种思想
设计上总是乐观的认为数据修改大部分场景都是没有线程并发修改,少量情况下才存在,线程安全上采取版本号控制(用户自己判断版本号,并处理)
悲观的认为总是有其他线程并发修改,每次都是加锁操作。
实现:自旋尝试设置值的操作
无锁操作,乐观锁
技术背景:当线程执行的任务量较小时,使用synchronized(多个线程同时竞争对象锁)保证多线程安全时,效率较低,竞争失败的线程很快的在阻塞态和被唤醒态之间转化,影响性能。
使用CAS的前提:代码块执行速度非常快
目的:在安全的前提下提高效率。(例如:保证线程安全的修改变量)
原理:CPU去更新一个值,但如果想改的值不再是原来的值,操作就失败,因为很明显,有其它操作先改变了这个值。
public final class Unsafe{
public final int getAndSetInt(Object var1,long var2,int var4){
int var5;
do{
var5 = this.getIntVolatile(var1,var2);
}while(!this.compareAndSwapInt(var1,var2,var5,var4));
return var5;
}
}
真实值:主内存中变量的实际值
旧值:拷贝到工作内存中的值
修改值:在旧值基础上的修改值
给定参数:内存中的实际值,之前拷贝到线程内的值,修改值,版本号。
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
因为CAS会先检查旧值有没有变化。若在其检查前,其他线程将值从A变回B,又从B变回A,当其区检查时,会发现旧值并没有变化依然为A,但是实际上已经发生了变化。
解决方法
采取乐观锁的设计,引入版本号做控制
jdk中,采取CAS实现的API:
自旋的实现:
缺点:
创建线程的第三种方式:
Callable 方式结合Future,可以获取线程的执行结果
//代码
**实现原理:**锁定对象的对象头,jdk内部使用monitor机制,编译为字节码时,会生成monitorenter、monitorexit指令。(字节码中会包含一个monitor指令以及多个monitor指令。这是因为java虚拟机要确保所获得的锁在正常执行路径和异常执行路径上都能被解锁)
对象头锁状态:
Synchronized锁不能降级只能升级:提高获得锁和释放锁的效率
原因:级别低的锁的功能都能被级别高的锁保证,这时如果降级就没有必要,得不偿失。
JVM对synchronized的优化方案:根据不同场景,使用不同的锁机制
此处的StringBuffer是类变量,可以被多个线程操作
当JVM检测到有一一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁个解锁操作,即在最后一次append方法调用完成后进行解锁。
public class Test{
//StringBuffer是线程安全的
private static StringBuffer sb = new StringBuffer();
public static void main(String[] args) {
sb.append("a");
sb.append("b");
sb.append("c");
//到最后一次才释放锁
}
}
此处的StringBuffer是方法内的局部变量
是线程私有的,不需要加锁。
public class Test{
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
sb.append("a").append("b").append("c");
}
}
代码逃逸:即代码可能被其他线程执行或数据被其他线程操作。
产生原因:(同步本质)一个线程等待另外一个线程执行完毕后才能继续执行。但是如果现在相关的几个线程彼此之间都在等待对方,就会造成死锁
至少有两个线程互相申请的对象锁,造成互相等待的局面,彼此都无法继续
执行。
后果:线程阻塞等待,无法向下执行
解决方法:
实际中检测死锁的手段:
使用jdk的监控工具,比如jconsole、jstack查看线程状态
避免死锁的方法:银行家算法
jdk提供的一种除过Synchronized之外的加锁方式,定义了锁对象来进行锁操作。
Lock锁的特点:虽然它失去了像synchronized关键字隐式加锁解锁的便捷性,但是却拥有了锁获取和释放的可操作性,可中断的获取锁以及锁等多种synchronized关键字所不具备的同步特性。
Lock的使用:
Lock lock = new ReentrantLock();
lock.lock();//设置当前线程的同步状态,并在队列中保存线程及线程的同步状态
//设置成功,往下执行
try{
.......
}finally{
lock.unlock();//线程出队列
}
注意:Lock必须抵用unlock()方法释放锁,因此再finally块中释放锁,而synchronized同步块执行完成或者遇到异常锁会自动释放。
AQS(AbstractQueuedSynchronizer):队列式的同步器
实现原理:双端队列保存线程及线程同步状态。并通过CAS提供设置同步状态的方法:(如ReentrantLock实现时,调用ReentranLock实现时,调用lock.lock( )操作,不不停的设置线程同步状态)
关于队列:(1)双端队列(2)AQS中保存了队列的头尾结点
Lock锁---->AQS------->CAS(设置AQS中的线程同步状态)
1.提供公平锁和非公平锁(是否按照入队的顺序设置线程同步状态):多个线程申请加锁操作时,是否按照时间顺序来加锁
2.AQS提供的独占式和共享式设置同步状态(独占锁、共享锁)
独占式:只允许一个线程获取到锁
共享式:一定数量的线程共享式获取锁
本质:设置线程的同步状态(CAS)
3.待带Reentrant关键字的lock包下的APL:可重入锁
允许多次获取同一个Lock对象的锁
支持公平性、重入、锁降级
使用场景:多线程执行某个操作时:允许读读并发/并行执行,不允许读写,写写并发/并行执行。如多线程读写文件读读并发、读写、写写互斥
读锁和写锁之间,只能降级不能升级
优势:针对读读并发、提高运行效率
Condition:线程间通信
(1)通过lock.new Condition()获取Condition对象
(2)调用condition.wait()阻塞当前线程,并释放锁(=synchronized锁对象.wait( ))
(3)调用Condition对象.signal()/signalAll()通知之前阻塞的线程(=synchronized锁对象.notify()/notifyAll())
使用场景:隔离线程间的变量,保证每个线程是使用自己线程内的变量副本
代码推荐写法:
new Thread(()->{
try{
threadLocal.set(值);
}finally{
threadLocal.remove();
}
}
原理:Thread对象中都有自己的ThreaLocalMap,调用ThreadLocal对象设置值set(value)、获取值get()、删除值remove()、都是对当前线程中的ThreadLocalMap对象的操作,所以每个变量是线程隔离的。
Entry[] table中存放的K是属于弱引用
弱引用:被弱引用关联的对象的生存期是在下一次垃圾回收之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
若果Entry没有继承弱引用类型(K),导致线程没有使用值时,也一直有引用指向V,产生内存泄漏。
假设线程长时间没有执行完,K是强引用:ThreadLocal对象一直不能被回收,V也没办法使用,导致内存泄漏
设置K为弱引用的好处:降低内存泄漏的风险
每次垃圾回收,只要没有其他强引用指向ThreadLocal对象,就回收。ThreadLocalMap实现时,检查键为null时,就会把V变量设置为null,v指向的对象就没有引用了,就可以回收。