java多线程学习(十一) 常见的单例模式线程安全性分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Ditto_zhou/article/details/78872914

类初始化锁

 怎么理解?

为什么需要了解?

常见的单例模式分析

懒汉式

为什么线程不安全

验证

饿汉式

为什么线程安全

双重检查锁定方式

演变由来

为什么线程不安全

如何解决线程不安全

静态类方式

为什么线程安全

结论


类初始化锁

Java语言规范规定,对于每一个类或接口C,都有一个唯一的初始化锁LC与之对应,从C到LC的映射,由JVM的具体实现去自由实现。JVM在初始化期间会获取这个初始化锁,并且每个线程至少获取一次锁来确保这个类被初始化了。
这个过程比较冗长,这里不做过多描述,总之就是JVM通过初始化锁同步了多个线程同时初始化一个对象的操作,保证类不会被多次初始化。

 怎么理解?

线程A 、线程B 同时去访问类的属性或者方法,两个线程都会去获取类初始化锁

1. 假设线程A先获取到锁,此时线程B阻塞等待。

2. 线程A获取到锁 -----> 对类初始化(初始化静态属性),设置state = initialized   ----->  释放锁

3. 线程B获取到锁,读取到state = initialized,得知类已经初始化了,释放锁

根据happen-before原则,线程A的释放锁happen-before线程B获取锁,这样线程A对类的初始化,线程B是可见的

结论就是,当类已经被初始化了,其他线程能够可见类的静态属性的值,但是如果一个线程在初始化之后,比如调用类的静态方法(静态方法没有做同步控制)改变类的属性的值,对其他线程不一定可见。

为什么需要了解?

单例模式,实际上都是多个线程通过静态方法访问一个类的静态变量

常见的场景是:

首次调用一个类的静态方法的过程,首先进行的是获取类锁、初始化、释放类锁,在调用类的静态方法。

如果一个类不是被首次访问,当前线程也会去获取类锁,读取到state = initialized 、释放锁,在调用类的静态方法。

静态方法对静态变量的修改线程之间不具有可见性,不是立即可见的。

常见的单例模式分析

懒汉式

public class SingleTon {
	private SingleTon() {}
	private static SingleTon instance;
	public static SingleTon getInstance() {
		if(instance==null) {
			instance = new SingleTon();
		}
		return instance;
	}
}

为什么线程不安全

多个线程访问,类只会被初始化一次,假设存在线程A和线程B调用getInstance方法,调用方法前类已经初始化,此时instance为null,对于两个线程而言,instance都是null。

线程A 执行到 instance ==null时候,向下执行,创建对象

对象的创建分为3步骤

  1. 给对象分配内存空间
  2. 对象初始化
  3. 将引用指向对象的内存空间

对于线程B而言,不会等待线程A对象创建完成,也会创建对象,这样就可能存在创建多个对象的可能性。

验证

为了模拟对象创建耗时的过程,在构造函数里面sleep 一段时间。

package cn.bing.singleton;

import java.util.Date;

/**
 * 懒汉式
 * @author Administrator
 *
 */
public class SingleTon {
	private SingleTon() {
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	private static SingleTon instance;
	public static SingleTon getInstance() {
		System.out.println(Thread.currentThread().getName()+" enter : "+System.currentTimeMillis());
		if(instance==null) {
			System.out.println(Thread.currentThread().getName()+" contruct : "+System.currentTimeMillis());
			instance = new SingleTon();
		}
		return instance;
	}
	public static void main(String[] args) {
		Runnable run = new Runnable() {
			@Override
			public void run() {
				System.out.println(SingleTon.getInstance());
			}
		};
		System.out.println("current time: "+System.currentTimeMillis());
			Thread t1 = new Thread(run, "线程A");
			t1.start();
			Thread t2 = new Thread(run,"线程B");
			t2.start();
	}
}

运行结果:

current time: 1541646424662
线程B enter : 1541646424663
线程B contruct : 1541646424663
线程A enter : 1541646424663
线程A contruct : 1541646424663
cn.bing.singleton.SingleTon@24e59eb1
cn.bing.singleton.SingleTon@52826699

饿汉式

为什么线程安全

懒汉式是因为类初始化的时候,没有对实例初始化,出现线程安全问题,那么类初始化的时候就创建对象(上面说过初始化静态属性的过程对于其他线程而言是可见的),就可以保证对象的一致性了,这就是饿汉式.

/**
 * 饿汉式
 * @author Administrator
 *
 */
public class SingleTonHungry {
	private static SingleTonHungry instance = new SingleTonHungry();
	private SingleTonHungry() {
	}
	public static SingleTonHungry getInstance() {
		return SingleTonHungry.instance;
	}
}

双重检查锁定方式

演变由来

懒汉式不安全的原因,静态方法被多个线程同时访问,只要只能一个线程去构建对象,其他线程只能阻塞,等到另一个线程释放锁了,也就是这个对象创建好了,再获取这个对象,便线程安全 了,于是在静态方法上加上类锁synchronize

public class SingleTonLazySynchronize {
	private static SingleTonLazySynchronize instance;
	private SingleTonLazySynchronize() {}
	public static synchronized SingleTonLazySynchronize getInstance() {
		if(instance == null) {
			instance = new SingleTonLazySynchronize();
		}
		return instance;
	}
}

优点:线程安全

缺点: 一个线程等待另一个线程执行完毕,在多线程环境下,效率很低

那么,是否只要控制对象的创建在同步代码块中的话,是不是就行了呢?

为什么线程不安全

package cn.bing.singleton;
/**
 * 双重检查锁定延迟加载
 * @author Administrator
 *
 */
public class SingleTonDoubleLock {
	private static volatile SingleTonDoubleLock instance;
	private SingleTonDoubleLock() {}
	public static SingleTonDoubleLock getInstance() {
		if(instance==null) {//1
			synchronized (SingleTonDoubleLock.class) {//2
				if(instance==null)
				instance = new SingleTonDoubleLock();
			}
		}
		return instance;
	}
}

假设线程A和线程B调用getInstance方法,线程A获取到类锁,进入2,创建对象

对象的创建分为3步骤

  1. 给对象分配内存空间
  2. 对象初始化
  3. 将引用指向对象的内存空间

jvm可能对上面的指定进行重排序,可能是1,3,2的顺序

此时,线程B执行到1,看到instance的地址不为空(由于重排序,可能对象还没有初始化),直接就返回了地址,但是此时对象还没有被初始化。

如何解决线程不安全

第一种方式,jvm禁止重排序

禁止对象创建过程中2,3的重排序,只要将instance申明为volatile类型.

package cn.bing.singleton;
/**
 * 双重检查锁定延迟加载
 * @author Administrator
 *
 */
public class SingleTonDoubleLock {
	private static volatile SingleTonDoubleLock instance;
	private SingleTonDoubleLock() {}
	public static SingleTonDoubleLock getInstance() {
		if(instance==null) {//1
			synchronized (SingleTonDoubleLock.class) {//2
				instance = new SingleTonDoubleLock();
			}
		}
		return instance;
	}
}

第二种方式,只要另一个线程看不到重排序(静态类解决方案)

静态类方式

package cn.bing.singleton;

public class SingleTonStaticClass {
	private SingleTonStaticClass() {}
	static class InstanceHolder{
		private static SingleTonStaticClass instance = new SingleTonStaticClass(); 
	}
	public static SingleTonStaticClass getInstance() {
		return InstanceHolder.instance;
	}
}

为什么线程安全

  假设存在两个线程A,B ,此时SingleTonStaticClass没有初始化

1. 线程A先获取到外部类的初始化锁,线程B只能等待。

2. 线程A执行类的初始化完毕,将state设置为initialized,释放外部类的锁,调用getInstance方法,获取内部类的锁

3. 线程B获取到外部类的锁,尝试调用getInstance方法,因为没有获取到内部类的锁,只能等待

4. 线程A完成内部类的初始化,释放内部类的锁,线程B拿到内部类的锁,因为内部类已经初始化了,不会继续初始化,直接释放锁。

这个过程中,线程B是看不到线程A对内部类的对象的重排序的。

根据happen-before原则, 线程A对内部类的静态属性初始化后的的值对线程B是可见的。

线程A,B在这个过程中是获取两次初始化锁,后续线程C调用getInstance只会获取一次外部类锁,读取到外部类state=initialized,释放锁。

-- 来自方腾飞《JAVA并发编程的艺术》

个人感觉和线程A、线程B一样还是要获取两次类锁

结论

考虑到延迟加载,线程安全的单例模式,选择基于volatile的双重检查方式或者基于静态类的方式创建。

参考:方腾飞《JAVA并发编程的艺术》


 

猜你喜欢

转载自blog.csdn.net/Ditto_zhou/article/details/78872914