测试类
public static void main(String[] args) {
// 创建一个可以存放20个线程的线程池(最好手动创建线程池)
ExecutorService threadPool = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++) {
// 执行创建线程(Runnable接口)
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":"+Singleton.getInstance());
}
});
}
// 关闭线程池
threadPool.shutdown();
}
1.饿汉模式
public class Singleton {
// 构造私有的静态成员变量(实例)
private final static Singleton singleton = new Singleton();
// 构造函数私有化
private Singleton(){}
// 构造得到实例的静态方法
public static Singleton getInstance() {
return singleton;
}
}
-
1.为什么说饿汉式单例模式是线程安全的:
饿汉式单例模式是在类加载的时候创建实例(静态成员变量用static修饰),所以线程是安全的。请点击:类加载过程的线程安全性保证
-
2.final关键字作用:
参考地址final关键词是代表此变量一经赋值,其指向的内存引用地址将不会再改变。
加final也仅仅是表示类加载的时候就初始化对象了,比不加载final要早一点。
如果存在释放资源的情况下,就不能加final修饰了,释放资源之后,如果需要重新使用这个单例,就必须存在重新初始化的过程,而final定义的常量是不能重新赋值的,所以不能加final,对于不需要释放资源的情况,可以加final。
对于需要释放资源饿汉单例模式的代码(释放资源代码参考如果此时singleton被声明为final,IDE会提示编译错误,因为singleton表示的内存引用地址不可变):
public class Singleton { // 不能用final修饰,否则编译不过 private static Singleton singleton = new Singleton(); private Singleton(){} public static Singleton getInstance() { return singleton; } public static void releaseInstance() { if (singleton != null) { singleton = null; } } }
2.懒汉模式
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
return new Singleton();
}
return singleton;
}
}
- 1.优点:用的时候再去创建,不浪费空间
- 2.缺点:线程不安全
2.1线程安全的懒汉式单例模式代码1
```java
public static synchronized Singleton getInstance() {
if (singleton == null) {
return new Singleton();
}
return singleton;
}
```
缺点:如果多个线程同时执行,那么每次执行`getInstance()`方法时每个线程都要先尝试获得锁再去执行方法体,若该线程没有获得锁,那么该线程就要等待,耗时很长。
2.2线程安全的懒汉式单例模式代码2
```java
public static Singleton getInstance(){
if (singleton == null) {
synchronized (Singleton.class){
singleton = new Singleton();
}
}
return singleton;
}
```
缺点:线程A和线程B,线程A读取`singleton`值为`null`,此时cpu被线程B抢去了,线程B再来判断`singleton`值为null,于是,它开始执行同步代码块中的代码,对`singleton`进行实例化。此时,线程A获得cpu,由于线程A之前已经判断`singleton`值为`null`,于是开始执行它后面的同步代码块代码。它也会去对`singleton`进行实例化。这样就会导致创建了两个不一样的实例。
2.3线程安全的懒汉式单例模式代码3
(双重检查加锁,volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,不建议使用)
“双重检查加锁”机制的实现会使用关键字`volatile`,它的意思是:被`volatile`修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。(被`volatile`修饰的`value`,某线程修改`value`的值时,其他线程看到的`value`值都是最新的`value`值)
**那么“双重检查加锁”机制的实现为什么要使用关键字`volatile`?**
需要volatile关键字的原因是,在并发情况下,如果没有volatile关键字,在第5行会出现问题。
instance = new Singleton();
可以分解为3行伪代码
a.memory = allocate() //分配内存
b. ctorInstanc(memory) //初始化对象
c. instance = memory //设置instance指向刚分配的地址
上面的代码在编译运行时,可能会出现重排序从a-b-c排序为a-c-b。
在多线程的情况下会出现以下问题。当线程A在执行第5行代码时,
B线程进来执行到第2行代码。假设此时A执行的过程中发生了指令重排序,
即先执行了a和c,没有执行b。
那么由于A线程执行了c导致instance指向了一段地址,
所以B线程判断instance不为null,会直接跳到第6行并返回一个未初始化的对象。
```java
public class Singleton {
// 用volatile修饰静态成员变量
private volatile static Singleton singleton;
private Singleton() {}
public static Singleton getInstance(){ //1
// 首先检查实例是否存在
if (singleton == null) { //2
// 同步代码块,保证线程安全的创建实例
synchronized (Singleton.class){ //3
// 再次检查实例是否存在
if (singleton == null) { //4
singleton = new Singleton(); //5
}
}
}
return singleton;
}
}
```
3.静态内部类单例模式
public class Singleton {
// 静态内部类
private static class SingletonHolder {
private static final Singleton singleton = new Singleton();
}
private Singleton() {}
private static Singleton getInstance() {
return SingletonHolder.singleton;
}
}
-
1.为什么静态内部类单例模式线程安全
静态内部类不会随着外部类的加载而加载,只有静态内部类的静态成员被调用时才会进行加载 , 这样既保证的惰性初始化(Lazy-Initialazation),又由JVM保证了多线程并发访问的正确性。请点击:类加载过程的线程安全性保证
4.枚举单例模式
public enum SingletonEnum {
INSTANCE;
public void doSomething() {
System.out.println("hello singleton");
}
public static void main(String[] args) {
SingletonEnum.INSTANCE.doSomething();
}
}
- 1.利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。
5.基于CAS的单例模式
不需要使用synchronized和lock即可保证安全性,代码如下:
public class SingletonCas {
private static final AtomicReference<SingletonCas> INSTANCE = new AtomicReference<>();
private SingletonCas() {
}
/**
* 用cas确保安全性
*/
public SingletonCas getInstance() {
for (;;) {
SingletonCas current = INSTANCE.get();
if (current != null) {
return current;
}
current = new SingletonCas();
if (INSTANCE.compareAndSet(null, current)) {
return current;
}
}
}
}
拓展
类加载过程的线程安全性保证
以上的静态内部类、饿汉等模式均是通过定义静态的成员变量,以保证单例对象可以在类初始化的过程中被实例化。
这其实是利用了ClassLoader的线程安全机制。ClassLoader的loadClass方法在加载类的时候使用了synchronized关键字。
所以, 除非被重写,这个方法默认在整个装载过程中都是线程安全的。所以在类加载过程中对象的创建也是线程安全的。
枚举的特性
枚举类型是线程安全的,并且只会装载一次。
使用javap -p SingletonEnum.class反编译class文件,得到如下代码
public final class com.demo.thymeleafstudy.singleton.SingletonEnum extends java.lang.Enum<com.demo.thymeleafstudy.singleton.SingletonEnum> {
public static final com.demo.thymeleafstudy.singleton.SingletonEnum INSTANCE;
private static final com.demo.thymeleafstudy.singleton.SingletonEnum[] $VALUES;
public static com.demo.thymeleafstudy.singleton.SingletonEnum[] values();
public static com.demo.thymeleafstudy.singleton.SingletonEnum valueOf(java.lang.String);
private com.demo.thymeleafstudy.singleton.SingletonEnum();
public void doSomething();
public static void main(java.lang.String[]);
static {};
}
从反编译结果可知:
-
枚举类型的关键字
enum,其实只是一个语法糖,编译器最终把它转化为一个final类,因此枚举是不可继承的。 -
枚举类型都继承自
java.lang.Enum类。 -
枚举的每一个取值被编译器传化为了一个个
static final属性,因为static类型的属性会在类被加载之后被初始化。(类似于饿汉单例模式)。
破坏单例模式的方法以及如何避免
1.通过反射破坏单例
除枚举方式外, 其他方法都可以通过反射的方式破坏单例。反射是通过调用构造方法生成新的对象,所以如果我们想要阻止单例破坏,可以在构造方法中进行判断,若已有实例, 则阻止生成新的实例,解决办法如下:
private Singleton() {
if (instance !=null) {
throw new RuntimeException("实例已经存在,请通过getInstance()方法获取");
}
}
2.通过反序列化破坏单例
如果单例类实现了序列化接口Serializable, 就可以通过反序列化破坏单例。验证如下(SerializeUtil工具类代码参考这个博客点我):
public class Singleton implements Serializable {
private static final long serialVersionUID = 1580706254912463448L;
private static class SingletonHolder {
private static final Singleton singleton = new Singleton();
}
private Singleton() {}
private static Singleton getInstance() {
return SingletonHolder.singleton;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 获取单例对象
Singleton singleton1 = Singleton.getInstance();
// 序列化
SerializeUtil.serialize(singleton1, "D://test_singleton.txt");
// 反序列化
Singleton singleton2 = (Singleton) SerializeUtil.deserialize("D://test_singleton.txt");
// 验证两个对象是否相等
System.out.println(singleton1 == singleton2);
}
}
输出结果如下:
false
因此得出结论,反序列化后的单例对象和原单例对象并不相等了。
解决办法是如果一个单例类必须要实现序列化接口,可以重写反序列化方法readResolve(), 反序列化时直接返回单例对象。
public Object readResolve() throws ObjectStreamException {
return instance;
}
CAS
什么是CAS
cas是compareandswap的简称,从字面上理解就是比较并更新(或者说比较并交换,用于保证并发时的无锁并发的安全性)。
简单来说:从某一内存上取值V,和预期值A进行比较,如果内存值V和预期值A的结果相等,那么我们就把新值B更新到内存,如果不相等,那么就重复上述操作直到成功为止。原文出处
CAS能够做什么
它可以解决多线程并发安全的问题,以前我们对一些多线程操作的代码都是使用synchronize关键字,来保证线程安全的问题。
现在我们将cas放入到多线程环境里我们看一下它是怎么解决的。我们假设有A、B两个线程同时执行一个int值value自增的代码,并且同时获取了当前的value。假设线程B比A快了那么0.00000001s,所以B先执行,线程B执行了cas操作之后,发现当前值和预期值相符,就执行了自增操作,此时这个value = value + 1;然后A开始执行,A也执行了cas操作,但是此时value的值和它当时取到的值已经不一样了。所以此次操作失败,重新取值然后比较成功,然后将value值更新,这样两个线程进入,value值自增了两次,符合我们的预期。原文出处
AtomicReference(提供以无锁方式访问共享资源的能力)
AtomicReference实例持有一个对象(这个对象就是共享资源)的引用,initialValue,用volatile修饰,并通过unsafe类来操作该引用。
public class AtomicReferenceTest {
public static void main(String[] args) throws InterruptedException {
AtomicReference<Integer> ref = new AtomicReference<>(new Integer(1000));
for (int i = 0; i < 1000; i++) {
Thread t = new Thread(new Task(ref), "Thread-"+i);
t.start();
}
// 保证前面线程全部执行完
Thread.sleep(10000);
System.out.println("####"+ref.get());
}
}
public class Task implements Runnable {
AtomicReference<Integer> ref;
public Task(AtomicReference<Integer> ref) {
System.out.println("construct="+ref.get());
this.ref = ref;
}
@Override
public void run() {
// for(;;)表示死循环,也就是自旋
for(;;) {
Integer oldV=ref.get();
System.out.println(Thread.currentThread().getName()+":"+oldV);
/**
* compareAndSet方法会将入参的expect变量所指向的对象和AtomicReference中的引用对象进行比较,
* 如果两者指向同一个对象,
* 则将AtomicReference中的引用对象重新置为update,修改成功返回true,失败则返回false。
* 也就是说,AtomicReference其实是比较对象的引用。
*/
if (ref.compareAndSet(oldV, oldV+1)) {
break;
}
}
}
}
下面看一下compareAndSet方法
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
public final native boolean compareAndSwapObject
(Object var1, long var2, Object var4, Object var5);
最终compareAndSet方法调用了UnSafe类的compareAndSwapObject方法(比较并交换)。
1.此方法是Java的native方法,并不由Java语言实现。
2.方法的作用是,读取传入对象var1在内存中偏移量为var2位置的值与期望值var4作比较。如果相等就把var5值赋值给var2位置的值。方法返回true。不相等,就取消赋值,方法返回false。