--------------------------------------------------------------------------------------------------
该文章为转载文章,直接复制粘贴的,请查看原文链接
--------------------------------------------------------------------------------------------------
在java中的锁分为以下(其实就是按照锁的特性和设计来划分):
1、公平锁/非公平锁
2、可重入锁
3、独享锁/共享锁
4、互斥锁/读写锁
5、乐观锁/悲观锁
6、分段锁
7、偏向锁/轻量级锁/重量级锁
8、自旋锁(java.util.concurrent包下的几乎都是利用锁)
从底层角度看常见的锁也就两种:Synchronized和Lock接口以及ReadWriteLock接口(读写锁)
从类关系看出Lock接口是jdk5后新添的来实现锁的功能,其实现类:ReentrantLock、WriteLock、ReadLock。
其实还有一个接口ReadWriteLock,读写锁(读读共享、读写独享、写读独享、写写独享)。
Lock接口与synchronized关键字本质上都是实现同步功能。
区别:ReentrantLock:使用上需要显示的获取锁和释放锁,提高可操作性、可中断的获取获取锁以及可超时的获取锁,默认是 非公平的但可以实现公平锁,悲观,独享,互斥,可重入,重量级锁。
ReentrantReadWriteLock:默认非公平但可实现公平的
,悲观,写独享,读共享,读写,可重入,重量级锁。
synchronized:关键字,隐式的获取锁和释放锁,不具备可中断、可超时,非公平、互斥、悲观、独享、可重入的重量级
Lock的使用也很简单:
Lock lock = new ReentrantLock();
lock.lock();
try{
}finally{
lock.unlock();
}
//注意:不要将lock方法写在try块中,因为如果在获取锁的时候发生异常,异常抛出的同时也会导致锁无故的释
//放 否则会程序会报监视状态异常
Exception in thread "线程一" java.lang.IllegalMonitorStateException
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:155)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1260)
at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:460)
//ReentrantLock必须要在finally中unlock(), 否则,如果在被加锁的代码中抛出了异常,那么这个锁将会永远无法释放.
//synchronized就没有这样的问题, 遇到异常退出时,会释放掉已经获得的锁.
Lock接口提供的 ,synchronized关键字所不具备的特性
特性 描述
尝试性非阻塞地获取锁(tryLock方法) 当前线程尝试的获取锁,如果这一时段没有被q其他线程获取,则成功的获取锁,否则直接返回false
能被中断的获取锁(lockInterruptibly()throws InterruptedException)
与synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放。
两种情况:
①:当前线程获取锁之前(并未参与获取锁)被其他线程标记interrupt中断,当调用此方法时直接抛出中断异常。
②:当前线程获取锁,并且锁被其他线程持有,则一直阻塞,此时其他线程来中断此线程,则会抛出中断异常。
超时获取锁(tryLock(long time,TimeUtil unit)throws InterruptedException)
在指定的时间内能够获取锁,超出时间仍热无法获取,则返回
会有以下3种情况:
①:当前线程在指定时间内获取了锁。
②:当前线程在指定时间内被中断,锁被释放。
3:当前线程在超出指定的时间,则直接返回false。
以下测试代码,测试
Lock lock = new ReentrantLock();
final MMT m = new MMT(lock);
Thread tt = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程一 开始执行。。。");
try {
m.update("张三");
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"被中断(锁释放)。。。");
}
System.out.println("线程一 结束执行。。。");
}
},"线程一");
Thread tt2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程二 开始执行。。。");
try {
m.update("李四");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
System.out.println(Thread.currentThread().getName()+"被中断(锁释放)。。。");
}
System.out.println("线程二 结束执行。。。");
}
},"线程二");
tt.start();
tt2.start();
//中断线程
tt.interrupt();
try {
tt.join();
tt2.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
class MMT {
String name;
Lock lock=null;
public MMT(Lock lock) {
this.lock=lock;
}
public void update(String name) throws InterruptedException{
// lock.lock();
// boolean tryLock = lock.tryLock();//尝试获取锁
//中断只是在当前线程获取锁之前,或者当前线程获取锁的时候被阻塞
// lock.lockInterruptibly();
lock.tryLock(3000, TimeUnit.SECONDS);
try{
setName(name);
System.out.println(Thread.currentThread().getName()+" 变换后的姓名为"+name);
}finally{
lock.unlock();
}
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
可实现公平锁
对于ReentrantLock而言,可实现公平锁 ,通过构造函数指定是否需要公平,默认是非公平,区别在与非公平随机性,并且高并发下吞吐量大,公平的话根据请求锁等待的时间长短,等待的长了优先,类似FIFO,吞吐量降低了。
锁绑定多个条件
指ReentrantLock对象可以同时绑定多个Condition条件对象,而在Synchroized中,锁对象的wait方法、notify方法、和notifyall方法可以实现一个隐含条件,如果需要多个,得额外的添加一个锁对象。在ReentrantLock中不需要,只需要创建多个条件对象即可(new Condition()),对应的await()、siganl()、signalAll()。
synchronized的优势
synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中
应用场景:
在资源竞争不激烈的情况下,synchronized关键字的性能优与ReentrantLock,相反,ReentrantLock的性能保持常态,优于关键字。
按照其性质划分:
公平锁/非公平锁
公平锁指多个线程按照申请锁的顺序来依次获取锁。非公平锁指多个线程获取锁的顺序并不是按照申请锁的顺序来获取,有可能后申请锁的线程比先申请锁的线程优先获取到锁,此极大的可能会造成线程饥饿现象,迟迟获取不到锁。由于ReentrantLock是通过AQS来实现线程调度,可以实现公平锁,,但是synchroized是非公平的,无法实现公平锁。
/**
* 公平锁与非公平锁测试
*/
public class FairAndUnFairThreadT {
public static void main(String[] args) throws InterruptedException {
//默认非公平锁
final Lock lock = new ReentrantLock(true);
final MM m = new MM(lock);
for (int i=1;i<=20 ;i++){
String name = "线程"+i;
Thread tt = new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<2;i++){
m.testReentrant();
}
}
},name);
tt.start();
}
}
}
class MM {
Lock lock = null;
MM(Lock lock){
this.lock = lock;
}
public void testReentrant(){
lock.lock();
try{
Thread.sleep(1);
System.out.println(Thread.currentThread().getName() );
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public synchronized void testSync(){
System.out.println(Thread.currentThread().getName());
}
}
但是未必绝对就是按照顺序,可能因为CPU准备原因,可能个别会不是公平的。
乐观锁与悲观锁
不是指什么具体类型的锁,而是指在并发同步的角度。悲观锁认为对于共享资源的并发操作,一定是发生xi修改的,哪怕没有发生修改,也会认为是修改的,因此对于共享资源的操作,悲观锁采取加锁的方式,认为,不加锁的并发操作一定会出现问题。乐观锁认为对于共享资源的并发操作是不会发生修改的,在更新数据的时候,会采用尝试更新,不断重试的方式更新数据。乐观的认为,不加锁的并发操作共享资源是没问题的。从上面的描述看除,乐观锁不加锁的并发操作会带来性能上的提升,悲观锁的使用就是利用synchroized关键字或者lock接口的特性。乐观锁在java中的使用,是无锁编程常常采用的是CAS自旋锁,典型的例子就是并发原子类,通过CAS自旋(spinLock)来更新值。
独享锁与共享锁
独享锁是指该锁一次只能被一个线程所持有。共享锁是指可被多个线程所持有。在java中,对ReentrantLock对象以及synchroized关键字而言,是独享锁的。但是对于ReadWriteLock接口而言,其读是共享锁,其写操作是独享锁。读锁的共享锁是可保证并发读的效率,读写、写写、写读的过程中都是互斥的,独享的。独享锁与共享锁在Lock的实现中是通过 AQS(抽象队列同步器)来实现的。
互斥锁与读写锁
互斥锁与读写锁就是具体的实现,互斥锁在java 中的体现就是synchronized关键字以及Lock接口实现类ReentrantLock,读写锁在java中的具体实现就是ReentrantReadWriteLock。
可重入锁
又名递归锁,是指同一个线程在外层的方法获取到了锁,在进入内层方法会自动获取到锁。对于ReentrantLock和synchronized关键字都是可重入锁的。最大的好处就是能够避免一定程度的死锁。
public sychrnozied void test() {
//执行逻辑,调用另一个加锁的方法
test2();
}
public sychronized void test2() {
//执行业务逻辑
}
在上面代码中,sychronized关键字加在类方法上,执行test方法获取当前对象作为监视器的对象锁,然后又调用test2同步方法。
一、如果锁是可重入的话,那么当前线程就在调用test2时并不需要再次获取当前锁对象,可以直接进入test2方法。
二、如果锁是不具备可重入的话,那么该线程在调用test2前会等待当前对象锁的释放,实际上该对象锁已被当前线程所持有不可能再此获得。那么就会发生死锁。
按照设计方案来分类(目的对锁的进一步优化)
自旋锁与自适应自旋锁(或者说是自旋锁的变种TicketLock、MCSLock、CLHLock)
底层采用CAS来保证原子性,自旋锁获取锁的时候不会阻塞,而是通过不断的while循环的方式尝试获取锁。优点:减少线程上下文切换的消耗,缺点是会消耗CPU。如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。
偏向锁、轻量级锁、重量级锁
这三种锁是指锁的状态,并且是针对Synchronized,在java通过引入锁升级的机制来实现高校的synchronized。锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁:指一段同步代码一直被同一个线程s所访问,那么该线程会自动的获取锁。降低获取锁的代价。
轻量级锁:当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁, 不会阻塞,提高性能。
重量级锁:当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没获取到锁
就会进入阻塞,该锁膨胀为重量级锁。重量级会让其他申请线程阻塞,性能降低。
偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。
一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个
线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将
对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。
一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。
分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
————————————————
版权声明:本文为CSDN博主「cuichunchi」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/cuichunchi/article/details/88532582