里面包括了老生常谈的饿汉式,懒汉式以及枚举类 静态代码块 序列化场景下,多线程场景下反射情况下的问题。
话不多说,直接开干。
饿汉式就是立即加载的意思,立即加载在中文中有着急,急迫的意思。所以就叫饿汉式吧。
1.饿汉式的最简洁版本
package 单例模式的几种写法.饿汉式;
/**
* @Author:FuYouJie
* @Date Create in 2020/1/23 13:32
*/
public class Singleton1 {
/**饿汉式:直接创建实例 不管你是否需要
* 1.构造器私有化 外部不能直接new实例化
* 2.自行创建 并且用静态变量保存
* 3.声明为public 对外公开这个实例
* 4.final 修饰
* 构造器 私有化
*/
public static final Singleton1 instance=new Singleton1();
private Singleton1(){}
}
测试代码:
Singleton1 singleton1 = Singleton1.instance;
Singleton1 s=Singleton1.instance;
//true
System.out.println(singleton1==s);
这里先不贴图,结果是一样的哈。==在这里比较的是对象地址。
2.枚举类的简单写法
public enum SingletonByEnum {
INSTANCE;
public void doA(){
System.out.println("AA");
}
}
测试代码:
//枚举
SingletonByEnum instance1 = SingletonByEnum.INSTANCE;
SingletonByEnum instance2 = SingletonByEnum.INSTANCE;
//true
System.out.println(instance1.hashCode()==instance2.hashCode());
3.静态代码块的写法:
public class SingletonStatic {
public static final SingletonStatic INSTANCE;
private String name;
static {
Properties properties=new Properties();
try {
properties.load(SingletonStatic.class.getClassLoader().getResourceAsStream("single.properties"));
} catch (IOException e) {
throw new RuntimeException();
}
INSTANCE = new SingletonStatic(properties.getProperty("name"));
}
//构造器 私有化
private SingletonStatic(String name){
this.name=name;
}
public String getName() {
return name;
}
}
这里的静态代码块实现了可以从配置文件给属性复制的功能,避免了在代码里面把属性写死的情况。
小结:饿汉式就是空间换时间,类加载的方式是按需加载,且只加载一次。因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用。换句话说,在线程访问单例对象之前就已经创建好了。再加上,由于一个类在整个生命周期中只会被加载一次,因此该单例类只会创建一个实例,也就是说,线程每次都只能也必定只可以拿到这个唯一的对象。因此就说,饿汉式单例天生就是线程安全的。
可是常说懒人改变世界,那么就一定有懒汉式。改变世界没有我不知道,至少改变了我的字数。
懒汉式也说延迟加载,就是在调用get方法的时候才创建实例。
1.懒汉式简单版本:
//Unsafe
public class Singleton1 {
private static Singleton1 INSTANCE;
public static Singleton1 getInstance(){
if (INSTANCE==null){
try {
//模拟准备时间
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE=new Singleton1();
}
return INSTANCE;
}
private Singleton1(){}
}
单线程下,这个代码是没有问题的。
测试代码:
//单线程下
Singleton1 singleton1=Singleton1.getInstance();
Singleton1 singleton2=Singleton1.getInstance();
System.out.println(singleton1==singleton2);
运行结果:
可以看出来对象是一样的。
如果是多线程呢?
首先我们创建一个可以接收返回值的Callable
Callable<Singleton1> callable=new Callable<Singleton1>() {
@Override
public Singleton1 call() throws Exception {
return Singleton1.getInstance();
}
};
然后创建一个线程池(因为装了阿里妈妈插件,直接创建线程屏幕一片黄)。
ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(2,
2,1,
TimeUnit.MINUTES,new LinkedBlockingQueue<>(5),
new ThreadPoolExecutor.CallerRunsPolicy());
然后提交再用Future接收
Future<Singleton1> singleton1Future = threadPoolExecutor.submit(callable);
Future<Singleton1> singleton2Future = threadPoolExecutor.submit(callable);
最后取结果
try {
//false
System.out.println(singleton1Future.get().hashCode()==singleton2Future.get().hashCode());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
threadPoolExecutor.shutdown();
运行结果:
那么这是为啥呢?
在线程1进去的时候,此时对象是不存在的,遇到sleep(1000),线程1开始罚站1秒。
在线程1罚站的时候,线程2也进来了。他们一起在刚才的位置罚站,此时一秒钟还没过去,对象依然不存在。
然后线程1罚站结束,进入后面代码,new一个对象。不久后,线程2到了后面,此时并没有非空判断,所以线程2页创建了一个对象。
解决办法,加锁。这里就使用synchronized
懒汉式方法加锁版本:
public class SingletonWithSynMethod {
private static SingletonWithSynMethod INSTANCE;
public synchronized static SingletonWithSynMethod getInstance(){
if (INSTANCE==null){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE=new SingletonWithSynMethod();
}
return INSTANCE;
}
private SingletonWithSynMethod(){}
}
测试一下:
class TestSingletonWithSynMethod{
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(2,
2,1,
TimeUnit.MINUTES,new LinkedBlockingQueue<>(5),
new ThreadPoolExecutor.CallerRunsPolicy());
Callable<SingletonWithSynMethod>singleton1=new Callable<SingletonWithSynMethod>() {
@Override
public SingletonWithSynMethod call() throws Exception {
return SingletonWithSynMethod.getInstance();
}
};
Future<SingletonWithSynMethod> future1= threadPoolExecutor.submit(singleton1);
Future<SingletonWithSynMethod> future2= threadPoolExecutor.submit(singleton1);
try {
//true
System.out.println(future1.get()==future2.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
threadPoolExecutor.shutdown();
}
}
但是本着锁块不锁方法的意思,我们可以改变一下synchronized的位置。
public class SingletonWithSynBlock {
private static SingletonWithSynBlock INSTANCE;
public static SingletonWithSynBlock getInstance(){
if (INSTANCE==null){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (SingletonWithSynBlock.class){
INSTANCE=new SingletonWithSynBlock();
}
}
return INSTANCE;
}
private SingletonWithSynBlock(){}
}
你以为这就完事了?
我们运行一下。
运行代码:
class TestSingletonWithSynBlock{
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(2,
2,1,
TimeUnit.MINUTES,new LinkedBlockingQueue<>(5),
new ThreadPoolExecutor.CallerRunsPolicy());
Callable<SingletonWithSynBlock> singleton1=new Callable<SingletonWithSynBlock>() {
@Override
public SingletonWithSynBlock call() throws Exception {
return SingletonWithSynBlock.getInstance();
}
};
Future<SingletonWithSynBlock> future1= threadPoolExecutor.submit(singleton1);
Future<SingletonWithSynBlock> future2= threadPoolExecutor.submit(singleton1);
try {
//false
System.out.println(future1.get()==future2.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
threadPoolExecutor.shutdown();
}
}
因此我们引入DCL双检查机制
代码:
public class SingletonWithDCL {
private volatile static SingletonWithDCL INSTANCE;
public static SingletonWithDCL getInstance(){
try {
if (INSTANCE != null){
}else {
Thread.sleep(3000);
synchronized (SingletonWithDCL.class){
if (INSTANCE == null){
INSTANCE=new SingletonWithDCL();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return INSTANCE;
}
private SingletonWithDCL(){}
}
测试代码:
class TestSingletonWithDCL{
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(2,
2,1,
TimeUnit.MINUTES,new LinkedBlockingQueue<>(5),
new ThreadPoolExecutor.CallerRunsPolicy());
Callable<SingletonWithDCL> singleton1=new Callable<SingletonWithDCL>() {
@Override
public SingletonWithDCL call() throws Exception {
return SingletonWithDCL.getInstance();
}
};
Future<SingletonWithDCL> future1= threadPoolExecutor.submit(singleton1);
Future<SingletonWithDCL> future2= threadPoolExecutor.submit(singleton1);
try {
//true
System.out.println(future1.get()==future2.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
运行:
懒汉模式也是可以用内部类的
public class SingletonWithInner {
/**
* 内部类不会随着外部类的加载初始化
*/
private static class MyClassHandler{
private static SingletonWithInner INSTANCE=new SingletonWithInner();
}
public static SingletonWithInner getInstance(){
return MyClassHandler.INSTANCE;
}
private SingletonWithInner(){}
}
但是没有这样的便宜,内部类在序列化的时候会有问题。
重写内部类
public class SingletonAndSer implements Serializable {
private static final long serialVersionUID=888L;
//内部类
private static class MyClassHandler{
private static final SingletonAndSer INSTANCE=new SingletonAndSer();
}
private SingletonAndSer(){}
public static SingletonAndSer getInstance(){
return MyClassHandler.INSTANCE;
}
// protected Object readResolve() throws ObjectStreamException{
// System.out.println("调用了本方法!");
// return MyClassHandler.INSTANCE;
// }
}
测试类:
class SaveAndRead{
public static void main(String[] args) {
SingletonAndSer singletonAndSer=SingletonAndSer.getInstance();
try {
FileOutputStream fosRef=new FileOutputStream(new File("test.txt"));
ObjectOutputStream oosRef=new ObjectOutputStream(fosRef);
oosRef.writeObject(singletonAndSer);
oosRef.close();
fosRef.close();
System.out.println(singletonAndSer.hashCode());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
try {
FileInputStream fileInputStream=new FileInputStream(new File("test.txt"));
ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);
SingletonAndSer singletonAndSer1= (SingletonAndSer) objectInputStream.readObject();
objectInputStream.close();
fileInputStream.close();
System.out.println(singletonAndSer1.hashCode());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
运行:
纳尼??
如果在反序列化的时候没有指定readResolve 那么还是多例的。
解决办法就是把注释去掉。
添加readResolve方法。
protected Object readResolve() throws ObjectStreamException{
System.out.println("调用了本方法!");
return MyClassHandler.INSTANCE;
}
再运行:
好了,到这里就差不多结束啦。但是最开始的枚举类直接暴露,违反了“职责单一原则”,现在来完善完善。
以Connection为例。
/**
* @Author:FuYouJie
* @Date Create in 2020/1/23 16:40
*/
public class Connection {
String url;
String name;
public Connection(String url, String name) {
this.url = url;
this.name = name;
}
}
public class MyObject {
public enum MyEnumSinggleton{
//工厂
connectionFactory;
private Connection connection;
private MyEnumSinggleton(){
System.out.println("创建MyObject");
String url="jdbc:mysql://127.0.0.1:3306/bookdb?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false";
String username="root";
connection=new Connection(url,username);
}
public Connection getConnection(){
return connection;
}
}
public static Connection getConnection(){
return MyEnumSinggleton.connectionFactory.getConnection();
}
}
测试代码:
class TestMyObject{
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(5,
5,1,
TimeUnit.MINUTES,new LinkedBlockingQueue<>(5),
new ThreadPoolExecutor.CallerRunsPolicy());
Callable<Connection> singleton1=new Callable<Connection>() {
@Override
public Connection call() throws Exception {
return MyObject.getConnection();
}
};
for(int i=0;i<5;i++){
Future<Connection> future= threadPoolExecutor.submit(singleton1);
System.out.println(future.get());
}
threadPoolExecutor.shutdown();
}
}
运行:
到了这里小小的总结一下,一般使用DCL的情况比较多。但是你以为这样完事儿吗?
java创建对象的方式有反射,克隆,实例化,序列化,你看到这里还不算看完。我们回到饿汉式,因为我可以少写几句代码。。。。
public class SingletonWithClone implements Cloneable {
private static volatile SingletonWithClone INSTANCE;
//国际惯例,构造函数私有化 防止new关键字
private SingletonWithClone(){}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
public static SingletonWithClone getInstance(){
if (INSTANCE == null){
synchronized (SingletonWithClone.class){
if (INSTANCE == null){
INSTANCE=new SingletonWithClone();
}
}
}
return INSTANCE;
}
}
测试代码:
class TestClone {
public static void main(String[] args) throws CloneNotSupportedException {
SingletonWithClone singleton1=SingletonWithClone.getInstance();
SingletonWithClone singleton2= (SingletonWithClone) singleton1.clone();
System.out.println(singleton1.hashCode());
System.out.println(singleton2.hashCode());
}
}
运行结果:
因为Object.clone是浅克隆。在内存上再开一片空间复制原来的值,所有是两个不同的对象。
解决办法:
重写clone方法,返回原来的实例。
@Override
public Object clone() throws CloneNotSupportedException {
return getInstance();
}
再测试:
序列化已经说过了,添加readResolve方法,可以使反序列化自己产生的对象无效被垃圾回收,使用你提供的对象。
下面演示反射的破坏性。反射我也是才了解不久,防君子不防小人说的就是反射吧。
class TestReflect{
public static void main(String[] args) throws IllegalAccessException,
InstantiationException, NoSuchMethodException,
InvocationTargetException {
Class cls=SingletonWithClone.class;
Constructor<SingletonWithClone> cloneConstructor= cls.getDeclaredConstructor();
cloneConstructor.setAccessible(true);
SingletonWithClone singleton=cloneConstructor.newInstance();
SingletonWithClone singleton1=SingletonWithClone.getInstance();
System.out.println("hashcode="+singleton.hashCode());
System.out.println("hashcode="+singleton1.hashCode());
}
}
运行效果:
因为执行反射调用了无参构造函数。
说一种解决办法添加一个字段,判断是否是第一次加载。
构造方法改造如下:
private SingletonWithClone(){
if(isFirst){
synchronized (SingletonWithClone.class){
if (isFirst){
isFirst=false;
}
}
}
else {
throw new RuntimeException("重复创建!");
}
}
写到这里,,我也累了,你也累了。难道就没有一种简简单单的方法吗??、!!!!
有枚举类
绕了一圈又回来了。枚举是绝对单例的。那么 我在这么BB半天干嘛?因为枚举是jdk5出现的,我妈们要还原历史,尊重历史。。
简单说说为什么枚举这么优秀突出,我手冻僵了,不多说了。
因为反射创建对象是调用无参构造器(别紧张。你不声明其他构造函数就会默认实现的),而枚举没有构造器。然而事情并不简单,其实一旦一个类声明为枚举,实际上就是继承了Enum,啊啊,还是看看代码吧。
,然后我也不知道自己在说啥,其实这些都不是。让我们看看反射的源码。
看的懂吧?这英文我都看得懂,不能反射创建枚举对象。
刚才说到哪儿了?枚举防止反射,说一下枚举防止序列化和克隆吧。
枚举无法克隆,没有这样的方法。没有构造函数,会抛出异常。就算你在枚举里加了构造函数,也是一样的。对于反序列化 Java 仅仅是将枚举对象的 name 属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf 方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了 writeObject、readObject、readObjectNoData、writeReplace 和 readResolve(眼熟吗) 等方法。所以,枚举才是实现单例模式的最好方式!