java线程安全-单例模式-线程通信
首先看看单例模式的写法
首先我们先来回顾一下饿汉式单例模式:
class Singleton{private static Singleton singleton=new Singleton();private Singleton(){}public static Singleton getInstrance(){return singleton;}
}
public class Test{public static void main(String[] args){Singleton singleton=Singleton.getInstrance();}
}
饿汉式是在类加载的时候创建实例,故不存在线程安全问题。
虚拟机会保证一个类的<clinit>
()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>
()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>
()方法完毕。
不用我们手工去保证线程安全问题
public class Demo1 {public static void main(String[] args){for(int i=0;i<10;i++) {new Thread(new Runnable() {@Overridepublic void run() {System.out.println(Singleton.getInstrance());}}).start();}}}
class Singleton{private static Singleton singleton=new Singleton();private Singleton(){System.out.println("我是饿汉模式");}public static Singleton getInstrance(){return singleton;}
}
---------------------------------------------------------------------------------
我是饿汉模式,创建的10个对象是同一个对象
cn.xiong.demo.sychnoized.singleton.Singleton@23b1870f
cn.xiong.demo.sychnoized.singleton.Singleton@23b1870f
cn.xiong.demo.sychnoized.singleton.Singleton@23b1870f
cn.xiong.demo.sychnoized.singleton.Singleton@23b1870f
cn.xiong.demo.sychnoized.singleton.Singleton@23b1870f
cn.xiong.demo.sychnoized.singleton.Singleton@23b1870f
cn.xiong.demo.sychnoized.singleton.Singleton@23b1870f
cn.xiong.demo.sychnoized.singleton.Singleton@23b1870f
cn.xiong.demo.sychnoized.singleton.Singleton@23b1870f
cn.xiong.demo.sychnoized.singleton.Singleton@23b1870f
优点:
类加载时完成初始化,获取对象的速度较快.
缺点:
类加载较慢.
懒汉式单例模式可以这样写:
class Singleton{private static Singleton singleton = null;private Singleton(){}public static Singleton getInstrance(){if(singleton==null){singleton=new Singleton;}return singleton;}
}
-----------------------------------------------------------------------
cn.xiong.demo.sychnoized.singleton.SingleTon2@56f2a97e
cn.xiong.demo.sychnoized.singleton.SingleTon2@56f2a97e
cn.xiong.demo.sychnoized.singleton.SingleTon2@56f2a97e
cn.xiong.demo.sychnoized.singleton.SingleTon2@56f2a97e
cn.xiong.demo.sychnoized.singleton.SingleTon2@2e3a9cb1
cn.xiong.demo.sychnoized.singleton.SingleTon2@56f2a97e
cn.xiong.demo.sychnoized.singleton.SingleTon2@56f2a97e
cn.xiong.demo.sychnoized.singleton.SingleTon2@56f2a97e
cn.xiong.demo.sychnoized.singleton.SingleTon2@56f2a97e //下面有一个不同于上面的对象
cn.xiong.demo.sychnoized.singleton.SingleTon2@7b69b864
现在我们学习多线程相关知识,就很容易能够发现上述懒汉式单例模式为什么线程不安全了。假设开始线程0进入,判断singleton为空,在将要创建实例时,cpu切换,
线程1又进来了,同样singleton为空 创建了实例,这是cpu切换回来到0线程,继续创建实例
可见,经过分析共创建了 两个实例,还谈什么单例。
怎么才能使懒汉式单例模式线程安全呢?
1.synchronized,volatile双层锁处理
public class Demo2 {public static void main(String[] args) {for(int i=0;i<10;i++) {new Thread(new Runnable() {@Overridepublic void run() {System.out.println(SingleTon2.getInstance());}}).start();}}}class SingleTon2{private volatile static SingleTon2 singleton=null; //第二层锁,volatile关键字让内存是否可见有关private SingleTon2() {}public static SingleTon2 getInstance() {if(singleton == null) { //第一层检查,检查是否有引用指向对象,高并发情况下会有多个线程同时进入synchronized (SingleTon2.class) { //第一层锁,保证只有一个线程进入,其它线程等执行完毕if(singleton ==null) //第二层检查,那么第一个线程创建完对象释放锁后,volatile保证了singleton 可见,第二个线程就进不去了singleton = new SingleTon2();}}return singleton;}
}
2.静态内部类
public class StaticInnerClassChuli {public static void main(String[] args) {for(int i=0;i<10;i++) {new Thread(new Runnable() {@Overridepublic void run() {System.out.println(Singleton3.getInstance());}}).start();}}}class Singleton3{private Singleton3() {}//静态内部类private static class SingleTonHoler{private static Singleton3 singleton3 = new Singleton3();}public static Singleton3 getInstance() {return SingleTonHoler.singleton3;}}
静态内部类的优点是
:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。
只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
同上面说的饿汉模式创建静态对象一样,类加载时, 虚拟机会保证一个类的<clinit>
()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>
()方法完毕。
那么,是不是可以说静态内部类单例就是最完美的单例模式了呢?其实不然,静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,所以,我们创建单例时,可以在静态内部类与懒汉式模式里自己斟酌。
3.枚举类的形式
public class EnumSingleChuli {public static void main(String[] args) {for(int i=0;i<10;i++) {new Thread(new Runnable() {@Overridepublic void run() {System.out.println(Singleton4.INSTANCETON);System.out.println(Singleton4.getInstance());}}).start();}}}enum Singleton4{INSTANCETON;public static Singleton4 getInstance() {return Singleton4.INSTANCETON;}
}
枚举单例模式在《Effective Java》中推荐的单例模式之一。但本人还是喜欢在饿汉模式或静态内部类方法中选择来用
线程通信
在前面我们讲了很多关于同步的问题,然而在现实开发中,是需要线程之间的协作,也就是需要线程之间实现通信的。
先了解一下最经典的程序设计模式之一的生产者-消费者模型
日常生活中,每当我们缺少某些生活用品时,我们都会去超市进行购买,那么,你有没有想过,你是以什么身份去的超市呢?相信大部分人都会说自己是消费者,确实如此,那么既然我们是消费者,又是谁替我们生产各种各样的商品呢?当然是超市的各大供货商,自然而然地也就成了我们的生产者。
如此一来,生产者有了,消费者也有了,那么将二者联系起来的超市又该作何理解呢?
诚然,它本身是作为一座交易场所而诞生。
将上述场景类比到我们实际的软件开发过程中,经常会见到这样一幕:
代码的某个模块负责生产数据(供货商),
而生产出来的数据却不得不交给另一 模块(消费者) 来对其进行处理,
在两个程序模块之间我们必须要有一个类似上述超市的东西来存储数据(超市),这就抽象出了我们的 生产者/消费者模型
其中,产生数据的模块,就形象地称为生产者;
而处理数据的模块,就称为消费者;
生产者和消费者之间的中介就叫做缓冲区,一般就是一个队列。
在生产者-消费者模型中,当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对队列的占用权。因为生产者如果不释放对队列的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。
因此,一般情况下,当队列满时,会让生产者交出对队列的占用权,并进入挂起状态。然后等待消费者消费了商品,然后消费者通知生产者队列有空间了。
同样地,当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了。