深入JVM内核(八)——jvm锁与jvm锁优化

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/gududedabai/article/details/81808627

由于之前看的容易忘记,因此特记录下来,以便学习总结与更好理解,该系列博文也是第一次记录,所有有好多不完善之处请见谅与留言指出,如果有幸大家看到该博文,希望报以参考目的看浏览,如有错误之处,谢谢大家指出与留言。

目录

JVM内置锁

一、线程安全

二、对象头Mark

三、偏向锁

四、轻量级锁

五、自旋锁

六、偏向锁,轻量级锁,自旋锁总结

锁优化方式

一、减少锁持有时间

二、减小锁粒度

思考:

三、锁分离

四、锁粗化

五、锁消除

六、无锁

七、总结-道可道,非常道


JVM内置锁

一、线程安全

多线程访问情况的线程安全举例如下:

1、多线程网站统计访问人数

        使用锁,维护计数器的串行访问与安全性,但性能比无锁情况相对较差。当所需的统计数量不需要精确是可以不进行所保护,来提升性能。

2、多线程访问ArrayList

public static List<Integer> numberList =new ArrayList<Integer>();
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) throws InterruptedException {
	Thread t1=new Thread(new AddToList(0));
	Thread t2=new Thread(new AddToList(1));
	t1.start();
	t2.start();
	while(t1.isAlive() || t2.isAlive()){
		Thread.sleep(1);
	}
	System.out.println(numberList.size());
}

main同时开启两个线程,然后运行结果如上。发现ArraryList,越界了,但ArraryList应是会自动扩展的,从结果看数据也不对,说明其中一个线程在运行过程中由于异常而停止了。另外一个则执行完毕了。为什么会差生越界呢,那是因为arraylist在多线程中不是线程安全的,当ArrayList存储空间不足的,则去自动扩展,但是在扩展的时候他是不可用的。在这个时候没有对他进行多线程的保护。此时另一个线程跑过来,往里插入数据;但是此时的arrayList不可用,所以报这个错误。

因此多线程中的锁是必要的,它不仅仅会造成数据上的不准确,还会导致程序的异常报错。 在讲解说的前先介绍下对象头,每个对象都会有一个对象头。

二、对象头Mark

1、Mark Word,他是32位对象头的标记,

2、描述对象的hash、锁信息,垃圾回收标记,年龄;指向锁记录的指针(对于来说可以记录指向锁的指针);指向monitor的指针(monitor可以锁定对象,也可以锁定函数);GC标记(在垃圾标记的时候我们可以做一些标记);偏向锁线程ID(在偏向锁中可以记录偏向锁的ID)

由上可看出,mark是个多功能的头,在很多场合都可以用到,除了在锁中用到,比如在垃圾回收中可以记录年龄,gc的标记等等。

三、偏向锁

1、大部分情况是没有竞争的,所以可以通过偏向来提高性能

2、所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程(也就是说,这个线程已经占有这个锁,当他在次试图去获取这个锁的时候,他会已最快的方式去拿到这个锁,而不需要在进行一些monitor操作,因此这方面他是会对性能有所提升的,因为在大部分情况下是没有竞争的,所以锁此时是没用的,所以使用偏向锁是可以提高性能的)

3、在使用偏向锁的时候会将对象头Mark的标记设置为偏向,并将拿到锁的线程的ID写入对象头Mark,这样就可以很快识别出这个线程是否拿到的锁。

4、只要没有竞争,获得偏向锁的线程,在将来进入同步块,不需要做同步(这段时间就省下来了)

5、当其他线程请求相同的锁时,偏向模式结束

6、-XX:+UseBiasedLocking

         默认启用

7、在竞争激烈的场合,偏向锁会增加系统负担

案例:vector内部是有同步锁的操作的,所以在jdk内部是有锁的。使用它也就是说明你此时的代码拥有锁了。

public static List<Integer> numberList =new Vector<Integer>();
public static void main(String[] args) throws InterruptedException {
	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);
}

方式一执行:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

使用偏向锁。BiasedLockingStartupDelay=0设置偏向锁的启动时间,为零0,则是在系统启动时就启用偏向锁,但一般在系统启动时竞争是非常大的,使用它是非常耗时的,这里案例因为代码执行会很短,为了测试效果,在运行程度时,就启动偏向锁,所以置为0.

方式二:-XX:-UseBiasedLocking

不使用偏向锁

运行结果:本例中,使用偏向锁,可以获得5%以上的性能提升

四、轻量级锁

在了解轻量锁之前,先了解下面jvm中一个锁:

1、BasicObjectLock

这个锁他是嵌入在线程栈中的对象,也就是说这个锁是放在线程栈中的。什么是线程栈,在之前博客也有介绍,这里简单说明:在系统调用过程中,它里面会存放一些线程执行的情况;比如说:局部变量,参数,操作数栈等等;还有一个就是这个BasicObjectLock,他是一个锁,他包含如下两部分:

分比为:BasicLock 而他的主要部分则是后面的markOop_displaced_header,他其实就是一个对象头。下面一部分就是一个指向锁的一个指针。这两部分组成。

2、普通的锁处理性能不够理想,轻量级锁是一种快速的锁定方法。

3、过程:如果对象没有被锁定; 将对象头的Mark指针保存到锁对象中;并且将对象头设置为指向锁的指针(在线程栈空间中)。

这样一来实际上就是对象的头是在指针当中,并且是放在的线程栈中的,而线程栈中有个指针是指向这个指针的,因此形成了相互间的引用关系。实现代码如下:首先指定锁中专门存放对象头的这个header,备份到锁中,把对象的mark放在锁中,里面进行交换,lock本身放入对象头当中去,形成相互交换;

lock->set_displaced_header(mark);
 if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
      TEVENT (slow_enter: release stacklock) ;
      return ;
}

lock位于线程栈中,因此如何判断线程持有这个锁,我们只需要判断这个对象头的指针所指向的方向,是不是在这个线程栈当中。如果是,则说明这个线程持有这把锁。

4、如果轻量级锁失败,表示存在竞争,升级为重量级锁(常规锁   也就是常说的monitor)

5、在没有锁竞争的前提下,减少传统锁(也就是重量级锁)使用OS互斥量产生的性能损耗(也就是说重量级锁他是会在操作系统上做一些操作,所以他的性能是非常糟糕的)

6、在竞争激烈时,轻量级锁多半会失败,因此轻量级锁会多做很多额外操作,导致性能下降

五、自旋锁

自旋锁其实就是一个线程自转,空转,什么都不操作,但也不挂起,在那里空循环。空循环的作用就是等待一把锁。自旋锁是明确的会产生竞争的情况下使用的。

1、当竞争存在时,如果线程可以很快获得锁,那么就没有必要在(操作系统)OS层面挂起线程(因为在操作系统层面去挂起,他的性能消耗是非常严重的,因此如果我们能假定他能很快获取锁,就不需要让线程挂起),而是让线程做几个空操作(称为自旋)

2、JDK1.6中-XX:+UseSpinning开启

3、JDK1.7中,去掉此参数,改为内置实现

4、如果同步块很长,会导致后面线程自旋的成功率,因此自旋失败,会降低系统性能。因为在自旋花去的时间后还没有获取到锁,这么长时间都是浪费的,所以会造成性能降低。

5、如果同步块很短,自旋成功成功率很高,因此自旋成功,会节省线程挂起切换时间,提升系统性能。

六、偏向锁,轻量级锁,自旋锁总结

1、这三个锁不是Java语言层面的锁优化方法。他是jvm中锁的优化,其实是jvm获取锁的步骤。

2、内置于JVM中的获取锁的优化方法和获取锁的步骤

          偏向锁可用会先尝试偏向锁

          轻量级锁可用会先尝试轻量级锁

          以上都失败,尝试自旋锁

          再失败,尝试普通锁,使用OS互斥量在操作系统层挂起

锁优化方式

一、减少锁持有时间

如例:

在方法没有必要做同步的时候,就不需要放在锁中,因此在高并发下,等待的时间就会减少,就会提高自旋锁的成功率。

二、减小锁粒度

1、将大对象,拆成小对象,大大增加并行度,降低锁竞争

2、偏向锁,轻量级锁成功率提高

3、HashMap的是如何同步实现?

             ->Collections.synchronizedMap(Map<K,V> m)  通过这个方法获取同步锁

             ->返回SynchronizedMap对象

            ->

但是这种方式会产生激烈的竞争,一个锁获取,其他锁大多都在等待。HashMap是如何减少所粒度呢?则是通过ConcurrentHashMap  ,如下:

4、ConcurrentHashMap  他的实现就是减少锁的获取时间  。他通过把数组分为多个段,操作时,只锁定其中一段,还可以同时在多线程下操作多个Segment(段),他们也是互补影响的。因此减少锁的粒度。

内部实现如下:

           把hashmap分为 若干个Segment :Segment<K,V>[]   (这个Segment可以理解为小的hashMap)

           segments Segment中维护HashEntry<K,V>

          当 put操作时

               先定位到Segment,锁定一个Segment,执行put

6、在减小锁粒度后, ConcurrentHashMap允许若干个线程同时进入

思考:

减少锁粒度后,可能会带来什么负面影响呢?以ConcurrentHashMap为例,说明分割为多个 Segment后,在什么情况下,会有性能损耗?

三、锁分离 

1、根据功能进行锁分离

2、ReadWriteLock

3、读多写少的情况,可以提高性能

                               

4、读写分离思想可以延伸,只要操作互不影响,锁就可以分离

5、LinkedBlockingQueue(所分离的扩展案例)锁分离的思想在很多场合下都可以使用。

              --队列

              --链表

                                         

其原理在之前博客有详细说明这里就不在赘述。

四、锁粗化

1、通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早的获得资源执行任务。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化,这种情况并不利于快进快出,反而需要增加锁的时间。

在两次锁之间的工作会很快完成的情况下,才可以进行锁合并。

五、锁消除

这种优化手段是jvm的优化手段,但我们是可以控制的。

1、在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作

public static void main(String args[]) throws InterruptedException {
	long start = System.currentTimeMillis();
	for (int i = 0; i < CIRCLE; i++) {
		craeteStringBuffer("JVM", "Diagnosis");
	}
	long bufferCost = System.currentTimeMillis() - start;
	System.out.println("craeteStringBuffer: " + bufferCost + " ms");
}

public static String craeteStringBuffer(String s1, String s2) {
	StringBuffer sb = new StringBuffer();
	sb.append(s1);
	sb.append(s2);
	return sb.toString();
}

StringBuffer由于是存在同步的,是线程安全的,在这种情况下,并不是我们由于我们应用的代码锁引出来的,不是我们代码中写的同步操作,如果使我们代码中去写也是没有必要的,因此锁消除也是没有必要的,因为在方法体内。对于局部变量进程操作的意义是不大的,因为局部变量本来就是线程私有的,不可能存在多线程访问,产生的线程安全问题。因此作为程序员很清楚他需不需要去进行锁操作。因此此时的sb变量是局部的,本身实现城安全的,所以引用了内部锁是多余的。因此这样的锁多余是可以被消除的,jvm在运行时就会判断不可能起作用的锁进行消除,所以就是实现了针对这类锁进行锁消除。

CIRCLE= 2000000

-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks

通过这个参数运行代码,同时打开的逃逸分析,为什么要打开逃逸分析呢?因为所消除是基于逃逸分析的,基于逃逸分析也是合理的,因为在锁消除是要判断这个局部变量是否可能会逃逸出局部的代码块。不过没有逃出当前的代码块,则进行锁消除,如果这个变量既可以在局部代码块中使用,也会在外部代码块中使用,那么你就没法判断他是否会在多线程被访问。如过在多线程下被访问,则是不可进行锁消除的。

createStringBuffer: 187 ms

-server -XX:+DoEscapeAnalysis -XX:-EliminateLocks

不使用锁消除

createStringBuffer: 254 ms

两种当时运行时间对比。

六、无锁

无招胜有招,武功的最高境界就是无招,所以锁也是,最好的锁也就是无锁。

1、无锁跟有锁对比,那么锁则是悲观的操作,为什么会使用所,因为我们预计这个时候竞争是存在的。所以要加锁

2、无锁是乐观的操作,因为认为是没有竞争存在的。比较乐观。

3、无锁的一种实现方式 CAS  (对比和交换)

        CAS(Compare And Swap) 他是非阻塞的同步,他不会去等待,上来就会不断尝试,尝试失败在尝试。要么失败要么直接成功,成功则退出。

       CAS(V,E,N)  可以传入三个参数,V旧值,E期望值,N新值。当且仅且V=E时在把N赋给V,然后返回,否则不做任何操作,不会进行赋值,最后不管是成功还是失败他会把V的真实值作为返回。如果V不等于期望值也就是说值受多线程影响了别的线程修改了这个值。所以我能做的就是通知线程不断重试,直到成功或者放弃。CAS是一条cup指令,可以保证不会再多线程中发生问题。所谓的CAS并不是由操作系统或者是一些锁去控制同步的方式,他更多的是在应用层面去尝试这个多线程是不是有问题。如果有干扰则是通知线程去重试。所以如果使用无锁操作的话,我们的程序会变为复杂,但性能会变得更好。

4、在应用层面判断多线程的干扰,如果有干扰,则通知线程重试

5、java.util.concurrent.atomic.AtomicInteger  (AtomicInteger原子包,原子操作)

这个包里面有很多原子操作。

例如下面这个方法:这个方法  设置新值,返回旧值。也就是更新值。

public final int getAndSet(int newValue) {
    for (;;) {
        int current = get();
        if (compareAndSet(current, newValue))
            return current;
    }
}

public final boolean compareAndSet(int expect, int update) 更新成功返回true

java.util.concurrent.atomic包使用无锁实现,性能远远高于一般的有锁操作。

七、总结

在锁的优化当中没有任何一个永远绝对方式,在不同的系统和环境下,锁优化会产生不同的结果,在不同的场景采用不同的方式,所以没有好与不好,没有在任何情况下都有正确的尊徐的道理。

展开阅读全文

没有更多推荐了,返回首页