线程安全与锁优化

本文从以下介绍线程安全与锁优化

一、线程安全

二、锁优化

一、线程安全

1、线程安全的定义

     Brian Goetz对线程安全的定义:

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

    线程安全代码具备的特征:

    代码本身封装了正确性的保障手段(如互斥同步),调用者无需关注多线程的问题,也无需实现任何措施来保证多线程的正确调用。

2、Java语言中的线程安全

      线程的安全程度,由强到弱,Java语言各种操作共享数据分五类:不可变,绝对线程安全、相对线程安全、线程兼容、线程对立

     (1)不可变

              不可变的对象一定是线程安全的,无论是对象的方法实现还是调用者调用该方法,都不需要任何的线程安全措施保障。

              java语言,共享数据为一个基本数据类型,只要将其用final修饰可以保证其不可变;

              如果共享数据为一个对象,就需要对象的行为对其状态(属性)没有任何影响,则可保证不可变,如:String,substring()方法,不会改变原来的字符串,方法执行后会返回一个新的值;

              保证对象行为不影响自己的状态方式很多:比如:将对象中带有状态的变量声明为final,如:Integer中的构造函数,用final声明了一个成员变量

              private final int value;

              public Integer(int value){

                  this.value = value;

              }

             

       (2)绝对线程安全

                绝对的线程安全,是符合Brain Goetz给出的线程安全的定义的,Java API标注的自己是线程安全的类大多数不是绝对的线程安全类,如Vector

              

package net.oschina.tkj.jvmstu.thread;

import java.util.Vector;

public class TestVector {

	private static final Vector v = new Vector();

	public static void main(String[] args) {

		Thread t = new Thread(new Runnable() {

			@Override
			public void run() {
				// TODO Auto-generated method stub
				for (int i = 0; i < v.size(); i++) {
					v.remove(i);
				}
			}
		});

		Thread t1 = new Thread(new Runnable() {

			@Override
			public void run() {
				// TODO Auto-generated method stub
				for (int i = 0; i < v.size(); i++) {
					System.out.println(v.get(i));

				}
			}
		});

		t.start();
		t1.start();
		while (Thread.activeCount() > 20)
			;

		while (true) {
			for (int i = 0; i < 10; i++) {
				v.add(i);
			}
		}

	}

}

  出现异常问题:

   

 

解决办法在run()方法内加synchronized。

因此,java中声明线程安全的api并非是绝对的线程安全的。

        (3)相对线程安全

                  对某个对象单独操作是线程安全的,如果多线程调用时,需要使用额外的线程安全手段来保证调用的正确性。如:上述的Vector,Hashtable等。

        (4)线程兼容

                 线程兼容对象本身不是线程安全,可以通过使用同步的操作来保证线程安全。

        (5)线程对立

                不管调用者是否采用同步措施,都无法在多线程的环境中并发使用代码。

3、线程安全的实现

      线程安全问题产生的原因:存在多个线程,并且多个线程有共享数据。

      线程安全问题的解决办法:

      (1)互斥同步(Mutual Exclusion &Synchronized)

        Java里互斥同步的手段就是同步关注字Synchronized。

1>同步关键字Synchronized

   

    ①同步关键字在编译后,会在同步块前后生成monitorenter、monitorexit两个字节码指令,这两个字节码指令都需要一个reference类型的参数来指定锁定和解锁的对象;

   

    ②JVM规范中指出,在执行monitorenter指令时,首先要尝试获取对象的锁。如果该对象没有被锁定,或者当前的线程已经拥有了那个对象的锁,把锁计数器加1。相应的在执行monitorexit指令时会把锁计数器减1,当计数器的值为0,则锁被释放。如果获取对象的锁失败,那当前线程处于阻塞等待状态,直到对象的锁被占用的线程释放为止;

   

    ③JVM规范描述中,关于monitorenter,monitorexist指令注意点:

        《1》synchronized对一个线程来说可重入,不会出现自己把自己锁死的情况;

        《2》同步块在已进入的线程执行之前,会阻塞后面的其他线程的进入。

    ④java的线程要映射到操作系统的原生线程之上,因此,线程的阻塞和唤醒,都需要操作系统参与,这就需要从用户态转入到核心态,因此状态的转换需要花费处理器的大量时间,所以,Synchronized为java语言一个重量级的操作;

      (2)java.util.concurrent.ReentrantLock实现同步

        ReentrantLocak与synchronizd的操作类似,不过ReentrantLock为显式声明锁定与解锁操作(lock与unlock方法配合try,finally语句块一起使用)

        ReentrantLock比synchronizd增加的功能如下:

        《1》等待可中断:持有锁的线程长时间不释放锁,等待的线程可以选择放弃等待处理其他事情,对执行时间过长的同步块有用;

        《2》公平锁:多线程等待同一个锁时,按照申请锁的先后顺序依次获得锁;而非公平锁,不保证这一点,任何一个等待锁的线程都有机会获得锁;synchronized为非公平锁,ReentrantLock默认为非公平锁,可通过构造方法的布尔值转为公平锁;

        《3》锁可以绑定多个条件:指一个ReentrantLock对象可以绑定多个Condition对象。

       (3)非阻塞同步

        互斥同步最主要的问题是,进行线程的阻塞和唤醒时性能的问题,因此这种同步也称为阻塞同步

         悲观并发策略:该种策略认为,如果不去做正确的同步操作,那就会出现问题。无论线程是否真的竞争共享数据都需要加锁的操作。

        乐观并发策略:基于冲突检测的乐观并发策略,简单讲就是先进行操作,如果没有线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,就进行其他补偿措施,这种乐观的并发策略都不需要将线程挂起,因此也称为非阻塞同步(Non-blocking synchronization)

二、锁优化

       高并发JDK1.6重要主题,实现锁优化的技术:适应性自旋,锁消除,锁粗化,轻量级锁,偏向锁,这些技术旨为解决线程间高效共享数据,解决竞争问题,提高程序执行效率。

1、自旋锁与自适应锁 

     同步互斥对性能最大的影响是阻塞实现,挂起线程以及恢复线程都需要转入内核态来完成,这给操作系统的并发性能带来很大压力。

     自旋锁:物理机器有一个以上处理器,可以同时处理多个线程并发,为了实现请求锁的线程等待,而不放弃处理器的执行时间。只需等待锁的线程执行一个忙循环(自旋),该技术为自旋锁。

     自旋锁优点:避免了线程状态转化的开销

     自旋锁缺点:需要占用处理器的时间,如果持有锁的线程持有锁时间过长,则会造成等待锁的线程持有处理器时间过长,资源浪费。所以给自旋锁设置自旋的次数默认为10。如果超过该次数未获取锁,则用传统的手段将该线程挂起。

     自适应自旋:自旋的时间不固定,由前一次在同一个锁上的自旋时间及suo的拥有者的状态决定。

2、锁消除

jvm的即时编译器在运行时,对一些代码上要求同步,但是当检测到不可能存在共享数据的争用情况则对锁进行消除。

3、锁粗化

      正常情况下,同步块使用时都是在尽量小的范围内使用,这样等待的锁的线程可以尽快拿到锁。

      特殊情况下:一个操作反复的出现加锁解锁的操作,甚至锁的操作在循环中,即使不出现竞争数据,也会造成性能的不必要浪费。因此,此时会报加锁的范围扩大到(粗化)整个操作的外部,这样保证只加一次锁的操作。如:StringBuffer的appen()就是锁粗化操作。多个append(),加锁放到第一个append()之前,解锁放到最后一个append()之后。

4、轻量级锁

      相对于使用系统的互斥量的传统锁而言。

       轻量级锁提升同步性能依据:对于大部分锁而言,整个同步周期内都是不存在竞争的。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,如果存在竞争,除了互斥量的开销外,还额外的进行CAS操作,因此有竞争的情况轻量级锁更慢。

5、偏向锁

      消除数据无竞争的情况下的同步,提高程序的性能。

      轻量级锁在无竞争数据的情况下通过CAS消除同步使用的互斥量,那么偏向锁就是在无竞争的情况下把整个同步都消除掉,CAS的操作也无需执行。

猜你喜欢

转载自1498116590.iteye.com/blog/2412750