前言
继续学习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++ 不是原子性
假设从上面的图可以看到两个线程
//假设当线程一的
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 单例模式的适用场景
- 无状态的工具类:比如日志工具类,不管是在哪里使用,我们需要的只是它帮我们记录日志信息,除此之外,并不需要在它的实例对象上存储任务状态,这时候我们就只需要一个实例对象即可。
- 全局信息类:比如我们在一个类上记录网站的访问次数,我们不希望有的访问被记录在对象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 新建对象的好处:
- 新建对象实际上有3个步骤
- 重排序会带来NPE
- 防止重排序
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.用那种单例的实现方案最好
- Joshua Bloch 大神在《Effective Java》中明确表达过的观点:
"使用"枚举实现单例方法虽然还没有广泛采用,
但是单元素的枚举类型已经成为实现Singleton最佳方法
- Joshua Bloch 大神在《Effective Java》中明确表达过的观点:
- 写法简单
- 线程安全有保障
- 避免反序列化破坏单例
8.5.各种写法的适用场合
- 最好的方法是利用枚举,因为还可以防止反序列化重新创建新的对象
- 非线程同步的方法不能使用
- 如果程序一开始要嘉爱的资源太多,那么就应该使用懒加载
- 饿汉式如果是对象的创建需要配置文件就不适用
- 懒加载虽然好,但是静态内部类这种方式会引入编程复杂性
8.6.什么是原子操作?Java中有哪些原子操作?生成对象的过程是不是原子操作
- 新建一个空的Person 对象
- 把这个对象的地址指向p
- 执行Person的构造函数
9.总结
大致上就把Java 多线程的volatile与单例模式学习了解,这是用看某学习视频总结而来的个人学习文章。希望自己也能对Java多线基础巩固起来。