Java虚拟机-锁与并发(十三)

  • 锁的基本概念和实现
    • 理解线程安全
      • 在多线程的环境下,无论多个线程如何访问目标对象,目标对象的状态应始终保持一致。
      • 示例:在多线程环境下使用ArrayList

public class ThreadUnSafe {

public static List<Integer> numberList = new AarryList<>();

public static class AddToList implements Runnable{

int startnum = 0;

public AddToList(int startnumber){

startnum = startnumber;

}

@Override

public void run() {

int count = 0;

while(count < 1000000){

numberList.add(startnum);

startnum+=2;

count++;

}

}

}

public static void main(String[] args) {

Thread t1 = new Thread(new AddToList(0));

Thread t2 = new Thread(new AddToList(1));

t1.start();

t2.start();

}

}

如此运行的话,会出现如下的异常:

Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: 10

分析:
两个线程
t1t1同时向numberList增加数据,由于Array List的线程并不是安全的。它们同时对集合进行读写操作,破坏了ArrayList内部数据的一致性,导致其中一个线程访问了错误的数组索引。

解决方案:
ArrayList更换为Vertor.

Vertor的实现中,使用了内部锁堆List对象进行控制,实现如下:

  • 对象头和锁
    • Java虚拟机的实现中每个对象都有一个对象头,用于保存对象的系统信息。对象头中有一个被称为Mark Word的部分,它是实现锁的关键;
      • 在32位系统中,它是一个32位的数据;在64为系统中,它占64位;
      • 它可以存放对象的哈希值,对象年龄,锁的指针等信息;
      • 一个对象是否占有锁,占有哪个锁,都记录在这里!
  • 以32位系统为例,普通对象的对象头就像下面这样:

它表示:Mark word中有25位比特表示对象的哈希值,4位比特表示对象的年龄,1位比特表示是否为偏向锁,2位比特表示锁的信息

  • 对于偏向锁的对象,它的格式如下:

前23位表示持有偏向锁的线程,后续两位比特表示偏向锁的时间戳,4位比特表示对象年龄,年龄后1位比特固定为1,表示偏向锁,最后2位为01表示可偏向/未锁定

  • 对象处于轻量级锁定时,Mark Word如下(00表示组后2位的值):

此时,它指向存放在获得锁的线程栈中的该对象真实对象头

  • 对象处于重量级锁定时,其Mark Word如下:

最后两位为10,整个Markk Word表示指向Monitor的指针

  • 对象处于普通的未锁定状态时,格式如下:

29位表示对象的哈希值,年龄等信息。倒数第3位为0,最后两位为01,表示未锁定。

  • 锁在Java虚拟机中的实现和优化
    • 偏向锁
      • 能力特点:在JDK1.6出现。核心思想是:如果程序没有竞争,则取消之前已经取得锁的线程同步操作;
      • 能力分析:当某一锁被线程获取后,便进入偏向模式。当线程再次请求这个锁时,无需再进行相关的同步操作,从而节省时间。如果在此之前有其他线程进行了锁请求,则锁退出偏向模式;
      • 属性:
        • -XX:+UseBiasedLocking:可以设置启用偏向锁;
      • 验证:当锁对象处于偏向模式时,对象头会记录获得锁的线程:

  • 举个栗子:来看看偏向锁的性能提升吧!

public class Biased {

public static List<Integer> numberList = new Vector<>();

public static void main(String[] args) {

long begin = System.currentTimeMillis();

int count=0;

int startnum=0;

while(count<10000000){

numberList.add(startnum);

startnum+=2;

count++;

}

long end = System.currentTimeMillis();

System.out.println(end-begin);

}

}

第一次执行不使用任何启动参数,观察其执行时间;(我的程序执行时间为:5966)

第二次使用如下启动参数,再次观察执行时间:(执行时间为:696)
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 -client -Xmx512m -Xms512m
高亮表示在程序启动后,立即启用偏向锁
 

  • 结论:偏向锁在少竞争的情况下,对系统性能有一定帮助。
    • 在竞争激烈的场合没有太强的优化效果,因为大量的竞争会导致持有锁的线程不停的切换,锁也很难一直保持在偏向模式,此时使用偏向锁还有降低系统性能的危险;
  • 轻量级锁
    • 能力特点:偏向锁失败,Java虚拟机会让线程申请轻量级锁;
    • 能力分析:该锁在虚拟机内部,使用一个称为BasicObjectLock的对象实现,这个对象内部由一个BasicLock对象和一个持有该锁的Java对象指针组成;
      • BasicObjectLock对象放置在Java栈的栈帧中;因此该指针必然指向持有该锁的线程栈空间。当需要判断某一线程是否持有该对象锁时,只需简单地判断对象头的指针是否在当前线程的栈地址范围内即可。同时,BasicLock对象的displaced_header字段,备份了原对象的Mark Word内容。BasicObjectLock对象的obj字段则指向该对象;
      • 该对象内部维护着displaced_header字段,用来备份对象头部的Mark Word;
    • 示意图:BasicLock通过set_displaced_header()方法备份了原对象的Mark Word。接着,使用CAS操作,尝试将BasicLock的地址复制到对象头的Mark Word。如果复制成功,那么加锁成功,否则认为加锁失败。如果加锁失败,那么轻量级锁就有可能被膨胀为重量级锁;
  • 锁膨胀
    • 能力特点:当轻量级锁失败,虚拟机就会使用重量级锁;
    • 能力分析:在轻量级锁处理失败后,虚拟机会先废弃前面BasicLock备份的对象头信息。之后则正式启用重量级锁。启用过程分为两步:首先通过inflate()方法进行锁膨胀,其目的是获得对象的ObjectMonitor;然后使用enter()方法尝试进入该锁;在此方法调用中,线程很可能会在操作系统层面被挂起。如果这样,线程间切换和调度的成本就会比较高;
  • 自旋锁
    • 能力特点:锁膨胀后,线程很可能会在操作系统层面被挂起,这样在线程上下文切换时,会损失较大的性能。在锁膨胀之后,虚拟机会努力让线程进入临界区而避免被操作系统挂起。
    • 能力分析:
      • 自旋锁可以使线程在没有取得锁时,不被挂起,转而去执行一个空循环(即自旋),在若干个空循环后,线程如果可以获得锁,则继续执行。若线程依然不能获得锁,才会被挂起。
      • 该锁使线程被挂起的几率相对减少,线程执行的连贯性相对加强。对于锁竞争不是很激烈,锁占用时间很短的并发线程,具有一定的积极意义,但对于锁竞争激烈,单线程锁占用时间长的并发程序,自旋锁在自旋等待后,往往依然无法获得对应的锁,即浪费了CPU时间,又浪费了系统资源。
    • JDK1.6属性:
      • -XX:-UseSpinning:开启自旋锁;
      • -XX:PreBlockSpin:设置自旋锁的等待次数;
    • 特别说明:
      • JDK1.7,该锁的参数被取消,虚拟机不再支持由用户配置自旋锁。它总是会执行,并自行调整自旋次数;
  • 锁消除
    • 能力特点:虚拟机在JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源的锁。通过它,可以节省请求锁的时间;
    • 问题:如果不存在竞争,为什么还要加锁?在开发中会使用一些JDK内置的API,比如StringBudder,Vector等。这些工具有对应的非线程完全版本,但是开发人员也很有可能在完全没有多线程竞争的场合使用它们。这时,工具类内部的同步方法就是不必要的。虚拟机可以在运行时,基于逃逸分析技术,捕获这些不可能存在竞争却有申请锁的代码段,并消除这些不必要的锁,从而提高系统性能;
    • 属性:锁消除必须在-server模式
      • -XX:+DoEscapeAnalysis:逃逸分析
      • -XX:+EliminateLocks:锁消除
    • 示例:下面的代码中sb变量的作用域仅限于方法体内部,不可能逃逸出该方法,因此它不可能被多个线程同时访问

 

public class LockEliminate {

private static final int CIRCLE = 2000000;

public static void main(String[] args) {

long start =System.currentTimeMillis();

for (int i = 0; i < CIRCLE; i++) {

createStringBuffer("JVM","Diagnosis");

}

long bufferCost = System.currentTimeMillis() -start;

System.out.println("createStringBuffer:"+bufferCost+" ms");

}

private static String createStringBuffer(String s1, String s2) {

StringBuffer sb = new StringBuffer();

sb.append(s1);

sb.append(s2);

return sb.toString();

}

}

使用参数:关闭锁消除
-server -XX:+DoEscapeAnalysis -XX:-EliminateLocks -Xcomp -XX:-BackgroundCompilation -XX:BiasedLockingStartupDelay=0
运行结果:
createStringBuffer:466 ms

使用参数:开启锁消除
-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks -Xcomp -XX:-BackgroundCompilation -XX:BiasedLockingStartupDelay=0
运行结果:
createStringBuffer:250 ms

结论:本实例使用了-XX:BiasedLockingStartupDelay参数迫使偏向锁在启动时生效。如果不开启此参数,性能差距会更大;

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值