单例模式我想这个设计模式大家都很熟悉,如果不熟悉的可以看我写的设计模式系列然后再来看本文。单例模式通常可以分为:饿汉式和懒汉式,那么分别和线程安全是否有关呢?

一、饿汉式

先看代码:

package com.roocon.thread.t5;

public class Singleton {

    private Singleton(){

    }

    private static Singleton instance = new Singleton();

    public static Singleton getInstance(){
return instance;
}
}
package com.roocon.thread.t5;

public class Main {

    public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
Singleton s3 = Singleton.getInstance();
Singleton s4 = Singleton.getInstance();
System.out.println(s1);
System.out.println(s1);
System.out.println(s1);
System.out.println(s1);
}
}

运行结果:

com.roocon.thread.t5.Singleton@5cad8086
com.roocon.thread.t5.Singleton@5cad8086
com.roocon.thread.t5.Singleton@5cad8086
com.roocon.thread.t5.Singleton@5cad8086

所谓的饿汉模式:不管是否使用到instance这个实例,我们都在创建的过程中就对它进行实例化。

那么,饿汉模式是否会出现线程安全问题呢?

出现线程安全问题需要满足三个条件:

1.多线程的环境下

2.必须有共享资源

3.对资源进行非原子性操作

根据以上代码,我们发现,使用饿汉式,在调动getInstance方法时,就只干了一件事,那就是返回Instance实例。这个操作是原子性操作。因此,饿汉式不存在线程安全问题。

饿汉式的不足:不管是否需要使用到该实例,在创建的时候就已经实例化了。但其实在程序中根本没有用到该实例变量,就没必要先对它进行实例化。

推荐是在使用它的时候才对它进行实例化,这就是所谓的懒汉式单例模式。

二、懒汉式

先看代码:

package com.roocon.thread.t5;

public class Singleton2 {

    private Singleton2(){

    }

    private static Singleton2 instance;

    public static Singleton2 getInstance(){
if(instance == null) {//1:读取instance的值
instance = new Singleton2();//2: 实例化instance
}
return instance;
} }
package com.roocon.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()+":"+Singleton2.getInstance());
}
});
}      threadPool.shutdown();
} }

运行结果:

pool-1-thread-4:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-14:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-10:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-8:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-5:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-12:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-1:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-9:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-6:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-2:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-16:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-3:com.roocon.thread.t5.Singleton2@1c208db1

发现,有个实例是Singleton2@1c208db1,也就说明,返回的不是同一个实例。这就是所谓的线程安全问题。

解释原因:对于以上代码注释部分,如果此时有两个线程,线程A执行到1处,读取了instance为null,然后cpu就被线程B抢去了,此时,线程A还没有对instance进行实例化。

因此,线程B读取instance时仍然为null,于是,它对instance进行实例化了。然后,cpu就被线程A抢去了。此时,线程A由于已经读取了instance的值并且认为它为null,所以,

再次对instance进行实例化。所以,线程A和线程B返回的不是同一个实例。

那么,如何解决呢?

1.在方法前面加synchronized修饰。这样肯定不会再有线程安全问题。

package com.roocon.thread.t5;

public class Singleton2 {

    private Singleton2(){

    }

    private static Singleton2 instance;

    public static synchronized Singleton2 getInstance(){
if(instance == null) {//
instance = new Singleton2();//
}
return instance;
} }

但是,这种解决方式,假如有100个线程同时执行,那么,每次去执行getInstance方法时都要先获得锁再去执行方法体,如果没有锁,就要等待,耗时长,感觉像是变成了串行处理。因此,尝试其他更好的处理方式。

2. 加同步代码块,减少锁的颗粒大小。我们发现,只有第一次instance为null的时候,才去创建实例,而判断instance是否为null是读的操作,不可能存在线程安全问题,因此,我们只需要对创建实例的代码进行同步代码块的处理,也就是所谓的对可能出现线程安全的代码进行同步代码块的处理。

package com.roocon.thread.t5;

public class Singleton2 {

    private Singleton2(){

    }

    private static Singleton2 instance;

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

但是,这样处理就没有问题了吗?同样的原理,线程A和线程B,线程A读取instance值为null,此时cpu被线程B抢去了,线程B再来判断instance值为null,于是,它开始执行同步代码块中的代码,对instance进行实例化。此时,线程A获得cpu,由于线程A之前已经判断instance值为null,于是开始执行它后面的同步代码块代码。它也会去对instance进行实例化。

这样就导致了还是会创建两个不一样的实例。

那么,如何解决上面的问题。

很简单,在同步代码块中instance实例化之前进行判断,如果instance为null,才对其进行实例化。这样,就能保证instance只会实例化一次了。也就是所谓的双重检查加锁机制。

再次分析上面的场景:

线程A和线程B,线程A读取instance值为null,此时cpu被线程B抢去了,线程B再来判断instance值为null。于是,它开始执行同步代码块代码,对instance进行了实例化。这是线程A获得cpu执行权,当线程A去执行同步代码块中的代码时,它再去判断instance的值,由于线程B执行完后已经将这个共享资源instance实例化了,所以instance不再为null,所以,线程A就不会再次实行实例化代码了。

package com.roocon.thread.t5;

public class Singleton2 {

    private Singleton2(){

    }

    private static Singleton2 instance;

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

但是,双重检查加锁并不代码百分百一定没有线程安全问题了。因为,这里会涉及到一个指令重排序问题。instance = new Singleton2()其实可以分为下面的步骤:

1.申请一块内存空间;

2.在这块空间里实例化对象;

3.instance的引用指向这块空间地址;

指令重排序存在的问题是:

对于以上步骤,指令重排序很有可能不是按上面123步骤依次执行的。比如,先执行1申请一块内存空间,然后执行3步骤,instance的引用去指向刚刚申请的内存空间地址,那么,当它再去执行2步骤,判断instance时,由于instance已经指向了某一地址,它就不会再为null了,因此,也就不会实例化对象了。这就是所谓的指令重排序安全问题。那么,如何解决这个问题呢?

加上volatile关键字,因为volatile可以禁止指令重排序

package com.roocon.thread.t5;

public class Singleton2 {

    private Singleton2(){

    }

    private static volatile Singleton2 instance;

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

参考资料:

《java并发编程与实战》 龙果学院

Java并发编程原理与实战十:单例问题与线程安全性深入解析的更多相关文章

  1. Java并发编程原理与实战十五:手动实现一个可重入锁

     package com.roocon.thread.ta1; public class Sequence { private MyLock lock = new MyLock(); private ...

  2. Java并发编程原理与实战十二:深入理解volatile原理与使用

    volatile:称之为轻量级锁,被volatile修饰的变量,在线程之间是可见的. 可见:一个线程修改了这个变量的值,在另一个线程中能够读取到这个修改后的值. synchronized除了线程之间互 ...

  3. Java并发编程原理与实战十四:Lock接口的认识和使用

    保证线程安全演进: synchronized volatile AtomicInteger Lock接口提供的方法: void lock():加锁 void unlock():解锁 void lock ...

  4. Java并发编程原理与实战十六:AQS

    一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronized(AQS)! 类如其名,抽象的队列式的同步器,AQ ...

  5. Java并发编程原理与实战十九:AQS 剖析

    一.引言在JDK1.5之前,一般是靠synchronized关键字来实现线程对共享变量的互斥访问.synchronized是在字节码上加指令,依赖于底层操作系统的Mutex Lock实现.而从JDK1 ...

  6. Java并发编程原理与实战十八:读写锁

    ReadWriteLock也是一个接口,提供了readLock和writeLock两种锁的操作机制,一个资源可以被多个线程同时读,或者被一个线程写,但是不能同时存在读和写线程. 基本规则: 读读不互斥 ...

  7. Java并发编程原理与实战五:创建线程的多种方式

    一.继承Thread类 public class Demo1 extends Thread { public Demo1(String name) { super(name); } @Override ...

  8. Java并发编程原理与实战二十五:ThreadLocal线程局部变量的使用和原理

    1.什么是ThreadLocal ThreadLocal顾名思义是线程局部变量.这种变量和普通的变量不同,这种变量在每个线程中通过get和set方法访问, 每个线程有自己独立的变量副本.线程局部变量不 ...

  9. Java并发编程原理与实战四十二:锁与volatile的内存语义

    锁与volatile的内存语义 1.锁的内存语义 2.volatile内存语义 3.synchronized内存语义 4.Lock与synchronized的区别 5.ReentrantLock源码实 ...

随机推荐

  1. css布局 弹性布局 和 网格布局

    这里就不写这两种布局的内容了 弹性布局链接:http://www.ruanyifeng.com/blog/2015/07/flex-grammar.html 网格布局链接:https://www.ji ...

  2. Beta版本测试第二天

    一. 每日会议 1. 照片 2. 昨日完成工作 登入界面的优化与注册界面的优化,之前的登入界面与注册界面没有设计好,使得登入界面与注册界面并不好看,这次对界面进行了优化.另外尝试了找回密码的功能. 3 ...

  3. 【第七周】B-1分数发布

    组名: 新蜂 组长: 武志远 组员: 宫成荣 谢孝淼 杨柳 李峤 项目名称: java俄罗斯方块 由于排名信息过于敏感,由以下方式进行. 宫成荣 = 魑,谢孝淼 = 魅,武志远 =   魉,杨柳 =  ...

  4. [总结] Visual Studio 报价已经对比

    来源微软官方网站 对比 https://visualstudio.microsoft.com/zh-hans/vs/compare/?rr=https%3A%2F%2Fwww.ithome.com%2 ...

  5. isset与empty 的区别

    isset()与empty()函数的区别,isset()只需要验证一个值是否存在: 而empty()不但需验证这个值是否存在,还需检验它的值是否非空和非0: 注:isset()只检验一个变量是否已经设 ...

  6. 第217天:深入理解Angular双向数据绑定的原理

    一.理解angular双向数据绑定 双向绑定是新的前端框架中频繁出现的一个新词汇,也是mvvm的核心原理.angularjs五条核心信念中的数据驱动,便是由双向绑定进行完成. 那么什么是双向绑定,下面 ...

  7. nowcoder 203A Knight(贪心+打表)

    题目链接 题目描述 有一张无限大的棋盘,你要将马从(0,0)移到(n,m). 每一步中,如果马在(x,y),你可以将它移动到(x+1,y+2),(x+1,y-2),(x-1,y+2),(x-1,y-2 ...

  8. nginx配置虚拟路径下载文件(.apk)

    公司将安卓apk文件放到服务器上,实现用户点击链接并下载 nginx version 1.14.1 nginx配置修改 server { listen 80; server_name localhos ...

  9. java并发编程中CountDownLatch和CyclicBarrier的使用

    在多线程程序设计中,经常会遇到一个线程等待一个或多个线程的场景,遇到这样的场景应该如何解决? 如果是一个线程等待一个线程,则可以通过await()和notify()来实现: 如果是一个线程等待多个线程 ...

  10. Discrete Square Roots UVALive - 4270(拓展欧几里得)

    a≡b(mod n)的含义是“a和b除以n的余数相同”,其充要条件是“a-b是n的整数倍”: 求所有满足条件r^2=x(mod m)的r 题目已经给定了一个初始的r,x,m #include < ...