Singleton的六种写法
饿汉模式
优点:在类加载时就初始化实例,避免了多线程的同步问题;
缺点:在类加载的同时就创建了单例对象(instance 变量为static修饰),如果长时间不使用此单例对象就会造成资源浪费,并且造成类加载过程变慢。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){
}
public static Singleton getInstance() {
return instance;
}
}
懒汉模式
优点:需要用的时候才初始化,避免了无效初始化
缺点:多线程同步问题(多个线程执行到(1)出时遇到线程切换,造成创建多个实例)
public class Singleton {
private static Singleton instance;
private Singleton (){
}
public static Singleton getInstance() {
if (instance == null) { //(1)
instance = new Singleton();
}
return SingletonObject2.instance;
}
}
懒汉锁模式
优点:不会存在多线程同步问题
缺点:每次获得实例都要加锁,效率低效,而且针对已经创建的实例仍然要串行获取(相当于多线程串行化运行,影响性能)
public class Singleton {
private static Singleton instance;
private Singleton (){
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
DoubleCheck模式
要注意instance必须声明为volaitle(synchronized保证了原子性、可见性和有序性,所以指令重排序的问题只出现在这里)
volaitle保证内存的可见性(多个线程得到的数据是同一版本)和有序性(禁止重排序),如果不加volaitle可能会导致Singleton的构造函数中的代码没有执行完成就被返回了( return instance; )从而造成空指针问题,而加上volaitle会保证对instance的初始化完成之后再进行返回。
优点:只有初始化实例对象才会串行,对于单纯读操作可以并行处理,创建实例的过程中只会加一次锁,当下一次获得实例时第一个if判断不为空,就会直接返回实例。
缺点:无
public class Singleton {
private volatile static Singleton instance;
private Singleton (){
}
public static Singleton getInstance() {
if (instance== null) {
synchronized (Singleton.class) {
if (instance== null) {
instance= new Singleton();
}
}
}
return instance;
}
}
静态内部类模式
优点:需要用的时候才初始化,避免了无效初始化,没有多线程同步问题,推荐做法
当调用getInstance()方法时才会对静态内部类进行加载,并且保证被初始化一次(当SingletonHolder类变量被调用时)
缺点:无
public class Singleton {
private Singleton(){
}
public static Singleton getInstance(){
return SingletonHolder.sInstance;
}
private static class SingletonHolder {
private static final Singleton sInstance = new Singleton();
}
}
枚举单例
优点:线程安全
缺点:可读性差,很少用
class Resource{
public static Resource getInstance(){
return SomeThing.INSTANCE.getInstance();
}
}
public enum SomeThing {
INSTANCE;
private Resource instance;
SomeThing() {
instance = new Resource();
}
public Resource getInstance() {
return instance;
}
}
Volatile 关键字
JAVA内存模型
- Java内存模型中规定所有变量都存储在主内存所有线程都可以访问。
- 线程对变量的操作(读取赋值等)必须在工作内存中进行,因此需要拷贝变量到自己的工作内存,操作结束后再写回主内存。
- 工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。
缓存不一致性
i=i+1;//执行那个i+1时的操作步骤如下
//当两个线程同时执行时,就会导致i只加一次,因为线程只能看到自己的工作空间
main memory->i
i->cache//将i加载到缓存中
i+1->cache//执行完i+1后保存到缓存
cache->main meory//缓存将加1后的i刷新到住内存
cpu解决方案:
- 给数据总线加锁,将数据修改完成后其他线程才可以得到给数据
- 利用高速缓存一致性协议,当有cpu对主存中的数据修改后会通知其他cpu中的缓存对该变量的保存是无效的。
JAVA解决方案 - 保证可见性:线程之间能够感知变量的修改
- 保证原子性:保证修改前感知变量是否修改
只有在以下条件下,才能保证一个线程对字段的更改对其他线程可见:
写入线程释放同步锁,读取线程随后获取相同的同步锁。释放锁的时候会强制从线程使用的工作内存中刷新所有写入,并且在获取锁的时候会强制重新加载可访问字段的值。
如果一个字段被声明为volatile,则写入线程会立即将修改后的值同步到主内存。读取线程必须在每次访问时重新加载volatile字段的值。
线程第一次访问一个对象的某个字段时,它会看到字段的初始值或来自某个其他线程写入的值。
当一个线程终止时,所有写入的变量都被刷新到主内存。例如:现有线程A,B,在B线程中调用A.join(),那么在B中可以保证看到A线程产生的影响。
System.out.println()是同步方法,也就是前面有说到synchronize保证了原子性,有序性和可见性,锁的获取
和释放都会刷新主内存值,所以调用了sout的时候不会出现缓存不一致性的原因。
并发编程3个重要概念
可见性
是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到。在1.1所举的例子就存在可见性的问题。
在Java中volatile、synchronized和final实现可见性。
对于使用volatile修饰的变量被修改后会立即被刷新到主存,并且告知其他缓存对该变量的缓存是无效的。
原子性
即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
对基本数据类型的变量的读取和赋值都是原子性的,这些操作不可被中断。
再比如a++,这个操作实际是a=a+1,是可分割的,所以它不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。
在Java中synchronized和在lock、unlock中操作或者原子操作类来保证原子性。
有序性
即程序执行的顺序按照代码的先后顺序执行。
在没有依赖关系的两个指令可能会发生指令重排序,指令重排序指的是在保证程序最终执行结果和代码顺序执行的结果一致的前提下,改变语句执行的顺序来优化输入代码,提高程序运行效率。但是这一前提条件在多线程的情况下就有可能出现问题.
Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile是因为其 本身包含禁止指令重排序 的语义,synchronized是由 一个变量在同一个时刻只允许一条线程对其进行 lock 操作 这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。