这三种锁特指synchronized锁的状态,经过对象头中的mark work字段表示锁状态。java
偏向锁:程序员
自始至终,对这把锁都不存在竞争,只须要作个标记,这就是偏向锁,每一个对象都是一个内置锁(内置锁是可重入锁),一个对象被初始化后,尚未任何线程来获取它的锁时,那么它就是可偏向的,当有线程来访问并尝试获取锁的时候,他就会把这个线程记录下来,之后若是获取锁的线程正式偏向锁的拥有者,就能够直接得到锁,偏向锁性能最好。编程
轻量级锁:安全
轻量级锁是指原来是偏向锁的时候,这时被另一个线程访问,存在锁竞争,那么偏向锁就会升级为轻量级锁,线程会经过自旋的形式尝试获取锁,而不会陷入阻塞。微信
重量级锁:并发
重量级锁是互斥锁,主要是利用操做系统的同步机制实现的,当多个线程直接有并发访问的时候,且锁竞争时间长的时候,轻量级锁不能知足需求,锁就升级为重量级锁,重量级锁会使得其余拿不到锁的线程陷入阻塞状态,重量级锁的开销相对较大。app
可重入锁:dom
可重入锁的意思是若是当前线程已经持有了这把锁,能再不释放的锁的状况下再次得到这把锁,若是一个线程试图获取它已经持有的锁,那么这个请求会成功,每一个锁关联一个获取计数值和一个全部者线程,当计数值为0的时候,认为没有线程持有该锁,当线程请求一个未被持有的锁的时候,JVM将记下锁的持有者,而且计数值置为1,若是同一个线程再次获取该锁的时候,计数值将递增,jvm
不可重入锁:ide
同理不可重入锁就是指虽然当前线程已经持有了这把锁,可是若是想要再次得到这把锁,必需要先释放锁后才能再次尝试获取。
共享锁:
共享锁就是咱们能够同一把锁被多个线程同时得到,最典型的就是读写锁中的读锁。
独占锁:
同理,独占锁就是线程只能对一个线程持有,类比读写锁中的写锁,
公平锁:
公平锁就是若是线程当前拿不到这把锁,那么线程就都会进入等待,开始排队,在等待队列里等待时间长的线程就会优先拿到这把锁,先来先得。
非公平锁:
非公平锁是指在必定的状况下,某些线程会忽略掉已经在排队的线程,发生插队的状况。
悲观锁:
悲观锁顾名思义,比较悲观,悲观锁认为若是不锁住这个共享资源,别的线程就回来竞争,就会形成数据结果错误,因此在获取共享资源前,必需要先拿到锁,以便达到“独占”的状态,,让其余线程没法访问该数据,这样就不会发生数据错误。常见的悲观锁例如synchronized关键字、Lock接口
乐观锁:
同理乐观锁是相对悲观锁而言的,乐观锁就是比较乐观了,它认为通常状况下数据不会发生冲突,只有在数据进行更新的时候,才会对比数据在被当前线程更新期间有咩有被修改过,若是没有被修改过,则能够正常更新数据,若是数据发生过修改和预期不同,那么本次更新操做就不能成功,因此能够放弃此次更新或者选择报错、重试等策略。常见的乐观锁例如:各类原子类
自旋锁:
自旋锁是指若是线程拿不到锁,那么线程并不直接陷入阻塞或者释放CPU资源而是开始自我旋转,不停的尝试获取锁,这个循环的过程成为自旋。
非自旋锁:
非自旋锁就是没有自旋的过程,若是拿不到锁就直接放弃或者进行其余逻辑处理,好比排队、阻塞。
可中断锁:
可中断锁指在获取锁的过程当中,能够中断锁以后去作其余事情,不须要一直等到获取到锁才继续处理
不可中断锁:
synchronized是不可中断锁,就是指一旦申请了锁,只能等到拿到锁之后才能进行其余的逻辑处理。
java中每一个对象中都持有一把锁与之关联,控制着对象的synchronized代码,想要执行对象的synchronized代码,
必须先得到对象的锁,这个锁就是对象的Monitor锁,synchronized实现加锁解锁就是利用Monitor锁实现的。
获取Monitor锁的惟一方式是进入由这个锁保护的同步代码块或者同步方法中,线程进入synchronized保护的代码以前得到锁,
而后在正常执行代码完成后或者异常退出,都会自动释放锁。
synchronized关键字在同步代码块中的应用:
咱们经过分析一下代码的反汇编内容来理解synchronized是如何利用monitor锁来工做的
咱们先来分析同步代码块的反汇编内容
public class TestSync { public void sync1(){ synchronized (this){ int ss = 10; System.out.println(ss); } } }
如上图代码,咱们定义的TestSync类中的sync1()方法中包含一个同步代码块,咱们经过指令:javap -verbose TestSync.class
查看方法对应的反汇编内容以下:
public void sync1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=4, args_size=1 0: aload_0 1: dup 2: astore_1 3: monitorenter //加锁 4: bipush 10 6: istore_2 7: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 10: iload_2 11: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 14: aload_1 15: monitorexit //解锁 16: goto 24 19: astore_3 20: aload_1 21: monitorexit 解锁 22: aload_3 23: athrow 24: return
上述中我能够看出同步代码块实际上多了monitorenter和monitorexit指令,
咱们能够理解为对应的加解锁,之因此有一个monitorenter对应两个monitorexit是由于jvm要
保证每一个monitorenter必须有与之对应的monitorexit,那么就须要在正常结束流程和异常结束流程中
分别执行monitorexit以保证正常或者抛出异常状况下都能释放锁。
monitorenter含义:
每一个对象维护着一个计数器,没有被锁定的对象计数器为0,执行monitorenter的线程尝试获取monitor的全部权,那么会有如下三种状况:
若是该monitor的计数为0,则线程得到该monitor后并将其计数设置成1,而后该线程就该monitor的持有者。若是线程已经获取了该monitor,那么该monitor的计数将累加。
若是线程已是其余线程已经获取了该monitor,那么当前想要获取该monitor的线程会被阻塞,知道该monitor的计数为0的时候,表明该monitor已经被释放了,而后当前线程就能够尝试获取该monitor了。
monitorexit含义:
monitorexit的做用是将monitor的计数减1,知道减为0为止,表明这个monitor已经被释放了,
那么此时其它线程就能够尝试获取该monitor的全部权了。
咱们再来看看同步方法反汇编后的内容又是怎么样的,咱们对一下内容执行反汇编。
public class TestSync { public synchronized void sync2(){ int aa = 10; System.out.println(aa); } }
反汇编代码以下:
public synchronized void sync2(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=2, locals=2, args_size=1 0: bipush 10 2: istore_1 3: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 6: iload_1 7: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 10: return
从上述代码中咱们能够看出同步方法和同步代码块的不一样之处是同步代码块
中依赖monitorenter和monitorexit来加解锁,同步方法中会多出一个
ACC_SYNCHRONIZED的flags修饰符,来代表他是同步方法。因此被synchronized修饰的方法会有一
个ACC_SYNCHRONIZED标志,那么当线程要访问某个方法的时候就会检查方法是否携带ACC_SYNCHRONIZED标志,
带的话就先去获取monitor锁,而后获取到锁后在执行方法,方法执行完后释放monitor锁。
相同点:
不一样点:
公平锁是指线程按照请求顺序来分配锁,非公平锁是指不彻底按照线程请求顺序分配锁,可是非公平锁并非彻底的随机分配而是”在合适的时机“插队执行。
什么是合适的时机
所谓合适的时机就是好比新来一个线程要获取锁,恰巧上一个持有锁线程正好执行完毕释放了锁,那么此时这个新来的线程能无论后面排队的线程而选择当即插队执行,可是若是上一个线程还未释放锁,那么新来的线程就须要去等待队列排队。
为何设置非公平锁
之因此设计非公平锁是由于相比公平锁排队执行,上一个线程释放锁后须要先唤醒下一个要执行的线程,而后去获取锁在执行,而采用非公平锁下,就能够上一个线程释放了锁,刚来一个新线程直接获取锁就插队去执行代码了,不须要额外的唤醒线程成本,并且也有可能在线程唤醒的这段时间内,插队线程已经获取锁而且执行完任务并释放了锁。
因此设置非公平锁,这样设计的缘由是为了提升系统总体的运行效率,并且ReentrantLock默认的是非公平锁。
公平锁和非公平锁经过设置ReentrantLock中boolean值来设置公平非公平锁,以下代码所示是设置为非公平锁。
Lock lock=new ReentrantLock(false);
公平锁代码展现:
/** * 描述:演示公平锁,分别展现公平和不公平的状况, * 非公平锁会让如今持有锁的线程优先再次获取到锁。 * 代码借鉴自Java并发编程实战手册2.7 */ public class FairAndNoFair { public static void main(String[] args) { PrintQueue printQueue = new PrintQueue(); Thread[] threads= new Thread[10]; for(int i=0;i<10;i++){ threads[i] = new Thread(new Job(printQueue),"Thread "+ i); } for (int i = 0; i < 10; i++) { threads[i].start(); try { Thread.sleep(100);//为了保证执行顺序 } catch (InterruptedException e) { e.printStackTrace(); } } } } public class Job implements Runnable{ private PrintQueue printQueue; public Job(PrintQueue printQueue){ this.printQueue=printQueue; } @Override public void run() { System.out.printf("%s: Going to print a job\n", Thread.currentThread().getName()); printQueue.printJob(); System.out.printf("%s: The document has been printed\n", Thread.currentThread().getName()); } } public class PrintQueue { private final Lock lock=new ReentrantLock(false); public void printJob(){ lock.lock(); try{ Long duration = (long) (Math。random()*10000); System.out.printf("%s:First PrintQueue: Printing a Job during %d seconds\n", Thread.currentThread().getName(), (duration / 1000)); Thread.sleep(duration); } catch (InterruptedException e){ e.printStackTrace(); } finally { lock.unlock(); } lock.lock(); try{ Long duration = (long) (Math.random()*10000); System.out.printf("%s:Second PrintQueue: Printing a Job during %d seconds\n", Thread.currentThread().getName(), (duration / 1000)); Thread.sleep(duration); } catch (InterruptedException e){ e.printStackTrace(); } finally { lock.unlock(); } } }
咱们先运行公平锁的打印结果以下:
Thread 0: Going to print a job Thread 0:First PrintQueue: Printing a Job during 9 seconds Thread 1: Going to print a job //线程1-9进入等待队列排队 Thread 2: Going to print a job Thread 3: Going to print a job Thread 4: Going to print a job Thread 5: Going to print a job Thread 6: Going to print a job Thread 7: Going to print a job Thread 8: Going to print a job Thread 9: Going to print a job Thread 1:First PrintQueue: Printing a Job during 5 seconds//线程0执行完释放了锁,线程1开始执行 Thread 2:First PrintQueue: Printing a Job during 1 seconds Thread 3:First PrintQueue: Printing a Job during 9 seconds Thread 4:First PrintQueue: Printing a Job during 7 seconds Thread 5:First PrintQueue: Printing a Job during 8 seconds Thread 6:First PrintQueue: Printing a Job during 5 seconds Thread 7:First PrintQueue: Printing a Job during 2 seconds Thread 8:First PrintQueue: Printing a Job during 9 seconds Thread 9:First PrintQueue: Printing a Job during 7 seconds Thread 0:Second PrintQueue: Printing a Job during 0 seconds Thread 1:Second PrintQueue: Printing a Job during 6 seconds Thread 0: The document has been printed Thread 1: The document has been printed Thread 2:Second PrintQueue: Printing a Job during 4 seconds Thread 2: The document has been printed Thread 3:Second PrintQueue: Printing a Job during 4 seconds Thread 3: The document has been printed Thread 4:Second PrintQueue: Printing a Job during 1 seconds Thread 4: The document has been printed Thread 5:Second PrintQueue: Printing a Job during 3 seconds Thread 5: The document has been printed Thread 6:Second PrintQueue: Printing a Job during 0 seconds Thread 6: The document has been printed Thread 7:Second PrintQueue: Printing a Job during 1 seconds Thread 7: The document has been printed Thread 8:Second PrintQueue: Printing a Job during 5 seconds Thread 8: The document has been printed Thread 9:Second PrintQueue: Printing a Job during 5 seconds Thread 9: The document has been printed Process finished with exit code 0
从上图能够看出线程直接获取锁的顺序是公平的,先到先得。
咱们运行非公平锁的打印结果以下:
Thread 0: Going to print a job Thread 0:First PrintQueue: Printing a Job during 5 seconds Thread 1: Going to print a job Thread 2: Going to print a job Thread 3: Going to print a job Thread 4: Going to print a job Thread 5: Going to print a job Thread 6: Going to print a job Thread 7: Going to print a job Thread 8: Going to print a job Thread 9: Going to print a job Thread 0:Second PrintQueue: Printing a Job during 2 seconds //线程0直接释放锁又获取了锁,体现了非公平锁 Thread 0: The document has been printed Thread 1:First PrintQueue: Printing a Job during 9 seconds Thread 1:Second PrintQueue: Printing a Job during 3 seconds Thread 1: The document has been printed Thread 2:First PrintQueue: Printing a Job during 0 seconds Thread 3:First PrintQueue: Printing a Job during 0 seconds Thread 3:Second PrintQueue: Printing a Job during 7 seconds Thread 3: The document has been printed Thread 4:First PrintQueue: Printing a Job during 3 seconds Thread 4:Second PrintQueue: Printing a Job during 8 seconds Thread 4: The document has been printed Thread 5:First PrintQueue: Printing a Job during 6 seconds Thread 5:Second PrintQueue: Printing a Job during 1 seconds Thread 5: The document has been printed Thread 6:First PrintQueue: Printing a Job during 0 seconds Thread 6:Second PrintQueue: Printing a Job during 7 seconds Thread 6: The document has been printed Thread 7:First PrintQueue: Printing a Job during 8 seconds Thread 7:Second PrintQueue: Printing a Job during 1 seconds Thread 7: The document has been printed Thread 8:First PrintQueue: Printing a Job during 9 seconds Thread 8:Second PrintQueue: Printing a Job during 8 seconds Thread 8: The document has been printed Thread 9:First PrintQueue: Printing a Job during 5 seconds Thread 9:Second PrintQueue: Printing a Job during 5 seconds Thread 9: The document has been printed Thread 2:Second PrintQueue: Printing a Job during 3 seconds Thread 2: The document has been printed Process finished with exit code 0
上图中能够看出线程0在释放了锁以后,马上有获取了锁继续执行,出现了抢锁插队现象(此时等待队列已经有了线程1-9再等待)。
公平锁和非公平锁有缺点
公平锁和非公平锁源码解析
首先公平锁和非公平锁都是继承了ReentrantLock类中的内部类Sync类,这个Sync类继承AQS(AbstractQueuedSynchronizer),Sync类代码以下:
//源码中能够看出Sync继承了AbstractQueuedSynchronizer类 abstract static class Sync extends AbstractQueuedSynchronizer {...}
//Sync有公平锁FairSync和非公平锁NonFairSync两个子类: static final class NonfairSync extends Sync {。。。} static final class FairSync extends Sync {。。}
公平锁获取锁的源码:
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { //这里和非公平锁对比多了个!hasQueuedPredecessors()判断 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
非公平锁获取锁源码:
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
经过对比能够发现,公平锁和非公平锁的区别主要是在获取锁的时候,
公平锁多了一个hasQueuedPredecessors()为false的判断,hasQueuedPredecessors()方法
就是判断等待队列中是否已经有线程在等待了,若是有则当前线程不能在尝试获取锁,对于非公平锁而言,
不管是否有线程在等待了,都先尝试获取锁,获取不到的话再去排队,tryLock()方法内部调用的是sync.nonfairTryAcquire(1)即非
公平锁,因此即便设置了公平模式,那么使用tryLock()也能够插队。
首先读写锁是为了提升系统的效率,
虽然普通的ReentrantLock能够保证线程安全,
可是若是是多个读取操做,就直接采用ReentrantLock会大大的浪费系统资源,
还有就是写操做是不安全的,当并发写或者在进行写操做的同时进行读取,都会发生线程安全问题,
那么设置的读写锁就起了做用,读写锁支持并发读来提升读的效率,同时又保证安全的写操做。
这里使用ReentrantReadWriteLock来演示,ReentrantReadWriteLock是ReadWriteLock的是实现类,最主要两个方法readLock()获取读锁,writeLock()获取写锁,
这里使用ReadWriteLock中的读写锁进行并发读写,代码展现以下:
public class ReadWriteLock { //定义读写锁 private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false); //获取读锁 private static final ReentrantReadWriteLock.ReadLock readLock= reentrantReadWriteLock.readLock(); //获取写锁 private static final ReentrantReadWriteLock.WriteLock writeLock= reentrantReadWriteLock.writeLock(); public static void read(){ readLock.lock(); try { System.out.println(Thread.currentThread().getName() +"获得读锁,正在读取"); Thread.sleep(500); }catch (InterruptedException e){ e.printStackTrace(); } finally { System.out.println("释放读锁"); readLock.unlock(); } } private static void write() { writeLock.lock(); try { System.out.println(Thread.currentThread().getName() + "获得写锁,正在写入"); Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println(Thread.currentThread().getName() + "释放写锁"); writeLock.unlock(); } } public static void main(String[] args) throws InterruptedException { new Thread(() -> read()).start(); new Thread(() -> read()).start(); new Thread(() -> write()).start(); new Thread(() -> write()).start(); } }
运行结果以下:
Thread-0获得读锁,正在读取 Thread-1获得读锁,正在读取 释放读锁 释放读锁 Thread-2获得写锁,正在写入 Thread-2释放写锁 Thread-3获得写锁,正在写入 Thread-3释放写锁
经过运行结果能够看出,读写锁支持并发读,而写操做是单独进行的。
首先读写锁ReentrantReadWriteLock支持公平锁和非公平锁,能够经过如下进行设置:
//后面的boolean值用来设置公平锁、非公平锁,其中的false设置为非公平锁,设置为true就是公平锁, ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);
若是设置为公平锁的时候对应的读写锁实现为:
static final class FairSync extends Sync { private static final long serialVersionUID = -2274990926593161451L; final boolean writerShouldBlock() { return hasQueuedPredecessors(); } final boolean readerShouldBlock() { return hasQueuedPredecessors(); } }
其中的hasQueuedPredecessors()方法就是检测等待队列中是否已经有线程在排序了,
若是有的话每当前获取锁的线程就会block去排序,因此符合公平锁定义。
若是设置为false非公平锁,则对应的实现为:
static final class NonfairSync extends Sync { private static final long serialVersionUID = -8159625535654395037L; final boolean writerShouldBlock() { return false; // writers can always barge } final boolean readerShouldBlock() { /* As a heuristic to avoid indefinite writer starvation, * block if the thread that momentarily appears to be head * of queue, if one exists, is a waiting writer。 This is * only a probabilistic effect since a new reader will not * block if there is a waiting writer behind other enabled * readers that have not yet drained from the queue。 */ return apparentlyFirstQueuedIsExclusive(); } }
上图中writerShouldBlock()方法直接返回false,能够看出对于想要获取写锁的线程而言,
因为返回的是false因此它能够随时插队,也符合非公平锁的设计,非公平锁下的获取读锁须要依据apparentlyFirstQueuedIsExclusive()方法的返回值,上图中对apparentlyFirstQueuedIsExclusive方法注释主要是说防止等待队
列头的写线程无饥饿等待下去,举个例子说明:
场景:若是有线程1和2同时读取,而且1和2已经持有了读锁,此时线程3想要写入,因此线程3进入等待队列,此时线程4忽然插队想要获取读锁。
此时就有两种策略:
而ReentrantReadWriteLock非公平锁下的获取读锁正是采用了不容许插队策略来实现的,避免了线程饥饿状况。
咱们经过代码展现一下上面的不容许插队策略,效果展现代码展现:
public class ReadWriteLock { //定义读写锁 private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false); //获取读锁 private static final ReentrantReadWriteLock.ReadLock readLock= reentrantReadWriteLock.readLock(); //获取写锁 private static final ReentrantReadWriteLock.WriteLock writeLock= reentrantReadWriteLock.writeLock(); public static void read(){ readLock.lock(); try { System.out.println(Thread.currentThread().getName() +"获得读锁,正在读取"); Thread.sleep(500); }catch (InterruptedException e){ e.printStackTrace(); } finally { System.out.println("释放读锁"); readLock.unlock(); } } private static void write() { writeLock.lock(); try { System.out.println(Thread.currentThread().getName() + "获得写锁,正在写入"); Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println(Thread.currentThread().getName() + "释放写锁"); writeLock.unlock(); } } public static void main(String[] args) throws InterruptedException { new Thread(() -> read()).start(); new Thread(() -> read()).start(); new Thread(() -> write()).start(); //以上代码没有改变,这里换成了读锁 new Thread(() -> read()).start(); } }
运行结果以下:
Thread-0获得读锁,正在读取 Thread-1获得读锁,正在读取 释放读锁 释放读锁 Thread-2获得写锁,正在写入 Thread-2释放写锁 Thread-3获得读锁,正在读取 释放读锁
经过运行结果咱们能够看出,ReentrantReadWriteLock选择了不容许插队的策略。
写锁的降级
写锁的降级,代码展现:
//定义读写锁 private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); //获取读锁 private static final ReentrantReadWriteLock.ReadLock readLock= reentrantReadWriteLock.readLock(); //获取写锁 private static final ReentrantReadWriteLock.WriteLock writeLock= reentrantReadWriteLock.writeLock(); //锁的降级 public static void downgrade(){ System.out.println(Thread.currentThread().getName()+"尝试获取写锁"); writeLock.lock();//获取写锁 try { System.out.println(Thread.currentThread().getName()+"获取了写锁"); //在不释放写锁的状况下直接获取读锁,这就是读写锁的降级 readLock.lock(); System.out.println(Thread.currentThread().getName()+"获取了读锁"); }finally { System.out.println(Thread.currentThread().getName()+"释放了写锁"); //释放了写锁,可是依然持有读锁,这里不释放读锁,致使后面的线程没法获取写锁 writeLock.unlock(); } } public static void main(String[] args) { new Thread(() -> downgrade()).start(); new Thread(() -> downgrade()).start(); }
上图运行结果以下:
图中咱们能够看出线程0能够在持有写锁的状况下获取到了读锁,这就是写锁的降级,由于线程0后面只是放了写锁,
并未释放读锁,致使后面的线程1不能获取到写锁,因此程序一直阻塞。
读锁的升级
接下来咱们在来看读锁的升级,代码展现:
private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false); //获取读锁 private static final ReentrantReadWriteLock.ReadLock readLock= reentrantReadWriteLock.readLock(); //获取写锁 private static final ReentrantReadWriteLock.WriteLock writeLock= reentrantReadWriteLock.writeLock(); //读锁升级 public static void upgarde(){ System.out.println(Thread.currentThread().getName()+"尝试获取读锁"); readLock.lock(); try{ System.out.println(Thread.currentThread().getName()+"获取到了读锁"); System.out.println(Thread.currentThread().getName()+"阻塞获取写锁"); //在持有读锁的状况下获取写锁,此处会阻塞,表示不支持读锁升级到写锁 writeLock.lock();//此处会阻塞 System.out.println(Thread.currentThread().getName()+"获取到了写锁"); }finally { readLock.unlock(); } } public static void main(String[] args) { new Thread(() -> upgarde()).start(); new Thread(() -> upgarde()).start(); }
运行结果以下:
上图中咱们能够看出线程0和线程1均可以成功的获取到读锁,可是在进行锁升级获取写锁的时候都阻塞了,这是由于ReentrantReadWriteLock 不支持读锁升级到写锁。
由于读锁是能够多个线程持有的,可是写锁只能一个线程持有,而且不可能存在读锁和写锁同时持有的状况,也正是由于这个缘由因此升级写锁的过程当中,须要等待全部的读锁都释放了,此时才能进行锁升级。
举个例子,好比ABC三个线程都持有读锁,其中线程A想要进行锁升级,必需要等到B和C都释放了读锁,此时线程A才能够成功升级并获取写锁。
可是这里也有一个问题,那就是假如A和B都想要锁升级,对于线程A来讲,他须要等待其余全部线程包括B线程释放读锁,而B线程也须要等待其余线程释放读锁包括A线程,那就会发生死锁。
因此若是咱们保证每次升级只有一个线程能够升级,那么就能够保证线程安全,而且实现。
自旋锁其实就是指循环,好比while或者for循环,一直循环去尝试获取锁,不像普通的锁获取不到就陷入阻塞。
自旋锁和非自旋锁流程图对好比下:
上图中咱们能够看出自旋锁获取获取锁失败并不会释放CPU资源而是经过自旋的方式等待锁的释放,直到成功获取到锁为止。
而非自旋锁若是尝试获取锁失败了,它就把本身的线程切换状态,让线程休眠,释放CPU时间片,而后直到以前持有这把锁的线程释放了锁,因而CPU再把以前的线程恢复回来,让这个线程再尝试去获取锁,若是再次失败就在让线程休眠,若是成功,就能够获取到同步资源的锁。
自旋锁的好处
自旋锁免去了耗时的阻塞和唤醒线程操做,避免了线程的状态切换等开销,提升了效率。
自旋锁的坏处
自旋锁虽然避免了线程切换的开销,可是也带来了新的开销,由于他须要不停的循环去尝试获取锁,若是因此只不释放,那么他就须要一直去尝试,这样会白白的浪费资源。
因此,自旋锁适用于并发度不是特别高,并且线程持有锁的时间较短的场景。举个例子好比java.util.concurrent包下的原子类大多数都是基于自旋锁的实现,好比AtomicInteger,咱们查看他的getAndIncrement()方法,以下:
public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
很明显,do...while()循环就是一个自旋操做,若是在修改过程当中遇到了其余线程致使没有修改为功,则就会执行循环不从的重试,直到修改为功为止。
实现代码以下:
//自定义实现可重入的自旋锁 public class CustomReentrantSpinLock { private AtomicReference<Thread> owner=new AtomicReference<>(); private int count = 0;//重入次数 public void lock() { Thread t = Thread.currentThread(); System.out.println(Thread.currentThread().getName()+"lock了"); if (t == owner.get()) { ++count; return; } //自旋获取锁 while (!owner.compareAndSet(null, t)) { System.out.println(Thread.currentThread().getName()+"自旋了"); } } public void unLock(){ Thread t=Thread.currentThread(); //只有当前线程才能解锁 if(t == owner.get()){ if(count >0){ --count; } else { owner.set(null); } } } public static void main(String[] args) { CustomReentrantSpinLock spinLock=new CustomReentrantSpinLock(); Runnable runnable=new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+"开始尝试获取自旋锁"); spinLock.lock(); try { System.out.println(Thread.currentThread().getName()+"获取到了自旋锁"); Thread.sleep(1); }catch (InterruptedException e){ e.printStackTrace(); } finally { spinLock.unLock(); System.out.println(Thread.currentThread().getName()+"释放了自旋锁"); } } }; Thread thread1=new Thread(runnable); Thread thread2=new Thread(runnable); thread1.start(); thread2.start(); } }
运行结果以下:
Thread-0开始尝试获取自旋锁 Thread-1开始尝试获取自旋锁 Thread-0获取到了自旋锁 Thread-1自旋了 . . . Thread-1自旋了 Thread-0释放了了自旋锁 Thread-1获取到了自旋锁 Thread-1释放了了自旋锁
从上图运行结果中能够看出,打印了不少Thread-1自旋了,说明自旋期间CPU依然不停运转,Thread-1并无释放CPU时间片。
从jdk1.6后HotSpot虚拟机对synchronized作了不少优化,包括自适应自选、锁消除、锁粗化、偏向锁、轻量级锁等,使得synchronized锁获得了很大的性能提高。
自适应自旋就是自旋的时间不在固定,而是根据自旋的成功率、失败率、以及当前锁的持有者的状态等多种因素来共同决定的,就是说自旋的时间是变化的,这样能够减小无用的自旋,提升效率。
锁消除是发生在编译器级别的一种锁优化方式,有时候咱们写的代码不须要加锁,就好比加锁的代码实际上只有一个线程会执行,并不会出现多个线程并发访问的状况,可是咱们却加上了synchronized锁,那么编译器就可能会消除掉这个锁,好比下面StringBuffer的append操做:
@Override public synchronized StringBuffer append(Object obj) { toStringCache = null; super.append(String。valueOf(obj)); return this; }
代码中咱们能够看到这个方法被synchronized修饰,由于它可能会被多个线程同时使用,
可是多数状况下它只会在一个线程内使用,若是编译器能肯定这个对象只会在一个线程内使用,那么就表示确定是线程安全的,编译器就会作出优化,把对应的synchronized消除,省去加解锁的操做以提高效率。
锁粗化主要是应对刚释放锁,什么还没作,就从新获取锁,例如以下代码:
public void lockCoarsening() { synchronized (this) { //do something } synchronized (this) { //do something } synchronized (this) { //do something } }
上述代码中,当线程在执行第一个同步代码块时须要先获取synchronized锁,而后执行完了同步代码块在释放synchronized锁,可是当线程执行完第一个同步代码块后已经释放了锁后,紧接着线程马上开始执行第二个同步代码块时就须要对相同的锁进行获取和释放,
这样释放和获取锁是彻底没有必要的,若是把同步区域扩大,也就是在最开始的时候加一次锁,在结束的时候释放锁,那么就能够把中间无心义的解锁和加锁的过程消除,至关于把几个synchronized块合并成为一个较大的同步块,好处就是无需频繁的释放锁和获取锁,减小系统开销。
可是锁粗化不适用在循环的场景,仅适用非循环的场景,由于以下代码所示,若是咱们在第一次循环中获取锁,在最后一次循环中释放锁,那么这就会致使其它线程长时间没法获取锁。
for (int i = 0; i < 1000; i++) { synchronized (this) { //do something } }
锁粗化默认是开启的,经过-XX:-EliminateLocks关闭该功能
这三种锁咱们最开始就介绍过,它们是指synchronized的锁状态的,经过在对象头中的mark work字段来代表锁的状态。
锁升级的路径以下图所示,偏向锁性能作好,避免了CAS操做,轻量级锁利用了自旋和CAS操做避免了重量级锁带来的线程阻塞和唤醒,性能中等,重量级锁会把获取不到锁的线程阻塞,性能最差。
-END
喜欢就扫描下方二维码或微信搜索“程序员内功心法”关注我吧