您的位置:首页 > 编程语言 > Java开发

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

2017-04-18 15:48 169 查看
锁的基本概念和实现

理解线程安全
在多线程的环境下,无论多个线程如何访问目标对象,目标对象的状态应始终保持一致。
示例:在多线程环境下使用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

分析:

两个线程t1和t1同时向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参数迫使偏向锁在启动时生效。如果不开启此参数,性能差距会更大;

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  虚拟机 java