java多线程无锁和工具类
1 无锁
(1) cas (compare and swap) 设置值的时候,会比较当前值和当时拿到的值是否相同,如果相同则设值,不同则拿新值重复过程;注意,在设置值的时候,取值+比较+设值 是一条cpu语句,在这个过程中不会有其他线程干扰,是原子操作。从指令层面保证操作可靠。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。
(2)无锁类 无锁算法都处于无限循环之内,先进行compare,成功后swap,不成功则一只循环compare。
AtomicInteger get() set() getAndIncrement() incrementAndGet() compareAndSet().... 封装了一个volatile的int value,以incrementAndGet()为例,会使用当前值,偏移量,和期望值做判断是否increament;getAndIncreament() 拿到值以后再加一,用当前值和期望值比较 如果相同则没被其他线程打扰,不同则重试。
AtomicReference 使用:如a= new AtomicReference("a") ,多线程调用compareAndSet("a","bc") 那么会使用cas给a对象新的引用,只有一个线程会修改成功(因为成功之后原始引用就变了 compare会失败),防止并发修改引用。
AtomicStampedReference stamped是时间戳,对atomicReference的加强。如果一个引用,经过了A→B→A的过程,一个线程在赋值进行cas的时候,compare得到后面一个A,认为没有变化于是set,这种情况对注重结果的义务没影响,但是对于过程敏感(需要记录过程变化的业务)的情况相当于忽略了一个变化过程。针对后一种情况,在每个引用改变的时候再加个时间戳,可以标注引用的变化情况,compare的时候即比较原始值又比较时间戳,失败的话重新拿值。
AtomicIntegerArray int型数组,每个元素都是cas线程安全的。
AtomicIntegerFieldUpdater 让普通变量也可以原子操作,.newUpdater(xxx.class,"字段名称") 新建更新器,用反射对某个类的某个字段使用原子操作,同时需要手动对该变量加上volatile关键字,使用更新器的increamentAndGet可以实现对该变量的原子操作。
*范例 vector 是加锁同步工具类,整体加锁。可以设计一个无锁vector,LockFreeVector. 内部封装一个AtomicReference<AtomicReference> 二维数组,当数据扩充的时候,直接在二维位置上加一个容量大一倍的数组,而不是像一般vector一样声明一个两倍大小的空数组再拷贝原来数据。相当于拥有好多个buckets,每个bucket大小都是前一个的两倍,总共30个bucket,绝对够用。扩容时,使用compareAndSet 期望原始值是null,新值是新的长度两倍的数组。add的时候 先算出应该在哪个bucket和bucket上那个位置,然后还是cas,期望原始值是null实际值为新值。
2 并发包 concurrent
(1)ReentrantLock sychronized升级版,lock=new ReentrantLock; lock.lock();lock.unlock();需要程序控制何时释放锁,增强灵活性;
可重入,可以多次lock(),lock几次就得unlock几次,每次重入lock值+1。
可中断,lock.lockinterruptibly(),这个函数加锁可中断,抓异常处理中断,可预防死锁。
可限时,lock.tryLock(),申请锁的时间加上限制,避免一直申请不到锁浪费时间。
公平锁,保证线程先到先得,但公平锁要处理排队,性能比一般锁差。
*synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过jdk代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中。
* 在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
(2)Condition 和reentrantlock成对使用,类似wait和notify,使用await()和signal(),挂起线程和唤醒,挂起释放锁,唤醒重新争夺锁。也和wait相似,抓中断异常来处理中断;如果调用awaitUninterruptibly() 则不会响应中断。原理是维护一个等待队列,把await的线程放入等待队列,让出cpu时钟,signal的时候把等待队列的第一个放入lock的执行队列。
1).await()方法是可以响应线程中断命令的;
2).await()和signal()方法在当前线程锁定了Condition对应的锁时才能使用;
3).一个锁对应多个Condition对象,每个Condition的signal()方法只通知相同Condition的await()方法,condition之间不会互相通知;
4).signal()被调用时,wait队列中的第一个对象会被唤醒,signalAll()时,wait队列中元素的全部按先入先出的顺序被唤醒;
5).如果既存在await的线程,又存在一直等待lock()的线程,当signal()的线程完成时,lock()的线程优先级比await()的线程优先级高,当所有lock()线程获取到锁并释放后,才会轮到await()线程。
(3)Semaphore 信号量,允许若干个线程进入临界区,如果信号量是1,就是普通的线程(具有排他性)。new Semaphore(5);初始化信号量个数,semp.acquire()和 semp.release()代表拿到信号量和释放信号量,如果初始信号量为1,代表只有一个线程可以进入临界区,跟普通加锁一样。信号量可以更灵活的资源分配。
(4)ReadWriteLock 允许多读单写,所有read线程是无等待并发。new ReentrantReadWriteLock(),读锁readLock(),写锁writeLock。读写互斥,读读允许。
读锁和写锁之间满足如下的约束:
1)当任一线程持有写锁或读锁时,其他线程不能获得写锁;
2)当任一线程持有写锁时,其他线程不能获取读锁;
3)多个线程可以同时持有读锁。
* 获取写锁以后不释放写锁可以继续获取读锁,但是获得读锁以后不释放获得写锁不可以。写锁可以降级为读锁,如果获得写锁在获得读锁,再释放写锁,读锁仍然持有。
(5)CountDownLatch 倒数计数器,所有线程都结束在继续某操作时可以使用。
(6)CyclicBarrier 循环栅栏,countDownLatch 是能设定一次,latch是–,完了以后就没用了,CyclicBarrier可以循环使用,计数是++。
*使用coutdownlatch,是把分散在各线程的路径最终统一到主线程(主线程await),cyclicBarrer是在各线程中await,每次await就++,到达初始设置的栅栏值后继续往下走,相当于所有参与者线程都到了(await)以后才继续往下走,使用时会抓异常防止某线程中断无法await导致其他线程都处在await状态无法往下走。
(7)LockSupport park() unPark() 挂起 继续线程,类似suspend,但是suspend有缺陷,如果resume发生在suspend之前,那么suspend挂起后会永远挂起;LockSupport如果先unPark 在park,不会挂起不动。park/unpark的设计原理核心是“许可”。park是等待一个许可。unpark是为某线程提供一个许可。如果某线程A调用park,那么除非另外一个线程调用unpark(A)给A一个许可,否则线程A将阻塞在park操作上。