水一篇单例模式
Posted 汤姆的奶酪
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了水一篇单例模式相关的知识,希望对你有一定的参考价值。
单例模式有啥好水的,啥是单例模式???这还不简单???
520孤单的日子,没有对象怎么办?找对象还不容易,我new!然后我这辈子就用这一个对象了。别人不给new,自己也不给new(我已经new过了,再new还是同一个)。
初级单例模式
顾名思义,饿嘛,我要马上吃,在初始化类的时候就把这个对象创建出来了,不管你要不要用。这种情况其实是不好的,浪费系统内存,不管我要不要先给我搞出来,要是我已经很忙了你还来这里添乱,况且我还不需要你,你这不是添乱嘛。所以出现了后面一系列的单例模式......
package com.test.gof23.singleton;
public class Singleton01 {//饿汉式
private static Singleton01 s=new Singleton01();
private Singleton01() {
}
public static Singleton01 getInstance() {
return s;
}
}
由于第一种饿汉式没有体现出懒加载的好处,所以第二种懒汉式出现,懒汉同样的,我懒,学校布置作业,下周交,我:急个毛线下周再写,交作业当晚:我:让我来参考参考同学们的作业,你不需要我我就先不给你创建出来,等你要的时候我在去给你创建。
但是!!这里会出现线程安全问题,所以会出现后面其他的单例模式
举个栗子
现在有两个线程过来,线程A在第12行CPU的时间片用完了,挂起.此时判断s是等于null的。线程B也执行到12行,s还是等于null,然后可以执行下去,new了一个对象。此时线程A继续执行,也new了一个。这里就new了两个出来了,不符合单例模式的理念
package com.test.gof23.singleton;
public class Singleton02 {//懒汉式
private static Singleton02 s;
private Singleton02() {
}
public static Singleton02 getInstance() {
if(s==null) {
s=new Singleton02();
}
return s;
}
}
这个单例模式最好写,java里枚举本身就是单例的
package com.test.gof23.singleton;
public enum Singleton05 {//枚举
INSTANCE;//本身就是单例模式
public void SingletonOperation() {//添加需要的操作
}
}
中级单例模式
懒加载存在上面所说的线程安全问题,怎么解决呢
这还不简单我加synchronized我加加加,啥花里胡哨的都给你搞定
package com.test.gof23.singleton;
public class Singleton02 {//懒汉式
private static Singleton02 s;
private Singleton02() {
}
public static synchronized Singleton02 getInstance() {
if(s==null) {
s=new Singleton02();
}
return s;
}
}
但是这样也太不专业了把,直接在方法上加synchronized
我:我改,我改,我改还不行么
我改!
package com.test.gof23.singleton;
public class Singleton03 {
private static Singleton03 instance;
private Singleton03() {
}
public static Singleton03 getInstance() {
if(null!=instance) {
return instance;
}
synchronized(Singleton03.class) {
if(instance==null) {
instance=new Singleton03();
return instance;
}
}
return instance;
}
}
不是这位同学,你为啥还要在synchronized前加空判断啊
这你就不懂了把,我难道每次不管有没有对象都要加锁判断么,那岂不是太消耗资源了把,加锁前判断如果已经存在对象了的话就可以跳过加锁过程了
(这个叫做Double-Checked双重检查锁定)
等等等等等等等
有问题
多线程情况下,当你执行到if(null!=instance)的时候,另一个线程在创建对象(对象还没有初始化完成)但是instance已经分配了内存空间,直接返回instance的话调用方得到的是一个没有初始化完成的对象,这是有问题的。
造成这个问题出现的主要原因是重排序
重排序:是编译器和处理器为了优化程序的性能而对指令执行顺序进行改变,原本应该先初始化完这个类的所有内容再把instance指向这块内存区域的。现在变成了先把instance指向这块内存区域,再慢慢初始化里面的内容
栗子
Singleton03里假如还有一个变量public static int i;该对象初始化完后要把int设置成9。假设另外一个线程需要执行这个
if(instance!=null){
System.out.println(instance.i+1);
}
我们预期的结果是10,可是如果该对象没初始化完成的话i是等于0的,结果会是1
其实解决这个问题很简单在privatestatic Singleton03 instance;加一个volatile
package com.test.gof23.singleton;
public class Singleton03 {
//其他都一样,改在这里!!!!看我,你看我啊!!
private static volatile Singleton03 instance;
private Singleton03() {
}
public static Singleton03 getInstance() {
if(null!=instance) {
return instance;
}
synchronized(Singleton03.class) {
if(instance==null) {
instance=new Singleton03();
return instance;
}
}
return instance;
}
}
volatile可以说是一个轻量级的锁,但是它又跟synchronized不一样。
synchronized它可以保证线程安全的三大特性:
1、原子性
2、可见性
3、有序性
volatile只能保证可见性和有序性
原子性:
即不可再分了,不能分为多步操作。
栗子:最常见的i++多线程结果用volatile同样会出错,i++是可以拆分步骤的,所以会出现线程安全问题
可见性:
一个变量由一个线程改变了之后,在其他线程能马上读到。
每个线程都有自己的工作空间,工作空间的内容是从主内存中读取的,每个线程只能跟自己的工作空间打交道,不能直接访问主存
此时如果线程1改了变量i从1修改为666,还没刷回主存,线程2读取主存里的i然后放到工作空间中,用i来计算,显然结果时错误的,这就不符合可见性原理。
所以使用synchronized和volatile都可以解决这个问题。
有序性:上面提到的重排序就是没有序。volatile使用内存屏障来实现有序性操作。同时有个叫happens-before原则,里面有一条就是volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。
高级单例模式
能不能不用synchronized加锁啊,太耗资源了(挠头)
这个,这个,这个
这个可以有
package com.test.gof23.singleton;
public class Singleton04 {
private static class SingletonInstance{
private static final Singleton04 instance = new Singleton04();
}
private Singleton04() {
}
public static Singleton04 getInstance() {
return SingletonInstance.instance;
}
}
这里使用了静态内部类,延迟加载
可你这你这哪里是延迟加载啊,还有线程安全问题呢
别急,同学们请翻开《深入理解java虚拟机》第210页......
首先先要了解什么情况下类会进行初始化,书中提到有且只有这五种情况类会初始化(这里字太多我就不一个一个打了,cv操作!)
1、遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时、读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。
2、使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。
3、当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
4、当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
5、当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
可以看到第一条,一开始这个内部类其实是没有初始化的,等到我们调用了getInstance的时候才会初始化内部类,才会创建对象,这里就达到了延迟加载的效果
线程安全问题怎么解决?
同样引用书中的一句话,类的生命周期:
在初始化阶段,虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。
这样解释没毛病了把
你别想忽悠我,你这没有直接用synchronized,但是间接用了
嘿嘿
这是ClassLoader类里复制过来的loadClass方法
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//这是啥同学,说好不用synchronized的呢
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
我ball ball你做个人把,这都要搞我
我再进化
那我用CAS大法!!!
CAS-->compare and swap
简单介绍下CAS,CAS是不加锁的,是一种乐观锁,CAS就是在你想要改变这个值的时候先获取它的值然后保存在一个变量中,当你真正执行改变的时候,你拿你原先保存的变量跟现在的值做比较,看是不是一样的,一样的表示在你保存到改变这段时间内没有其他线程去改变它,所以你就可以改值了。如果不一样,就重新获取最新的值,保存在变量中,然后再来一次直到可以改变为止,这里用了一个死循环,一直尝试,这个叫做自旋
看着没毛病,其实还有点小问题,ABA问题,当另一个线程改了一次,然后又改回来了,你判断值还是一样的,这样就不太好了。这里解决办法是加一个时间戳,具体就不细讲了,JUC包下的AtomicStampedReference就可以解决ABA问题
package com.test.gof23.singleton;
import java.util.concurrent.atomic.AtomicReference;
public class SingletonCAS {
private static final AtomicReference<SingletonCAS> INSTANCE = new AtomicReference<SingletonCAS>();
private SingletonCAS(){
}
public static SingletonCAS getInstance(){
for(;;){
SingletonCAS instance = INSTANCE.get();
if(instance!=null){
return instance;
}
instance = new SingletonCAS();
if(INSTANCE.compareAndSet(null,instance)){
return instance;
}
}
}
}
这里先创建了对象,然后判断预期对象是否为空,是则用CAS把创建的对象放进去。
这里很明显的一个缺点,就是多线程CAS失败自旋太多次,会创建很多次对象,造成内存溢出
其他
其实还有一个使用ThreadLocal实现的单例模式,但是不是严格意义上的单例模式,它是对同一个线程来说是单例的,全局来看不是单例的
package com.test.gof23.singleton;
public class SingletonThreadLocal {
private static final ThreadLocal<SingletonThreadLocal> instance =
new ThreadLocal<SingletonThreadLocal>(){
protected SingletonThreadLocal initialValue() {
return new SingletonThreadLocal();
}
};
private SingletonThreadLocal(){
}
public static SingletonThreadLocal getInstance(){
return instance.get();
}
}
ThreadLocal不同的线程调用get()方法得到的是本线程set进去的值,一开始我以为是简单的利用HashMap把Thread.currentThread()做为key放进去的,看了源码之后才知道直接太年轻,它的key就是自身threadLocal,每一个Thread类中都有一个ThreadLocalMap保存线程自己的东西......具体还涉及到了WeakReference弱引用内存溢出等问题这里就不详细写了
到这里大概就是我知道的几种单例模式了,有问题的地方请大佬轻喷
小小的改进
其实还有一些小细节需要改进的地方就是单例模式虽然构造方法用了private来修饰,但其实是可以用反射机制来破解的,是否记得反射里面有一个setAccessable(true);这样我们就可以创建对象了,单例模式直接爆炸
还有一个问题是我们可以用序列化和反序列化来获得不同的对象
解决方法其实挺简单的
package com.test.gof23.singleton;
import java.io.ObjectStreamException;
import java.io.Serializable;
public class Singleton05 implements Serializable{
private static Singleton06 instance;
private Singleton05(){//防止反射破解
if(instance!=null) {
throw new RuntimeException("非法创建实例");
}
}
public static synchronized Singleton05 getInstance(){
if(instance==null) {
instance=new Singleton05();
}
return instance;
}
//防止反序列化破解
private Object readResolve() throws ObjectStreamException{
return instance;
}
}
完
别看了,真没了,再多的我不懂了
以上是关于水一篇单例模式的主要内容,如果未能解决你的问题,请参考以下文章