1 概述
本章的知识点非常重要。在单例模式与多线程技术相结合的过程中,我们能发现很多以前从未考虑过的问题。这些不良的程序设计如果应用在商业项目中将会带来非常大的麻烦。本章的案例也充分说明,线程与某些技术相结合中,我们要考虑的事情会更多。在学习本章的过程中,我们只需要考虑一件事情,那就是:如果使单例模式与多线程结合时是安全、正确的。
2 单例模式与多线程
在标准的23个设计模式中,单例模式在应用中是比较常见的。但多数常规的该模式教学资料并没有结合多线程技术进行介绍,这就造成在使用结合多线程的单例模式时会出现一些意外。
3 立即加载/饿汉模式
立即加载指的是,使用类的时候已经将对象创建完毕。常见的实现办法就是new实例化,也被称为“饿汉模式”。
public class MyObject {
//立即加载方法 == 饿汉模式
private static MyObject object = new MyObject();
private MyObject(){
}
public static MyObject getInstance(){
return object;
}
}
public class MyThread extends Thread{
@Override
public void run(){
System.out.println(MyObject.getInstance().hashCode());
}
}
public class Run1 {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
控制台打印的hashcode是同一个值,说明对象是一个,也就实现了立即加载型单例模式。此代码为立即加载模式,缺点是不能有其他实例变量,因为getInstance()方法没有同步,所以有可能出现非线程安全问题。
4 延迟加载/懒汉模式
延迟加载就是调用get()方法时,实例才被创建。常见的实现办法就是在get()方法中进行new实例化,也被称为“懒汉模式”。
4.1 延迟加载解析
先看下面一段代码。
public class MyObject {
private static MyObject object;
public MyObject() {
}
public static MyObject getInstance(){
if(object == null){
object = new MyObject();
}
return object;
}
}
public class MyThread extends Thread{
@Override
public void run(){
System.out.println(MyObject.getInstance().hashCode());
}
}
public class Run1 {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
4.2 延迟加载的缺点
前面两个实验虽然使用“立即加载”和“延迟加载”实现了单例模式,但在多线程环境中,“延迟加载”示例中的代码完全是错误的,根本不能保持单例的状态。下面来看如何在多线程环境中结合错误的单例模式创建出多个实例的。
public class MyObject {
private static MyObject object;
public MyObject() {
}
public static MyObject getInstance(){
try {
if(object == null){
//模拟在创建对象之前做一些准备工作
Thread.sleep(3000);
object = new MyObject();
}
}catch (InterruptedException e){
e.printStackTrace();
}
return object;
}
}
public class MyThread extends Thread{
@Override
public void run(){
System.out.println(MyObject.getInstance().hashCode());
}
}
public class Run1 {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
控制台打印3个不同的hashCode,说明创建了3个对象,并不是单例的。这就是“错误的单例模式”,如何解决呢?
4.3 延迟加载的解决方案
(1)声明synchronzied关键字
既然多个线程可以同时进入getInstance()方法,只需要对getInstance()方法声明synchronzied关键字即可。修改MyObject.java类。
public class MyObject {
private static MyObject object;
public MyObject() {
}
synchronized public static MyObject getInstance(){
try {
if(object == null){
//模拟在创建对象之前做一些准备工作
Thread.sleep(3000);
object = new MyObject();
}
}catch (InterruptedException e){
e.printStackTrace();
}
return object;
}
}
此方法在加入同步synchronzied关键字后得到相同实例的对象,但运行效率很低。下一个线程想要取得 对象,必须等待上一个线程释放完锁之后,才可以执行。那换成同步代码块可以解决吗?
(2)尝试同步代码块
修改MyObject.java类。
public class MyObject {
private static MyObject object;
public MyObject() {
}
public static MyObject getInstance(){
try {
synchronized (MyObject.class){
if(object == null){
//模拟在创建对象之前做一些准备工作
Thread.sleep(3000);
object = new MyObject();
}
}
}catch (InterruptedException e){
e.printStackTrace();
}
return object;
}
}
此方法加入同步synchronzied语句块后得到相同实例对象,但运行效率也非常低,和synchronzied同步方法一样是同步运行的。下面继续更改代码,尝试解决这个问题。
(3)针对某个重要的代码进行单独的同步。
修改MyObject.java类。
public class MyObject {
private static MyObject object;
public MyObject() {
}
public static MyObject getInstance(){
try {
if(object == null){
//模拟在创建对象之前做一些准备工作
Thread.sleep(3000);
synchronized (MyObject.class) {
object = new MyObject();
}
}
}catch (InterruptedException e){
e.printStackTrace();
}
return object;
}
}
此方法使同步synchronzied语句块只对实例化对象的关键代码进行同步。从语句的结构上讲,运行效率却是得到了提升,但遇到多线程的情况还是无法得到同一个实例对象。
(4)使用DCL双检查锁机制
public class MyObject {
private volatile static MyObject object;
public MyObject() {
}
public static MyObject getInstance(){
try {
if(object == null){
Thread.sleep(2000);
synchronized (MyObject.class){
if(object == null){
object = new MyObject();
}
}
}
}catch (InterruptedException e){
e.printStackTrace();
}
return object;
}
}
使用volatile修改变量object,使该变量在多个线程间可见,另外禁止 object = new MyObject()代码重排序。object = new MyObject()包含3个步骤:
1、memory = allocate();//分配对象的内存空间
2、ctorInstance(memory);//初始化对象
3、object = memory;//设置instance指向刚分配的内存地址
JIT编译器有可能将这三个步骤重新排序。
1、memory = allocate();//分配对象的内存空间
2、object = memory;//设置instance指向刚分配的内存地址
3、ctorInstance(memory);//初始化对象
这时,构造方法虽然还没有执行,但object对象已具有内存地址,即值不是null。当访问object对象中的值时,是当前声明数据类型的默认值。
创建线程类MyThread.java代码如下。
public class MyThread extends Thread{
@Override
public void run(){
System.out.println(MyObject.getInstance().hashCode());
}
}
public class Run1 {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
可见,使用DCL双检查锁成功解决了懒汉模式下的多线程问题。DCL也是大多数多线程结合单例模式使用的解决方案。
5 使用静态内置类实现单例模式
DCL可以解决多线程单例模式的非线程安全问题。还可以使用其他办法达到同样的效果。
public class MyObject {
private static class MyObjectHandler{
private static MyObject object = new MyObject();
}
public MyObject() {
}
public static MyObject getInstance(){
return MyObjectHandler.object;
}
}
public class MyThread extends Thread{
@Override
public void run(){
System.out.println(MyObject.getInstance().hashCode());
}
}
public class Run1 {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
6 使用static代码块实现单例模式
静态代码块中的代码在使用类的时候就已经执行,所以可以使用静态代码块的这个特性实现单例模式。
public class MyObject {
private static MyObject object = null;
public MyObject() {
}
static {
object = new MyObject();
}
public static MyObject getInstance(){
return object;
}
}
public class MyThread extends Thread{
@Override
public void run(){
for (int i = 0; i < 5; i++) {
System.out.println(MyObject.getInstance().hashCode());
}
}
}
public class Run1 {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}