所谓“懒汉式”与“饿汉式”的区别,是在与建立单例对象的时间的不同。
- “懒汉式”是在你真正用到的时候才去建这个单例对象
- “饿汉式是在类创建的同时就已经创建好一个静态的对象,不管你用的用不上,一开始就建立这个单例对象
代码实现:
- 懒汉模式:
public class Singleton2 {private volatile static Singleton2 singleton; // 5private Singleton2() {System.out.println("懒汉式单例初始化!");}public static Singleton2 getInstance () {if(singleton ==null) { // 1synchronized(Singleton2.class) { // 2if(singleton == null) { // 3singleton = new Singleton2(); //4}}}return singleton;}}
代码1 处的判空是为了减少同步方法的使用,提高效率
代码2,3 处的加锁和判空是为了防止多线程下重复实例化单例。
代码5 处的volatile是为了防止多线程下代码4 的指令重排序
Volatile关键字的作用: 禁止进行指令的重排序
模拟没有volatile出错场景:
以第一段代码为例,我们可以模拟一下两条线程运行的情况:
- 线程A 访问了getInstance 方法,发现两次 singleton== null 检查都是 true,于是线程A 开始初始化 Singleton2 此处时间点设为 x
- 在时间点 x 线程B 访问了 getInstance 方法,此时它发现 singleton已经是非 null 了,于是高高兴兴地返回了 Singleton2 实例,并且外部调用开始使用 Singleton2 的实例,此处时间点设为 y
问题在于:时间点 y 的 singletonInstance 实例真的初始化好了吗?
由于第一段代码里忽略了 SingletonInstance 类的其他实例域,我们可以假设充实一下SingletonInstance 类
public class SingletonPattern {private int num;private boolean really;private SingletonPattern(){this.num = 2020;this.really = true;}private static SingletonPattern singletonInstance;public static SingletonPattern getSingletonInstance() {if (singletonInstance == null) {synchronized (SingletonPattern.class) {if (singletonInstance == null) {singletonInstance = new SingletonPattern();}}}return singletonInstance;}
}
在JVM新建一个对象实例时,首先会赋予这个实例的所有域初始值(比如 int 的初始值是 0, boolean的初始值是 false),然后再调用构造器方法。也就是说,有那么一个时刻(比如就是时间点 y )新建的SingletonPattern实例的 num 值为 0, really 值为 false。
此外,JVM 的指令重排可能导致了这个对象实例先被赋值给了变量,然后才开始初始化,所以时间点 y 变量已经不为 null,而实例尚未初始化结束。
正在 时间点 y,线程B已经开始使用了SingletonPattern实例,这就是一个 bug 了,因为SingletonPattern实例根本还没初始化结束!
类似的,第二段懒汉式代码一样有这个问题。
既然上面那两段代码有问题,那如何修正呢?
很简单,加个关键字就好了 —— volatile
静态域版本
public class SingletonPattern {// ... 忽略其他域定义和方法private static volatile SingletonPattern singletonInstance;public static SingletonPattern getSingletonInstance() {if (singletonInstance == null) {synchronized (SingletonPattern.class) {if (singletonInstance == null) {singletonInstance = new SingletonPattern();}}}return singletonInstance;}
}
实例域版本
public class MyClass {private volatile MyField field;public MyField initField() {if (field == null) {synchronized (MyClass.class) {if (field == null) {field = new MyField();}}}return field;}
}
为什么 volatile 可以解决以上问题?
依然以静态域版本为例,当 volatile 修饰了singletonInstance变量之后,就会禁止JVM对这个变量相关操作做指令重排 —— 即在调用 new SingletonPattern() 初始化SingletonPattern实例时(写操作),JVM 会保证SingletonPattern实例完全初始化结束后才允许其他线程访问(读操作)这个实例。
有更好的实现吗?
对于静态域的延迟初始化, lazy initialization holder class 模式不仅性能更好,代码可读性也更高
public class SingletonPattern {// ... 忽略其他域定义和方法private static class SingletonPatternHolder{static final SingletonPattern instance = new SingletonPattern();}public static SingletonPattern getSingletonInstance() {return SingletonPatternHolder.instance;}
}
不算括号,4行代码就搞定
当getSingletonInstance()被调用时,SingletonPatternHolder类才第一次被JVM加载,且JVM会以线程安全的方式初始化好SingletonPatternHolder类的静态域——也就是 SingletonPatternHolder.instance
所以 getSingletonInstance() 是线程安全的。
而实例域的延迟初始化的这种双重检查模式,除了加 volatile 关键字也可以尝试把这个域改造成不可变的对象—— 原理是读和写一个不可变对象都是原子性的,也就不可能读到一个初始化未完成的不可变对象。
-
饿汉式
public class Singleton1 {private final static Singleton1 singleton = new Singleton();private Singleton1() {System.out.println("饿汉式单例初始化!");}public static Singleton1 getSingleton () {return singleton;}
}
在类静态变量里直接new一个单例
懒汉模式构建ESRestAPIUtils
import lombok.extern.slf4j.Slf4j;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicHeader;
import org.apache.http.util.EntityUtils;
import org.elasticsearch.client.*;import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class ESRestAPIUtils {/*** 声明一个对象的引用*/private static volatile RestClient restClient;private static volatile RequestOptions COMMON_OPTIONS;/*** 私有构造方法*/private ESRestAPIUtils(){}/*** @DESC: 初始化ES客户端*/private static RestClient initClient() {RestClientBuilder builder = RestClient.builder(assembleESHost().toArray(new HttpHost[assembleESHost().size()]));Header[] header = new Header[]{new BasicHeader("header", "value")};builder.setDefaultHeaders(header);return builder.build();}/*** @DESC: 初始化RequestOptions*/private static RequestOptions initRequestOptions(){RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();builder.setHttpAsyncResponseConsumerFactory(new HttpAsyncResponseConsumerFactory.HeapBufferedResponseConsumerFactory(800 * 1024 * 1024));return builder.build();}/**判断一下如果对象为null,在创建一个对象* 多线程的中使用的时候,线程优化,加一个锁* @DESC: 获取RequestOptions 的COMMON_OPTIONS对象,懒汉模式*/public static RequestOptions getRequestOptions() {if (null == COMMON_OPTIONS) {synchronized (ESRestAPIUtils.class) {if (null == COMMON_OPTIONS) {try {COMMON_OPTIONS = initRequestOptions();//esClusterClient.settings();} catch (Exception e) {log.error("RequestOptions创建失败...." + COMMON_OPTIONS, e);}}}}return COMMON_OPTIONS;}/**判断一下如果对象为null,在创建一个对象* 多线程的中使用的时候,线程优化,加一个锁* @DESC: 获取RestClientBuilder,懒汉模式*/public static RestClient getRestClient() {if (null == restClient) {synchronized (ESRestAPIUtils.class) {if (null == restClient) {try {restClient = initClient();//esClusterClient.settings();} catch (Exception e) {log.error("ESClient创建失败...." + restClient, e);}}}}return restClient;}
/*** @return : 返回请求后的json字符串* @DESC: 可接受所有对于ES的查询操作,包括数据写入、删除、一般的条件查询和聚合查询*/public static String query(String queryJson, String method, String endpoint) {String resultJson = "";
// RestClient restClient = restClient.build();// .setMaxRetryTimeoutMillis(10 * 60 * 1000)/**设置查询超时时长,默认30秒*/.build();HttpEntity entity = new StringEntity(queryJson, ContentType.APPLICATION_JSON);//说明提供的是一个json格式的queryResponse response;try {Request request = new Request(method, endpoint);request.setEntity(entity);request.addParameter("pretty", "true");RequestOptions options = ESRestAPIUtils.getRequestOptions();request.setOptions(options);RestClient client = ESRestAPIUtils.getRestClient();response = client.performRequest(request);resultJson = EntityUtils.toString(response.getEntity(), "UTF-8");
// try { //为什么要去掉关闭restclient:懒汉的目的,就是让服务始终保持有一个RestClient,如果我们关闭后,新启的查询又需要去重新new一个对象,这样浪费时间。懒汉的初衷就是空间换时间。
// client.close();//关闭后,对象并不为空
// client =null;//设置对象为空
// } catch (IOException e) {
// log.error("Rest客户端关闭失败...", e);
// }return resultJson;} catch (IOException e) {log.error("查询请求失败..." + queryJson, e);return resultJson;}}/*** 通过ConsulUtils获取ES的节点配置,并封装为HttpHost* @DESC: 组装ES的hosts为HttpHost*/private static List<HttpHost> assembleESHost() {ArrayList<HttpHost> esHostList = new ArrayList<>();String[] esAddrArray = ConfigUtils.getSingleConf("es/cluster.nodes").split(",");for (String es : esAddrArray)esHostList.add(new HttpHost(es, Integer.valueOf(ConfigUtils.getSingleConf("es/cluster.http.port"))));return esHostList;}
}