2、保证同步方案(点个赞呗)
2020-07-08 18:36:22 0 举报
AI智能生成
juc并发同步方案
作者其他创作
大纲/内容
保证同步方案
synchronized(最底层汇编语句:lock cmpxchg,执行加锁)(优先使用,非常追求效率的话用新锁)
特性
1、互斥性:同一时间只有单个线程持有对象锁,也称为操作的原子性
2、可见性:确保在释放锁后,其他线程可见该变量的修改
3、线程安全
4、可重入、非公平锁
锁升级(早期为重量级锁,1.6以后引入锁升级概念)
1、偏向锁(new完对象默认启动,但jvm启动后4s才会开始加锁,只在对象头两位记录线程号,实际不加锁,2bit,有其他线程竞争时出现锁膨胀,并进行锁撤销,撤销偏向锁升级自旋锁,有调用wait方法直接升级重量级锁)
问题
1、偏向锁效率一定比自旋锁高吗
因为偏向锁升自旋锁有一个锁撤销操作,所以如果明确知道会有多个线程进行争抢,那就直接上自旋锁好了
2、为什么jvm启动后4s才会启动偏向锁
jvm启动后会加载很多资源,此时如果有多个线程进行争抢锁的话,那就没必要启动偏向锁了直接升级为自旋锁,所以就延迟一下可以通过调节参数 -xx:BiasedLockingStartupDelay=0 设置延迟时间为0
2、自旋锁(CAS-无锁优化,默认自旋10次升级,-XX:PreBlockSpin控制) 轻量级锁主要包括:自旋锁、自适应自旋锁(自适应升级循环次数)
cas
性质
非阻塞同步、乐观锁,让线程空循环等待
原理
cas(对象-A,期望值-x,预设值-y)
1、先获取A的值:x=A的值
2、然后判断:若A=x,则设值A=y 若A≠x,则不设值,等下再来判断一次,直到判断成功
cpu原语支持(指令连续执行)确保不会出现刚判断完A=x,还没设值A=y的时候有线程将A改成z
实现
AtomicInteger等AtomicXXX原子类和LongAdder类底层也是通过cas实现,性能方面:(循环数多,线程多):LongAdder>AtomicLong>synchronized,因为longAdder使用分段锁(将线程分成n份分别执行,然后累加结果)所以性能高,因为cas为无锁操作,所以比synchronized性能高。
适合场景
适合:线程数量小,且方法执行时间短的情况,不需要加锁,速度快
不适合:线程数量大,方法执行时间长的情况,大量线程自旋或长时间自旋容易造成cpu资源开销过大
与重量级锁对比
优点
1、线程安全
2、不需要cpu锁,速度快
缺点
1、可能造成资源过多问题:若线程量大,方法执行时间长的话容易造成cpu开销过大
2、只能保证一个共享变量的原子操作
3、可能存在ABA问题:在获取A的值后,A对象值变为B,然后再变回A, 如果是int等类型则无所谓,如果是对象的话可能会造成不可预计的问题(可通过加version版本号进行解决)
3、重量级锁(cpu锁,也被称为互斥锁)
阻塞同步、悲观锁,依赖对象内部monitor锁,而monitor锁依赖操作系统的mutexLock(互斥锁)
耗时长,用户态转换到内核态(kernal)需要操作系统帮忙,切换时间可能比方法执行时间还长
锁目标
锁的是对象不是代码(Object lock)
wait()
阻塞,释放锁
notify()
叫醒,不释放锁
简单来说,对于同一个lock对象,当第一个线程lock.wait()后,第二个线程lock.notify(),会唤醒第一个线程,但由于notify不会释放锁,所以第一个线程还是会等第二个线程执行完才能执行,除非第二个线程notify后,马上wait让出锁,那第一个线程就会继续执行,第一个线程执行完记得要notify唤醒第二个线程
可加位置
1、静态(锁该类所有对象)
锁“类”(class)
类
静态方法
2、非静态(this:锁当前对象)
锁“对象”(Object、this)不要锁:Integer、Long、String等基本对象类型
非静态方法
代码块
对象创建在内存中存放
设:类T中有一个int变量m,现在new T();
1、markWord
大小
8字节
记录内容
1、锁信息(无锁态-new、偏向锁、轻量级锁。。。)
2、GC标记信息
3、hashCode(如果有调用)
意义
我们平时所说的加锁实际就是修改markWord里面的内容
hotspot实现
2、klass pointer
默认4字节,指向T.class的指针,就是这个对象属于哪个class的就指向这个class的地址
3、m
4个字节(int)
4、padding
对齐,补全,如虚拟机为64位,那么4部分总体加起来应该是8的倍数,不够的字节就在这里补全
volatile(最底层汇编语句:lock add,内存屏障,64位操作系统是addl)
1、线程不安全
2、保证线程可见性
通过cpu的缓存一致性协议(EMSI)确保一个线程修改变量后,其他线程可以马上去读取
3、禁止指令重排序(cpu)
实现(cpu层)
读屏障、写屏障防止指令重排序
DCL单例(懒汉模式-双重检查加锁-Double Check Lock)
两次检查
第一次检查:不加锁,效率高
第二次检查:加锁,防止两个线程都判空后会重复new对象
疑问点
1、为什么不把synchronized加在方法上:锁细化,内部可能有其他业务逻辑
2、为什么需要加volatile:为了禁止指令重排序,让对象初始化按顺序执行。如果没有volatile,指令重排序可能在对象初始化(1)执行完后就赋值给执行(3),此时还未执行(2)的时候,第二个线程看到有值了(此时是初始值,未赋值),就拿去用了,那就会造成问题
对象初始化过程
1、new对象赋初始值
2、给变量赋值
3、指向变量地址(即赋值给对象)
4、非基本字段不应该用volatile修饰。其原因是volatile修饰对象或数组时,只能保证他们的引用地址的可见性,如果对象的属性加了volatile,那这个对象对其他的线程都是可见的,只是他并的更新时间不一定,修改后并不一定能马上读取到
AQS
JUC:基于cas的新类型锁(底层基于AQS的队列实现)
ReentrantLock(替代synchronized)
常用方法
new ReentrantLock(true)
设true为公平锁(默认为非公平锁):新线程会检查队列中有没有内容,如果有(即有线程在等这把锁)则进队列等别人先运行
lock
获取锁
unlock
释放锁,try中获取锁后必须在finally中释放锁
tryLock
尝试锁定,比如5秒钟获得锁,那就进行unlock前的操作,否则就不进行
lockInterruptibly
可以被打断的加锁(对interrupt方法做出响应),如果只是用lock方法的话,那获取不到锁就会卡死在这
interrupt
停止线程等待,停止lockInterruptibly的加锁等待
与synchronized区别
1、synchronized系统自动加锁解锁,ReentrantLock需要手动加解锁
2、R使用CAS实现,S通过锁升级
其他:tryLock、lockInterruptibly,以及可以进行公平与非公平的切换,可以指定唤醒某些线程
CountDownLatch(和线程的join类似功能)
new CountDownLatch(线程数)
用线程数设置门栓数
countDown
门栓数-1,在线程方法结束时使用该方法(也可以在一个线程中countDown多次)
await
阻塞,直到门栓数=0时解除阻塞
CyclicBarrier(满parties个线程就继续执行,否则阻塞)
new CyclicBarrier(parties)
创建一个CyclicBarrier,解除标准是线程等待数=parties
等待数+1
Phaser(阶段控制)
概念
可以控制有n个线程全部完成某个阶段的任务时才能进入下一步
ReentrantReadWriteLock(读写锁、大量读少量写)
共享锁(读)、排他锁(写)
读线程加锁后,另一个读线程进来可以读,另一个写线程进来则阻塞
ReadWriteLock readWriteLock = new ReentrantReadWriteLock()
设置读写锁
Lock readLock = readWriteLock.readLock ()
设置读锁,在读线程加读锁
Lock writeLock = readWriteLock.writeLock()
设置写锁,在写线程加写锁
使用场景
大量读线程、少量写线程时,读写锁可以大量提升效率,否则所有线程都要一个一个锁住执行,效率很低,比如公司结构图(大量读该结构,很少修改该结构)
补充学习
StampedLock
Semaphore(信号灯,限流)
简单来说就是设置一个信号灯,灯亮着的时候就可以执行,不亮就不能执行
new Semaphore(permits)
指定允许数量
new Semaphore(permits,fair)
fair指定是否为公平锁,默认为false非公平
acquire
得到一个信号量(得到信号量后需要在finally中释放)
release
释放得到的信号量
限流:最多有多少线程运行,比如最多只能开5个窗口卖票
Exchanger(交换器)
简单来说就是一个线程和另一个线程通过exchange方法交换数据
Exchanger<String> exchanger = new Exchanger<>();
定义一个交换器并制定类型
exchange
将数据放到exchanger中并阻塞(可以指定timeout超时时间),当有另一个线程来放的时候,把两个值交换,然后释放,有点CyclicBarrier的味道,满两个才交换,然后继续执行
游戏中两个人交换装备
底层实现
组成(volatile+cas)
1、volatile int state
资源状态,每种锁自己具体实现,比如ReentrantLock就是1表示占有资源
2、transient Thread exclusiveOwnerThread;
占据资源的线程(该变量是继承的AbstractOwnableSynchronizer抽象类)
3、存放Thread的node节点
CLH 同步等待队列
拓展
jdk1.9以后设置CLH后置节点有一个VarHandle的类
1、能做到普通属性的原子性操作
2、比反射快,直接操作二进制码修改变量
LockSupport
是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后可以通过unpark方法唤醒。
park()
当前线程阻塞
LockSupport.unpark(t)
叫醒t线程,如果unpark在start后马上调用,就可以让中间的park失效,不会阻塞(两次park会阻塞)。unpark可以先于park调用
拓展:实现一个同步容器(背过)
synchronized和wait、notify
ReentrantLock
ThreadLocal相关
java引用类型
强
无引用时被回收
软(new SoftReference(student))
内存不够时回收软引用对象
可以做缓存用,Memcache
弱(new WeakReference(student))
只要发生垃圾回收对象就会被回收,一般用在容器里
存在的意义就是指向该引用的强引用没有了,他就应该被回收
ThreadLocal底层的map中用到。Tomcat、WeakHashMap
没法get到对象,垃圾回收时会把引用扔到队列中(通过启动一个线程监测该队列来进行清理引用的操作),用在引用堆外内存的场景
作用
管理堆外内存,由于jvm无法回收堆外内存,所以当调用一个堆外内存的时候可以通过虚引用指向该内存,当虚引用放到队列中后,先去检查并干掉堆外内存对象,再干掉虚引用
ThreadLocal
线程本地对象,创建一个只能在该线程中能用的对象
场景
(spring中事务的connection)一般在方法层层嵌套的情况下想快速取某个对象不想方法间传参的情况设置
set
先找到当前线程,然后将threadLocal当k设值到线程的map中(该map中的entry继承弱引用,k是弱引用的)
1、为什么threadLocalMap的entry要用弱引用?
如果不使用弱引用的话,那么即便外部调用threadLocal的引用没了被置空,但map的key还是指向这个对象,那这个对象就会一直存在无法被回收,那就可能会造成内存泄露的问题
2、使用弱引用就没有内存泄露了吗?
不是,还是会存在,因为一旦发生垃圾回收,该弱引用对象被回收了,key变成null,那会导致value无法被访问到,因此也存在内存溢出问题
threadLocal在set后主动进行remove操作,就可以避免出现内存泄露
问题总结
key若为强引用,threadLocal对象 = null;此时key指向对象无法被回收,内存泄露
解决:key设置为弱引用
key是弱引用被回收,key = null;
value指向对象无法被回收,内存泄露
解决
1、在调用ThreadLocal的get、set方法时候会对key==null的对象进行清除回收(但可能后面不会调用到这些方法,导致无法被回收)
2、用完threadLocal对象后手动调用remove方法回收该对象(此时无内存泄露问题)
问题遗留
1、如果在调用remove方法前key被回收,那就无法调用remove方法了,还是存在value内存泄露
2、如果既然需要调用remove方法进行手动回收,那为什么还需要将key设置为弱引用
声明事务,保证同一个connection
0 条评论
下一页
为你推荐
查看更多