关于synchronized:Java6对synchronized的优化锁升级过程详细过程

前言以前咱们在温习synchronized的时候,和lock一比照,总说它是一个重量级锁,性能很差,不如lock来的不便,但其实synchronized作为Java中元老级的关键字,很多jdk原始代码都有用到它,因而Java开发团队还是十分中意这个“私生子”的,所以为了给它减重,开发者也早在Java6就为synchronized专门设计了一套优化处理过程。 一、synchronized应用加到办法上,锁就是这个对象,避免多个线程同时拜访这个对象的synchronized办法;加到静态方法,锁就是这个类的class对象,避免多个线程同时拜访这个类的synchronized办法,对该类所有对象实用;加到代码块,锁就是该代码块所在的对象,避免多个线程同时拜访该代码块;二、JVM对synchronized的实现形式代码块同步:通过应用 monitorenter 和 monitorexit 指令实现的同步办法:ACC_SYNCHRONIZED 润饰三、Java对象头(存储锁类型)理解锁降级之前,必须先晓得Java对象头,正式对象头记录的信息才实现了锁降级过程中的线程判断。 Java对象: Java对象头:蕴含两局部:MarkWord 和 类型指针 MarkWord:Mark Word 用于存储对象本身的运行时数据,如 HashCode、GC分代年龄、锁状态标记、线程持有的锁、偏差线程ID等等。 占用内存大小与虚拟机位长统一(32位JVM -> MarkWord是32位,64位JVM -> MarkWord是64位) 四、锁降级过程无锁状态 -> 偏差锁 -> 轻量级锁(自旋锁,自适应自旋) -> 重量级锁 各种锁状态下mark word的内容:状态偏差锁位标记位存储内容未锁定001对象哈希码、对象分代年龄偏差锁101偏差线程ID、偏差工夫戳、对象分代年龄轻量级锁定000指向锁记录的指针收缩(重量级锁定)010执行重量级锁定的指针GC标记011空(不须要记录信息)无锁状态 -> 偏差锁首先当处于无锁状态时,MarkWord中存储的是对象的hash code等信息,偏差锁为0,此时线程1进入,发现没有记录线程ID信息,则间接CAS操作将本人的线程ID设置进去,设置胜利则获取到偏差锁,偏差锁标记为改为1;失败则要降级为轻量级锁; 线程1执行完后,将markWord中的线程ID设置为空,即开释偏差锁; 如果在线程1开释之前,此时线程2进入,先查markWord中记录的线程ID是否是本人的(这也是偏差锁实用的场景:教训表明,大多数状况下,都是同一个线程进入同一个同步代码块),若是,则获取锁胜利; 不是,则阐明曾经偏差给了其余线程,即呈现了线程竞争的状况,此时个别状况下,偏差锁是不能够重偏差的,所以须要降级,如果是能够重偏差的,则会尝试CAS操作替换markWord中的线程id,胜利则偏差给线程2,失败则进行锁降级; 偏差锁在降级前,须要先将偏差锁撤销。 如果此时曾经偏差给了某个线程,则须要依据markWord中记录的线程id查问该线程是否存活,不存活,则进入无锁状态,或者可重偏差状态,存活则撤销偏差锁,将锁对象的markWord复制到线程栈中,开始降级 偏差锁 -> 轻量级锁呈现两个及以上线程竞争时(曾经偏差给了一个线程,此时又呈现了另外一个线程竞争,即降级),开始降级为轻量级锁。 加锁时,线程会在本人的栈帧中创立一个Lock Record锁记录(会将对象原来的markWord内容复制过去,作为一个lockRecord,蕴含对象分代年龄等信息,而原来的markWord只会有一个指针),同时CAS将锁对象的markWord中替换为指向该lockRrecord的指针,操作胜利则获取到轻量级锁; 线程执行完,解锁时,只须要移除栈帧中的lockRecord即可,而不必批改markWord中的内容。 轻量级锁 -> 重量级锁设置失败,则证实有竞争,此时会进入自旋状态。 线程超过10次自旋, 或者自旋线程数超过CPU核数的一半,(JDK1.6之后,退出自适应自旋 Adapative Self Spinning , JVM本人管制)。就会降级为重量级锁。 降级重量级锁: 向操作系统申请资源,CPU从3级-0级零碎调用,线程挂起,进入期待队列,期待操作系统的调度,而后再映射回用户空间 所以降级为重量级锁后,用户线程被零碎线程挂起,须要从用户态到内核态的转换。这也是最开始说synchronized重,性能差的起因! 总结:synchronized锁降级过程是单向的,不可逆;降级过程围绕锁对象中markWord的内容变动;每种锁都适应不同的场景,比方偏差锁实用于同一个线程屡次进入同步代码的状况,轻量级锁实用竞争小,且同步执行工夫短(自旋一会儿就能获取到锁)的状况对了,synchronized是可重入的,这点不光能够从偏差锁看进去,即便降级到重量级锁,也是反对的

June 18, 2022 · 1 min · jiezi

关于synchronized:Synchronized锁及其膨胀

一、序言在并发编程中,synchronized锁因其应用简略,在线程间同步被广泛应用。上面对其原理及锁降级过程进行探索。 二、如何应用1、润饰实例办法当实例办法被synchronized润饰时,通过以后实例调用此办法的所有线程共用一把锁,不同对象调用此办法线程间互不影响。 public class A { public synchronized void func() { }}当应用synchronized锁润饰实例办法,锁增加在以后类的实例上,有多少个实例可增加多少把锁。 2、润饰代码块润饰代码块比润饰办法颗粒度更小。当实例办法代码块被synchronized润饰时,通过以后实例调用此办法的所有线程共用一把锁,不同对象调用此办法线程间互不影响。 public class B { public void func() { synchronized (this) { } }}当应用synchronized锁润饰代码块,锁增加在以后类的实例上,有多少个实例可增加多少把锁。 3、润饰静态方法当静态方法被synchronized润饰时,整个JVM所有调用此办法的线程均受同一个锁的束缚。 public class C { public static synchronized void func() { }}当应用synchronized锁润饰静态方法,锁增加在以后类的类对象上,最多增加一把锁。 非必要不应用synchronized润饰静态方法三、锁的降级Java 8所应用的synchronized锁是通过优化后的,存在偏差锁、轻量级锁、重量级锁等状态。 (一)偏差锁线程间不存在锁的竞争行为,至少只有一个线程有获取锁的需要,常见场景为单线程程序。 1、识别方法判断是不是偏差锁的标识是查看调用此办法的线程是否有且仅有一个。 在多线程编程里,被锁润饰的办法仅被繁多线程调用简直不存在,因而偏差锁比拟鸡肋:如果可能明确繁多线程调用指标办法,应用无锁编程更为适合。 2、性能比拟无锁与偏差锁的性能差别十分靠近,简直能够忽略不计。 (二)轻量级锁线程间存在锁的伪竞争行为,即同一时刻相对不会存在两个线程申请获取锁,各线程只管都有应用锁的需要,然而是交替应用锁。 1、识别方法当有两个及以上线程调用被锁润饰的办法时,那么至多能确定是轻量级锁。 2、性能比拟轻量级锁因为同一时刻不存在两个线程相互竞争锁,因而不存在线程阻塞-唤醒的上下文切换,因而性能绝对重量级锁要高很多。 (三)重量级锁线程间存在锁的实质性竞争行为,线程间都有获取锁的需要,然而工夫不可交织,互斥锁的阻塞期待。 1、识别方法当可能必定至多有两个及以上线程调用被锁润饰的办法时,线程调用办法是随机的,那么大概率是重量级锁。 2、性能比拟重量级锁因为波及到线程阻塞-唤醒的上下文切换,造成相比拟与无锁状态,效率低很多。 四、其它内容(一)锁的性质1、公平性synchronized锁是非偏心锁,没有FIFO队列机制保障竞争锁的线程肯定有几率取得锁。 2、重入性synchronized锁是可重入锁,可重入意味着嵌套调用不会产生死锁问题。 3、乐(悲)观锁synchronized锁是一种乐观锁,通过加锁实现线程间同步。 (二)了解重量级锁在多线程环境下,如果应用synchronized锁,那么大概率会降级到重量级锁。偏差锁和轻量级锁非刻意为之,很难存在,更大的意义是比照帮忙了解重量级锁的性能。 重量级锁只管会对性能产生很大影响,然而仍旧是解决线程间同步的无效伎俩。 1、选用锁的倡议当被锁润饰的办法或者代码块执行工夫较长时,选用基于线程阻塞-唤醒切换上下文的形式进行线程同步效率绝对较高。 当被锁润饰的办法或者代码块执行工夫较短时,应选用其它代替锁,比方自旋锁等。 (三)了解synchronized锁在理论多线程场景开发中,synchronized锁大概率会降级到重量级锁,因其单向降级的特点,重量级状态的synchronized锁可能会对理论业务的并发产生不利影响,手动选用其它锁可能会更适合。 synchronized锁仅可用于解决同一过程内不同线程间同步,对于分布式我的项目跨进城线程同步依赖于分布式锁,synchronized锁更多的意义是了解锁的过程。 喜爱本文点个♥️赞♥️反对一下,如有须要,可通过微信dream4s与我分割。相干源码在GitHub,视频解说在B站,本文珍藏在博客天地。

April 22, 2022 · 1 min · jiezi

关于synchronized:深入JVM内置锁-synchronized-底层

前言上一章节带着大家理解了Java对象头的组成,本节带着大家理解synchronized 关键字的底层原理以及锁的降级过程 synchronized原理详解synchronized内置锁是一种对象锁(锁的是对象而非援用),作用粒度是对象,能够用来实现对临界资源的同步互斥拜访,是可重入的 什么是Monitor在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的。Synchronized的对象锁,MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址。其次要数据结构如下 ObjectMonitor() { _header = NULL; _count = 0; // 记录个数 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; // 处于wait状态的线程,会被退出到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; // 处于期待锁block状态的线程,会被退出到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保留ObjectWaiter对象列表( 每个期待锁的线程都会被封装成ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时拜访一段同步代码时: ...

April 19, 2022 · 1 min · jiezi

关于synchronized:java开发技术之synchronized的使用浅析

synchronized 这个关键字的重要性显而易见,简直能够说是并发、多线程必须会问到的关键字了。synchronized 会波及到锁、降级降级操作、锁的撤销、对象头等。所以了解 synchronized 十分重要,本篇文章就带你从 synchronized 的根本用法、再到 synchronized 的深刻了解,对象头等,为你揭开 synchronized 的面纱。浅析 synchronizedsynchronized 是 Java 并发模块 十分重要的关键字,它是 Java 内建的一种同步机制,代表了某种外在锁定的概念,当一个线程对某个共享资源加锁后,其余想要获取共享资源的线程必须进行期待,synchronized 也具备互斥和排他的语义。什么是互斥?咱们想必小时候都玩儿过磁铁,磁铁会有正负极的概念,同性相斥异性相吸,相斥相当于就是一种互斥的概念,也就是两者互不相容。 synchronized 也是一种独占的关键字,然而它这种独占的语义更多的是为了减少线程安全性,通过独占某个资源以达到互斥、排他的目标。在理解了排他和互斥的语义后,咱们先来看一下 synchronized 的用法,先来理解用法,再来理解底层实现。synchronized 的应用对于 synchronized 想必你应该都大抵理解过• synchronized 润饰实例办法,相当于是对类的实例进行加锁,进入同步代码前须要取得以后实例的锁• synchronized 润饰静态方法,相当于是对类对象进行加锁• synchronized 润饰代码块,相当于是给对象进行加锁,在进入代码块前须要先取得对象的锁 上面咱们针对每个用法进行解释synchronized 润饰实例办法synchronized 润饰实例办法,实例办法是属于类的实例。synchronized 润饰的实例办法相当于是对象锁。上面是一个 synchronized 润饰实例办法的例子。public synchronized void method(){ // ...}像如上述 synchronized 润饰的办法就是实例办法,上面咱们通过一个残缺的例子来认识一下 synchronized 润饰实例办法public class TSynchronized implements Runnable{ static int i = 0;public synchronized void increase(){ i++; System.out.println(Thread.currentThread().getName());}@Overridepublic void run() { for(int i = 0;i < 1000;i++) { increase(); }}public static void main(String[] args) throws InterruptedException { TSynchronized tSynchronized = new TSynchronized(); Thread aThread = new Thread(tSynchronized); Thread bThread = new Thread(tSynchronized); aThread.start(); bThread.start(); aThread.join(); bThread.join(); System.out.println("i = " + i);}}下面输入的后果 i = 2000 ,并且每次都会打印以后现成的名字来解释一下下面代码,代码中的 i 是一个动态变量,动态变量也是全局变量,动态变量存储在办法区中。increase 办法由 synchronized 关键字润饰,然而没有应用 static 关键字润饰,示意 increase 办法是一个实例办法,每次创立一个 TSynchronized 类的同时都会创立一个 increase 办法,increase 办法中只是打印进去了以后拜访的线程名称。Synchronized 类实现了 Runnable 接口,重写了 run 办法,run 办法外面就是一个 0 - 1000 的计数器,这个没什么好说的。在 main 办法中,new 出了两个线程,别离是 aThread 和 bThread,Thread.join 示意期待这个线程解决完结。这段代码次要的作用就是判断 synchronized 润饰的办法可能具备独占性。synchronized 润饰静态方法synchronized 润饰静态方法就是 synchronized 和 static 关键字一起应用public static synchronized void increase(){}当 synchronized 作用于静态方法时,java培训示意的就是以后类的锁,因为静态方法是属于类的,它不属于任何一个实例成员,因而能够通过 class 对象管制并发拜访。这里须要留神一点,因为 synchronized 润饰的实例办法是属于实例对象,而 synchronized 润饰的静态方法是属于类对象,所以调用 synchronized 的实例办法并不会阻止拜访 synchronized 的静态方法。 ...

September 29, 2021 · 4 min · jiezi

关于synchronized:使用了synchronized竟然还有线程安全问题

实战中受过的伤,能力领悟的更透彻,二师兄带你剖析实战案例。线程平安问题始终是零碎亘古不变的痛点。这不,最近在我的项目中发了一个谬误应用线程同步的案例。外表上看曾经应用了同步机制,所有岁月静好,但实际上线程同步却毫无作用。 对于线程平安的问题,基本上就是在挖坑与填坑之间博弈,这也是为什么面试中线程平安必不可少的起因。上面,就来给大家剖析一下这个案例。 有隐患的代码先看一个脱敏的代码实例。代码要解决的业务逻辑很简略,就是多线程拜访一个单例对象的成员变量,对其进行自增解决。 SyncTest类实现了Runnable接口,run办法中解决业务逻辑。在run办法中通过synchronized来保障线程平安问题,在main办法中创立一个SyncTest类的对象,两个线程同时操作这一个对象。 public class SyncTest implements Runnable { private Integer count = 0; @Override public void run() { synchronized (count) { System.out.println(new Date() + " 开始休眠" + Thread.currentThread().getName()); count++; try { Thread.sleep(10000); System.out.println(new Date() + " 完结休眠" + Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) throws InterruptedException { SyncTest test = new SyncTest(); new Thread(test).start(); Thread.sleep(100); new Thread(test).start(); }}在上述代码中,两个线程拜访SyncTest的同一个对象,并对该对象的count属性进行自增操作。因为是多线程,那就要保障count++的线程平安。 ...

July 26, 2021 · 2 min · jiezi

关于synchronized:详解synchronized与Lock的区别与使用

技术点: 1、线程与过程: 在开始之前先把过程与线程进行辨别一下,一个程序起码须要一个过程,而一个过程起码须要一个线程。关系是线程–>过程–>程序的大抵组成构造。所以线程是程序执行流的最小单位,而过程是零碎进行资源分配和调度的一个独立单位。以下咱们所有探讨的都是建设在线程根底之上。 2、Thread的几个重要办法: 咱们先理解一下Thread的几个重要办法。a、start()办法,调用该办法开始执行该线程;b、stop()办法,调用该办法强制完结该线程执行;c、join办法,调用该办法期待该线程完结。d、sleep()办法,调用该办法该线程进入期待。e、run()办法,调用该办法间接执行线程的run()办法,然而线程调用start()办法时也会运行run()办法,区别就是一个是由线程调度运行run()办法,一个是间接调用了线程中的run()办法!! 看到这里,可能有些人就会问啦,那wait()和notify()呢?要留神,其实wait()与notify()办法是Object的办法,不是Thread的办法!!同时,wait()与notify()会配合应用,别离示意线程挂起和线程复原。 这里还有一个很常见的问题,顺带提一下:wait()与sleep()的区别,简略来说wait()会开释对象锁而sleep()不会开释对象锁。这些问题有很多的材料,不再赘述。 3、线程状态: 线程总共有5大状态,通过下面第二个知识点的介绍,了解起来就简略了。 新建状态:新建线程对象,并没有调用start()办法之前 就绪状态:调用start()办法之后线程就进入就绪状态,然而并不是说只有调用start()办法线程就马上变为以后线程,在变为以后线程之前都是为就绪状态。值得一提的是,线程在睡眠和挂起中复原的时候也会进入就绪状态哦。 运行状态:线程被设置为以后线程,开始执行run()办法。就是线程进入运行状态 阻塞状态:线程被暂停,比如说调用sleep()办法后线程就进入阻塞状态 死亡状态:线程执行完结 4、锁类型 可重入锁:在执行对象中所有同步办法不必再次取得锁 可中断锁:在期待获取锁过程中可中断 偏心锁: 按期待获取锁的线程的等待时间进行获取,等待时间长的具备优先获取锁权力 读写锁:对资源读取和写入的时候拆分为2局部解决,读的时候能够多线程一起读,写的时候必须同步地写 synchronized与Lock的区别1、我把两者的区别分类到了一个表中,不便大家比照: 类别 synchronized Lock存在档次 Java的关键字,在jvm层面上 是一个类锁的开释 1、以获取锁的线程执行完同步代码,开释锁 2、线程执行产生异样,jvm会让线程开释锁 在finally中必须开释锁,不然容易造成线程死锁锁的获取 假如A线程取得锁,B线程期待。如果A线程阻塞,B线程会始终期待 分状况而定,Lock有多个锁获取的形式,具体上面会说道,大抵就是能够尝试取得锁,线程能够不必始终期待锁状态 无奈判断 能够判断锁类型 可重入 不可中断 非偏心 可重入 可判断 可偏心(两者皆可)性能 大量同步 大量同步或者,看到这里还对LOCK所知甚少,那么接下来,咱们进入LOCK的深刻学习。 Lock具体介绍与Demo以下是Lock接口的源码,笔者修剪之后的后果: public interface Lock { /** * Acquires the lock. */ void lock(); /** * Acquires the lock unless the current thread is * {@linkplain Thread#interrupt interrupted}. */ void lockInterruptibly() throws InterruptedException; /** * Acquires the lock only if it is free at the time of invocation. */ boolean tryLock(); /** * Acquires the lock if it is free within the given waiting time and the * current thread has not been {@linkplain Thread#interrupt interrupted}. */ boolean tryLock(long time, TimeUnit unit) throws InterruptedException; /** * Releases the lock. */ void unlock(); }从Lock接口中咱们能够看到次要有个办法,这些办法的性能从正文中能够看出:lock():获取锁,如果锁被暂用则始终期待 ...

July 22, 2021 · 3 min · jiezi

关于synchronized:乐观锁和悲观锁

乐观锁和乐观锁

June 10, 2021 · 1 min · jiezi

关于synchronized:验证synchronized锁升级时对象头变化全过程-springboot实战电商项目mall4j

springboot实战电商我的项目mall4j (https://gitee.com/gz-yami/mall4j) java开源商城零碎 验证synchronized锁降级时对象头变动全过程jdk版本:1.8 零碎:window10 64位 jvm 启动参数:-XX:BiasedLockingStartupDelay=0 (勾销提早加载偏差锁) 首先须要已知几个概念 java 非数组对象(一般对象)的内存构造 如果是 array 对象,则会再占用一个 length 空间(4 字节),记录数组的长度。 java object 的 markword 格局(64位虚拟机上) 3.synchronized 四锁降级流程以及何时降级(不赘述) 通过理论编码查看别离在这四个锁状态时,锁标记位是否相应变动 引入 jol 工具包 <dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version></dependency>无锁态 @Testpublic void test01() throws Exception { Object o = new Object(); System.out.println(ClassLayout.parseInstance(o).toPrintable());}执行后果 java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000101 00000000 00000000 00000000) (5) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) 12 4 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes total第一、二行的 object header 是 markword 的内存,第三行的 object header 是 class pointer 的内容,第四行是对齐填充。 ...

May 13, 2021 · 4 min · jiezi

关于synchronized:13张图深入理解Synchronized

前言Java并发编程系列第二篇Synchronized,文章格调仍然是图文并茂,通俗易懂,本文带读者们由浅入深了解Synchronized,让读者们也能与面试官疯狂对线。 在并发编程中Synchronized始终都是元老级的角色,Jdk 1.6以前大家都称说它为重量级锁,绝对于J U C包提供的Lock,它会显得轻便,不过随着Jdk 1.6对Synchronized进行各种优化后,Synchronized性能曾经十分快了。 内容纲要 Synchronized应用形式Synchronized是Java提供的同步关键字,在多线程场景下,对共享资源代码段进行读写操作(必须蕴含写操作,光读不会有线程平安问题,因为读操作人造具备线程平安个性),可能会呈现线程平安问题,咱们能够应用Synchronized锁定共享资源代码段,达到互斥(mutualexclusion)成果,保障线程平安。 共享资源代码段又称为临界区(critical section),保障临界区互斥,是指执行临界区(critical section)的只能有一个线程执行,其余线程阻塞期待,达到排队成果。 Synchronized的食用形式有三种 润饰一般函数,监视器锁(monitor)便是对象实例(this)润饰动态动态函数,视器锁(monitor)便是对象的Class实例(每个对象只有一个Class实例)润饰代码块,监视器锁(monitor)是指定对象实例一般函数一般函数应用Synchronized的形式很简略,在拜访权限修饰符与函数返回类型间加上Synchronized。 多线程场景下,thread与threadTwo两个线程执行incr函数,incr函数作为共享资源代码段被多线程读写操作,咱们将它称为临界区,为了保障临界区互斥,应用Synchronized润饰incr函数即可。 public class SyncTest { private int j = 0; /** * 自增办法 */ public synchronized void incr(){ //临界区代码--start for (int i = 0; i < 10000; i++) { j++; } //临界区代码--end } public int getJ() { return j; }}public class SyncMain { public static void main(String[] agrs) throws InterruptedException { SyncTest syncTest = new SyncTest(); Thread thread = new Thread(() -> syncTest.incr()); Thread threadTwo = new Thread(() -> syncTest.incr()); thread.start(); threadTwo.start(); thread.join(); threadTwo.join(); //最终打印后果是20000,如果不应用synchronized润饰,就会导致线程平安问题,输入不确定后果 System.out.println(syncTest.getJ()); }}代码非常简略,incr函数被synchronized润饰,函数逻辑是对j进行10000次累加,两个线程执行incr函数,最初输入j后果。 ...

May 7, 2021 · 3 min · jiezi

关于synchronized:synchronized-是王的后宫总管线程是王妃

synchronized 是王的后宫总管,线程是王妃关注 「码哥字节」每一篇都是硬核,读者群已开明,后盾回复 「加群」一起成长。如果 synchronized 是「王」身边的「大总管」,那么 Thread 就像是他后宫的王妃。「王」每日只能抉择一个==王妃==陪伴,王妃们会千方百计争宠取得陪伴权,大总管须要通过肯定的伎俩让王「翻牌」一个「王妃」与王相伴。 在JMM 透析 volatile 与 synchronized 原理一文中解说了内存模型与并发实现原理的深层关系,今日听「码哥」胡说八道解开 synchronized 大总管如何调度「王妃」陪伴「王」,王妃不同状态的变动到底经验了什么?且看 synchronized 大总管又采取了哪些伎俩更加高效「翻牌」一个王妃,宫斗还有 30 秒达到战场!!! [toc] 「码哥字节」讲通过几个故事,通俗易懂的让读者敌人齐全把握 synchronized 的锁优化(偏差锁 -> 轻量级锁 -> 重量级锁)原理以及线程在 6 种状态之间转换的神秘。 形象出三个概念:Thread 对应后宫佳丽「王妃」,synchronized 是后宫大总管负责安顿调度王妃,「王」则是被王妃们想要竞争的资源。 王妃的 6 种状态后宫佳丽等级森严,王妃们在这场权贵的游戏中每个人的目标都是为获取「王」钟爱,在游戏中本身的状态也随着变动。 就像生物从出世到长大、最终死亡的过程一样,「王妃」也有本人的生命周期,在 王妃的生命周期中一共有 6 种状态。 New(新入宫):Thread state for a thread which has not yet started.Runnable 可运行、就绪:(身材舒服,筹备好了),Java 中的 Runable 状态对应操作系统线程状态中的两种状态,别离是 Running 和 Ready,也就是说,Java 中处于 Runnable 状态的线程有可能正在执行,也有可能没有正在执行,正在期待被调配 CPU 资源。Blocked 阻塞(身材欠佳、被打入冷宫)WAITING(期待):(期待传唤)Timed Waiting(计时期待):在门外计时期待Terminated(终止):嗝屁 王妃在任意时刻只能是其中的一种状态,通过 getState() 办法获取线程状态。 ...

December 17, 2020 · 3 min · jiezi

深入分析synchronized实现原理

实现原理Synchronized可以保证一个在多线程运行中,同一时刻只有一个方法或者代码块被执行,它还可以保证共享变量的可见性和原子性在Java中每个对象都可以作为锁,这是Synchronized实现同步的基础。具体的表现为一下3种形式: 普通同步方法,锁是当前实例对象;静态同步方法,锁是当前类的Class对象;同步方法快,锁是Synchronized括号中配置的对象。当一个线程试图访问同步代码块时,它必须先获取到锁,当同步代码块执行完毕或抛出异常时,必须释放锁。那么它是如何实现这一机制的呢?我们先来看一个简单的synchronized的代码: public class SyncDemo { public synchronized void play() {} public void learn() { synchronized(this) { } }}利用javap工具查看生成的class文件信息分析Synchronized,下面是部分信息 public com.zzw.juc.sync.SyncDemo(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/zzw/juc/sync/SyncDemo; public synchronized void play(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=0, locals=1, args_size=1 0: return LineNumberTable: line 10: 0 LocalVariableTable: Start Length Slot Name Signature 0 1 0 this Lcom/zzw/juc/sync/SyncDemo; public void learn(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: aload_0 1: dup 2: astore_1 3: monitorenter 4: aload_1 5: monitorexit 6: goto 14 9: astore_2 10: aload_1 11: monitorexit 12: aload_2 13: athrow 14: return Exception table: from to target type 4 6 9 any 9 12 9 any从上面利用javap工具生成的信息我们可以看到同步方法是利用ACC_SYNCHRONIZED这个修饰符来实现的,同步代码块是利用monitorenter和monitorexit这2个指令来实现的。 ...

November 4, 2019 · 2 min · jiezi

且听我一个故事讲透一个锁原理之synchronized

微信公众号:IT一刻钟大型现实非严肃主义现场一刻钟与你分享优质技术架构与见闻,做一个有剧情的程序员关注可第一时间了解更多精彩内容,定期有福利相送哟。故事从这里展开蜀国有一个皇帝叫蜀道难,他比较难伺候,别的皇帝早朝都是在大殿上同时接见所有大臣,共商国是。他不一样,他说早朝你们不要有事没事都跑过来叽叽喳喳,有事则来,无事则该干啥干啥去,然后安排太监每天早上在大门口守着,每次只允许一个大臣进来汇报情况。“你敢多放进来一个就砍脑袋的干活。“太监赶紧下跪,说“谪!“。第一天,太监传话钦天监求见,皇帝允了,钦天监上殿报曰:”臣禀报,昨日我司夜观星象,西方忽现王星忽明忽暗,恐戎狄那边有乱。““朕知道了,退下吧”。一日无事。第二天,太监传话钦天监求见,皇帝允了。一日无事。第三天,太监传话钦天监求见……一日无事。第四天,钦天监……一日无事。第五天,皇帝不耐烦了,和贾太监说,钦天监这老家伙整天是不是闲着没事,以后他来了不用给我禀报,直接放他上殿讲,讲完让他走吧。国泰民安的日子依旧过着,每天只有钦天监一个人来报告,贾太监每次看到是钦天监来了,也懒得搭理了,直接放他进去了。(这就是偏向锁,稍后我细细道来)又一日,钦天监如往常进殿报道,贾太监站在门口打着盹,忽然耳边传来一个声音:“贾太监,帮我禀告圣上,工部李尚书求见。”“emmm…进去吧…嗯?等等,尚书大人你先等等,钦天监在里面,你等会再来求见吧。”太监一阵后怕,寻思着钦天监还在里面呢,这要是放进去了,我这脑袋可就没了,果然嗜睡误事。过了一会儿,李尚书回来询问求见,被告知钦天监还没走,只好又离去。又过了一会儿,李尚书又回来询问求见,正巧钦天监走了,太监进殿传话说工部李尚书求见,皇帝宣觐见,李尚书进殿上报了一番东南连连大雨,已派人去监察水利,修缮河堤。(这就是轻量级锁)忽一日,西戎狄和北匈奴同时对帝国西方和北方发难,前线战事消息如片片雪花纷纷涌入京城,瞬间殿外来了一群大臣有要事禀告。一会儿这个来问贾公公我可以进去了吗?一会儿那个来问贾公公我可以进去了吗?把贾太监累的哟,一天下来光说“稍后再来”都把嘴皮子磨破了,没几日,贾太监就跪在皇帝面前哭泣道:“圣上啊,快想想办法呀,奴才这身子骨就要交代在门口了。”皇帝一听,说你傻啊,叫他们一个个在门外排队啊,谁叫你要他们稍后来求见的。贾太监细思大喜,觉得有理,次日在门口竖起一个牌子“禀报要事者,这边排队”,贾太监再也不用一个人对着一群人反复回话,只需要每次出来一个,然后传话放进去一个,就可以了。(这就是重量级锁)上面这个故事,分别讲述了synchronized内部四种级别的状态,分别是:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。重量级锁状态我们首先从重量级锁开始讲,重量级锁是通过互斥量(Mutex)来实现的,即一个线程进入了synchronized同步块,在未完成任务时,会阻塞后面的所有线程。就像上面的故事所讲的,要禀告要事的大臣只能在大殿门口外一个接一个的阻塞排队。之所以称它为重量级锁,是因为Java线程是映射到操作系统的原生线程上的,如果要阻塞或唤醒一个线程,都需要依靠操作系统从当前用户态转换到核心态中,这种状态转换需要耗费处理器很多时间,对于简单同步块,可能状态转换时间比用户代码执行时间还要长,导致实际业务处理所占比偏小,性能损失较大。当然这个在虚拟机层面进行了一些比如自旋等待,锁粗化等等的优化,避免陷入频繁的切换状态。在这里我就不细讲了,有兴趣的可以关注我,我后续再和各位看官讲上一讲。轻量级锁状态轻量级锁是JDK6引入的,它的轻量是相较于通过系统互斥量实现的传统锁,轻量锁并不是用来取代重量级锁的,而是在没有大量线程竞争的情况下,减少系统互斥量的使用,降低性能的损耗。轻量级锁是通过CAS(Compare And Swap)机制实现的,即如果锁被其他线程所占用,当前线程会通过自旋来获取锁,从而避免用户态与核心态的转换。就像上面故事所说的,大殿中钦天监在汇报工作,工部尚书要求见,并不需要贾太监每次都进去问一下皇帝,惹得皇帝龙颜大怒,而是大臣自己隔一段时间便来询问贾太监能不能进去,不能就稍后再来问,直到可以进去为止。偏向锁状态偏向锁也是JDK6引入的,它存在的依据是“大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得”。它是通过记录第一次进入同步块的线程id来实现的,如果下一个要进入同步块的线程与记录的线程id相同,则说明这个锁由此线程占有,可以直接进入到同步块,不用执行CAS。就像故事中的,如果每天只有钦天监一个人来的话,就不用贾太监禀告了,贾太监每次一看到钦天监,寻思着,哟,钦天监呢,您自个儿直接进去吧,说完自个儿出来吧。如果说轻量锁是为了消除系统互斥量带来的性能损耗,那么偏向锁就是为了消除CAS带来的性能损耗,使之在无竞争的情况下消除整个同步,性能无限接近非同步。如何通过这四种状态实现性能大幅度提升的Java对象头要说这个问题,我们需要先讲一下Java对象头,每个对象都会有一个对象头,它分为三个部分:内容说明Mark Word存储对象的hashcode或锁信息Class Metadata Address存储到对象类型数据的指针Array length数组的长度(如果当前对象是数组)从表格可见,synchronized锁的信息是存在对象头里一个叫Mark Word的区域里的,考虑到虚拟机的空间效率,Mark Word被设计成非固定的数据结构,会根据对象的状态复用存储空间来存储不同的内容:锁的升级当JVM启用了偏向锁模式(JDK6以上默认开启),新创建对象的Mark Word是未锁定,未偏向但可偏向状态,此时Mark Word中的Thread id为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)。偏向锁状态—>无锁不可偏向状态/轻量级锁状态当第一个线程尝试进入同步块时,发现Mark Word中线程ID为0,则会使用CAS将自己的线程ID设置到Mark Word中,并且,在当前线程栈中由高到低顺序找到可用的Lock Record,将线程ID记录下。完成这些,此线程就获取了锁对象的偏向锁。当该偏向线程再次进入同步块时,发现锁对象偏向的就是当前线程,会往当前线程的栈中添加一条Displaced Mark Word为空的Lock Record中,用来统计重入的次数,然后继续执行同步块代码,因为线程栈是私有的,不需要CAS指令进行操作,所以在偏向锁模式下,同一个线程,只会执行一个CAS,之后获取释放锁只需要对Lock Record做操作,性能损耗基本可以忽略。当另外一个线程试图进入同步块时,发现Mark Word中线程ID与自己不相符,这个时候就会引发偏向锁的撤销,变成无锁不可偏向状态或轻量级锁状态,当然,这只是宏观上的描述,严格意义上讲是不准确的,因为里面还存在重偏向机制,这里就不过于深入,在后续的文章中,我会专门出一篇文章,给各位看官详细介绍偏向锁到底是怎么回事。无锁不可偏向状态—>轻量级锁状态当锁对象变成无锁不可偏向状态时,多个线程运行到同步块以后,会检查锁对象状态值标志是否加锁,如果没有锁,就把锁对象的Mark Word信息拷贝存储到当前线程栈桢中Lock Record里,然后通过CAS尝试把对象的Mark Word的值改变成一个指向自己线程的指针。如果成功,则当前线程获得锁对象的轻量级锁,其他线程的CAS就会失败,因为锁对象的Mark Word已经变成一个新的指针了,必须等待线程释放锁,此时其他线程则通过自旋来竞争锁。当获取锁的线程执行完毕释放锁的时候,会将Lock Record里面之前拷贝的值还原到锁对象的Mark Word中。轻量级锁状态—>重量级锁状态当自旋次数超过JVM预期上限,会影响性能,所以竞争的线程就会把锁对象的Mark Word指向重锁,所谓的重锁,实际上就是一个堆上的monitor对象,即,重量级锁的状态下,对象的Mark Word为指向一个堆中monitor对象的指针。然后所有的竞争线程放弃自旋,逐个插入到monitor对象里的一个队列尾部,进入阻塞状态。当成功获取轻量级锁的线程执行完毕,尝试通过CAS释放锁时,因为Mark Word已经指向重锁,导致轻量级锁释放失败,这时线程就会知道锁已经升级为重量级锁, 它不仅要释放当前锁,还要唤醒其他阻塞的线程来重新竞争锁。大概流程如下图所示:这里有一点需注意的是:锁只能升级,不能降级。锁的对比锁优点缺点适用场景偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距如果线程间存在锁竞争,会带来额外的锁撤销的消耗适用于只有一个线程访问同步块场景轻量级锁竞争的线程不会堵塞,提高了程序的响音速度始终得不到锁的线程,使用自旋会消耗CPU追求响应时间,同步块执行速度非常快重量级锁线程竞争不使用自旋,不会消耗CPU线程阻塞,响应时间缓慢追求吞吐量,同步块执行速度较慢synchronized的底层实现synchronized无非以下两种:1.对象锁:修饰非静态方法,修饰代码块2.类锁:修饰静态方法,修饰代码块其中按照修饰类型来分,又可以分为代码块同步和方法同步代码块同步代码块同步锁的是对象,使用monitorenter和monitorexit指令实现的。虽然我知道多一行代码少一位看官的定理,但是这里还是必须贴一张代码图,来证明我没有瞎说,是有理有据的“理据服”。想要降服妖怪,就得先将其打回原形,所以我们先对一段简单的代码进行反编译,得到它的字节码。 final Object lock = new Object(); public int subtr(int i){ synchronized (lock){ return i-1; } }字节码:可以看出,monitorenter指令是在编译后插入到同步代码块的开始位置,monitorexit插入到同步代码块结束的地方,正常情况下monitorenter和monitorexit是一对一的匹配,而后面又出现了一个monitorexit,是因为那里是异常处,用来保证方法执行异常的时候,可以自动解锁,而不会造成死锁。方法同步方法同步的实现官方没有透露,我们尝试对一个方法同步的代码进行反编译。 public synchronized int add(int i){ return i+1; }字节码:从字节码里也看不到monitorenter和monitorexit,智能发现flags那里,多了一个ACC_SYNCHRONIZED的标示,没什么头绪。不过我猜想,底层应该是锁方法所属的对象或类。这就是synchronized的大致原理,打回原形之后来看,是不是就觉得也不过如此?有什么疑问或更好的解读,可以在下方留言,我们进行愉快友好的磋商交流。如果觉得有用,记得分享~

April 13, 2019 · 1 min · jiezi

[Java并发-3]Java互斥锁,解决原子性问题

在前面的分享中我们提到。一个或者多个操作在 CPU 执行的过程中不被中断的特性,称为“原子性”思考:在32位的机器上对long型变量进行加减操作存在并发问题,什么原因!?原子性问题如何解决我们已经知道原子性问题是线程切换,如果能够禁用线程切换不就解决了这个问题了嘛?而操作系统做线程切换是依赖 CPU 中断的,所以禁止 CPU 发生中断就能够禁止线程切换。在单核 CPU 时代,这个方案的确是可行的。这里我们以 32 位 CPU 上执行 long 型变量的写操作为例来说明这个问题,long 型变量是 64 位,在 32 位 CPU 上执行写操作会被拆分成两次写操作(写高 32 位和写低 32 位,如下图所示)。图在单核 CPU 场景下,同一时刻只有一个线程执行,禁止 CPU 中断,获得 CPU 使用权的线程就可以不间断地执行,所以两次写操作一定是:要么都被执行,要么都没有被执行,具有原子性。但是在多核场景下,同一时刻,有可能有两个线程同时在执行,一个线程执行在 CPU-1 上,一个线程执行在 CPU-2 上,此时禁止 CPU 中断,只能保证 CPU 上的线程连续执行,并不能保证同一时刻只有一个线程执行,如果这两个线程同时写 long 型变量高 32 位的话,还是会出现问题。同一时刻只有一个线程执行这个条件非常重要,我们称之为互斥。如果我们能够保证对共享变量的修改是互斥的,那么,无论是单核 CPU 还是多核 CPU,就都能保证原子性了。简易锁模型互斥的解决方案,锁。大家脑中的模型可能是这样的。图线程在进入临界区之前,首先尝试加锁 lock(),如果成功,则进入临界区,此时我们称这个线程持有锁;否则就等待,直到持有锁的线程解锁;持有锁的线程执行完临界区的代码后,执行解锁 unlock()。这样理解本身没有问题,但却很容易让我们忽视两个非常非常重要的点:我们锁的是什么?我们保护的又是什么?改进后的锁模型我们知道在现实世界里,锁和锁要保护的资源是有对应关系的,比如我用我家的锁保护我家的东西。在并发编程世界里,锁和资源也应该有这个关系,但这个关系在我们上面的模型中是没有体现的,所以我们需要完善一下我们的模型。图首先,我们要把临界区要保护的资源标注出来,如图中临界区里增加了一个元素:受保护的资源 R;其次,我们要保护资源 R 就得为它创建一把锁 LR;最后,针对这把锁 LR,我们还需在进出临界区时添上加锁操作和解锁操作。另外,在锁 LR 和受保护资源之间,增加了一条连线,这个关联关系非常重要,这里很容易发生BUG,容易出现了类似锁自家门来保护他家资产的事情。Java语言提供的锁锁是一种通用的技术方案,Java 语言提供的synchronized 关键字,就是锁的一种实现。synchronized关键字可以用来修饰方法,也可以用来修饰代码块,基本使用:class X { // 修饰非静态方法 synchronized void foo() { // 临界区 } // 修饰静态方法 synchronized static void bar() { // 临界区 } // 修饰代码块 Object obj = new Object(); void baz() { synchronized(obj) { // 临界区 } }} 参考我们上面提到的模型,加锁 lock() 和解锁 unlock() 这两个操作在Java 编译会自动加上。这样做的好处就是加锁 lock() 和解锁 unlock() 一定是成对出现的。上面的代码我们看到只有修饰代码块的时候,锁定了一个 obj 对象,那修饰方法的时候锁定的是什么呢?这个也是 Java 的一条隐式规则:当修饰静态方法的时候,锁定的是当前类的 Class 对象,在上面的例子中就是 Class X;当修饰非静态方法的时候,锁定的是当前实例对象 this。class X { // 修饰静态方法 synchronized(X.class) static void bar() { // 临界区 }}class X { // 修饰非静态方法 synchronized(this) void foo() { // 临界区 }}锁解决 count+1 问题我们来尝试下用synchronized解决之前遇到的 count+=1 存在的并发问题,代码如下所示。SafeCalc 这个类有两个方法:一个是 get() 方法,用来获得 value 的值;另一个是 addOne() 方法,用来给 value 加 1,并且 addOne() 方法我们用 synchronized 修饰。那么我们使用的这两个方法有没有并发问题呢?class SafeCalc { long value = 0L; long get() { return value; } synchronized void addOne() { value += 1; }}我们先来看看 addOne() 方法,首先可以肯定,被 synchronized 修饰后,无论是单核 CPU 还是多核 CPU,只有一个线程能够执行 addOne() 方法,所以一定能保证原子操作,那是否有可见性问题呢?让我们回顾下之前讲一条 Happens-Before的规则。管程中锁的规则:对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。管程,就是我们这里的 synchronized.我们知道 synchronized 修饰的临界区是互斥的,也就是说同一时刻只有一个线程执行临界区的代码;而这里指的就是前一个线程的解锁操作对后一个线程的加锁操作可见.我们就能得出前一个线程在临界区修改的共享变量(该操作在解锁之前),对后续进入临界区(该操作在加锁之后)的线程是可见的。按照这个规则,如果多个线程同时执行 addOne() 方法,可见性是可以保证的,也就说如果有 1000 个线程执行 addOne() 方法,最终结果一定是 value 的值增加了 1000。我们在来看下,执行 addOne() 方法后,value 的值对 get() 方法是可见的吗?这个可见性是没法保证的。管程中锁的规则,是只保证后续对这个锁的加锁的可见性,而 get() 方法并没有加锁操作,所以可见性没法保证。那如何解决呢?很简单,就是 get() 方法也 synchronized 一下,完整的代码如下所示。class SafeCalc { long value = 0L; synchronized long get() { return value; } synchronized void addOne() { value += 1; }}上面的代码转换为我们提到的锁模型,就是下面图示这个样子。get() 方法和 addOne() 方法都需要访问 value 这个受保护的资源,这个资源用 this 这把锁来保护。线程要进入临界区 get() 和 addOne(),必须先获得 this 这把锁,这样 get() 和 addOne() 也是互斥的。图锁和受保护资源的关系我们前面提到,受保护资源和锁之间的关联关系非常重要,他们的关系是怎样的呢?一个合理的关系是:受保护资源和锁之间的关联关系是 N:1 的关系上面那个例子我稍作改动,把 value 改成静态变量,把 addOne() 方法改成静态方法,此时 get() 方法和 addOne() 方法是否存在并发问题呢?class SafeCalc { static long value = 0L; synchronized long get() { return value; } synchronized static void addOne() { value += 1; }}如果你仔细观察,就会发现改动后的代码是用两个锁保护一个资源。这个受保护的资源就是静态变量 value,两个锁分别是 this 和 SafeCalc.class。我们可以用下面这幅图来形象描述这个关系。由于临界区 get() 和 addOne() 是用两个锁保护的,因此这两个临界区没有互斥关系,临界区 addOne() 对 value 的修改对临界区 get() 也没有可见性保证,这就导致并发问题了。图:小结:互斥锁,在并发领域的知名度极高,只要有了并发问题,大家首先容易想到的就是加锁,加锁能够保证执行临界区代码的互斥性。synchronized 是 Java 在语言层面提供的互斥原语,其实 Java 里面还有很多其他类型的锁,但作为互斥锁,原理都是相通的:锁,一定有一个要锁定的对象,至于这个锁定的对象要保护的资源以及在哪里加锁 / 解锁,就属于设计层面的事情。如何一把锁保护多个资源?保护没有关联关系的多个资源当我们要保护多个资源时,首先要区分这些资源是否存在关联关系。同样这对应到编程领域,也很容易解决。例如,银行业务中有针对账户余额(余额是一种资源)的取款操作,也有针对账户密码(密码也是一种资源)的更改操作,我们可以为账户余额和账户密码分配不同的锁来解决并发问题,这个还是很简单的。相关的示例代码如下,账户类 Account 有两个成员变量,分别是账户余额 balance 和账户密码 password。取款 withdraw() 和查看余额 getBalance() 操作会访问账户余额 balance,我们创建一个 final 对象 balLock 作为锁(类比球赛门票);而更改密码 updatePassword() 和查看密码 getPassword() 操作会修改账户密码 password,我们创建一个 final 对象 pwLock 作为锁(类比电影票)。不同的资源用不同的锁保护,各自管各自的,很简单。class Account { // 锁:保护账户余额 private final Object balLock = new Object(); // 账户余额 private Integer balance; // 锁:保护账户密码 private final Object pwLock = new Object(); // 账户密码 private String password; // 取款 void withdraw(Integer amt) { synchronized(balLock) { if (this.balance > amt){ this.balance -= amt; } } } // 查看余额 Integer getBalance() { synchronized(balLock) { return balance; } } // 更改密码 void updatePassword(String pw){ synchronized(pwLock) { this.password = pw; } } // 查看密码 String getPassword() { synchronized(pwLock) { return password; } }}当然,我们也可以用一把互斥锁来保护多个资源,例如我们可以用 this 这一把锁来管理账户类里所有的资源:但是用一把锁就是性能太差,会导致取款、查看余额、修改密码、查看密码这四个操作都是串行的。而我们用两把锁,取款和修改密码是可以并行的。用不同的锁对受保护资源进行精细化管理,能够提升性能 。这种锁还有个名字,叫 细粒度锁保护有关联关系的多个资源如果多个资源是有关联关系的,那这个问题就有点复杂了。例如银行业务里面的转账操作,账户 A 减少 100 元,账户 B 增加 100 元。这两个账户就是有关联关系的。那对于像转账这种有关联关系的操作,我们应该怎么去解决呢?先把这个问题代码化。我们声明了个账户类:Account,该类有一个成员变量余额:balance,还有一个用于转账的方法:transfer(),然后怎么保证转账操作 transfer() 没有并发问题呢?class Account { private int balance; // 转账 void transfer(Account target, int amt){ if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } }相信你的直觉会告诉你这样的解决方案:用户 synchronized 关键字修饰一下 transfer() 方法就可以了,于是你很快就完成了相关的代码,如下所示。class Account { private int balance; // 转账 synchronized void transfer(Account target, int amt){ if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } }在这段代码中,临界区内有两个资源,分别是转出账户的余额 this.balance 和转入账户的余额 target.balance,并且用的是一把锁this,符合我们前面提到的,多个资源可以用一把锁来保护,这看上去完全正确呀。真的是这样吗?可惜,这个方案仅仅是看似正确,为什么呢?问题就出在 this 这把锁上,this 这把锁可以保护自己的余额 this.balance,却保护不了别人的余额 target.balance,就像你不能用自家的锁来保护别人家的资产,也不能用自己的票来保护别人的座位一样。下面我们具体分析一下,假设有 A、B、C 三个账户,余额都是 200 元,我们用两个线程分别执行两个转账操作:账户 A 转给账户 B 100 元,账户 B 转给账户 C 100 元,最后我们期望的结果应该是账户 A 的余额是 100 元,账户 B 的余额是 200 元, 账户 C 的余额是 300 元。我们假设线程 1 执行账户 A 转账户 B 的操作,线程 2 执行账户 B 转账户 C 的操作。这两个线程分别在两颗 CPU 上同时执行,那它们是互斥的吗?我们期望是,但实际上并不是。因为线程 1 锁定的是账户 A 的实例(A.this),而线程 2 锁定的是账户 B 的实例(B.this),所以这两个线程可以同时进入临界区 transfer()。同时进入临界区的结果是什么呢?线程 1 和线程 2 都会读到账户 B 的余额为 200,导致最终账户 B 的余额可能是 300(线程 1 后于线程 2 写 B.balance,线程 2 写的 B.balance 值被线程 1 覆盖),可能是 100(线程 1 先于线程 2 写 B.balance,线程 1 写的 B.balance 值被线程 2 覆盖),就是不可能是 200。使用锁的正确知识在上一篇文章中,我们提到用同一把锁来保护多个资源,也就是现实世界的“包场”,那在编程领域应该怎么“包场”呢?很简单,只要我们的 锁能覆盖所有受保护资源 就可以了。这里我们用 Account.class· 作为共享的锁。Account.class 是所有 Account 对象共享的,而且这个对象是 Java 虚拟机在加载 Account 类的时候创建的,所以我们不用担心它的唯一性。class Account { private int balance; // 转账 void transfer(Account target, int amt){ synchronized(Account.class) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } }下面这幅图很直观地展示了我们是如何使用共享的锁 Account.class 来保护不同对象的临界区的。图思考下:上面的写法不是最佳实践,锁是可变的。小结对如何保护多个资源已经很有心得了,关键是要分析多个资源之间的关系。如果资源之间没有关系,很好处理,每个资源一把锁就可以了。如果资源之间有关联关系,就要选择一个粒度更大的锁,这个锁应该能够覆盖所有相关的资源。除此之外,还要梳理出有哪些访问路径,所有的访问路径都要设置合适的锁。思路:在第一个示例程序里,我们用了两把不同的锁来分别保护账户余额、账户密码,创建锁的时候,我们用的是:private final Object xxxLock = new Object();如果账户余额用 this.balance 作为互斥锁,账户密码用 this.password 作为互斥锁,你觉得是否可以呢? ...

April 10, 2019 · 3 min · jiezi

synchronized锁了什么

前言synchronized翻译为中文的意思是同步的,它是Java中处理线程安全问题常用的关键字。也有人称其为同步锁。既然是锁,其必然有锁的东西,下面先会简单介绍一下synchronized,再通过一个示例代码展示synchronized锁了什么。(这里先提前透露答案synchronized锁的是代码)介绍定义synchronized提供的同步机制确保了同一个时刻,被修饰的代码块或方法只会有一个线程执行。用法synchronized可以修饰方法和代码块:修饰普通方法修饰静态方法修饰代码块根据修饰情况,分为对象锁和类锁:对象锁:普通方法(等价于代码块修饰this)代码块修饰的是是类的一个对象类锁类方法(等价于代码块修饰当前类Class对象)代码块修饰的是是类Class对象原理synchronized底层原理是使用了对象持有的监视器(monitor)。但是同步代码块和同步方法的原理存在一点差异:同步代码块是使用monitorenter和monitorexit指令实现的同步方法是由方法调用指令读取运行时常量池中方法的ACC_SYNCHRONIZED 标识隐式实现,实际上还是调用了monitorenter和monitorexit指令测试示例计数器一个特殊的计数器,自增方法increase()被synchronized修饰,而获取当前值方法getCurrent()则没有被synchronized修饰。/** * 计数器 * @author RJH * create at 2019-03-13 /public class Counter { /* * 全局对象,总数 / private static int i = 0; /* * 自增 * @return / public synchronized int increase() { try { //使用休眠让结果更明显 Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } return ++i; } /* * 获取当前值 * @return / public int getCurrent() { return i; }}测试代码使用自增线程和获取当前值的线程来验证synchronized锁的是代码,而不是全局变量/* * synchronized锁了什么 * @author RJH * create at 2019-03-02 /public class LockWhatTest { public static void main(String[] args) { Counter counter =new Counter(); IncreaseThread increaseThread1=new IncreaseThread(counter); IncreaseThread increaseThread2=new IncreaseThread(counter); GetThread getThread=new GetThread(counter); increaseThread1.start(); increaseThread2.start(); //直到increaseThread的线程启动才执行下一步 while (increaseThread1.getState().compareTo(Thread.State.NEW)==0 && increaseThread1.getState().compareTo(Thread.State.NEW)==0){ } getThread.start(); } /* * 自增线程 / static class IncreaseThread extends Thread{ private Counter counter; public IncreaseThread(Counter counter) { this.counter = counter; } @Override public void run() { System.out.println(“After increase:” + counter.increase()+",trigger time:"+System.currentTimeMillis()); } } /* * 获取当前值的线程 */ static class GetThread extends Thread{ private Counter counter; public GetThread(Counter counter) { this.counter = counter; } @Override public void run() { System.out.println(“Current:"+ counter.getCurrent()+",trigger time:"+System.currentTimeMillis()); } }}执行结果Current:0,trigger time:1552487003845After increase:1,trigger time:1552487008846After increase:2,trigger time:1552487013848结果分析从测试结果可以得知在两个自增线程启动后,获取当前值的线程才启动,但是获取当前值的线程是先被执行完成了。根据自增线程执行完成的时间戳间隔可以得知,两个自增线程是依次执行的。从而可以证明synchronized并不是锁定方法内访问的变量synchronized锁定的是同一个监视器对象监视的代码 ...

March 14, 2019 · 1 min · jiezi

Java Lock示例 - ReentrantLock

引言在多线程环境下,通常我们使用 synchronized 关键字来保证线程安全。大多数情况下,用 synchronized 关键字就足够了,但它也有一些缺点, 所以在 Java Concurrency 包中引入了 Lock API 。从Java 1.5版开始在 java.util.concurrent.locks 包中提供了处理并发的 Concurrency API 的 Lock 锁接口和一些实现类来改进 Object 锁定机制。Java Lock API中的一些重要接口和类Java Lock API中的一些重要接口和类是:锁(Lock):这是Lock API的基本接口。它提供了 synchronized 关键字的所有功能,以及为锁定创建不同条件的其他方法,为线程等待锁定提供超时功能。一些重要的方法是 lock() 获取锁,unlock() 释放锁,tryLock() 等待锁定一段时间,newCondition() 创建条件等。条件(Condition):条件对象类似于对象等待通知( Object wait-notify)模型,具有创建不同等待集的附加功能。Condition 对象始终由 Lock 对象创建。一些重要的方法是 await(),类似于Object.wait() 和 signal(),signalAll(),类似于 Object.notify() 和 Object.notifyAll() 方法。读写锁(ReadWriteLock):它包含一对关联的锁,一个用于只读操作,另一个用于写入。只要没有写入线程,读锁可以由多个读取线程同时保持。写锁是独占的。重入锁(ReentrantLock):这是最广泛使用的 Lock 接口实现类。此类以与 synchronized 关键字类似的方式实现 Lock 接口。除了 Lock 接口实现之外,ReentrantLock 还包含一些实用程序方法来获取持有锁的线程,等待获取锁线程等。synchronized 块synchronized 块本质上是可重入的,即如果一个线程锁定了监视对象,并且另一个同步块需要锁定在同一个监视对象上,则线程可以进入该代码块。我认为这就是类名是ReentrantLock的原因。让我们通过一个简单的例子来理解这个特性。public class Test{public synchronized foo(){ //do something bar(); } public synchronized bar(){ //do some more }}如果一个线程进入 foo(),它就会锁定Test对象,所以当它尝试执行 bar() 方法时,允许该线程执行 bar() 方法,因为它已经在 Test 对象上持有锁,即与 synchronized(this) 效果是一样的。Java Lock 示例 - Java 中的 ReentrantLock现在让我们看一个简单的例子,我们将使用 Java Lock API 替换 synchronized 关键字。假设我们有一个 Resource 类,其中包含一些操作,我们希望它是线程安全的,以及一些不需要线程安全的方法。package com.journaldev.threads.lock;public class Resource { public void doSomething(){ //do some operation, DB read, write etc } public void doLogging(){ //logging, no need for thread safety }}现在假设我们有一个 Runnable 类,我们将使用 Resource 方法。package com.journaldev.threads.lock;public class SynchronizedLockExample implements Runnable{ private Resource resource; public SynchronizedLockExample(Resource r){ this.resource = r; } @Override public void run() { synchronized (resource) { resource.doSomething(); } resource.doLogging(); }}请注意,我使用 synchronized 块来获取 Resource 对象上的锁。我们可以在类中创建一个虚拟对象,并将其用于锁定的目的。现在让我们看看我们如何使用 Java Lock API 并重写上面的程序而不使用 synchronized 关键字。我们将在Java 中使用 ReentrantLock。package com.journaldev.threads.lock;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class ConcurrencyLockExample implements Runnable{ private Resource resource; private Lock lock; public ConcurrencyLockExample(Resource r){ this.resource = r; this.lock = new ReentrantLock(); } @Override public void run() { try { if(lock.tryLock(10, TimeUnit.SECONDS)){ resource.doSomething(); } } catch (InterruptedException e) { e.printStackTrace(); }finally{ //release lock lock.unlock(); } resource.doLogging(); }}正如你所看到的,我正在使用 tryLock() 方法来确保我的线程只等待一定的时间,如果它没有获得对象的锁定,它只是记录和退出。另一个要注意的重点是使用 try-finally 块来确保即使 doSomething() 方法调用抛出任何异常也会释放锁定。Java Lock 与 synchronized 比较基于以上细节和程序,我们可以很容易地得出 Java Lock 和同步之间的以下差异。Java Lock API 为锁定提供了更多的可见性和选项,不像在线程可能最终无限期地等待锁定的同步,我们可以使用tryLock() 来确保线程仅等待特定时间。用同步关键字的代码更清晰,更易于维护,而使用 Lock,我们不得不尝试使用 try-finally 块来确保即使在 lock() 和 unlock() 方法调用之间抛出异常也会释放 Lock。同步块或方法只能覆盖一种方法,而我们可以在一种方法中获取锁,并使用 Lock API 在另一种方法中释放它。synchronized 关键字不提供公平性,而我们可以在创建 ReentrantLock 对象时将公平性设置为 true,以便最长等待的线程首先获得锁定。我们可以为 Lock 创建不同的等待条件(Condition),不同的线程可以针对不同的条件来 await() 。这就是 Java Lock 示例,Java 中的 ReentrantLock 以及使用 synchronized 关键字的比较分析。作 者:关于Pankaj如果你走得这么远,那就意味着你喜欢你正在读的东西。为什么不直接在Google Plus,Facebook或Twitter上与我联系。我很想直接听到你对我的文章的想法和意见。最近我开始创建视频教程,所以请在Youtube上查看我的视频。 ...

December 31, 2018 · 2 min · jiezi

Synchronized 关键字使用、底层原理、JDK1.6 之后的底层优化以及 和ReenTrantLock 的对比

以下内容摘自我的 Gitchat :Java 程序员必备:并发知识系统总结,欢迎订阅!Github 地址:https://github.com/Snailclimb/JavaGuide/edit/master/Java相关/synchronized.mdsynchronized关键字最主要的三种使用方式的总结修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 。也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 和 synchronized 方法一样,synchronized(this)代码块也是锁定当前对象的。synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。这里再提一下:synchronized关键字加到非 static 静态方法上是给对象实例上锁。另外需要注意的是:尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓冲功能!下面我已一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单利模式的原理呗!”双重校验锁实现对象单例(线程安全)public class Singleton { private volatile static Singleton uniqueInstance; private Singleton() { } public static Singleton getUniqueInstance() { //先判断对象是否已经实例过,没有实例化过才进入加锁代码 if (uniqueInstance == null) { //类对象加锁 synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; }}另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:为 uniqueInstance 分配内存空间初始化 uniqueInstance将 uniqueInstance 指向分配的内存地址但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出先问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。synchronized 关键字底层原理总结synchronized 关键字底层原理属于 JVM 层面。① synchronized 同步语句块的情况public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println(“synchronized 代码块”); } }}通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行javap -c -s -v -l SynchronizedDemo.class。从上面我们可以看出:synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。② synchronized 修饰方法的的情况public class SynchronizedDemo2 { public synchronized void method() { System.out.println(“synchronized 方法”); }}synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。在 Java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。JDK1.6 之后的底层优化JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。①偏向锁引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉。偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步!关于偏向锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。② 轻量级锁倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。 关于轻量级锁的加锁和解锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁!③ 自旋锁和自适应自旋轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋。百度百科对自旋锁的解释:何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,“自旋"一词就是因此而得名。自旋锁在 JDK1.6 之前其实就已经引入了,不过是默认关闭的,需要通过–XX:+UseSpinning参数来开启。JDK1.6及1.6之后,就改为默认开启的了。需要注意的是:自旋等待不能完全替代阻塞,因为它还是要占用处理器时间。如果锁被占用的时间短,那么效果当然就很好了!反之,相反!自旋等待的时间必须要有限度。如果自旋超过了限定次数任然没有获得锁,就应该挂起线程。自旋次数的默认值是10次,用户可以修改–XX:PreBlockSpin来更改。另外,在 JDK1.6 中引入了自适应的自旋锁。自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定,虚拟机变得越来越“聪明”了。④ 锁消除锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。⑤ 锁粗化原则上,我们再编写代码的时候,总是推荐将同步快的作用范围限制得尽量小——只在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。Synchronized 和 ReenTrantLock 的对比① 两者都是可重入锁两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。② synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 APIsynchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。③ ReenTrantLock 比 synchronized 增加了一些高级功能相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。④ 性能已不是选择标准在JDK1.6之前,synchronized 的性能是比 ReenTrantLock 差很多。具体表示为:synchronized 关键字吞吐量岁线程数的增加,下降得非常严重。而ReenTrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。JDK1.6 之后,synchronized 和 ReenTrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReenTrantLock 的文章都是错的!JDK1.6之后,性能已经不是选择synchronized和ReenTrantLock的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作。 ...

October 26, 2018 · 2 min · jiezi

值得保存的 synchronized 关键字总结

该文已加入开源文档:JavaGuide(一份涵盖大部分Java程序员所需要掌握的核心知识)。地址:https://github.com/Snailclimb…本文是对 synchronized 关键字使用、底层原理、JDK1.6之后的底层优化以及和ReenTrantLock对比做的总结。如果没有学过 synchronized 关键字使用的话,阅读起来可能比较费力。两篇比较基础的讲解 synchronized 关键字的文章:《Java多线程学习(二)synchronized关键字(1)》《Java多线程学习(二)synchronized关键字(2)》synchronized 关键字的总结synchronized关键字最主要的三种使用方式的总结修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 。也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。所以如果一个线程A调用一个实例对象的非静态synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 和 synchronized 方法一样,synchronized(this)代码块也是锁定当前对象的。synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。这里再提一下:synchronized关键字加到非 static 静态方法上是给对象实例上锁。另外需要注意的是:尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓冲功能!synchronized 关键字底层实现原理总结synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 在 Java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。所有用户程序都是运行在用户态的, 但是有时候程序确实需要做一些内核态的事情, 例如从硬盘读取数据, 或者从键盘获取输入等. 而唯一可以做这些事情的就是操作系统,synchronized关键字底层优化总结JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。偏向锁引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉。偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步!关于偏向锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。轻量级锁倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。 关于轻量级锁的加锁和解锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁!自旋锁和自适应自旋轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋。百度百科对自旋锁的解释:何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,“自旋"一词就是因此而得名。自旋锁在 JDK1.6 之前其实就已经引入了,不过是默认关闭的,需要通过–XX:+UseSpinning参数来开启。JDK1.6及1.6之后,就改为默认开启的了。需要注意的是:自旋等待不能完全替代阻塞,因为它还是要占用处理器时间。如果锁被占用的时间短,那么效果当然就很好了!反之,相反!自旋等待的时间必须要有限度。如果自旋超过了限定次数任然没有获得锁,就应该挂起线程。自旋次数的默认值是10次,用户可以修改–XX:PreBlockSpin来更改。另外,在 JDK1.6 中引入了自适应的自旋锁。自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定,虚拟机变得越来越“聪明”了。锁消除锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。锁粗化原则上,我们再编写代码的时候,总是推荐将同步快的作用范围限制得尽量小——只在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。ReenTrantLock 和 synchronized 关键字的总结推荐一篇讲解 ReenTrantLock 的使用比较基础的文章:《Java多线程学习(六)Lock锁的使用》两者都是可重入锁两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 APIsynchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。ReenTrantLock 比 synchronized 增加了一些高级功能相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。性能已不是选择标准在JDK1.6之前,synchronized 的性能是比 ReenTrantLock 差很多。具体表示为:synchronized 关键字吞吐量岁线程数的增加,下降得非常严重。而ReenTrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。JDK1.6 之后,synchronized 和 ReenTrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReenTrantLock 的文章都是错的!JDK1.6之后,性能已经不是选择synchronized和ReenTrantLock的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作。参考《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版第13章《实战Java虚拟机》https://blog.csdn.net/javazej…https://blog.csdn.net/qq83864...http://cmsblogs.com/?p=2071你若盛开,清风自来。 欢迎关注我的微信公众号:“乐趣区”,一个有温度的微信公众号。公众号后台回复关键字“1”,你可能看到想要的东西哦! ...

September 7, 2018 · 1 min · jiezi