Java多线程基础与使用详细篇(五)----volatile与单例模式

前言

继续学习Java多线程基础与使用详细篇(四)----Java内存模型下的知识。本篇会涉及volatile关键字以及单例模式。

1. volatile 是什么

(1).volatile是一种同步机制,比synchronized或者Lock相关类更轻量,因为使用volatile并不会发生上下文切换等开销很大的行为
(2).如果一个变量别修饰成volatile,那么JVM就知道了这个变量可能会被并发修改。
(3).但是开销小,相应的能力也小,虽然说volatile是用来同步的保证线程安全的,但是volatile做不到synchronized那样的原子保护, volatile仅在很有限的场景下才能发挥作用。

2.volatile 适用场景

2.1 不适用: a++

代码演示:
与AtomicInteger 相比下,
打印出的值没有到预期的结果,只有AtomicInteger 是预期的,
所以a ++的时候没有起到原子保护

public class NoVolatile implements Runnable {

    volatile int a;
    AtomicInteger realA = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        Runnable r =  new NoVolatile();
        Thread thread1 = new Thread(r);
        Thread thread2 = new Thread(r);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(((NoVolatile) r).a);
        System.out.println(((NoVolatile) r).realA.get());
    }
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            a++;
            realA.incrementAndGet();
        }
    }
}
18909
20000
2.2 不适用场合2

在下面适用了 done = !done;,在前面运行的时候有起到原子保护作用一直是flase,
但是在多次运行之后就是true了,因此这样是不适应的场景。

public class NoVolatile2 implements Runnable {

    volatile boolean done = false;
    AtomicInteger realA = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        Runnable r =  new NoVolatile2();
        Thread thread1 = new Thread(r);
        Thread thread2 = new Thread(r);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(((NoVolatile2) r).done);
        System.out.println(((NoVolatile2) r).realA.get());
    }
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            flipDone();
            realA.incrementAndGet();
        }
    }

    private void flipDone() {
        done = !done;
    }
}
2.3 适用场合1

适用 boolean flag,如果一个共享变量自始至终只被
各个线程赋值,而没有其他的操作,那么就可以用volatile来代替
synchronized或代替原子变量,因为赋值自身是有原子性的,而volatile
又保证了可见性,所以就足以保证线程安全

public class UseVolatile1 implements Runnable {

    volatile boolean done = false;
    AtomicInteger realA = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        Runnable r =  new UseVolatile1();
        Thread thread1 = new Thread(r);
        Thread thread2 = new Thread(r);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(((UseVolatile1) r).done);
        System.out.println(((UseVolatile1) r).realA.get());
    }
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            setDone();
            realA.incrementAndGet();
        }
    }

    private void setDone() {
        done = true;
    }
}
true
20000
2.4 适用场合2

作为刷新之前变量的触发器

// 例如声明一个 volatile 关键字
volatile boolean flag = false;
.....
//  Thread A
.....
flag = true  // 赋值为true

....
//  Thread B
if(!flag){      //此时已经刷新了,被线程B完全的看到了
}    

3. volatile的作用: 可见性、禁止重排序

3.1. 可见性:读一个volatile变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一个volatile 属性会立即刷入到主内存
3.2. 禁止指令重排序优化:解决单例双重锁乱序的问题

4.volatile和synchronized的关系

volatile在这方面可以看做是轻量版的synchronized:如果一个共享变量
自始至终只被各个线程赋值,而没有其他的操作,那么就可以用
volatile来代替synchronized或者代替原子变量,因为赋值自身是有原
子性的 ,而volatile又保证了可见性,所以就足以保证线程安全。

5. volatile 学习小结

(1).volatile修饰符适用于以下场景:某个属性被多个线程共享,其中
有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如
Boolean flag ; 或者作为触发器,实现轻量级同步
(2). volatile属性的读写操作都是无锁的,它不能替代synchronized
因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和
释放锁上,所以说它是低成本的。
(3). volatile只能作用于属性,我们用volatile修饰属性,这样compilers就不会对这个属性做指令重排序。
(4).volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile 属性不会被线程缓存,始终从主存中读取。
(5).volatile 提供了happens-before保证,对volatile变量v的写入happens-before所有其他线程后续对v的读操作。
(6).volatile可以使得long和double的赋值是原子的,后面马上会讲long和double的原子性

6.能保证可见性的措施

(1).除了 volatile可以让变量保证可见性外,synchronized、lock、并发集合、
Thread.join()和Thread.start()等都可以保证可见性
(2). 具体看happens-before 原则的规定
(3). 升华:对 synchronized 可见性正确理解
synchronized不仅保证了原子性、还保证了可见性
synchronized不仅被保护的代码安全,还近朱者赤
演示假设演示:

   // 假设 声明  a b c
    int a  = 1 ;
    int b  = 3 ;
    int c  = 2 ;
void change(){
      a  = 3;
      b  =  4;
sysnchronized(this){    // a,b 发生在sysnchronized 解锁之前,第一个线程进入到sysnchronized内后就进行解锁。
       c = 5;
}
void prinft(){
    sysnchronized(this){
          int  a1 = a;    //  利用sysnchronized happen-before 原则
                              //  第二个线程进到这里的时候它就可看到之前所有的变化,包括a,b内的值
     }
      int b2 = b;
      int c2 = c;
    }
}

7.原子性

什么是原子性
(1). 一系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一般的情况,是不可分割。
(2). 例如ATM里取钱这样的例子
(3). i++ 不是原子性

image.png

假设从上面的图可以看到两个线程

//假设当线程一的
i = 1时,
//执行
i = i +1 ,
//最终结果
i = 2,
但是在线程二的时候没读到或者没有看到,它是看到i=1,所以就不是原子性的
7. 1用synchronized 实现原子性

由于上面 i ++ 导致不是原子性的问题,可以使用synchronized保证同时只有一个线程运行
,这样就是实现了原子性操作。

      //例如
     synchronized (this){
     ......
         i = i +1 ;
  }
7.2. Java中的原子操作有哪些

(1). 除 long 和double 之外的基本类型(int, byte. boolean,short,char,float)的赋值操作
(2). 所有引用reference的赋值操作,不管是32位的机器还是64位的机器
(3). Java.concurrent.Atomic. 包中所有类的原子操作*

7.3. long和double的原子性

(1). 问题描述:官方文档、对于64位的值的写入,可以分为两个32位的操作进行写入、
读取错误、使用volatile解决
(2). 结论:在32位上的JVM上。long 和double 的操作不是原子的,但是在64位的JVM是原子的
(3). 实际开发中:商用Java虚拟机中不会出现

7.4 原子操作+ 原子操作 != 原子操作

(1). 简单地把原子操作组合在一起,并不能保证整体依赖具有原子性
(2). 比如我去ATM机两次取钱是两次独立的原子操作,但是期间有可能银行卡被借给
别人,也就是被其它线程打断并被修改。
(3). 全同步的HashMap也不完全安全

8. 单例模式
8.1. 单例模式的作用

为什么需要单例?
节省内存和计算,保证结果正确,方便管理。

8.2 单例模式的适用场景
  1. 无状态的工具类:比如日志工具类,不管是在哪里使用,我们需要的只是它帮我们记录日志信息,除此之外,并不需要在它的实例对象上存储任务状态,这时候我们就只需要一个实例对象即可。
  2. 全局信息类:比如我们在一个类上记录网站的访问次数,我们不希望有的访问被记录在对象A上,有的却记录在对象B上,这时候我们就让这个类成为单例。
8.3. 单例模式的八种写法
(1). 饿汉式(静态常量) [可用]

优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题
缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。

public class Singleton1 {

    private final static Singleton1 INSTANCE = new Singleton1();

    private Singleton1() {

    }

    public static Singleton1 getInstance() {
        return INSTANCE;
    }

}
(2).饿汉式(静态代码块)[可用]

这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也就是类初始化的时候已经加载了

public class Singleton2 {

    private final static Singleton2 INSTANCE;

    static {
        INSTANCE = new Singleton2();
    }

    private Singleton2() {
    }

    public static Singleton2 getInstance() {
        return INSTANCE;
    }
}
(3).懒汉式(线程不安全) [不可用]

这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,会产生多个实例

public class Singleton3 {

    private static Singleton3 instance;

    private Singleton3() {

    }

    public static Singleton3 getInstance() {
        if (instance == null) {
            instance = new Singleton3();
        }
        return instance;
    }
}
(4).懒汉式(线程安全,同步方法)[不推荐]

解决上面第三种实现方式的线程不安全问题,做个线程同步就可以了,
缺点:同步效果导致效率低。

public class Singleton4 {

    private static Singleton4 instance;

    private Singleton4() {

    }

    public synchronized static Singleton4 getInstance() {
        if (instance == null) {
            instance = new Singleton4();
        }
        return instance;
    }
}
(5).懒汉式(线程不安全,同步代码块)[不推荐]

即便是修改成同步代码块,效果也会跟上面一样导致多个线程会产出多个实例

public class Singleton5 {

    private static Singleton5 instance;

    private Singleton5() {

    }

    public static Singleton5 getInstance() {
        if (instance == null) {
            synchronized (Singleton5.class) {
                instance = new Singleton5();
            }
        }
        return instance;
    }
}
(6).双重检查[推荐用]

Double-Check是两次if (singleton == null)检查,这样就可以保证线程安全了。
这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例。
优点:线程安全;延迟加载;效率较高。
使用 volatile 新建对象的好处:

  1. 新建对象实际上有3个步骤
  2. 重排序会带来NPE
  3. 防止重排序
public class Singleton6 {

    private volatile static Singleton6 instance;

    private Singleton6() {

    }

    public static Singleton6 getInstance() {
        if (instance == null) {
            synchronized (Singleton6.class) {
                if (instance == null) {
                    instance = new Singleton6();
                }
            }
        }
        return instance;
    }
}
(7).静态内部类[推荐用]

静态内部类方式在Singleton7类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton7的实例化。
类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的
优点:更优雅的方式、规范
1.保证懒加载
2.线程安全
3.效率特别高

public class Singleton7 {

    private Singleton7() {
    }

    private static class SingletonInstance {

        private static final Singleton7 INSTANCE = new Singleton7();
    }

    public static Singleton7 getInstance() {
        return SingletonInstance.INSTANCE;
    }
}
(8).枚举单例(线程安全)[推荐]

优点:
1.线程安全
2.只被装载一次

public class Singleton8 {

    private Singleton8() {
    }

    private enum Singleton {
        INSTANCE;

        private final Singleton8 instance;

        //构建枚举的函数的时候已经被创建了
        Singleton() {
            instance = new Singleton8();
        }

        public Singleton8 getInstance() {
            return instance;
        }
    }
    public static Singleton8 getInstance() {
        return Singleton.INSTANCE.getInstance();
    }
8.4.用那种单例的实现方案最好
    1. Joshua Bloch 大神在《Effective Java》中明确表达过的观点:
      "使用"枚举实现单例方法虽然还没有广泛采用,
      但是单元素的枚举类型已经成为实现Singleton最佳方法
  1. 写法简单
  2. 线程安全有保障
  3. 避免反序列化破坏单例
8.5.各种写法的适用场合
  1. 最好的方法是利用枚举,因为还可以防止反序列化重新创建新的对象
  2. 非线程同步的方法不能使用
  3. 如果程序一开始要嘉爱的资源太多,那么就应该使用懒加载
  4. 饿汉式如果是对象的创建需要配置文件就不适用
  5. 懒加载虽然好,但是静态内部类这种方式会引入编程复杂性
8.6.什么是原子操作?Java中有哪些原子操作?生成对象的过程是不是原子操作
  1. 新建一个空的Person 对象
  2. 把这个对象的地址指向p
  3. 执行Person的构造函数
9.总结

大致上就把Java 多线程的volatile与单例模式学习了解,这是用看某学习视频总结而来的个人学习文章。希望自己也能对Java多线基础巩固起来。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,110评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,443评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,474评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,881评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,902评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,698评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,418评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,332评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,796评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,968评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,110评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,792评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,455评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,003评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,130评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,348评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,047评论 2 355