由双重锁实现单例引发的疑问

下面是双重锁实现单例的具体代码

public Singleton{

	private static volatile Singleton singleton;

	private Singleton(){};

	public static Singleton  getSingleton(){
		if(singleton==null){
			synchronized(Singleton.class){
				if(singleton==null){
					singleton=new Singleton();
				}
			}
		}
		return singleton;
	}
}

注意这里 synchronized 锁的是 Singleton.class ,锁的是 Singleton 这个类。
通常来说我们是这样创建锁的:

Object object=new Object();
synchronized(object){
	//...
	//临界区
	//...
}

那上述单例的经典实现为什么选择使用类锁呢?锁类和锁对象究竟有什么区别呢?类锁究竟锁了什么?

在回答问题之前我们先来再熟悉一下 synchronized 的一些基本用法。

class Demo{
  // 修饰非静态方法
  synchronized void foo() {
    // 临界区
  }
  // 修饰静态方法
  synchronized static void bar() {
    // 临界区
  }
  // 修饰代码块
  Object obj = new Object();
  void baz() {
    synchronized(obj) {
      // 临界区
    }
  }
}

在上面这三种用法中,我们可以看到修饰代码块时,我们锁了 obj 这个对象,但是修饰静态方法和非静态方法时我们究竟锁了什么呢?
Java 中有这样一条隐式规则:

在修饰静态方法时,锁定的是当前类的 Class 对象,在上面的例子中就是 Demo.class。
当修饰非静态方法时,锁定的就是 this 对象,即当前的实例化对象。

先来看看什么是 Class 类?

可以这样理解 Java 普通类和 Class 类的关系:

//万物皆对象  
万物=对象
//类是对象的抽象   
类<------(抽出像的部分)-------对象
//Class类是对类的抽象   
Class类<------(抽出像的部分)-------类
//综上
Class类<------(抽出像的部分)-------类<------(抽出像的部分)-------对象=万物

从某种意义上来说 Java 中有两种对象:实例对象和 Class 对象。 每个类的运行时类型信息就是通过Class类来表示的。
我们使用的实例对象就是通过Class对象来创建的,Java 使用 Class 对象执行其 RTTI (运行时类型识别 Run-Time Type Identification),多态就是基于 RTTI 来实现的。

每个类都有一个 Class 对象 ,每当编译一个新的类就会生成一个对应的 Class 对象。包括 int long float double char byte short boolean 这些基本类型都是有对应的 Class 对象的,以及数组、void (void.class)。

Class 类是没有公共的构造方法的,所以我们没办法显示的声明一个 Class 对象,Class 对象是由 Java虚拟机通过调用类加载器中的 defineClass 方法自动构造的。

对象锁和类锁究竟有何区别?

由上可以看出,对象锁和类锁其实本质上都是一种锁,区别在于一个类可以有多个对象锁,但类锁只有一个。
故使用 synchronized 对类加锁是唯一的,当一个线程拿到类锁,其他线程在执行到任何需要类锁的方法时都将阻塞。
总体来说类锁具有唯一性

下面我来用对象锁重写一下单例:

public Singleton{

	private static volatile Singleton singleton;

	private Singleton(){};

	public static Singleton getSingleton(){
		Object obj=new Object();
		if(singleton==null){
			synchronized(obj){
				if(singleton==null){
					singleton=new Singleton();
				}
			}
		}
		return singleton;
	}
}

这样做看似没有问题,但是在多线程情况下就出现创建多个 Singleton 对象的情况,下面我们做一个简单的测试

public class Singleton {
    private static volatile Singleton singleton;

    private Singleton(){};

    public static Singleton getSingleton(){
        Object obj=new Object();
        if(singleton==null){
            synchronized(obj){
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if(singleton==null){
                    System.out.println("创建一个单例对象");
                    singleton=new Singleton();
                }
            }

        }
        return singleton;
    }

    public static void main(String[] args) {
        Thread thread1=new Thread(Singleton::getSingleton);
        Thread thread2=new Thread(Singleton::getSingleton);
        Thread thread3=new Thread(Singleton::getSingleton);
        Thread thread4=new Thread(Singleton::getSingleton);
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

运行结果

创建一个单例对象
创建一个单例对象
创建一个单例对象
创建一个单例对象

Process finished with exit code 0

这里我们看到竟然 new 了四次,为什么会这样呢?因为 synchronized(obj) 在每次执行的时候都会实例化一个新的 Object 作为对象锁,这些锁是互不关联的,这样加锁和不加锁实际上没有区别,并不会有互斥的情况发生。这时类锁的用处就体现了出来。

总结

类锁:

每个类都有一个唯一的 Class 对象,Class 对象的创建由 JVM 来管理,对 Class 对象加锁具有全局唯一性,这里我们称它为类锁。

对象锁:

类的实例化对象可以有多个,对每个实例化对象加锁是互不干扰的,它们不具备全局唯一性,这里我们称它为对象锁。
对象锁一般可以用于细粒度锁的创建。

Java 锁和事务_多线程 Java 锁和事务_并发编程_02