设计模式之——单例模式
一、什么是单例模式
单例模式:即某个类在程序运行过程中只被实例化一次,也就是说该类在程序的生存周期里只有一个实例对象。
二、使用单例模式好处
由于这个类只实例化一次,不管多少个类中用到了这个类,也都只有一个该类的对象。因此,减少了类实例对象的创建-->减小了GC压力-->提升了程序的性能。
三、单例模式的几种常见写法
1、饿汉式
/**
- 饿汉式(线程安全)。类加载时就创建唯一的单例实例,不管后面用不用都创建了再说
- 空间换时间的思想
*优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
*缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。
*/
public class Singleton {
//该有什么属性和方法就有什么
+final是为了防止通过反射来创建对象。
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
2、饿汉式(静态代码块)
/**
- 饿汉变种模式,使用静态代码块。包括上面的那种饿汉式写法也都是线程安全的
- 因为这两种方法实际上间接地使用了synchronized关键字,具体怎么用到的呢?这就要去了解类加载的机制和过程了
*与上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,就是在类加载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的。
*/
public class Singleton{
private static Singleton instance = null;
static{
instance = new Singleton();
}
private Singleton(){}
public static Singleton getInstance(){
return this.instance;
}
}
3、懒汉式(线程不安全) 【不可用】
这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会多次创建实例。所以在多线程环境下不可使用这种方式。
/**
-
懒汉式(非线程安全,可以在创建函数前加synchronized关键字变为线程安全)
-
单例实例在使用时才创建
*/
public class Singleton{
private static Singleton instance = null;private Singleton(){
}
public static Singleton getInstance(){ //方法前加synchronized关键字变为线程安全,但是会增加创建的时间消耗
if (instance==null){
instance = new Singleton();
}
return instance;
}
}
4、懒汉式(线程安全,同步方法)【不推荐用】
解决上面第三种实现方式的线程不安全问题,做个线程同步就可以了,于是就对getInstance()方法进行了线程同步。
缺点:效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低要改进。
/**
-
描述: 懒汉式(线程安全)(不推荐)
*/
public class Singleton4 {
//这里就不能加final final要么在定义时初始化,要么在构造方法中初始化
private static Singleton4 instance;private Singleton4() {
}
public synchronized static Singleton4 getInstance() {
if (instance == null) {
instance = new Singleton4();
}
return instance;
}
}
5.懒汉式(线程安全,同步代码块)【不可用】
由于第四种实现方式同步效率太低,所以摒弃同步方法,改为同步产生实例化的的代码块。但是这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。
/**
-
描述: 懒汉式(线程不安全)(不推荐)
*/
public class Singleton5 {private static Singleton5 instance;
private Singleton5() {
}
public static Singleton5 getInstance() {
if (instance == null) {
synchronized (Singleton5.class) {
instance = new Singleton5();
}
}
return instance;
}
}
6、双重检查【推荐用】
Double-Check概念对于多线程开发者来说不会陌生,如代码中所示,我们进行了两次if (singleton == null)检查,这样就可以保证线程安全了。这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象。这里的synchronized不能采用synchronized(this),因为getInstance是一个静态方法,在它内部不能使用未静态的或者未实例的类对象。
6.1 为什么要double-check
6.1.1 需要第二重的原因
考虑这样一种情况,就是有两个线程同时到达,即同时调用getInstance()方法,此时由于instance == null,所以很明显,两个线程都可以通过第一重的singleton== null,进入第一重if语句后,由于存在锁机制,所以会有一个线程进入lock语句并进入第二重 singleton == null,而另外的一个线程则会在lock语句的外面等待。
而当第一个线程执行完new Singleton()语句后,便会退出锁定区域,此时,第二个线程便可以进入lock语句块,此时,如果没有第二重singleton == null的话,那么第二个线程还是可以调new Singleton()语句,这样第二个线程也会创建一个Singleton实例,这样也还是违背了单例模式的初衷的,所以这里必须要使用双重检查锁定。
6.1.2 需要第一重的原因
细心的朋友一定会发现,如果去掉第一重singleton == null,程序还是可以在多线程下安全运行的。
考虑在没有第一重 singleton == null 的情况:当有两个线程同时到达,此时,由于 lock 机制的存在,假设第一个线程会进入lock语句块,并且可以顺利执行new Singleton(),当第一个线程退出lock语句块时,singleton这个静态变量已不为 null 了,所以当第二个线程进入lock时,会被第二重singleton == null挡在外面,而无法执行new Singleton(),以在没有第一重singleton == null的情况下,也是可以实现单例模式的。
那么为什么需要第一重singleton == null呢?
这里就涉及一个性能问题了,因为对于单例模式的话,new Singleton()只需要执行一次就 OK 了, 而如果没有第一重singleton == null的话,每一次有线程进入getInstance()时,均会执行锁定操作来实现线程同步,这是非常耗费性能的,而如果我加上第一重singleton == null的话,那么就只有在第一次执行锁定以实现线程同步,而以后的话,便只要直接返回 Singleton 实例就OK了,而根本无需再进入lock语句块了,这样就可以解决由线程同步带来的性能问题了。
6.1.3 为什么要用volatile
主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话做了下面 3 件事情:
1.给instance分配内存
2.调用Singleton的构造函数来初始化成员变量
3.将instance对象指向分配的内存空间(执行完这步instance就为非null了)
但是在JVM的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,已经线程一被调度器暂停,此时线程二刚刚进来第一重检查,看到的 instance 已经是非 null 了(但却没有初始化,里面的值可能是null/false/0,总之不是构造函数中指定的值),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错或者是看到了非预期的值(因为此时属性的值是默认值而不是所需要的值)。
不过,如果线程一已经从把synchronized 同步块的代码都执行完了,那么此时instance一定是正确构造后的实例了,这是由synchronized的heppens-before保证的。
优点:线程安全;延迟加载;效率较高。
/**
-
懒汉方式(线程安全双重检查锁版本)
*/
public class Singleton{
private volatile static Singleton singleton;
private Singleton(){}public static Singleton getSingleton() {
if (singletonnull){ //第一重检查
synchronized (Singleton.class){
if (singletonnull){ //第二重检查
singleton = new Singleton();
}
}
}
return singleton;
}
7.静态内部类【推荐用】
/**
-
描述: 静态内部类方式,可用
*/
public class Singleton7 {private Singleton7() {
}private static class SingletonInstance {
private static final Singleton7 INSTANCE = new Singleton7();}
public static Singleton7 getInstance() {
return SingletonInstance.INSTANCE;
}
}
这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
优点:避免了线程不安全,延迟加载,效率高。
8、枚举【推荐用】
借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,还是懒加载,而且还能防止反序列化重新创建新的对象。
/**
- 枚举实现线程安全的单例模式
- 其底层是依赖Enum类实现的,而枚举类的成员变量其实都是静态类型的,并且是在
- 静态代码块中实例化的,有点像饿汉模式,也是天然线程安全的
*/
public Enum Singleton{
INSTANCE;
public void getInstance{
}
}
/**
- 使用ThreadLocal实现线程安全的单例
- 也是空间换时间的方式(因为ThreadLocal会为每一个线程提供一个独立的副本)
- 它是多个线程对数据的访问相互独立
*/
public class Singleton{
private static final TheadLocalinstance= new ThreadLocal (){
@Override
protected Singleton initialValue(){
return new Singleton();
}
};
public static Singleton getInstance(){
return instance.get();
}
private Singleton(){}
}
三、 单例模式在Redis工具类中的使用
import org.apache.commons.beanutils.BeanUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Tuple;
/**
- Redis连接池配置及使用方法
*/
public class Redis {
private static final Logger logger = LoggerFactory.getLogger(Redis.class);
private static ReentrantLock lock = new ReentrantLock();
private static Redis instance;
private JedisPool pool = null;
public Redis(){
}
//线程安全的单例模式
public static Redis getInstance(){
if (instancenull){
lock.lock();
if (instancenull){
instance = new Redis();
}
lock.unlock();
}
return instance;
}
public void initialRedisPool() {
//Redis服务器IP
String ADDR = "localhost";
//Redis的端口号
int PORT = 6379;
//可用连接实例的最大数目,默认值为8;
//如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted,再获取jedis就会报错了。
//这里我们设置2000就足够了
int MAX_ACTIVE = 2000;
//一个pool最多有多少个状态为idle(空闲的)的jedis实例,默认值也是8。
int MAX_IDLE = 200;
//等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接抛出JedisConnectionException;
int MAX_WAIT = 10000;
//在borrow一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的;
boolean TEST_ON_BORROW = true;
/**
* 初始化Redis连接池
*/
try {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(MAX_ACTIVE);
config.setMaxIdle(MAX_IDLE);
config.setMaxWaitMillis(MAX_WAIT);
config.setTestOnBorrow(TEST_ON_BORROW);
pool = new JedisPool(config, ADDR, PORT);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
- 获取Jedis对象
- @return Jedis
*/
public synchronized Jedis getJedis() {
Jedis jedis = null;
if (pool == null){
initialRedisPool();
}
jedis = pool.getResource();
return jedis;
}
//下面就是一些其他的对应于redis命令的工具方法了
//比如set(...),get(...),lpush(...),hset(...)等
使用起来就很简单了,比如:
String value = Redis.getInstance().get(String key)
//或者是
Redis redisObj = Redis.getInstance()
String value = redisObj.get(String key)
四、单例模式在线程池创建中的使用
项目中碰到一个这样的场景:
1)某个接口的并发请求较大;
2)对收到的数据要进行复杂的验证及数据库相关操作;
3)响应速度不能太慢,至少得2秒内吧;
于是正好可以拿线程池来练练手,下面分享一下我的练手代码(项目实战中根据需求稍作修改即可应用):
1 任务类(这是一个实现Callable的线程任务,因为我需要返回结果)
package service;
import java.util.concurrent.Callable;
/**
-
任务类
*/
public class MyTask implements Callable {
//假设我们需要处理传入进来的数据
private final String data;
public MyTask(final String data){
this.data = data;
}@Override
public Object call() throws Exception {
System.out.println("==============正在处理收到的data:" + data);
Thread.sleep(1000); //模拟处理数据需要花点小时间
return "处理成功";
}
}
2 处理任务的线程工具类
package service;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
public class TaskUtil {
private static ThreadPoolExecutor poolExecutor = ThreadPoolConfig.getInstance();
public static String submit(Callable callable) throws ExecutionException, InterruptedException {
String result = "";
//使用submit()方法提交任务,execute()方法不接受Callable线程任务
Future
//获取结果
result = future.get();
return result;
}
3 线程池创建类
package service;
public class ThreadPoolConfig {
//核心线程数
private static final int corePoolSize = 32;
//最大线程数
private static final int maxPoolSize = 48;
//线程最大空闲时间
private static final int keepAlive = 30;
//线程池缓冲队列
private static final BlockingQueue poolQueue = new LinkedBlockingQueue(64);
private static ThreadPoolExecutor poolExecutor;
private ThreadPoolConfig(){
}
/**
* 单例模式获取
* @return
*/
public static ThreadPoolExecutor getInstance(){
if (poolExecutor == null){
//使用synchronized保证多线程情况下也是单例的
synchronized (ThreadPoolConfig.class){
if (poolExecutor == null){
poolExecutor = new ThreadPoolExecutor(corePoolSize,maxPoolSize,keepAlive,TimeUnit.SECONDS,poolQueue,new
ThreadPoolExecutor.DiscardOldestPolicy());
}
}
}
return poolExecutor;
}
}
//这里给使用ThreadPoolExecutor创建线程池的重要参数解释(来自源码的一部分)
//百度上搜一下也一大把的解释,但是也基本都是来自于源码(建议狠一点,多看源码中的注释和代码)
/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
* @param keepAliveTime when the number of threads is greater than
* the core, this is the maximum time that excess idle threads
* will wait for new tasks before terminating.
* @param unit the time unit for the {@code keepAliveTime} argument
* @param workQueue the queue to use for holding tasks before they are
* executed. This queue will hold only the {@code Runnable}
* tasks submitted by the {@code execute} method.
* @param threadFactory the factory to use when the executor
* creates a new thread
* @param handler the handler to use when execution is blocked
* because the thread bounds and queue capacities are reached
* @throws IllegalArgumentException if one of the following holds:
* {@code corePoolSize < 0}
* {@code keepAliveTime < 0}
* {@code maximumPoolSize <= 0}
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue}
* or {@code threadFactory} or {@code handler} is null
/
/public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}*/
}
4 测试类
package service;
import java.util.concurrent.ExecutionException;
public class TestThreadPool {
public static void main(String[] args) throws ExecutionException, InterruptedException {
for (int i = 0; i < 50; i++) {
System.out.println("-------收到请求任务" + i+"--------");
//模拟从请求中拿到的数据
String requestData = "this is request data to deal with"+i;
//将数据处理任务丢给线程池异步处理
String re = TaskUtil.submit(new MyTask(requestData));
//打印返回的结果(实际项目中将结果封装一下返回给前端就行了)
System.out.println("返回结果="+re);
}
}
}
浙公网安备 33010602011771号