在Java单例类简单介绍了单例类,仔细分析其中的代码:
class Singleton{
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance()
{
if (instance == null)
{
instance = new Singleton();
}
return instance;
}
}
public class Hello {
public static void main(String[] args)
{
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1 == instance2);
}
}
乍一看,上面的单例似乎没有什么问题,运行结果是true。
但是如果换成多线程?这就不能保证是单例了。
保证一个类只有一个实例,并提供一个访问它的全局访问点。
当在instance = new Singleton();
加上断点,采用如下方式调用的时候,就会出现问题。
public static void main(String[] args)
{
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() { // anonymous class
Singleton s = Singleton.getInstance();
System.out.println(s.hashCode());
}
}).start();
}
}
hashCode不一致,就是不同的对象,也就是说,这种方式无法保证对象只有一个。
编译之后,通过javap -verbose Singleton
查看Singleton字节码。
D:\books>javap -verbose Singleton
Classfile /D:/books/Singleton.class
Last modified 2022年7月30日; size 356 bytes
MD5 checksum bf33d3d37bf9439e50f687fa4d5cff42
Compiled from "Test.java"
class Singleton
minor version: 0
major version: 55
flags: (0x0020) ACC_SUPER
this_class: #3 // Singleton
super_class: #5 // java/lang/Object
interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #5.#17 // java/lang/Object."":()V
#2 = Fieldref #3.#18 // Singleton.instance:LSingleton;
#3 = Class #19 // Singleton
#4 = Methodref #3.#17 // Singleton."":()V
#5 = Class #20 // java/lang/Object
#6 = Utf8 instance
#7 = Utf8 LSingleton;
#8 = Utf8
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 getInstance
#13 = Utf8 ()LSingleton;
#14 = Utf8 StackMapTable
#15 = Utf8 SourceFile
#16 = Utf8 Test.java
#17 = NameAndType #8:#9 // "":()V
#18 = NameAndType #6:#7 // instance:LSingleton;
#19 = Utf8 Singleton
#20 = Utf8 java/lang/Object
{
public static Singleton getInstance();
descriptor: ()LSingleton;
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field instance:LSingleton;
3: ifnonnull 16
6: new #3 // class Singleton
9: dup
10: invokespecial #4 // Method "":()V
13: putstatic #2 // Field instance:LSingleton;
16: getstatic #2 // Field instance:LSingleton;
19: areturn
LineNumberTable:
line 6: 0
line 8: 6
line 10: 16
StackMapTable: number_of_entries = 1
frame_type = 16 /* same */
}
SourceFile: "Test.java"
解释一下dup指令作用:也初始化指令会使当前对象的引用出栈,如果不复制一份,操作数栈中就没有当前对象的引用了,后面再进行其他的关于这个对象的指令操作时,就无法完成。
在多线程情况下,第一个线程走完了3,进入了6,此时第二个线程走到3,由于初始化未完成,所以第二个线程依然会走6,这样就初始化了2次,对象就不一致了。
很明显,需要加锁。
public synchronized static Singleton getInstance()
{
if (instance == null)
{
instance = new Singleton();
}
return instance;
}
让方法变成线程安全的。但是锁的范围有点大,于是就有了下面这种加锁方式,缩小锁的范围
public static Singleton getInstance()
{
if (instance == null)
{
synchronized(Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
可是在instance = new Singleton();
中加入断点后发现对象不一致:
查看字节码内容如下:
PS D:\books> javap -verbose Singleton
Classfile /D:/books/Singleton.class
Last modified 2022年7月30日; size 435 bytes
MD5 checksum 2493cd5ece248c4395dd76e87508cd04
Compiled from "Test.java"
class Singleton
minor version: 0
major version: 55
flags: (0x0020) ACC_SUPER
this_class: #3 // Singleton
super_class: #5 // java/lang/Object
interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #5.#18 // java/lang/Object."":()V
#2 = Fieldref #3.#19 // Singleton.instance:LSingleton;
#3 = Class #20 // Singleton
#4 = Methodref #3.#18 // Singleton."":()V
#5 = Class #21 // java/lang/Object
#6 = Utf8 instance
#7 = Utf8 LSingleton;
#8 = Utf8
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 getInstance
#13 = Utf8 ()LSingleton;
#14 = Utf8 StackMapTable
#15 = Class #22 // java/lang/Throwable
#16 = Utf8 SourceFile
#17 = Utf8 Test.java
#18 = NameAndType #8:#9 // "":()V
#19 = NameAndType #6:#7 // instance:LSingleton;
#20 = Utf8 Singleton
#21 = Utf8 java/lang/Object
#22 = Utf8 java/lang/Throwable
{
public static Singleton getInstance();
descriptor: ()LSingleton;
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=0
0: getstatic #2 // Field instance:LSingleton;
3: ifnonnull 31
6: ldc #3 // class Singleton
8: dup
9: astore_0
10: monitorenter
11: new #3 // class Singleton
14: dup
15: invokespecial #4 // Method "":()V
18: putstatic #2 // Field instance:LSingleton;
21: aload_0
22: monitorexit
23: goto 31
26: astore_1
27: aload_0
28: monitorexit
29: aload_1
30: athrow
31: getstatic #2 // Field instance:LSingleton;
34: areturn
Exception table:
from to target type
11 23 26 any
26 29 26 any
LineNumberTable:
line 6: 0
line 8: 6
line 9: 11
line 10: 21
line 12: 31
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 26
locals = [ class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
}
对于多线程,第一个线程执行完3的时候,第二个线程也执行3,然后第二个线程获取锁,实例化,然后第一个线程获取锁,再实例化。由此,产生了不同的对象。
- astore操作的index必须位于局部变量表中
- astore指令操作的是栈顶的returnAddress类型或reference类型的数
- astore用于弹出栈顶元素,赋值给局部变量(index)
于是就诞生了著名的双检锁技术
public static Singleton getInstance()
{
if (instance == null)
{
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
自己码内容如下:
0: getstatic #2 // Field instance:LSingleton;
3: ifnonnull 37
6: ldc #3 // class Singleton
8: dup
9: astore_0
10: monitorenter
11: getstatic #2 // Field instance:LSingleton;
14: ifnonnull 27
17: new #3 // class Singleton
20: dup
21: invokespecial #4 // Method "":()V
24: putstatic #2 // Field instance:LSingleton;
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field instance:LSingleton;
40: areturn
在锁之内再次判空,保证了只有一次实例话。由于Java中JIT的存在,所以需要把instance声明为private static volatile Singleton instance;
当然单例还有其他写法,比如内部类,通过JVM保证线程安全,还可以使用枚举,既保证了线程安全,又防止了序列化。
单例分在懒汉和饿汉模式,而存在线程不安全问题的只在懒汉模式出现。所以可以的话,用饿汉式就可以,避免了很多没必要的麻烦。
private static Singleton instance = new Singleton();
这中缺点就是即使不要要也会实例化,但大多数情况下不会差这一点的内存。
鸿蒙系统中又很多地方使用单例(C++),而且还用还提供了一个模板类了,代码如下,其实它没有保证构造函数私有,不过这又有什么关系那,重要的是模式,而不是那个死板的定义,一个模板简化可多少的操作。
template
class Singleton : public NoCopyable {
public:
static T &GetInstance()
{
return instance_;
}
private:
static T instance_;
};
template
T Singleton::instance_;
}
}
}
上面的双检锁技术依然存在问题,这个是Java的内存模型导致的,在并发的情况下依然不能保证只实例化一次。而C#没有这个问题,这个在下一篇文章中会细说。
更多内容,欢迎关注我的微信公众号: 半夏之夜的无情剑客。