做盗版电影网站犯法吗,网站的兼容性,做化妆品网站的意义,上海jsp网站建设目录
一. 前言
二. 可见性
2.1. 可见性概述
2.2. 内存屏障
2.3. 代码实例
三. 不保证原子性
3.1. 原子性概述
3.2. 如何解决 volatile 的原子性问题呢#xff1f;
四. 禁止指令重排
4.1. volatile 的 happens-before 关系
4.2. 代码实例
五. volatile 应用场景
5…目录
一. 前言
二. 可见性
2.1. 可见性概述
2.2. 内存屏障
2.3. 代码实例
三. 不保证原子性
3.1. 原子性概述
3.2. 如何解决 volatile 的原子性问题呢
四. 禁止指令重排
4.1. volatile 的 happens-before 关系
4.2. 代码实例
五. volatile 应用场景
5.1. 状态标志
5.2. 一次性安全发布one-time safe publication
5.3. 独立观察independent observation
5.4. volatile bean 模式
5.5. 开销较低的读写锁策略
5.6. 双重检查double-checked 一. 前言 volatile 可以看做是轻量级的 synchronized它只保证了共享变量的可见性是Java虚拟机提供的轻量级的同步机制。在线程 A 修改被 volatile 修饰的共享变量之后线程 B 能够读取到正确的值。Java 在多线程中操作共享变量的过程中会存在指令重排序与共享变量工作内存缓存的问题。volatile 一共有三大特性保证可见性不保证原子性禁止指令重排。
二. 可见性
2.1. 可见性概述 首先提一个JMM的概念JMM是Java内存模型它描述的是一组规范或规则通过这组规范定义了程序中各个变量实例字段静态字段和构成数组对象的元素的访问方式。JMM规定所有的变量都在主内存主内存是公共的所有线程都可以访问线程对变量的操作必须是在自己的工作内存中。在这个过程中可能出现一个问题。 现在假设主物理内存中存在一个变量他的值为7现在线程A和线程B要操作这个变量所以他们首先要将这个变量的值拷贝一份放到自己的工作内存中如果A将这个值改为1这时候线程B要使用这个变量但是B线程工作内存中的变量副本是7 不是新修改的1 这就会出现问题。
所以JMM规定线程解锁前一定要将自己工作内存的变量写回主物理内存中线程加锁前一定要读取主物理内存的值。也就是说一旦线程A修改了变量值线程B马上能知道并且能更新自己工作内存的值。这就是可见性。
2.2. 内存屏障
volatile 变量的内存可见性是基于内存屏障Memory Barrier实现。
内存屏障又称内存栅栏是一个 CPU 指令。在程序运行时为了提高执行性能编译器和处理器会对指令进行重排序JMM 为了保证在不同的编译器和 CPU 上有相同的结果通过插入特定类型的内存屏障来禁止 特定类型的编译器重排序和处理器重排序插入一条内存屏障会告诉编译器和 CPU不管什么指令都不能和这条 Memory Barrier 指令重排序。
2.3. 代码实例
设计思路首先我们新建一个类里面有一个 number然后写一个方法可以让他的值变成60这时候在主线程中开启一个新的线程让他 3s 后将number值改为60然后主线程写一个循环如果主线程能立刻监听到number值的改变则主线程输出改变后的值此时说明有可见性。如果一直死循环说明主线程没有监听到number值的更改说明不具有可见性。
class MyData {public int number 0;public void change() {number 60;}
}public class VolatileTest {public static void main(String[] args) {MyData myData new MyData();new Thread(()-{System.out.println(Thread.currentThread().getName() number is : myData.number);try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}myData.change();System.out.println(Thread.currentThread().getName() has changed : myData.number);}, A).start();while(myData.number 0){}System.out.println(Thread.currentThread().getName() number is: myData.number);}
}
看一下结果 结果是进入了死循环一直空转说明不具有可见性。下面我们在number前面加上关键字volatile。 public volatile int number 0; 证明能监控到number值已经修改说明加上volatile具有可见性。
三. 不保证原子性
3.1. 原子性概述 原子性指的是不可分割完整性也即某个线程正在做某个业务时不能被分割要么同时成功要么同时失败。 为了证明 volatile 能不能保证原子性我们可以通过一个案例来证明一下。首先我们在之前的MyData 类中加入一个方法 addplus() 能让number加1然后我们创建20个线程每个线程调用1000次 addplus()。看看结果如果number是20000那么他就能保证原子性如果不是20000那么就不能保证原子性。
class MyData{public static volatile int number 0;public void change(){number 60;}public void incre(){number;}
}public class VolatileDemo {public static void main(String[] args) {MyData data new MyData();for (int i 0; i 20; i) {new Thread(()- {for (int j 0; j 1000; j) {data.incre();}}, String.valueOf(i)).start();}while(Thread.activeCount() 2) {Thread.yield();}System.out.println(number is: MyData.number);}
} 结果不是20000说明不能保证原子性。
不保证原子性的原因number 这个操作一共有3步第一步从主物理内存中拿到number的值第二步 number 1第三步写回主物理内存。
假设一开始主物理内存的值为0线程A、线程B分别读取主物理内存的值到自己的工作内存然后执行加1操作。这时候按理说线程A 将1写回主物理内存然后线程B 读取主物理内存的值然后加1变成2但是在线程A写回的过程中突然被打断线程A挂起线程B 将1写回主物理内存这时候线程A重新将1写回主物理内存最终主物理内存的值为1两个线程加了两次最后值居然是1出错了。
3.2. 如何解决 volatile 的原子性问题呢 我们需要使用原子类原子类是保证原子性的。加入一个 AtomicInteger 类的成员然后调用他的getAndIncrement() 方法就是把这个数加1底层用CAS保证原子性。原子类的具体讲解请参见《JUC之Atomic原子类》。
运行结果
这就解决了不保证原子性的问题。
四. 禁止指令重排 禁止指令重排又叫保证有序性。计算机编译器在执行代码的时候不一定非得按照你写代码的顺序执行。他会经历编译器优化的重排指令并行的重排内存系统的重排最终才会执行指令多线程环境更是如此可能每个线程代码的执行顺序都不一样这就是指令重排。
4.1. volatile 的 happens-before 关系
happens-before 规则中有一条是 volatile 变量规则对一个 volatile 域的写happens-before 于任意后续对这个 volatile 域的读。
// 假设线程A执行writer方法线程B执行reader方法
class VolatileExample {int a 0;volatile boolean flag false;public void writer() {a 1; // 1 线程A修改共享变量flag true; // 2 线程A写volatile变量} public void reader() {if (flag) { // 3 线程B读同一个volatile变量int i a; // 4 线程B读共享变量……}}
}
根据 happens-before 规则上面过程会建立 3 类 happens-before 关系。 1. 根据程序次序规则1 happens-before 2 且 3 happens-before 4。 2. 根据 volatile 规则2 happens-before 3。 3. 根据 happens-before 的传递性规则1 happens-before 4。 因为以上规则当线程 A 将 volatile 变量 flag 更改为 true 后线程 B 能够迅速感知。
4.2. 代码实例
public class VolatileReSort {int a 0;boolean flag false;public void methodA() { // 线程Aa 1;flag true;}public void methodB() { // 线程Bif(flag) {a a 5;System.out.println(**********retValue a);}}
}
假设现在线程A、线程B 分别执行上面两个方法由于指令的重排序可能线程A中的两条语句发生了指令重排flag先变为true这时候线程B突然进来判断flag为true然后执行下面的最后输出结果为a 5但是也有可能先执行a 1那这样结果就是a 6所以由于指令重排可能导致结果出现多种情况。现在加上volatile关键字他会在指令间插入一条Memory Barrier来保证指令按照顺序执行不被重排。 五. volatile 应用场景
5.1. 状态标志 也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志用于指示发生了一个重要的一次性事件例如完成初始化或请求停机。
volatile boolean shutdownRequested;
......
public void shutdown() { shutdownRequested true; }public void doWork() { while (!shutdownRequested) { // do stuff}
}
5.2. 一次性安全发布one-time safe publication 缺乏同步会导致无法实现可见性这使得确定何时写入对象引用而不是原始值变得更加困难。在缺乏同步的情况下可能会遇到某个对象引用的更新值由另一个线程写入和该对象状态的旧值同时存在。这就是造成著名的双重检查锁定【double-checked-locking】问题的根源其中对象引用在没有同步的情况下进行读操作产生的问题是您可能会看到一个更新的引用但是仍然会通过该引用看到不完全构造的对象。
public class BackgroundFloobleLoader {public volatile Flooble theFlooble;public void initInBackground() {// do lots of stufftheFlooble new Flooble(); // this is the only write to theFlooble}
}public class SomeOtherClass {public void doWork() {while (true) { // do some stuff...// use the Flooble, but only if it is readyif (floobleLoader.theFlooble ! null) doSomething(floobleLoader.theFlooble);}}
}
5.3. 独立观察independent observation 安全使用 volatile 的另一种简单模式是定期发布观察结果供程序内部使用。例如假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器并更新包含当前文档的 volatile 变量。然后其他线程可以读取这个变量从而随时能够看到最新的温度值。
public class UserManager {public volatile String lastUser;public boolean authenticate(String user, String password) {boolean valid passwordIsValid(user, password);if (valid) {User u new User();activeUsers.add(u);lastUser user;}return valid;}
}
5.4. volatile bean 模式
在 volatile bean 模式中JavaBean 的所有数据成员都是 volatile 类型的并且 getter 和 setter 方法必须非常普通 —— 除了获取或设置相应的属性外不能包含任何逻辑。此外对于对象引用的数据成员引用的对象必须是有效不可变的。这将禁止具有数组值的属性因为当数组引用被声明为 volatile 时只有引用而不是数组本身具有 volatile 语义。对于任何 volatile 变量不变式或约束都不能包含 JavaBean 属性。
ThreadSafe
public class Person {private volatile String firstName;private volatile String lastName;private volatile int age;public String getFirstName() { return firstName; }public String getLastName() { return lastName; }public int getAge() { return age; }public void setFirstName(String firstName) { this.firstName firstName;}public void setLastName(String lastName) { this.lastName lastName;}public void setAge(int age) { this.age age;}
}
5.5. 开销较低的读写锁策略 volatile 的功能还不足以实现计数器。因为 x 实际上是三种操作读、添加、存储的简单组合如果多个线程凑巧试图同时对 volatile 计数器执行增量操作那么它的更新值有可能会丢失。 如果读操作远远超过写操作可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。 安全的计数器使用 synchronized 确保增量操作是原子的并使用 volatile 保证当前结果的可见性。如果更新不频繁的话该方法可实现更好的性能因为读路径的开销仅仅涉及 volatile 读操作这通常要优于一个无竞争的锁获取的开销。
ThreadSafe
public class CheesyCounter {// Employs the cheap read-write lock trick// All mutative operations MUST be done with the this lock heldGuardedBy(this) private volatile int value;public int getValue() { return value; }public synchronized int increment() {return value;}
}
5.6. 双重检查double-checked
传统的单例模式在单线程下其实是没有什么问题的多线程条件下就不行了。
public class SingletonDemo {private static SingletonDemo instance null;private SingletonDemo() {System.out.println(Thread.currentThread().getName() :构造方法被执行);}public static SingletonDemo getInstance() {if(instance null) {instance new SingletonDemo();}return instance;}public static void main(String[] args) {for(int i 0; i 10; i) {new Thread(() - {SingletonDemo.getInstance();}, String.valueOf(i)).start();}}
} 可以看出多线程下单例模式将会失效。我们通过DCL双重检查锁可以解决上述问题。
public static SingletonDemo getInstance() {if(instance null) {synchronized (SingletonDemo.class) {if(instance null) {instance new SingletonDemo();}}}return instance;
} 但是这种方式也有一定的风险。原因在于某一个线程执行到第一次检测读取到的 instance 不为null 时instance的引用对象可能没有完成初始化。instance new SingletonDemo(); 可以分为以下3步完成1. 分配对象内存空间2. 初始化对象3. 设置instance指向刚分配的内存地址此时instance!null。但是由于编译器优化可能会对2、3两步进行指令重排也就是先设置instance指向刚分配的内存地址但是这时候对象还没有初始化如果这时候新来的线程调用了这个方法就会发现instance ! null 然后就返回 instance实际上 instance 没被初始化也就造成了线程安全的问题。为了避免这个问题我们可以使用 volatile 对其进行优化禁止他的指令重排就不会发生上述问题了给变量加上 volatile如 private static volatile SingletonDemo instance null;。