运行后,程序只输出了一个flag = true,然后就死循环卡住了,不会输出:flag为true!原因是:
我们在子线程中修改了flag的值,但是主线程并不知道这个更改,使用的依旧是之前的旧值,所以会一直死循环。
而只要我们为flag添加volatile修饰,程序就能正常结束了:
除此之外为if(test.ifFlag())加上synchronized
锁也可以解决可见性问题~
线程在进入synchronized代码块前后,会获得锁,清空工作内存,从主内存拷贝共享变量最新的值到工作内存称为副本,执行代码,将修改后的副本的值刷回主内存中,最后线程释放锁。
最后一点,volatile无法保证原子性(一次操作,要么完全成功,要么完全失败),比如下面的代码示例:
public class VolatileTest { public static volatile int count = 0; public static void increase() { count++; } public static void main(String[] args) { Thread[] threads = new Thread[20]; for(int i = 0; i < threads.length; i++) { threads[i] = new Thread(() -> { for(int j = 0; j < 1000; j++) { increase(); } }); threads[i].start(); } // 等待所有累加线程结束,此处>2的原因是idea执行用户代码时会创建一个监控线程Monitor // 可以调用 Thread.currentThread().getThreadGroup().list() 查看一番~ while (Thread.activeCount() > 2) { Thread.yield(); } System.out.println(count); } }
创建了20个线程,每个线程对变量count进行1000此自增,并发结果正常应该是20000,但实际运行过程中结果很多时候都不够20000,原因是count++这个自增操作不是院子操作。解决方法也很简单,要么加锁,要么使用原子类,如:AtomicInteger。
总结下就是:
volatile是JVM提供的一种最轻量级的同步机制,可看做轻量版的synchronized,但不保证原子性,如果是对共享变量进行多个线程的赋值而没有其他操作,那么可以用volatile来代替synchronized。
⑤ 静态内部类(懒加载,线程安全,非常推荐)
public class Singleton { private Singleton() { } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } }
与饿汉式类似,两者都是通过类加载机制来保证初始化instance时只有一个线程,从而避免线程安全问题。
不同之处是Singleton类被加载时,不会立即初始化,只有调用getInstance()函数时,才会装载SingletonHolder类,从而实例化instance,间接实现了懒加载。
0x3、单例的其他安全问题
上述的单例写法都是围绕着 线程安全问题
进行的,即限制了new创建对象,而Java中除了这种创建对象的方式外,还有三种 克隆、反射和序列化
,下面演示下如何通过这三种方式破坏单例。
① 克隆破坏单例
clone()是Object自带函数,每个对象都有,直接调用下clone函数,就能创建一个新对象了,那不就把单例破坏了吗?
答:想太多,被克隆类要实现 Cloneable
接口,然后重写clone()
函数,才能完成对象克隆,而一般我们的单例是不会实现这个接口的,所以不存在此问题。
② 反射破坏单例
以静态内部类实现的单例为例,我们通过下述代码构建了两个对象,以此破坏单例:
public class ReflectTest { public static void main(String[] args) { try { Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(); constructor.setAccessible(true); // 禁用访问安全检查 Singleton s1 = constructor.newInstance(); Singleton s2 = constructor.newInstance(); System.out.println(s1.equals(s2)); // 输出结果:false } catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) { e.printStackTrace(); } } }
一个最简单的解决方式就是添加一个标志位,当二次调用构造函数时抛出异常,示例如下:
public class Singleton { private static boolean flag = true; private Singleton() { if (flag) { flag = !flag; } else { throw new RuntimeException("有不法之徒想创建第二个实例"); } } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } }
此时再运行反射代码:
Tips:当然先通过反射修改你的flag,在反射调构造方法依旧是可以破坏的~
③ 序列化破坏单例
同样以静态内部类实现的单例为例,先序列化到文件,然后在反序列化恢复为Java对象:
public class SingletonTest { public static void main(String[] args) { Singleton singleton1 = Singleton.getInstance(); Singleton singleton2 = null; // 序列化 try { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file")); oos.writeObject(singleton1); } catch (IOException e) { e.printStackTrace(); } // 反序列化 try { ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file")); try { singleton2 = (Singleton) ois.readObject(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } catch (IOException e) { e.printStackTrace(); } System.out.println(singleton1 == singleton2); // 输出:false } }
输出false,单例再次被破坏,接着我们来看下这个新对象是怎么创建出来的,从 readObject
跟到 readOrdinaryObject
,定位到下述代码:
isInstantiable()
:一个serializable/externalizable的类是否可以在运行时被实例化;
desc.newInstance()
:通过反射的方式调用无参构造函数创建一个新对象;
这就是反序列化破坏单例的原理,接着说下怎么规避,在创建新对象的代码处往下走一些:
desc.hasReadResolveMethod()
:判断类是否实现了readResolve()函数;
desc.invokeReadResolve(obj)
:有的反射调用此函数,如果在此函数中返回实例就可以了;
修改后的单例类代码:
import java.io.Serializable; public class Singleton implements Serializable { private Singleton() { } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Object readResolve() { return getInstance(); } }
此时再运行反序列单例时的代码,会输出:true,即同一个对象。
0x4、枚举单例(安全简单,没有懒加载,最佳实践)
上面讲解了除线程安全问题外,三种破坏单例的方式及解决方式,其实用枚举实现单例就能规避这些问题。一个简单的枚举单例代码示例如下:
public enum SingletonEnum { INSTANCE; private final AtomicLong id = new AtomicLong(0); public long getId() { return id.incrementAndGet(); } } // 调用 SingletonEnum.INSTANCE.getId()
得益于jdk的enum语法糖,这么简单的代码就能预防这四种问题,接下来一一看下原理。