Java内存模型JMM之六深入理解synchronized(2)

本文承接未完待续的 Java内存模型JMM之六深入理解synchronized(1)

3.7 锁消除

消除同步锁是JVM另外一种锁的优化,这种优化更彻底, JVM通过运行时JIT编译(可以理解为当某段代码即将第一次被执行时进行编译,又称即时编译),对一些在代码上要求添加同步,但是通过数据逃逸技术分析发现不可能存在共享数据竞争,这时JVM会对这些没有必要的同步锁进行消除。所以锁消除可以节省毫无意义的请求锁的时间,那么明知不存在数据竞争为何还进行了同步操作?作为程序员来说,这的确不会写出这样的代码,但是有时候程序并不是我们所想的那样,我们虽然没有显示的使用同步锁,但是我们在使用一些JDK内置的API时,如StringBuffer、Vector、HashTable等,这个时候会存在隐形的同步加锁操作。比如StringBuffer的append()方法,Vector的add()方法。

public void add(String str1, String str2) {
	//StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
	//因此sb属于不可能共享的资源,JVM会自动消除内部的锁
	StringBuffer sb = new StringBuffer();
	sb.append(str1)
	  .append(str2);
}

 以上代码很常见吧,特别是在拼接一些SQL、HQL等操作的时候, 由于该处的sb对象是一个局部变量,并且不会被其他线程引用到,但是StringBuffer的append方法却是个同步方法,此时,JVM就会通过锁消除机制将其锁消除。

3.7 锁粗化

原则上,我们在编写代码的时候,总是推荐奖同步块的作用范围限制的尽量小,只在共享数据的实际作用域中才进行同步,这是为了使得需要同步的操作尽可能的少,如果存在锁竞争也能够使等待锁的线程能够尽快地拿到锁。大部分情况下,这样的原则是没有问题的,但是如果一系列的连续操作都是对同一个锁的反复加锁和解锁,甚至在循环体中进行反复的加解锁操作,这样的情况即使没有线程竞争,频繁的进行互斥同步操作也会导致不必要的性能损耗。

锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,如下代码示例:

public void vectorTest(){
	Vector<String> vector = new Vector<String>();
	for(int i = 0 ; i < 10 ; i++){
		vector.add(i + "");
	}

	System.out.println(vector);
}
以上代码是很常见的在循环体中进行集合元素添加操作,我们知道vector的add方法是一个同步方法,每次add都需要加锁,执行完之后又需要解锁操作, 这时候JVM检测到对同一个对象(vector)连续加锁、解锁操作,就会合并一个更大范围的加锁、解锁操作,即将加锁解锁操作移到for循环之外,以一次性的加解锁操作取代多次的加解锁操作。

3.8 synchronized不能被interrupt中断

线程的中断方法说的是,只能中断正处于阻塞状态或者正准备执行一个阻塞操作的线程,这里说的阻塞其实就是指调用了Join, wait, sleep等方法导致线程进入的WAITING/TIMED_WAITING状态,因为这些方法能够感知到中断操作从而抛出中断异常。而所谓的synchronized不能被interrupt方法中断,指的是当线程执行到synchronized方法或者代码块的时候,由于对象锁被占用导致线程不得不通过调用底层的park方法进入死等状态,这时线程其实也是进入了阻塞状态,但是由于没有显示的调用能够抛出中断异常的方法,从而导致被阻塞的线程不能从阻塞状态恢复过来,使得线程一直处于锁等待的阻塞状态。如下代码示例可以作为一种说明:

public class ThreadTest implements Runnable{
	
	public ThreadTest() {
		//该线程已持有当前实例锁
        new Thread() {
            public void run() {
                f(); // Lock acquired by this thread
            }
        }.start();
	}

	public synchronized void f() {
        System.out.println("Trying to call f()");
        while(true) // Never releases lock
            Thread.yield();
    }
	
	public void run() {
        //中断判断
        while (true) {
            if (Thread.interrupted()) {
                System.out.println("中断线程!!");
                break;
            } else {
                f();
            }
        }
    }

	public static void main(String[] args) throws InterruptedException {
		Thread t = new Thread(new ThreadTest()); 
		t.start();
        TimeUnit.SECONDS.sleep(1);
        //中断线程,无法生效
        t.interrupt();
	}

}

 在创建ThreadTest示例时,其构造方法里面立即就执行了synchronized方法f,f方法会导致其一直持有对象锁,当线程t被创建出来也调用f方法的时候,将会一直处于锁等待状态,也是阻塞状态,虽然后来调用了interrupt中断方法,但是线程t并不能被中断,因为没有可以抛出中断异常的方法,线程t只能一直等待直到获取到对象锁。

3.9 synchronized锁及锁优化总结

原理 优点 缺点 适用场景
偏向锁 如果一个线程获得了锁,那么该锁就进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作  省去重复获取/释放锁的开销 如果竞争激烈,会带来额外的锁撤销的消耗 大多数时候都只有一个相同的线程,并且多次访问同步块的场景,不存在竞争
轻量级锁 仅仅通过CAS指令和自旋操作达到锁的获取和释放 避免了线程从用户态切换到内核态的消耗,线程并不会被阻塞,提高了程序的响应速度 如果存在锁竞争,将会通过自旋消耗CPU 不存在竞争,多线程之间交替执行,并且同步块执行较快的场景
重量级锁 通过操作系统底层的实现,阻塞竞争的线程和唤醒被阻塞的线程 比起其它情况,几乎没有优点 线程阻塞/唤醒需要在用户态和内核态之间切换,消耗大量成本,并且由于阻塞,使线程响应变慢 竞争激烈,同步代码块逻辑复杂需占用大量时间的场景
自旋/自适用自旋 通过执行多次无意义的指令,避免不必要的挂起和恢复线程时状态转换造成的性能消耗 避免了线程从用户态切换到内核态的消耗 白白消耗CPU 轻量级锁和重量级锁产生竞争时,在切换到内核态之前
锁消除 通过数据逃逸技术分析对不存在数据共享的同步锁实施锁消除 使其消除了不必要的获取/释放锁的消耗 还好吧,完全由JVM自行完成,依赖JVM的JIT编译 JVM的即时编译,针对JDK底层的API
锁粗化 将多次对同一个锁对象的获取/释放操作,扩展为一次获取/释放操作 省去重复获取/释放锁的开销 完全依赖JVM自身 将同步块置于循环操作中的场景
扫描二维码关注公众号,回复: 239854 查看本文章

猜你喜欢

转载自pzh9527.iteye.com/blog/2407163
今日推荐