1.单例模式
Singleton模式主要作用是保证在Java应用程序中,一个类只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为,比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。Singleton模式限制了实例的个数,有利于Java垃圾回收(garbage collection)。单例写法众多,值得注意的是要保证线程安全性以及单例对象数据可见性。
2.并发编程三个概念
1)原子性
即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行(事务回滚)。
2)可见性
指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
3)有序性
即程序执行的顺序按照代码的先后顺序执行。
指令重排序:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。
在介绍单例模式前,先介绍一下
3.happens-before 原则
Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
下面就来具体介绍下happens-before原则(先行发生原则):
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
这8条原则摘自《深入理解Java虚拟机》。
4.volatile关键字
被volatile修饰的变量:
1.保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2.禁止进行指令重排序,确保指令重排序时排到内存屏障(lock前缀指令)时不会乱序执行内存屏障前后指令,强制对缓存的修改操作立即写入主内存,写操作导致其他CPU对应的缓存行无效。
5.单例模式8种写法及优缺点
package com.example.designmodel;
public class Singleton {
private Singleton() {}
/**
* 饿汉式(静态常量) - 可用、可能内存浪费
* 优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
* 缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。
*/
private final static Singleton INSTANCE = new Singleton();
public static Singleton getInstance1(){
return INSTANCE;
}
/**
* 饿汉式(静态代码块) - 可用、可能内存浪费
* 优点:避免了线程同步问题。
* 缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。
*/
private static Singleton instance2;
static {
instance2 = new Singleton();
}
public static Singleton getInstance2() {
return instance2;
}
/**
* 懒汉式(线程不安全) - 不可用、多线程下不安全
* 这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,两个线程同时通过if判断,
* 这时便会产生多个实例。所以在多线程环境下不可使用这种方式。
*/
private static Singleton instance3;
public static Singleton getInstance3() {
if (instance3 == null) {
instance3 = new Singleton();
}
return instance3;
}
/**
* 懒汉式(线程安全,同步方法) - 不推荐用
* 为解决线程不安全问题,做个线程同步就可以了,于是就对getInstance()方法进行了线程同步。
* 优点:线程安全
* 缺点:效率低,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。
* 而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低要改进。
*/
private static Singleton instance4;
public static synchronized Singleton getInstance4() {
if (instance4 == null) {
instance4 = new Singleton();
}
return instance4;
}
/**
* 双重检查 Double Check Locking(线程安全,同步代码块) - 不可用
* 每个线程都有一个缓存区,存放从主内存读取的数据,数据修改后先修改缓存数据,再将缓存数据更新到主内存。
* 当线程1获取到锁时,会实例化Singleton,将线程1缓存的str从null更新为“str”,将a更新为1,
* 这个涉及到指令重排序,也可能先将a更新为1,str更新为“str”。
* 如果线程1在锁释放前将主内存中a更新为1,还未将主内存中str更新为“str”,这时线程2获取到Singleton的实例不为null,
* 将数据返回a=1,str=null,产生了错误数据,相当于幻读。
*/
private static Singleton instance5;
private String str = null;
private int a = 0;
public Singleton(int b){
str = "str";
a = b;
}
public static Singleton getInstance5() {
if (instance5 == null) {
synchronized (Singleton.class) {
instance5 = new Singleton(1);
}
}
return instance5;
}
/**
* 双重检查 Double Check Locking(volatile修饰实例,线程安全,同步代码块) - 推荐用
* volatile禁止了指令重排序,volatile修饰的变量禁用线程缓存,变量修改后直接修改主内存,保证了变量可见性。
*/
private static volatile Singleton instance6;
public static Singleton getInstance6() {
if (instance6 == null) {
synchronized (Singleton.class) {
if (instance6 == null) {
instance6 = new Singleton();
}
}
}
return instance6;
}
/**
* 静态内部类 - 推荐用
* 这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。
* 不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,
* 而静态内部类方式在Singleton类被装载时并不会立即实例化(static修饰的方法变量在加载类的过程中完成静态变量的内存分配),
* 而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
* 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
* 优点:避免了线程不安全,延迟加载,效率高。
*/
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance7() {
return SingletonInstance.INSTANCE;
}
/**
* 枚举类 - 推荐用
* 借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
* 可能是因为枚举在JDK1.5中才添加,所以在实际项目开发中,很少见人这么写过。
* 优点:系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。
* 缺点:当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new,可能会给其他开发人员造成困扰,特别是看不到源码的时候。
*/
public enum SingletonEnum {
INSTANCE;
public void getInstance8() {
}
}
}
4.单例模式优缺点
1)优点
利用单例模式可以保证系统某些数据的一致性,达到系统一致性,比如登录人数等。节省系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提供系统性能。
2)缺点
当想实例化一个单例类的时候,需要记住使用相应的获取对象方法,而不是使用new来获取。