多线程下的单例模式详解
Posted 若曦`
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程下的单例模式详解相关的知识,希望对你有一定的参考价值。
1. 单例模式
(1) 单例模式简介
单例模式的作用
单例模式是为了确保一个类只有一个实例,而且能自行的实例化,并向整个整个系统提供这个实例
单例模式的特点
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
单例模式的使用场景
-
整个程序的运行中只允许有一个类的实例
-
需要频繁实例化然后销毁的对象
-
创建对象时耗时过多或者耗资源过多,但又经常用到的对象
一般是对于那些业务逻辑上限定不能存在多实例的情况
例如:序列号生成器(解决不能id自增的问题),计数器—统计网站访问人数等场景,单例线程池等,都需要使用一个系统唯一实例来进行记录,若多实例计数则会不准确
单例模式的优缺点
优点:只有一个实例,节约了内存资源,提高了系统性能
缺点
- 没有抽象层,不能扩展
- 职责过重,违背了单一性原则
(2) 实现方式
① 饿汉式
饿汉式是指在第一次加载类的时候,就实例化对象,也就是在构造函数中创建单例对象
/**
* @author ruoxi
*/
public class TestSingleTon {
public static void main(String[] args) {
SingleTon singleTon1 = SingleTon.getSingleTon();
SingleTon singleTon2 = SingleTon.getSingleTon();
System.out.println(singleTon1==singleTon2); //true
}
}
/**
* 饿汉式
*/
class SingleTon{
/**
* 注意需要使用 static和final修饰 并在这里直接实例化
*/
private static final SingleTon singleTon = new SingleTon();
/**
* 定义private私有构造器,表示只在类内部使用,亦指单例的实例只能在单例类内部创建
*/
private SingleTon(){}
/**
* 返回内部的singleTon实例
* @return
*/
public static SingleTon getSingleTon(){
return singleTon;
}
/* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
public Object readResolve() {
return instance;
}
}
② 懒汉式
懒汉式就是不在类加载时就创建类的单例,而是在第一次使用实例的时候再创建
/**
* @author ruoxi
*/
public class TestSingleTon {
public static void main(String[] args) {
SingleTon2 singleTon21 = SingleTon2.getSingleTon();
SingleTon2 singleTon22 = SingleTon2.getSingleTon();
System.out.println(singleTon21==singleTon22); //true
}
}
/**
* 懒汉式
*/
class SingleTon2{
/**
* 懒汉式不在此处实例化
*/
private static SingleTon2 singleTon=null;
private SingleTon2(){}
/**
* 如果singleTon为空则进行实例化
* @return
*/
public static SingleTon2 getSingleTon(){
if(singleTon==null) {
singleTon = new SingleTon2();
}
return singleTon;
}
/* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
public Object readResolve() {
return instance;
}
}
2. 多线程下的单例模式
对于饿汉式的实现方式,在多线程下也能保证单一实例
但是对于懒汉式来说,在一个线程获取实例的时候,可能会有另一个线程也在获取实例,导致产生两个及以上的实例对象出现
(1) Synchronized
/**
* synchronized实现多线程的单例模式
*/
class ThreadSingleTon{
private static ThreadSingleTon singleTon = null;
private ThreadSingleTon(){}
/**
* 使用synchronized防止多个线程同时调用这个方式去创建
* @return
*/
public static synchronized ThreadSingleTon getThreadSingleTon(){
//为空则创建对象
if(singleTon==null) {
singleTon = new ThreadSingleTon();
}
return singleTon;
}
}
使用synchronized修饰方法,可以实现多线程下的单例模式
但是每次调用该方法,都会给方法加锁,而只有第一次创建对象的时候需要加锁,其他时候都不需要,这样会导致程序的效率地下,那么可以使用下面的方式(双重检查锁)
(2) 双重检查锁
/**
* 双重检查锁
*/
class ThreadSingleTon2{
private static ThreadSingleTon2 singleTon = null;
private ThreadSingleTon2(){}
/**
* 双重检查锁 具体解释看代码注释
* @return
*/
public static ThreadSingleTon2 getThreadSingleTon(){
if(getThreadSingleTon()==null) {
//如果对象为空,则是第一次实例化,这时锁住对象
//给ThreadSingleTon.class加锁也可以
synchronized (singleTon){
//第二次判断是否为空,防止多线程操作时,在执行第一次判断后另一个线程完成了实例化
if(singleTon==null){
singleTon = new ThreadSingleTon2();
}
}
}
return singleTon;
}
}
使用上述方式,但还是会出现意外情况,这和java的线程工作内存有关,我们用下图流程来说明
也就是当线程1实例化后,还未放入主内存的间隙中,线程2拿到锁开始执行,又创建一个实例化对象
这时可以使用valotile使主内存中的对象对线程可见,来解决上述问题
(3) 双重检查锁+Volatile
这时就是完美的多线程下的单例模式了,解决了所有可能出现的问题
/**
* 双重检查锁+volatile
*/
class ThreadSingleTon2{
/**
* 使用volatile使主内存中的singleTon对线程可见
*/
private volatile static ThreadSingleTon2 singleTon = null;
private ThreadSingleTon2(){}
/**
* 双重检查锁 具体解释看代码注释
* @return
*/
public static ThreadSingleTon2 getThreadSingleTon(){
if(getThreadSingleTon()==null) {
//如果对象为空,则是第一次实例化,这时锁住对象
//给ThreadSingleTon.class加锁也可以
synchronized (singleTon){
//第二次判断是否为空,防止多线程操作时,在执行第一次判断后另一个线程完成了实例化
if(singleTon==null){
singleTon = new ThreadSingleTon2();
}
}
}
return singleTon;
}
/* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
public Object readResolve() {
return getInstance();
}
}
以上是关于多线程下的单例模式详解的主要内容,如果未能解决你的问题,请参考以下文章