Java的各种锁
2018-08-24 本文已影响41人
帅可儿妞
-
锁的分类
- 自旋锁:自旋,jvm默认是10次吧,有jvm自己控制。for去争取锁
- 阻塞锁:被阻塞的线程,不会争夺锁。
- 可重入锁:多次进入改锁的域
- 读写锁:
- 互斥锁:锁本身就是互斥的
- 悲观锁:不相信,这里会是安全的,必须全部上锁
- 乐观锁:相信,这里是安全的。
- 公平锁:有优先级的锁
- 非公平锁:无优先级的锁
- 偏向锁:无竞争不锁,有竞争挂起,转为轻量锁
- 对象锁:锁住对象
- 线程锁:
- 锁粗化:多锁变成一个,自己处理
- 轻量级锁:CAS 实现
- 锁消除:偏向锁就是锁消除的一种
- 锁膨胀:jvm实现,锁粗化
- 信号量:使用阻塞锁 实现的一种策略
- 排它锁:X锁,若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。这就保证了其他事务在T释放A上的锁之前不能再读取和修改A。
-
自旋锁
- 自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时 才能进入临界区
// 注:该例子为非公平锁,获得锁的先后顺序,不会按照进入lock的先后顺序进行。 public class SpinLock { private AtomicReference<Thread> sign =new AtomicReference<>(); public void lock(){ Thread current = Thread.currentThread(); while(!sign .compareAndSet(null, current)){ } } public void unlock (){ Thread current = Thread.currentThread(); sign .compareAndSet(current, null); } }
- 使用了CAS原子操作,lock函数将owner设置为当前线程,并且预测原来的值为空。unlock函数将owner设置为null,并且预测值为当前线程。当有第二个线程调用lock操作时由于owner值不为空,导致循环一直被执行,直至第一个线程调用unlock函数将owner设置为null,第二个线程才能进入临界区。由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。
- 自旋锁还有三种常见的锁形式:TicketLock ,CLHlock 和MCSlock
- Ticket锁主要解决的是访问顺序的问题,主要的问题是在多核CPU上,代码如下,每次都要查询一个serviceNum 服务号,影响性能(必须要到主内存读取,并阻止其他CPU修改)。
import java.util.concurrent.atomic.AtomicInteger; public class TicketLock { private AtomicInteger serviceNum = new AtomicInteger(); private AtomicInteger ticketNum = new AtomicInteger(); private static final ThreadLocal<Integer> local = new ThreadLocal<Integer>(); public void lock() { int myticket = ticketNum.getAndIncrement(); local.set(myticket); while (myticket != serviceNum.get()) {} } public void unlock() { int myticket = local.get(); serviceNum.compareAndSet(myticket, myticket + 1); } }
- CLHLock:Craig, Landin, and Hagersten Locks,是一个自旋锁,能确保无饥饿性,提供先来先服务的公平性;CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋;
- 当一个线程需要获取锁时:
- 创建一个的CLHNode,将其中的locked设置为true表示需要获取锁;
- 线程对tail域调用getAndSet方法,使自己加入到队列的尾部,同时获取一个指向其前趋结点的引用preNode;
- 该线程就在前趋结点的locked字段上旋转,直到前趋结点释放锁;
- 当一个线程需要释放锁时,将当前结点的locked域设置为false,同时回收前趋结点;
- 示例代码如下:
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; public class CLHLock { public static class CLHNode { private volatile boolean isLocked = true; } private volatile CLHNode tail; private static final ThreadLocal<CLHNode> local = new ThreadLocal<CLHNode>(); private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> updater = AtomicReferenceFieldUpdater.newUpdater(CLHLock.class, CLHNode.class, "tail"); public void lock() { CLHNode node = new CLHNode(); local.set(node); CLHNode preNode = updater.getAndSet(this, node); if (preNode != null) { while (preNode.isLocked) {} preNode = null; local.set(node); } } public void unlock() { CLHNode node = local.get(); if (!updater.compareAndSet(this, node, null)) { node.isLocked = false; } node = null; } }
- 当一个线程需要获取锁时:
- MCSLock则是对本地变量的节点进行循环。MSC与CLH最大的不同并不是链表是显示还是隐式,而是线程自旋的规则不同:CLH是在前趋结点的locked域上自旋等待,而MCS是在自己的结点的locked域上自旋等待。正因为如此,它解决了CLH在NUMA系统架构中获取locked域状态内存过远的问题;不存在CLHlock 的问题。
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; public class MCSLock { public static class MCSNode { volatile MCSNode next; volatile boolean locked = true; } private static final ThreadLocal<MCSNode> node = new ThreadLocal<MCSNode>(); private volatile MCSNode queue; private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> updater = AtomicReferenceFieldUpdater.newUpdater(MCSLock.class, MCSNode.class, "queue"); public void lock() { MCSNode currentNode = new MCSNode(); node.set(currentNode); MCSNode preNode = updater.getAndSet(this, currentNode); if (preNode != null) { preNode.next = currentNode; while (currentNode.locked) {} } } public void unlock() { MCSNode currentNode = node.get(); if (currentNode.next == null) { if (updater.compareAndSet(this, currentNode, null)) { } else { while (currentNode.next == null) {} } } else { currentNode.next.locked = false; currentNode.next = null; } } }
- Ticket锁主要解决的是访问顺序的问题,主要的问题是在多核CPU上,代码如下,每次都要查询一个serviceNum 服务号,影响性能(必须要到主内存读取,并阻止其他CPU修改)。
- 自旋锁总结
- 从代码上 看,CLH 要比 MCS 更简单;
- CLH 的队列是隐式的队列,没有真实的后继结点属性;
- MCS 的队列是显式的队列,有真实的后继结点属性;
- JUC ReentrantLock 默认内部使用的锁 即是 CLH锁(有很多改进的地方,将自旋锁换成了阻塞锁等等);
- 自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时 才能进入临界区
- 阻塞锁
- 与自旋锁不同,改变了线程的运行状态。在JAVA环境中,线程Thread有如下几个状态:1,新建状态;2,就绪状态;3,运行状态;4,阻塞状态;5,死亡状态
- 阻塞锁,可以说是让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。
- JAVA中,能够进入\退出、阻塞状态或包含阻塞锁的方法有 ,synchronized 关键字(其中的重量锁),ReentrantLock,Object.wait()\notify(),LockSupport.park()/unpart()(JUC经常使用)
- 下面是一个JAVA 阻塞锁实例
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.concurrent.locks.LockSupport; public class CLHLock1 { public static class CLHNode { private volatile Thread locked; } private volatile CLHNode tail; private static final ThreadLocal<CLHNode> local = new ThreadLocal<CLHNode>(); private static final AtomicReferenceFieldUpdater<CLHLock1, CLHNode> updater = AtomicReferenceFieldUpdater.newUpdater(CLHLock1.class, CLHNode.class, "tail"); public void lock() { CLHNode node = new CLHNode(); local.set(node); CLHNode preNode = updater.getAndSet(this, node); if (preNode != null) { preNode.locked = Thread.currentThread(); LockSupport.park(this); preNode = null; local.set(node); } } public void unlock() { CLHNode node = local.get(); if (!updater.compareAndSet(this, node, null)) { System.out.println("unlock\t" + node.locked.getName()); LockSupport.unpark(node.locked); } node = null; } }
- 在这里我们使用了LockSupport.unpark()的阻塞锁。 该例子是将CLH锁修改而成。阻塞锁的优势在于,阻塞的线程不会占用cpu时间, 不会导致 CPu占用率过高,但进入时间以及恢复时间都要比自旋锁略慢。在竞争激烈的情况下 阻塞锁的性能要明显高于 自旋锁。理想的情况则是; 在线程竞争不激烈的情况下,使用自旋锁,竞争激烈的情况下使用,阻塞锁。