Java的并发编程:单例模式与线程安全性的解析

作者: Arvin Chen 分类: Java 来源: Break易站(www.breakyizhan.com)

对于单例模式,主要是有饿汉式懒汉式这两种模式,而这篇文章主要是讲

  • 饿汉式 (没有线程安全性问题)
  • 懒汉式(双重检查加锁解决线程安全性问题)

单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例。现在,我们就这两种模式来举例分析线程安全性性的相关问题。

单例饿汉式没有线程安全性的问题

Java的并发编程:线程的安全性问题的分析 这篇文章,我们知道,线程的安全性问题要满足下面三个条件:

  • 多线程环境下
  • 多个线程共享一个资源
  • 对资源进行非原子性操作

而对于单例饿汉式确不满足第三个条件,我们可以用下面的Java程序示例来看一下(实现是创建一个饿汉式的类,再用20个线程去调用):


package com.breakyizhan.thread.t5;

public class Singleton {
	
	// 私有化构造方法
	private Singleton () {}

	private static Singleton instance = new Singleton();
	
	public static Singleton getInstance() {
		return instance;
	}
	
	// 多线程的环境下
	// 必须有共享资源
	// 对资源进行非原子性操作
	
	
}

在getInstance()创建实例的方法中,没有对资源进行非原子性操作,我们所获取的对象都是一样的,可以用一个MultiThreadMain.java方法来看一下,创建20个线程去调用:

package com.breakyizhan.thread.t5;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MultiThreadMain {
	
	public static void main(String[] args) {
		
		ExecutorService threadPool = Executors.newFixedThreadPool(20);
		
		for(int i = 0;i<20;i++) {
			threadPool.execute(new Runnable() {
				@Override
				public void run() {
					System.out.println(Thread.currentThread().getName() + ":" +Singleton.getInstance());
				}
			});
		}
		
		threadPool.shutdown();
		
		
	}

结果是:

pool-1-thread-8:com.breakyizhan.thread.t5.Singleton@39117c62
pool-1-thread-14:com.breakyizhan.thread.t5.Singleton@39117c62
pool-1-thread-4:com.breakyizhan.thread.t5.Singleton@39117c62
pool-1-thread-2:com.breakyizhan.thread.t5.Singleton@39117c62
pool-1-thread-15:com.breakyizhan.thread.t5.Singleton@39117c62
pool-1-thread-3:com.breakyizhan.thread.t5.Singleton@39117c62
pool-1-thread-9:com.breakyizhan.thread.t5.Singleton@39117c62
pool-1-thread-10:com.breakyizhan.thread.t5.Singleton@39117c62
pool-1-thread-12:com.breakyizhan.thread.t5.Singleton@39117c62
pool-1-thread-7:com.breakyizhan.thread.t5.Singleton@39117c62
pool-1-thread-6:com.breakyizhan.thread.t5.Singleton@39117c62
pool-1-thread-19:com.breakyizhan.thread.t5.Singleton@39117c62
pool-1-thread-18:com.breakyizhan.thread.t5.Singleton@39117c62
pool-1-thread-17:com.breakyizhan.thread.t5.Singleton@39117c62
pool-1-thread-16:com.breakyizhan.thread.t5.Singleton@39117c62
pool-1-thread-13:com.breakyizhan.thread.t5.Singleton@39117c62
pool-1-thread-11:com.breakyizhan.thread.t5.Singleton@39117c62
pool-1-thread-1:com.breakyizhan.thread.t5.Singleton@39117c62
pool-1-thread-5:com.breakyizhan.thread.t5.Singleton@39117c62
pool-1-thread-20:com.breakyizhan.thread.t5.Singleton@39117c62

可以看到,对于饿汉式的单例模式,是没有线程安全性问题的。但是饿汉式会造成对资源的浪费,比如说我没有调用这个Singleton类的时候,它已经创建好给我们了。正常情况下,我们是要用这个类才去创建的,是不是?所以,单例模式就有了懒汉式的这个模式。

单例懒汉式的线程安全性的问题

我们知道饿汉式没有线程安全性的问题,那么懒汉式有没有线程安全性的问题呢?那当然是有的,我们可以看一下下面这个Java示例(实现是创建一个懒汉式的类,再用20个线程去调用)


package com.breakyizhan.thread.t5;

public class Singleton2 {
	
	private Singleton2() {}
	
	private static Singleton2 instance;
	
	public static Singleton2 getInstance () {
		if(instance == null) {
					instance = new Singleton2(); 
			}
		return instance;
	}

}

再用一个MultiThreadMain.java方法来看一下,创建20个线程去调用:

package com.breakyizhan.thread.t5;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MultiThreadMain {
	
	public static void main(String[] args) {
		
		ExecutorService threadPool = Executors.newFixedThreadPool(20);
		
		for(int i = 0;i&lt;20;i++) {
			threadPool.execute(new Runnable() {
				@Override
				public void run() {
					System.out.println(Thread.currentThread().getName() + ":" +Singleton2.getInstance());
				}
			});
		}
		
		threadPool.shutdown();
		
		
	}

结果是:

pool-1-thread-6:com.breakyizhan.thread.t5.Singleton2@61d50c2d
pool-1-thread-7:com.breakyizhan.thread.t5.Singleton2@61d50c2d
pool-1-thread-13:com.breakyizhan.thread.t5.Singleton2@246f9732
pool-1-thread-19:com.breakyizhan.thread.t5.Singleton2@246f9732
pool-1-thread-2:com.breakyizhan.thread.t5.Singleton2@61d50c2d
pool-1-thread-5:com.breakyizhan.thread.t5.Singleton2@61d50c2d
pool-1-thread-15:com.breakyizhan.thread.t5.Singleton2@246f9732
pool-1-thread-9:com.breakyizhan.thread.t5.Singleton2@61d50c2d
pool-1-thread-12:com.breakyizhan.thread.t5.Singleton2@e9c9830
pool-1-thread-20:com.breakyizhan.thread.t5.Singleton2@246f9732
pool-1-thread-18:com.breakyizhan.thread.t5.Singleton2@246f9732
pool-1-thread-16:com.breakyizhan.thread.t5.Singleton2@246f9732
pool-1-thread-14:com.breakyizhan.thread.t5.Singleton2@246f9732
pool-1-thread-8:com.breakyizhan.thread.t5.Singleton2@61d50c2d
pool-1-thread-11:com.breakyizhan.thread.t5.Singleton2@c0647e4
pool-1-thread-4:com.breakyizhan.thread.t5.Singleton2@246f9732
pool-1-thread-3:com.breakyizhan.thread.t5.Singleton2@61d50c2d
pool-1-thread-1:com.breakyizhan.thread.t5.Singleton2@246f9732
pool-1-thread-10:com.breakyizhan.thread.t5.Singleton2@61d50c2d
pool-1-thread-17:com.breakyizhan.thread.t5.Singleton2@246f9732

我们可以看到,很多线程都自己创建了不少的对象,这样就造成了线程的安全性的问题。那么,我们要解决这个问题怎么办呢?很简单,就是用synchronized关键字就可以了。如下:

package com.breakyizhan.thread.t5;

public class Singleton2 {
	
	private Singleton2() {}
	
	private static Singleton2 instance;
	
	public static synchronized Singleton2 getInstance () {
               //自旋 while(true)
		if(instance == null) {
		instance = new Singleton2(); 
			}
		return instance;
	}

}

得到的结果如下:

pool-1-thread-10:com.breakyizhan.thread.t5.Singleton2@502880f2
pool-1-thread-12:com.breakyizhan.thread.t5.Singleton2@502880f2
pool-1-thread-8:com.breakyizhan.thread.t5.Singleton2@502880f2
pool-1-thread-6:com.breakyizhan.thread.t5.Singleton2@502880f2
pool-1-thread-7:com.breakyizhan.thread.t5.Singleton2@502880f2
pool-1-thread-3:com.breakyizhan.thread.t5.Singleton2@502880f2
pool-1-thread-4:com.breakyizhan.thread.t5.Singleton2@502880f2
pool-1-thread-2:com.breakyizhan.thread.t5.Singleton2@502880f2
pool-1-thread-5:com.breakyizhan.thread.t5.Singleton2@502880f2
pool-1-thread-1:com.breakyizhan.thread.t5.Singleton2@502880f2
pool-1-thread-11:com.breakyizhan.thread.t5.Singleton2@502880f2
pool-1-thread-9:com.breakyizhan.thread.t5.Singleton2@502880f2
pool-1-thread-13:com.breakyizhan.thread.t5.Singleton2@502880f2
pool-1-thread-14:com.breakyizhan.thread.t5.Singleton2@502880f2
pool-1-thread-16:com.breakyizhan.thread.t5.Singleton2@502880f2
pool-1-thread-15:com.breakyizhan.thread.t5.Singleton2@502880f2
pool-1-thread-18:com.breakyizhan.thread.t5.Singleton2@502880f2
pool-1-thread-17:com.breakyizhan.thread.t5.Singleton2@502880f2
pool-1-thread-19:com.breakyizhan.thread.t5.Singleton2@502880f2
pool-1-thread-20:com.breakyizhan.thread.t5.Singleton2@502880f2

Java的懒汉式双重检查加锁

那结合锁的概念Java的并发编程:偏向锁,轻量级锁和重量级锁,我们可以知道,有三种锁,

  • 偏向锁
  • 轻量级锁
  • 重量级锁

由于是多线程的环境,那么偏向锁是不能用了,那么我们可以看一下轻量级锁,轻量级锁在进入代码块之后,如果有线程调用的时候,会在进入代码块的地方进行自旋,等到上一个线程执行完毕之后,才会继续执行,不过自旋很浪费CPU的资源,还不如wait,wait是不消耗CPU资源的。如果在上面的代码的话,全部都会变成重量级锁,这样子没办法保证CPU的资源。我们可以看到线程安全性的问题是出现在创建的时候instance = new Singleton2(); 那么,我们只要在出现线程安全性问题的时候加synchronized 就可以,读的操作return instance;是不存在线程的安全性问题的,所以最后优化之后的代码如下:


package com.breakyizhan.thread.t5;

public class Singleton2 {
	
	private Singleton2() {}
	
	private static volatile Singleton2 instance;
	
	/**
	 * 双重检查加锁
	 * 
	 * @return
	 */
	public static Singleton2 getInstance () {
		// 自旋   while(true)
		if(instance == null) {
			synchronized (Singleton2.class) {
				if(instance == null) {
					instance = new Singleton2();  // 指令重排序
					
					// 申请一块内存空间   // 1
					// 在这块空间里实例化对象  // 2
					// instance的引用指向这块空间地址   // 3
				}
			}
		}
		return instance;
	}
	
	// 多线程的环境下
	// 必须有共享资源
	// 对资源进行非原子性操作

}

上面代码的volatile关键字是为了保证不会出现指令的重排序问题。

  •   本文标题:Java的并发编程:单例模式与线程安全性的解析 - Break易站
    转载请保留页面地址:https://www.breakyizhan.com/java/6634.html

    发表笔记

    电子邮件地址不会被公开。 必填项已用*标注

    更多阅读