重庆分公司,新征程启航
为企业提供网站建设、域名注册、服务器等服务
一、公平锁/非公平锁
成都创新互联公司是专业的广灵网站建设公司,广灵接单;提供做网站、成都网站制作,网页设计,网站设计,建网站,PHP网站建设等专业做网站服务;采用PHP框架,可快速的进行广灵网站开发网页制作和功能扩展;专业做搜索引擎喜爱的网站,专业的做网站团队,希望更多企业前来合作!
公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
二、可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。说的有点抽象,下面会有一个代码的示例。
对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁。
对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。
synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception{
Thread.sleep(1000);
}
上面的代码就是一个可重入锁的一个特点,如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。
三、独享锁/共享锁
独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。
对于Java
ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
对于Synchronized而言,当然是独享锁。
四、互斥锁/读写锁
上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
互斥锁在Java中的具体实现就是ReentrantLock
读写锁在Java中的具体实现就是ReadWriteLock
五、乐观锁/悲观锁
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。
从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
六、分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
七、偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对Synchronized。在Java
5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
八、自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
典型的自旋锁实现的例子,可以参考自旋锁的实现
首先Java中的ReentrantLock 默认的lock()方法采用的是非公平锁。
也就是不用考虑其他在排队的线程的感受,lock()的时候直接询问是否可以获取锁,而不用在队尾排队。
下面分析下公平锁的具体实现。
重点关注java.util.concurrent.locks.AbstractQueuedSynchronizer类
几乎所有locks包下的工具类锁都包含了该类的static子类,足以可见这个类在java并发锁工具类当中的地位。
这个类提供了对操作系统层面线程操作方法的封装调用,可以帮助并发设计者设计出很多优秀的API
ReentrantLock当中的lock()方法,是通过static 内部类sync来进行锁操作
public void lock()
{
sync.lock();
}
//定义成final型的成员变量,在构造方法中进行初始化
private final Sync sync;
//无参数默认非公平锁
public ReentrantLock()
{
sync = new NonfairSync();
}
//根据参数初始化为公平锁或者非公平锁
public ReentrantLock(boolean fair)
{
sync = fair ? new FairSync() : new NonfairSync();
}
一、synchronized和lock的用法区别
synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
lock:需要显示指定起始位置和终止位置。一般使用ReentrantLock类做为锁,多个线程中必须要使用一个ReentrantLock类做为对象才能保证锁的生效。且在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。
用法区别比较简单,这里不赘述了,如果不懂的可以看看Java基本语法。
二、synchronized和lock性能区别
synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。
说到这里,还是想提一下这2中机制的具体区别。据我所知,synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。
现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。
我也只是了解到这一步,具体到CPU的算法如果感兴趣的读者还可以在查阅下,如果有更好的解释也可以给我留言,我也学习下。
三、synchronized和lock用途区别
synchronized原语和ReentrantLock在一般情况下没有什么区别,但是在非常复杂的同步应用中,请考虑使用ReentrantLock,特别是遇到下面2种需求的时候。
1.某个线程在等待一个锁的控制权的这段时间需要中断
2.需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程
3.具有公平锁功能,每个到来的线程都将排队等候
java使用锁来保证多线程操作共享资源的原子性、可见性、有序性。同时锁一般这样性质:可重入、是否公平。
java使用synchronized来获取锁,并会隐式释放锁,该锁是一个可重入的非公平锁。
在jdk5后,可以使用ReentranLock来完成多线程同步问题,具有跟synchronized一样的功能,支持可重入和非公平锁
如果我们查看源码源码,就会发现
加锁:lock.lock() - Sync.lock - AbstractQueuedSynchronizer.acquire(1)
释放锁:lock.unlock() - Sync.release(1) - AbstractQueuedSynchronizer.acquire(1)
锁的实现主要靠AbstractQueuedSynchronizer(抽象队列同步器)
主要的逻辑是,先尝试获取锁,获取失败则线程封装成一个节点添加到线程同步列表中,并响应线程中断
这两个方法通过cas实现的一个同步队列,保证了添加节点,在多线程环境中将会正确的被添加到队列的末尾。compareAndSetHead()和compareAndSetTail()就是通过cas来完成头部、尾部结点的替换的
acquire方法主要做的是:如果一开始尝试获取锁失败,则把线程封装成同步队列的结点,并加入到同步队列尾结点,同时开始自旋,当该线程结点是同步队列的首个线程结点(非头结点),尝试获取锁。这一条件判断是final Node p = node.predecessor(); if (p == head);为什么是判断线程结点的前驱指向头结点来确定是否是首个线程结点?而不是取头结点的后驱结点?就是因为addWaiter所构建的同步队列,在对象线程中能保证 head - node - tail的前驱指向性的正确,无法保证后驱的指向性是正确的。锁的获取到这一步就完成
释放锁的时候,先尝试释放锁,成功则查看同步队列的头结点下个节点(首个线程节点)是否等待唤醒,如果需要唤醒,这个唤醒盖线程。这个主要是条件等待引起的,先不讨论。
ReentranLock是一个可重入锁且实现公平锁和非公平锁,其内部同步队列器具体实现是
线程同步是否成功修改同步器的state来判断是否获取锁成功。nonfairTryAcquire,如果获取锁成功,设置当前线程为锁的独占线程。如果已有线程获取到锁,则判断当前线程是否是锁的独占线程,是则算获取锁成功,以此实现锁可重入的问题
公平锁比非公平锁在尝试获取锁多一个判断hasQueuedPredecessors(),该方法是判断同步队列是否存在线程结点,且同步队列首个线程结点中的线程不等于当前线程。从而保证当每个线程获取锁的时候,如果同步队列存在线程结点,该线程获取锁失败从而加入到同步队列的尾结点,保证获取锁的顺序,即公平性。在可重入的实现与非公平锁一致。
因为这独占锁,先判断获取锁的线程是否是当前线程,如果state归零,代表当前线程不持有锁,设置锁的独占线程为空,然后修改同步器state。
首先来看公平锁和非公平锁,我们默认使用的锁是非公平锁,只有当我们显示设置为公平锁的情况下,才会使用公平锁,下面我们简单看一下公平锁的源码,如果等待队列中没有节点在等待,则占有锁,如果已经存在等待节点,则返回失败,由后面的程序去将此线程加入等待队列
通过上面的代码,我们可以推断,当使用公平锁的情况下,并且同一个线程的执行时间较长时,线程内部进行了多次的锁的获取和释放,效率非常低下,可以参加Lesson8中的demo:
demo Lesson8LockIntPerform:在使用ReentrantLock加非公平锁的情况下100个线程循环下单数为:857239882
demo Lesson8LockIntPerform:在使用ReentrantLock加非公平锁的情况下100个线程循环下单数为:860364303
demo Lesson8LockFairIntPerform:在使用ReentrantLock加公平锁的情况下100个线程循环下单数为:19153640
demo Lesson8LockFairIntPerform:在使用ReentrantLock加公平锁的情况下100个线程循环下单数为:19076567
上面的demo中,在使用公平锁的情况下性能明显降低,非公平锁的性能是公平锁性能的几十倍以上,这和公平锁每次试图占有锁时,都必须先要进等待队列,按照FIFO的顺序去获取锁,因此在我们的实验情景下,使用公平锁的线程进行了频繁切换,而频繁切换线程,性能必然会下降的厉害,这也告诫了我们在实际的开发过程中,在需要使用公平锁的情景下,务必要考虑线程的切换频率。
接下来我们来看一下读写锁,通过看读写锁的实现源码,我们可以发现,读锁和写锁共用同一个等待队列,那么在采用非公平锁的情况下,如果读锁的线程执行时间比较长,并且读锁的并发比较高,那么写锁的线程便永远都拿不到锁,那么实际的情况会不会是这样呢?
demo Lesson3WriteReadLock:此demo的读线程在不断的占用读锁,按照推论,写锁的线程是没有机会获取到锁的,但是实际情况是写锁的线程可以正常的获取到锁,那么是什么原因使得写锁的线程可以获取到锁的了?通过查看源代码,会发现有这样的一个方法:
上面的方法,实现了一个新的读线程获取锁的中断,它会读取等待队列中下一个等待锁的线程,如果它是获取写锁的线程,那么此方法返回为真,调用它的程序会把这个试图获取读锁的线程加入到等待队列,从而终止了读线程一直都在占有锁的情况。
ReentrantLock和condition是配合着使用的,就像wait和notify一样,提供一种多线程间通信机制。
ReentrantLock 的lock方法有两种实现:公平锁与非公平锁
看newCondition的源码实现:
final ConditionObject newCondition() {
return new ConditionObject();}
其实就是只实例化一个个conditionObject对象绑定到lock罢了。也就是拿到了监视器,再深入到conditionObject这个里面实现看看await方法:
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException(); Node node = addConditionWaiter(); int savedState = fullyRelease(node); int interruptMode = 0; while (!isOnSyncQueue(node)) {
LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break; }
if (acquireQueued(node, savedState) interruptMode != THROW_IE)
interruptMode = REINTERRUPT; if (node.nextWaiter != null) // clean up if cancelled unlinkCancelledWaiters(); if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);}
大概就是将当前线程加入等待队列,其中做一些逻辑判断,再来看看唤醒的方法:singal和singalAll:
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}
其实就是将等待队列里面的线程依次唤醒罢了,doSingalAll:
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
transferForSignal将线程转移到syncQueue重新排队,这里主要用到CAS(lock free)算法改变状态:
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
Node p = enq(node);
int ws = p.waitStatus;
if (ws 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
篇幅有限,没有详细描述...反正多看看源码吧,结合着实例分析