1.代码实现
public class Test
{
public static Test T;
private Test(){}
public static Test getInstance(){
if(T== NULL){
try{
Thread.sleep(1);
}catch(Exception e){
e.printStackTrace();
}
return T = new Test();
}
return T;
}
public static void main(String[] args)
{
for(int i=0;i<100;i++){
new Thread(()->System.out.println(Test.getInstance().hashCode())).start();
}
}
}
这种方式在多线程的访问下比起单例模式将会有更大的问题——不能在多线程的情况下保证同一个实例。
2.优化方式
- 为获取对象方法加一把锁
public static synchronized Test getInstance(){
if(T== NULL){
try{
Thread.sleep(1);
}catch(Exception e){
e.printStackTrace();
}
return T = new Test();
}
return T;
}
这样处理没有问题,这种叫做多线程的原子性,但是这种方式太耗时。
3.再次优化
以下方式又叫双重验证(DCL —— Double Check Lock)。
- 1.为了防止在获取对象的方法中有其它的业务代码
- 2.直接在方法上加锁会使得消耗时间变长
因此代码再次优化如下:
public static Test getInstance(){
if(T == NULL){ // Double Check Lock
// 双重检查
synchronized (Test.class){
if(T == NULL){
try{
Thread.sleep(1);
}catch(Exception e){
e.printStackTrace();
}
return T = new Test();
}
}
}
return T;
}
思考,这种情况下是否就没有任何问题了?(美团经典问题)
答案是,不,此时还有问题。原因是synchronized保证原子性,可见性,但是不能保证有序性。
4.在3中产生的具体原因
在java中,实例化一个对象主要有3步。
- 1.new申请一块内存空间,并赋默认值(与C++不同的是,java为保证安全才赋的默认值,而C++中申请到的空间中会包含上次空间使用过的数据)
- 2.调用构造方法并赋值
- 3.建立变量和空间的关联
在单线程的访问中,只要不影响最终的一致性,俩条语句可以交换次序(即指令重排),因此在多线程的访问过程中其它线程可能会访问到对象的半初始化状态
解释:上锁代码和不上锁代码是并发运行的,各个线程之间没有互斥和序列化,因此就算在加锁状态下,其它线程也可以访问的未加锁代码的中间状态。
5.禁止指令重排
为声明的单例变量加volatile关键字
public static volatile Test T;
public static Test getInstance(){
if(T == NULL){ // Double Check Lock
// 双重检查
synchronized (Test.class){
if(T == NULL){
try{
Thread.sleep(1);
}catch(Exception e){
e.printStackTrace();
}
return T = new Test();
}
}
}
return T;
}
所以volatile在DCL单例中不是使用它的线程可见性,而是禁止指令重排序,在DCL中是否需要加volatile关键字(是)也是常见面试题之一。