容器的注册式单例
除了上文中提到的单例实现方式以外,还有一种通过容器注册的方式完成单例的实现。
代码样例如下:
public class ContainerSingleton {
private ContainerSingleton() {
}
//ConcurrentHashMap 保证容器put线程安全
private static final Map<String, Object> container = new ConcurrentHashMap<>();
public static Object getBean(String className) {
//double check 保证单例模式
synchronized (container) {
if (!container.containsKey(className)) {
Object o = null;
try {
o = Class.forName(className).newInstance();
container.put(className, o);
return o;
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
return container.get(className);
}
}
//测试代码
public class User {
private String userId;
private String userName;
}
public class TestContainerSingleton {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
User user1 = (User) ContainerSingleton.getBean("com.zixiu.singleton.User");
System.out.println(Thread.currentThread().getName() + ":" + user1);
});
Thread thread2 = new Thread(() -> {
User user2 = (User) ContainerSingleton.getBean("com.zixiu.singleton.User");
System.out.println(Thread.currentThread().getName() + ":" + user2);
});
thread1.start();
thread2.start();
System.out.println("Execute thread.");
}
}
这种方式比较巧妙的是通过Map的特性保证对象名称的唯一性,通过类似于double check的方式确保单例的安全。
线程内的单例模式
ThreadLocal:ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。
上文说到了容器注册式的单例实现,那么针对TheadLocal关键字来说同样可以实现单例模式。
public class ThreadLocalSingleton {
//私有化构造器
private ThreadLocalSingleton() {
}
//单例实现
private static final ThreadLocal<ThreadLocalSingleton> instance = ThreadLocal.withInitial(ThreadLocalSingleton::new);
//访问点
public static Object getInstance() {
return instance.get();
}
}
//测试代码
public class TestThreadLocalSingleton {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
Thread thread1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
});
Thread thread2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
});
thread1.start();
thread2.start();
System.out.println("Execute thread.");
}
}
测试结果如下:
可以看出针对主线程来说,创建的对象是不变的,同样在Thread0和Thread1中,在线程内的创建的对象也是不变的。
通过ThreadLocal
的源码可以解释这种情况的原因,在ThreadLocal
的实现中,是一个key
为线程对象,value
为自定义的对象的Map
结构。
...
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
...
ThreadLocal
会通过set
方法将声明的对象以及当前的线程信息存入到ThreadLocalMap
。ThreadLocal
这种实现方式也是容器注册的一种方式,不过容器是每个线程互不干扰的,因此创建的单例也是每个线程保证单例的。
使用场景:例如可以通过数据源的名称,完成多数据源动态的切换,完成数据源的路由。
单例模式的优缺点
优点
- 使用简单
- 减少内存的占用,仅提供一个对象,减少对资源的占用
- 设置全局的访问点,保证安全性
缺点
- 单例模式从某种程度上来说是不符合Java的开闭原则的,需要修改代码才能拓展单例模式的内容
- 没有对应的接口,拓展性不强