志之所趋,无远勿届,穷山距海,不能限也。

内容

  • 1. 线程安全
  • 2. 互斥同步
  • 3. synchronized 特性
  • 4. synchronized 基础
    • 4.1 synchronized 代码块
    • 4.2 synchronized 方法
    • 4.3 同步代码块与同步方法的使用注意事项
  • 5. synchronized 原理 (重量级锁 JDK1.6以前)
    • 5.1 对象头
    • 5.2 Mark Word
    • 5.3 重量级锁JVM层面的实现原理:monitor
      • 5.3.1 monitor 结构
      • 5.3.2 monitor的获取和释放(重量级锁的获取和释放)
      • 5.3.3 同步方法和同步代码块的实现原理
        • 5.3.3.1 同步代码块原理
        • 5.3.3.2 同步代码块原理
    • 5.4 重量级锁OS层面的实现原理:互斥锁
      • 5.4.1 互斥锁
      • 5.4.2 重量级锁开销大的原因
  • 6. synchronized 优化(JDK1.6开始)
    • 6.1 锁升级
      • 6.1.1 偏向锁
        • 6.1.1.2 偏向锁的获取
        • 6.1.1.3 偏向锁的释放
        • 6.1.1.4 偏向锁的撤销(升级)
        • 6.1.1.5 批量重偏向、批量撤销
        • 6.1.1.6 偏向锁的关闭
      • 6.1.2 轻量级锁
        • 6.1.2.1 轻量级锁的获取
        • 6.1.2.2 轻量级锁的解锁
      • 6.1.3 自旋锁
      • 6.1.4 偏向锁、轻量级锁、重量级锁的比较
      • 6.1.5 对象信息存在哪
    • 6.2 锁消除
    • 6.3 锁粗化

1. 线程安全

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以活得正确的结果,那么这个对象就是线程安全的。

Java语言中得各种操作共享数据可以分成五类,按安全程度由强到弱,来排序:

  1. 不可变:不可变的对象一定是线程安全的,无论对象的方法实现还是方法的调用者都不需要在采取安全措施。

    如果共享数据是一个基本数据类型,那么只要在定义的时候使用final关键字修饰就可以保证它不可变;如果是共享数据是一个对象,需要对象自行保证其行为不会对其状态产生任何影响。

  2. 绝对线程安全:绝对线程安全是不管运行是环境如何,调用者都不需要任何额外的同步措施。通常需要付出很大的甚至不切实际的代价。

  3. 相对线程安全:就是通常意义的线程安全,确保这个对象单独的操作是操作安全的。Java语言中,大部分声称线程安全的类都属于这种类型,如:Vector、Hashtable等。

  4. 线程兼容:指的是本身对象并不是线程安全的,但是可以通过调用端的正确使用同步手段来保证对象的安全使用。通常我们说一个类不是线程安全的,指的就是这种状态,如:ArrayList、HashMap等。

  5. 线程对立:无论调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。

2. 互斥同步

互斥和同步是一种最常见、最主要的实现并发正确性(相对线程安全)的保障手段。

  • 同步:指的是在多个线程并发访问共享数据时,保证共享数据在同一个时刻只能被一个线程使用。

  • 互斥指的是实现同步的一种手段,临界区互斥量和信号量都是主要的互斥实现方式。

在 Java 中,最基本的互斥同步手段就是synchronized关键字。synchronized可以保证线程竞争共享资源的正确性(多个线程并发访问共享数据时,保证共享数据在同一个时刻只能被一个线程使用)。

3. synchronized 特性

  • 原子性:确保线程互斥的访问同步代码。synchronized保证只有一个线程拿到锁,进入同步代码块操作共享资源,因此具有原子性。

  • 可见性:保证共享变量的修改能够及时可见。执行 synchronized时,会对应执行 lock 、unlock原子操作。lock操作,就会清空工作空间该变量的值;执行unlock操作之前,必须先把变量同步回主内存中。

  • 有序性:synchronized内的代码和外部的代码禁止排序,至于内部的代码,则不会禁止排序,但是由于只有一个线程进入同步代码块,因此在同步代码块中相当于是单线程的,根据 as-if-serial 语义,即使代码块内发生了重排序,也不会影响程序执行的结果。

  • 悲观锁:synchronized是悲观锁。每次使用共享资源时都认为会和其他线程产生竞争,所以每次使用共享资源都会上锁。

  • 独占锁(排他锁):synchronized是独占锁(排他锁)。该锁一次只能被一个线程所持有,其他线程被阻塞。

  • 非公平锁:synchronized是非公平锁。线程获取锁的顺序可以不按照线程的阻塞顺序。允许线程发出请求后立即尝试获取锁。

  • 可重入锁:synchronized是可重入锁。持锁线程可以再次获取自己的内部的锁。

Tips:

按锁的性质可以将锁分为以下几种类别:

  1. 悲观锁 or 乐观锁:是否一定要锁

  2. 共享锁 or 独占锁(排他锁):是否可以有多个线程同时拿锁

  3. 公平锁 or 非公平锁:是否按阻塞顺序拿锁

  4. 可重入锁 or 不可重入锁:拿锁线程是否可以多次拿锁

4. synchronized 基础

4.1 synchronized 代码块

public class SyncTest {public static void main(String[] args) {synchronized (SyncTest.class) {}}
}

同步代码块中,锁为syncronized括号中配置的对象。

4.2 synchronized 方法

public class SyncTest {public synchronized void fun(){}
}

普通同步方法,锁是当前实例对象this。

public class SyncTest {public synchronized static void fun(){}
}

静态同步方法,锁是当前类的Class对象。

4.3 同步代码块与同步方法的使用注意事项

  1. 只有同步代码块与同步方法的锁对象是同一个对象对象时,才能保证同一时刻,只有一个线程访问共享资源。
class SyncWrong implements Runnable {private static int i = 0;private synchronized void add() {i++;}@Overridepublic void run() {for (int j = 0; j < 1000000; j++) {add();}}public static void main(String[] args) throws Exception {Thread t1 = new Thread(new SyncWrong());Thread t2 = new Thread(new SyncWrong());t1.start();t2.start();t1.join();t2.join();System.out.println(i);}
}

某次输出结果:


可以看到,虽然我们对 add方法加了锁,但依然不能保证输出结果的正确性。

这段代码违反了上述规则,add方法是普通同步方法,因此该方法的锁对象是当前实例对象this,主函数中的 t1、t2 线程传入了不同的SyncWrong 对象,因此,synchronized 锁对象不是同一个,自然就无法达到同步的目的。解决办法就是让 synchronized 锁对象是同一个。

方法一:只创建一个SyncWrong对象。

public static void main(String[] args) throws Exception {SyncWrong wrong = new SyncWrong();Thread t1 = new Thread(wrong);Thread t2 = new Thread(wrong);t1.start();t2.start();t1.join();t2.join();System.out.println(i);}

此时,synchronized 锁对象 就是 wrong 这个对象了。

方法二:将add方法改为静态方法。

private synchronized static void add() {i++;
}

静态同步方法的锁对象是当前类的Class对象,类被装载后,在内存中有且仅有一个该类的Class对象,因此 可以保证静态同步方法的锁对象是同一个。

  1. 每个访问共享资源的代码都必须被同步。
class Resource{private static int i = 0;public synchronized void add() {i++;}public void reduce() {i--;}
}

对于上述代码,add 和 reduce 都对共享资源 i 进行了处理,你必须同步这两个方法(将 reduce 改造为同步方法)。如果不这样做,那么reduce()方法就会完全忽略这个对象锁,也就是在当前线程获得到锁,执行add操作时,其他线程可以随意的调用 reduce方法,从而导致并发修改的问题。

  1. 私有化共享资源。

共享资源一般以对象形式存在于内存中,因此要控制共享资源的访问。

class Resource{public static int i = 0;public synchronized void add() {i++;}public synchronized void get() {return i;}
}

上述代码的问题在于任何线程都可以通过 Resource.i 来获取共享变量的值,若当前线程获得锁,正在执行add同步方法的内容,其他线程通过 Resource.i 则会忽略锁,可能获取到中间结果。正确的方法是将 共享资源 i 私有化(用 private 修饰), 然后提供一个被同步的获取方式。

5. synchronized 原理 (重量级锁 JDK1.6以前)

5.1 对象头

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头( Header )、实例数据( Instance Data ) 和对齐填充( Padding )。


HotSpot虚拟机的一般对象头(Header)包括两部分信息:“Mark Word”、“Class Pointer”,数组类对象还包括“Array Length”。

  • Mark Word:存储对象自身的运行时数据
  • Class Pointer:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
  • Array Length:如果是数组对象,则 Array Length 存储数组的长度。

Tip:

  • 对齐填充( Padding ):Java对象占用空间是8字节对齐的,即所有Java对象占用bytes数必须是8的整数倍。若Java对象的bytes数不是8的整数倍,则会填充一些字节,让Java对象占用的空间是8的整数倍。

5.2 Mark Word

synchronized底层实现与Java对象头中的Mark Word密不可分的。Java对象头里的Mark Word存储对象自身的运行时数据,如 HashCode、分代年龄和锁标记位。在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化

32位JVM在不同状态下Mark Word的组成如下表:

64位JVM在不同状态下Mark Word的组成如下表:

  • unused:就是表示没有使用的区域,在64位虚拟机的对象头中会出现。

  • hash:对象的hashcode,如果对象没有重写hashcode()方法,那么通过System.identityHashCode()方法获取。采用延迟加载技术,不会主动计算,但是一旦生成了hashcode,JVM会将其记录在 Mark Word 中。

  • age:4位的Java对象GC年龄。对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15。最大值也是15。

  • biased_lock:1位的偏向锁标记,为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。

  • lock:2位的锁状态标记位,其中无锁和偏向锁的锁标志位都是01,只是在前面的1 bit的biased_lock区分了这是无锁状态还是偏向锁状态。lock和biased_lock共同表示对象处于什么状态:

  • thread Id:持有偏向锁的线程ID。

  • epoch:偏向锁的时间戳。

  • Lock record address:轻量级锁状态下,指向栈中锁记录的指针。

  • Monitor address:重量级锁状态下,指向对象监视器Monitor的指针。

5.3 重量级锁JVM层面的实现原理:monitor

Monitor是一种用来实现同步的工具,又称为对象监视器/管程。每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针。

注意:JDK1.6之后对synchronized 进行了优化,在重量级锁的基础上,又引入了偏向锁和轻量级锁,但只有重量级锁的实现是和monitor相关的。

5.3.1 monitor 结构

在HotSpot虚拟机中,最终采用C++的ObjectMonitor类实现monitor。

ObjectMonitor C++ 结构:

ObjectMonitor() {//成员变量简单的初始化_header       = NULL;  //markOop对象头,重量级锁储存锁对象头信息的地方_count        = 0;_waiters      = 0,  //等待线程数_recursions   = 0;   //锁的重入次数,作用于可重入锁_object       = NULL; //监视器锁寄生的对象。锁不是平白出现的,而是寄托存储于对象中。_owner        = NULL; //指向持有ObjectMonitor对象的线程地址_WaitSet      = NULL; //处于wait状态的线程,会被包装成ObjectWaiter,加入到_WaitSet集合(调用wait方法)_WaitSetLock  = 0 ;  // 保护等待队列,作用于自旋锁。_Responsible  = NULL ;_succ         = NULL ;_cxq          = NULL ;  //阻塞在EntryList上的最近可达的的线程列表FreeNext      = NULL ;_EntryList    = NULL ; //处于等待锁block阻塞状态的线程,会被包装成ObjectWaiter,加入到该列表_SpinFreq     = 0 ;_SpinClock    = 0 ;
OwnerIsThread = 0 ;_previous_owner_tid = 0;// 监视器前一个拥有者线程的ID}


ObjectMonitor 中 几个比较重要的字段:

  • _WaitSet :处于wait状态的线程,会被加入到_WaitSet。
  • _EntryList:处于等待锁block状态的线程,会被加入到_EntryList。
  • _owner:指向持有ObjectMonitor对象的线程。
  • _count:锁计数器,获取锁则 count+1,释放锁 count-1,count直到为0,才可以被其他线程持有。
  • _recursions:记录锁的重入次数。

5.3.2 monitor的获取和释放(重量级锁的获取和释放)

对于重量级锁,获取锁的过程实际上就是获取monitor的过程,释放锁的过程实际上就是释放monitor的过程。monitor的竞争获取是在ObjectMonitor的enter方法中,而释放则是在exit方法中。

  1. 通过CAS尝试把monitor的_owner字段设置为当前线程,然后对_count++,若在设置_owner为当前线程时,发现原来的_owner指向当前线程,则说明当前线程再次进入monitor,还应让_recursions++;

  2. 如果获取锁失败,则当前线程加入_EntryList,等待锁的释放;

  3. 已获取锁的线程执行wait操作,则 _count–,_recursions–,设置_owner 为 null,然后当前线程加入 _WaitSet中,等待被唤醒。

  4. 已获取锁的线程通过 notify 唤醒 _WaitSet中的线程,被唤醒的线程继续竞争锁。

  5. 已获取锁的线程执行完同步代码块时,释放锁(ObjectMonitor.exit),_count–,_recursions–,若 _count = 0 ,则设置 _owner 为 null,然后唤醒 _EntryList竞争锁(竞争是非公平的)。

Tip:

  • Java中的wait、notify、notifyAll等方法依赖于ObjectMonitor对象内部方法来完成,这也就是为什么要在同步方法或同步代码块中调用他们的原因(需要先获取对象的锁),否则就会抛出 IllegalMonitorException 。

  • 由于每个Java对象都可以关联一个Monitor对象,所以任何对象都是可以是锁对象,而基本数据类型不是对象,所以不能作为锁。

5.3.3 同步方法和同步代码块的实现原理

5.3.3.1 同步代码块原理
public class SyncTest {public static void main(String[] args) {synchronized (SyncTest.class) {}}
}

对上面同步代码块编译:javac -encoding UTF-8 SyncTest.java

然后反汇编:javap -v SyncTest.class

得到字节码文件如下:


同步语句块的实现使用的是monitorenter 和 monitorexit指令:

  • monitorenter指令指向同步代码块的开始位置
  • monitorexit指令则指明同步代码块的结束位置。
  • 为了保证方法异常结束时正确释放锁,编译器会自动产生一个异常处理器表,当发生异常时进行处理,确保该线程能正确的执行monitorexit指令。
5.3.3.2 同步代码块原理
public class SyncTest {public synchronized void fun(){}
}

反汇编后得到的字节码文件内容:


同步方法通过ACC_SYNCHRONIZED来标志一个方法是同步方法。

当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor, 然后再执行方法,最后在无论方法是否正常完成,都会释放monitor。

5.4 重量级锁OS层面的实现原理:互斥锁

重量级锁是依赖对象内部的monitor锁来实现的,而monitor又是依赖操作系统的MutexLock(互斥锁、互斥量)来实现的。

5.4.1 互斥锁

互斥锁(互斥量)是一个二元变量,其状态为0/1,0表示未加锁,1表示已加锁。

  • 访问公共资源前,必须申请该互斥锁,若处于未加锁状态,则申请到锁对象,并立即占有该锁。

  • 如果该互斥锁处于锁定状态,则阻塞当前线程。

  • 持锁线程使用完共享资源,释放共享资源,将互斥锁置0,并唤醒被阻塞的线程。

5.4.2 重量级锁开销大的原因

在JDK1.6之前,synchronized属于重量级锁,效率低下,因为Monitor是依赖于底层的操作系统的互斥原语mutex来实现,JDK1.6之前实际上加锁时会调用Monitor的enter方法,解锁时会调用Monitor的exit方法。

多个线程竞争monitor时,被阻塞的线程不会消耗CPU,但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙完成,这会导致线程在 “用户态 和 内核态” 之间来回切换,这个状态之间的转换需要相对比较长的时间,对性能有较大影响。

6. synchronized 优化(JDK1.6开始)

JDK1.6开始,对synchronized 进行了很多方面的优化,如:适应性自旋、锁消除、锁粗化、轻量级锁、偏向锁等,这些技术都是为了在线程之间更高效地共享数据及解决竞争问题,从而提高程序的执行效率。

6.1 锁升级

JDK1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在JDK 1.6中,锁对象一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。


锁可以升级但不能降级,这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

Tip:实际上锁也是可以发生降级的,只不过锁降级的条件比较苛刻。而且锁升降级效率较低,如果频繁升降级的话对性能就会造成很大影响。

synchronized的锁强度究竟可以降级吗?

6.1.1 偏向锁

偏向锁指,锁偏向于第一个获取他的线程,若接下来的执行过程中,该锁一直没有被其他线程获取,则持有偏向锁的线程永远不需要再进行同步。

  • 目的:消除数据在无竞争情况下的同步,也就是说优化了只有一个线程执行 同步代码块 时的同步操作。
  • 原理:CAS
6.1.1.2 偏向锁的获取

  1. 检测锁对象的MarkWord是否为可偏向状态,即是否为偏向锁标识1,锁标识位为01;
  2. 如果为可偏向状态,判断线程ID是否指向当前线程,如果是进入步骤(5),否则进入步骤(3)。若为不可偏状态,直接升级为轻量级锁,进入轻量级锁逻辑。
  3. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。
  4. 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
  5. 执行同步代码。

6.1.1.3 偏向锁的释放

偏向锁使用了一种等到竞争出现才释放偏向锁的机制:偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。

6.1.1.4 偏向锁的撤销(升级)

偏向锁释放失败(其他线程通过CAS替换原来线程的ThreadID失败),说明当前存在多线程竞争锁的情况,则开始进行偏向锁的撤销(升级)。

  1. 对持有偏向锁的线程进行撤销时,需要等待到达全局安全点。当到达全局安全点(safepoint,代表了一个状态,在该状态下所有线程都是暂停的)时,获得偏向锁的线程被挂起。

  2. 如果获得偏向锁的线程如果已经退出了临界区,也就是同步代码块执行完了,不存在锁竞争,那么这个时候争抢锁的线程可以基于 CAS 重新偏向当前线程,此过程称为重偏向

  3. 如果获得偏向锁的线程的同步代码块还没执行完,处于临界区之内,存在锁竞争,这个时候会把原获得偏向锁的线程升级为轻量级锁(标志位为“00”),并将指向当前线程的锁记录地址的指针放入对象头Mark Word,后唤醒持有锁的当前线程,进入轻量级锁的竞争模式。

注意:其他情况下的偏向锁撤销

  • 当对象计算过一致性哈希吗后(Object::hashCode() 或 System::identityHashCode(Object)方法的调用,重写的hashCode方法则不会),立即撤销偏向锁
  • 调用 wait/notify 方法
6.1.1.5 批量重偏向、批量撤销
  • 批量重偏向:当一个锁对象类的撤销次数达到20次时,虚拟机会认为这个锁不适合再偏向于原线程,于是会在偏向锁撤销达到20次时让这一类锁尝试偏向于其他线程。

  • 批量撤销:当一个锁对象类的撤销次数达到40次时,虚拟机会认为这个锁根本就不适合作为偏向锁使用,因此会将类的偏向标记关闭(0),之后现存对象加锁时会升级为轻量级锁,锁定中的偏向锁对象会被撤销,新创建的对象默认为无锁状态。

6.1.1.6 偏向锁的关闭

偏向锁在JDK1.6、JDK1.7中是默认启用的,并且它在应用程序启动后几秒钟才被激活。

  • JVM参数关闭延迟:-XX:BiasedLockingStartupDelay=0

如果你确定应用程序里所有的锁通常情况下处于竞争状态,则可以用以下参数关闭偏向锁,那么程序默认会进入轻量级锁状态:

  • JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false

6.1.2 轻量级锁

轻量级锁是JDK1.6时加入的新型锁机制,“轻量级” 是相对于使用操作系统的 互斥量 实现的传统锁而言的。

  • 目的:同步周期内,没有多线程竞争的前提下(多线程之间交替执行同步代码块,没有发生竞争),避免使用互斥量带来的性能损耗。
  • 原理:CAS + 自旋
  • 特点:非阻塞同步、乐观锁
6.1.2.1 轻量级锁的获取
  1. 在代码进入同步块时,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。

  2. 拷贝对象头中的Mark Word复制到锁记录中。

  3. 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向object mark word。如果更新成功,则执行步骤(4),否则执行步骤(5)。

  1. 如果这个更新动作成功,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。

  2. 如果这个更新操作失败,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧。如果是就说明当前线程已经拥有了这个对象的锁,说明是一次重入,创建锁记录并设置Displaced Mark Word为null,起到了一个重入计数器的作用,然后进入同步块继续执行。否则执行步骤(6)。

  3. 若Mark Word不指向当前线程的栈帧,则说明多个线程竞争锁,此时强锁失败的线程通过自旋,不断重试步骤(3),自旋次数超过10次,则进行锁膨胀,即轻量级锁膨胀为重量级锁。

6.1.2.2 轻量级锁的解锁

执行完同步代码块代码,退出同步代码块,使用CAS开始轻量级锁解锁,线程会使用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来

  • CAS 替换成功,成功释放锁,恢复到无锁的状态(01)。
  • CAS 替换失败,则释放锁,唤醒被挂起阻塞的线程。

当超过自旋阈值,竞争的线程就会把锁升级为重量级锁,因此CAS 替换可能会失败,由于此时已经是重量级锁,则唤醒被挂起阻塞的线程,之后开始重量级锁的竞争逻辑了。

锁升级的整个流程:

6.1.3 自旋锁

自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待(让该线程执行一段无意义的忙循环,CPU忙等待),而不是把该线程给阻塞,直到获得锁的线程释放锁之后,这个线程就可以马上获得到锁。

轻量级锁的竞争就是采用的自旋锁机制。

  • 引入自旋锁的原因互斥同步对性能最大的影响是阻塞的实现,因为阻塞和唤醒线程需要用户态和内核态的相互转换,这些操作给系统的并发性能带来很大的影响。

  • 自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开启;在JDK1.6中默认开启。

  • 自旋锁的缺点自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好;反之,自旋的线程就会白白消耗掉处理器的资源,它不会做任何有意义的工作,这样反而会带来性能上的浪费。

  • 自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该进入阻塞状态。通过参数-XX:PreBlockSpin可以调整自旋次数,默认的自旋次数为10

  • 自适应自旋锁JDK1.6引入自适应的自旋锁,自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定: 在同一锁对象上,若线程刚刚的一次自旋操作成功过,那么JVM会认为这次自旋成功的可能性会很高,就多自旋几次;反之,就少自旋甚至不自旋。

  • 适用范围:适用于临界区代码少,执行快、锁竞争频率较低的情况。

6.1.4 偏向锁、轻量级锁、重量级锁的比较

优点 缺点 适用场景
偏 向 锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 适用于只有一个线程访问同步块场景。
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度。 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 追求响应时间。同步块执行速度非常快。
重量级锁 线程竞争不使用自旋,不会消耗CPU。 线程阻塞,响应时间缓慢。 追求吞吐量。同步块执行速度较长。

6.1.5 对象信息存在哪

  • 重量级锁:对象信息存储在ObjectMonitor的_header成员变量中;

  • 轻量级锁的实现中,会通过线程栈帧的锁记录存储Displaced Mark Word;

  • 当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为轻量级锁或者重量锁;

  • 当一个对象已经计算过identity hash code,它就无法进入偏向锁状态;

偏向锁与hashcode能共存吗?

6.2 锁消除

锁消除指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。

JVM参数禁止锁消除优化: -XX:-EliminateLocks

StringBuffer类的append()方法:

@Override
public synchronized StringBuffer append(String str) {toStringCache = null;super.append(str);return this;
}
package cu.edu.xupt.acat.sync;public class EliminateLocksTest {public static void fun() {StringBuffer buffer = new StringBuffer();buffer.append("hello");buffer.append("world");}public static void main(String[] args) {long start = System.currentTimeMillis();for (int i = 0; i < 10000000; i++) {fun();}long end = System.currentTimeMillis();System.out.println(end - start);}
}

StringBuffer作为局部变量,作用于方法内部,不可能逃逸出该方法,因此他就不可能被多个线程同时访问,也就没有资源的竞争,但是StringBuffer的append操作却需要执行同步操作,因此Java即时编译器会进行锁消除的优化。

三组测试结果:

6.3 锁粗化

通常情况下,为了保证多线程间的有效并发,应该将临界区范围限制的尽量小,但是某些情况下,如果一系列的连续操作都对 同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

  • 如果虚拟机检测到有一串零碎的操作都是对同一对象的加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。

频繁加锁、解锁:

for(int i=0;i<size;i++){synchronized(lock){}
}

锁粗化后:

synchronized(lock){for(int i=0;i<size;i++){}
}

The End…

Java并发深度总结:synchronized 关键字相关推荐

  1. Java并发编程之synchronized关键字解析

    前言 公司加班太狠了,都没啥时间充电,这周终于结束了.这次整理了Java并发编程里面的synchronized关键字,又称为隐式锁,与JUC包中的Lock显示锁相对应:这个关键字从Java诞生开始就有 ...

  2. java并发中的Synchronized关键词

    文章目录 为什么要同步 Synchronized关键词 Synchronized Instance Methods Synchronized Static Methods Synchronized B ...

  3. Java并发编程:volatile关键字解析(转载)

    转自https://www.cnblogs.com/dolphin0520/p/3920373.html Java并发编程:volatile关键字解析 Java并发编程:volatile关键字解析 v ...

  4. Java并发编程:synchronized

    虽然多线程编程极大地提高了效率,但是也会带来一定的隐患.比如说两个线程同时往一个数据库表中插入不重复的数据,就可能会导致数据库中插入了相同的数据.今天我们就来一起讨论下线程安全问题,以及Java中提供 ...

  5. 【Java并发编程:volatile关键字之解析】

    Java并发编程:volatile关键字解析 - Matrix海子 - 博客园 在Java 5之前,volatile是一个备受争议的关键字:因为在程序中使用它往往会导致出人意料的结果.在Java 5之 ...

  6. 转载:Java并发编程:volatile关键字解析

    看到一篇写的很细致的文章,感谢作者 作者:Matrix海子 出处:http://www.cnblogs.com/dolphin0520/ 本博客中未标明转载的文章归作者Matrix海子和博客园共有,欢 ...

  7. Java并发:明白Synchronized实现原理,锁什么?

    最近看到synchronized的知识点,做些简单记录. 一.Synchronized的基本使用 Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法.Synchr ...

  8. Java 并发编程:Synchronized 及其实现原理

    作者:liuxiaopeng 原文链接: www.cnblogs.com/paddix/p/5367116.html 一.Synchronized的基本使用 Synchronized是Java中解决并 ...

  9. 理解Java并发编程:volatile关键字解析

    文章目录 volatile关键字作用详解 原子/可见/有序 happen-before原则 volatile的作用 volatile的原理 volatile关键字作用详解 讲到Java中的volati ...

最新文章

  1. SQL Server日志清除的两种方法 .
  2. 无需卷积,完全基于Transformer的首个视频理解架构TimeSformer出炉
  3. 基于Windows环境下MyEclipse10快捷键总结
  4. Angular开发文档中一些常用的词汇
  5. 使用ansible 批量分发SSH Key
  6. Nodejs从小工到专家系列(一)
  7. eclipse 版本理解
  8. 关于ASPNET_Membership用户被锁的解决
  9. Python刷题-4
  10. iOS开发UI篇—实现一个私人通讯录小应用(一)
  11. Windows Server2016+SQL Server 2016 Cluster安装及配置
  12. DevExpress 汉化(简单、实用、快速) 转
  13. Ubuntu下的几种常见输入法
  14. 二进制计算机课教案,《二进制与计算机》教学设计
  15. 北京三大春天赏花圣地
  16. 移动互联网创业团队开发管理经验
  17. Linux解决中文乱码问题及LANG与NLS_LANG的区别
  18. 头歌 Linux 远程联机服务(二)- Rsh服务器
  19. 【原创】关于企业或组织实行OKR的心得
  20. 操作系统原理学习笔记(基础概念与进程)

热门文章

  1. 淘宝网的技术发展史(三)――分布式时代
  2. Java根据两点经纬度计算距离
  3. Cornerstone无法上传静态库文件(.a文件)
  4. 基于MSP430F5529的后驱四轮小车
  5. python 把matplotlib绘制的图片显示到html中
  6. 关联分析(Apriori算法) 面包 牛奶 尿布 啤酒 ...
  7. 开启安全测试评估专业赛道:永信至诚“数字风洞”产品体系战略发布
  8. P1135 奇怪的电梯题解
  9. 从NIB中加载VIEW
  10. Linux云计算网络服务CentOS7.5