线程安全与锁优化——线程安全


什么是线程安全?许多对线程安全的定义都不恰当,这是Brian Goetz的描述:

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


一、java中的线程安全

java中,按线程安全程度由强都弱:不可变、绝对线程安全、相对线程安全、线程兼容、线程对立。

1.1 不可变

不可变对象一定是线程安全的。不可变的对象有String,基本类型的包装类,Number类的对象。一直说他们是不可变的,那么他们为什么是不可变的呢?
首先,什么是不可变?

对于基本类型的变量,当用final修饰时就行了。然而对于对象来说,保证该对象的行为(方法)不会改变对象的状态(实例成员),而保证对象不改变对象的状态的最简单的方式就是将对象的状态用final修饰就行(个人看法,只要你不修改状态就行了,用不用final修饰无所谓)。

现在我们来看看String是怎样实现的?

implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];//用final修饰了

    /** Cache the hash code for the string */
    private int hash; // Default to 0,这个没有final修饰,但是它只能在构造函数中被修改(初始化)

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;//用final修饰了

所以看到下面这段代码不要感到疑惑(为什么s是不可变对象,可它却被重新赋值了)。

String s=new String("123");
s=new String("321");//是被重新赋值

还有

class SuperClass{
	private int a;
	public void  setA(int a){
		this.a=a;
	}
}

class SubClass extends SuperClass{
	private int b;
	public void setB(int b){
		this.b=b;
	}
}

public static void main(String args[]){
	//这里只能说SubClass产生的对象不是不可变对象,因为有setB改变对象状态的值。你只能说sc这个基本类型(java虚拟机层面的基本类型reference)是不可变变量。
	final SubClass sc=new SubClass();
	sc.setB(3);
}

1.2 绝对线程安全

java中的绝对线程安全的定义就是Brian Goetz所描述的。于是我们在java中常说的绝对线程安全的对象就不是绝对线程安全的了。
反例:Vector、Hashtable、List。比如Vector

import java.util.Vector;

public class Test {
	public static void main(String argc[]) {
		Vector<Integer> v= new Vector<>();
		for(int i=0;i<10;++i) {
			v.add(i);
		}
		Thread tremove=new Thread(()-> {
			for(int i=0;i<v.size();++i) {//这里使用到了v,v默认使用了final修饰
				System.out.println("v删除了"+v.remove(i));
			}
		});
		Thread tget=new Thread(()->{
			for(int i=0;i<v.size();++i) {
				System.out.println("v查询得到"+v.get(i));
			}
		});
		
		tremove.start();
		tget.start();
		
		while(Thread.activeCount()>0);
	}
}

上面这个是线程安全的。

import java.util.Vector;

public class Test {
	public static Vector<Integer> v = new Vector<>();

	public static void main(String argc[]) {
		for (int j=0;j<2000;++j) {
			for (int i = 0; i < 10; ++i) {
				v.add(i);
			}

			Thread tremove = new Thread(() -> {
				for (int i = 0; i < v.size(); ++i) {// 这里使用到了v,v默认使用了final修饰,A
					System.out.println("v删除了" + v.remove(i));//B
				}
			});
			Thread tget = new Thread(() -> {
				for (int i = 0; i < v.size(); ++i) {//C
					System.out.println("v查询得到" + v.get(i));//D
				}
			});

			tremove.start();
			tget.start();
			//while(Thread.activeCount()>20);
		}
		// while(Thread.activeCount()>0);
	}
}

这个时候就报错了

Exception in thread "Thread-499" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 8
	at java.util.Vector.get(Vector.java:751)
	at Test.lambda$1(Test.java:19)
v删除了4
	at java.lang.Thread.run(Thread.java:748)

这是为什么呢?

当两个线程运行到A,C时,获取的size()值都是正确的,因为size方法是个同步方法。然而到了B,D时,假如tremove线程先执行,将对应位置的元素删了,然后v重新调整空间大小,然后tget继续执行,他就可能在相应位置获取不到元素,因为实际的v的大小已经改变了。

为了避免出现问题,我们分别对两个for循环进行同步控制

Thread tremove = new Thread(() -> {
				synchronized (v) {// 这里

					for (int i = 0; i < v.size(); ++i) {// 这里使用到了v,v默认使用了final修饰
						System.out.println("v删除了" + v.remove(i));
					}

				}
			});
			Thread tget = new Thread(() -> {
				synchronized (v) {// 这里

					for (int i = 0; i < v.size(); ++i) {
						System.out.println("v查询得到" + v.get(i));
					}

				}
			});

1.3 相对线程安全

上面举得那个例子就是相对线程安全的。相对线程安全是指对象的单独的操作是线程安全的。比如上面例子中对v的size()、remove()、get()的单独操作是安全的,可以使对for循环及里面的remove()/get()操作就不是线程安全的。

Vector、Hashtable、List这些都是相对线程安全的。

1.4 线程兼容

需要进行同步控制就能实现线程安全。这种线程安全是指对象的单独操作都不是线程安全的,可通过同步控制后就变成了线程安全的了。

1.5 线程对立

就算做了同步控制,也不能包装线程安全。比如Thread类的suspend()(使线程挂起)和resume()(恢复 因suspend()方法挂起的线程,使之重新能够获得CPU执行).


二、线程安全的实现方法

2.1 互斥同步

互斥同步最主要的实现方式是使用sychnorized关键字。其原理:

每一个同步代码块的开始和结束都分别有monitorenter与monitorexit两条字节码指令(同步解释器可以是任何对象,但推荐是临界资源)。当执行monitorenter指令时,同步监视器的锁+1,当执行monitorexit时,同步监视器的锁-1。对于同一个线程,synchronized代码块是可重入的。对于简单的同步块,状态切换消耗的时间可能比代码真正执行的时间还要长。

reentryantLock也是实现同步的一种方式。可以说reentryantLock是API层面的同步实现方式,而synchronized是原生语法层面的同步实现方式。synchronized与reentryantLock的区别如下:

  1. 在进行线程通信时,reentryantLock可以用newCondition来产生多个condition对象。然而synchronized只能使用wait(),notifyAll()(相当于一个隐式的condition)。即reentryantLock对应1个或多个condition,而synchronized对应与一个condition。
  2. 等待可中断。什么是等待可中断,当一个线程等待排它锁久了时,可以放弃等待转而处理其他事情。这是renentryantLock所有的,而synchronized不具有的。
  3. 公平锁。当有很多线程等待排他锁时,必须按照他们申请锁的时间给与排他锁。而在synchronized中,获取锁的线程是随机的。其实reentryantLock默认情况下也是非公平锁的,但是可以在构造时传入参数指定该reentryantLock是公平锁的。

在jdk1.5之前,renentryantLock的性能的确优与synchronized。然而,在jdk1.6及其之后,对synchronized进行了优化,在性能方法已经不输于renentryantLock了,所以在jdk1.6之后如果能够用synchronized实现线程安全的情况下,推荐使用synchronized

2.2 非阻塞的同步方式

什么叫做非阻塞同步方式。

用synchronized方式、reentryantLock方式实现的同步是阻塞的同步方式。因为每一次都要将没有获取到排他锁的线程切换为阻塞状态,这是要消耗相对多的时间的。这是一种悲观锁,如果要实现线程安全就要进行状态切换等。而非阻塞同步方式,它是一种基于乐观并发策略的乐观锁实现的。它会先执行,如果不存在共享资源竞争,那自然就成功了;如果存在共享资源竞争,那它会采取补救措施(最简单的补救措施就是不断重试)。

非阻塞同步方式的实现是需要物理指令的支持的。乐观并发策略需要的物理指令:

  • 测试并设置(Test-and-Set)
  • 获取并增加(Fetch-and-Increment)
  • 交换(Swap)
  • 比较并交换(Compare-and-Swap,CAS)
  • 加载链接/条件存储(Load-Linked/Store-Conditional,LL/SC)

在java中,通过Unsafe类来提供CAS操作。例子,解决volatile在并发情况下不具有:

import java.util.concurrent.atomic.AtomicInteger;
public class Test {
	// public static Integer   race=0;
    public static AtomicInteger   race=new AtomicInteger(0);//这里
    private static final   int THREAD_COUNT=20;//同时执行的线程数
    public static void increase(){
        //race++;
    	race.incrementAndGet();//这里
    }
    public static void main(String[] args) {
        Thread[] threads=new Thread[THREAD_COUNT];
        for(int i=0;i<THREAD_COUNT;i++){
            threads[i]=new Thread(new Runnable() {
				
				@Override
				public void run() {
					for(int j=0;j<10000;j++){
	                    increase();
	                }
				}
			});
            threads[i].start();
        }
        //让所有线程都结束
        while (Thread.activeCount()>1){
            Thread.yield();//让主线程让步一下,是开启的所有子线程都执行完
        }
        System.out.println(race);
    }

}

这样结果就一定为:200000。

CAS的缺点,虽然CAS看起来很完美,但是它不能使用与所有场景。在java中,Unsafe不是给用户程序使用的,只有bootstrap加载的类才可以房屋Unsafe.getUnSafe()。所以在不通过反射加载当前类的情况下,只能使用java API来间接使用Unsafe类。所以下面会报错。

try {
			Class.forName("sun.misc.Unsafe");
			Unsafe.getUnsafe();
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

错误:

Exception in thread "main" java.lang.SecurityException: Unsafe
	at sun.misc.Unsafe.getUnsafe(Unsafe.java:68)
	at Test.main(Test.java:16)

2.3 无同步方案

线程安全并不一定都需要进行同步。同步只是保证共享数据争用时的正确性手段。只要一个方法本身不涉及共享数据,那么他本身就是线程安全的。入下面两种情况
可重入代码

在代码执行的任意时刻中断它,然后执行其他代码。在回到原来的位置,继续执行,执行后的结果不出错误(和预期一致),那么这就是可重入代码。可重入代码具有如下特征:不依赖堆上的数据和公用的系统资源,用到的状态量都由参数中传入、不调用非可重入的方法等。

线程本地存储

如果存在共享数据,那么看共享数据是否可以将其可见范围限制在同一个线程之内,这样无需同步也能保证线程安全。Java中可以通过TheadLocal来实现线程本地存储。

猜你喜欢

转载自blog.csdn.net/wobushixiaobailian/article/details/84190323