volatile的特点
- 保证可见性
package com.JUC编程.volatile0;
import java.util.concurrent.TimeUnit;
public class demo1 {
private static volatile Boolean flag=true;
public static void main(String[] args) {
new Thread(()->{
while(flag){
}
}).start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag=false;
System.out.println("修改了flag的值");
}).start();
}
}
- 不保证原子性
package com.JUC编程.volatile0;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class demo2 {
public static void main(String[] args) throws InterruptedException {
//原子性的验证
//使用栅栏保证所有的线程有执行完
CountDownLatch countDownLatch = new CountDownLatch(100);
Resource resource = new Resource();
for (int i = 0; i < 100; i++) {
new Thread(()->{
try {
resource.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println(resource.num);
}
}
class Resource{
volatile int num=0;
// AtomicInteger atomicInteger=new AtomicInteger();使用原子类可以解决
public void add() throws InterruptedException {
for (int i = 0; i < 20; i++) {
TimeUnit.MICROSECONDS.sleep(20);
num++;
}
}
}
- 禁止指令重排
- 重排序:为了提高性能,编译器和处理器会对既定的代码执行顺序进行指令重排序。再不改变程序执行结果的前提下,尽可能提高执行效率,JVM对底层尽量减少约束,使其能够发挥自身优势
happens-before
- 什么是happens-before?
上面的内容我们知道,为了提高处理速度,JVM会对代码进行优化,也就是指令重排序优化,并发编程下指令重排序会带来一些安全隐患:如指令重排序会导致的多个线程操作之间的不可见性。从JDK1.5开始,提出了happens-before的概念通过这个概念来阐述操作之间的内存可见性,**如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。**这里提到的两个操作既可以是在一个线程内,也可以是在两个线程之间。
所以为了解决多线程的可见性问题,就高出了happens-before原则,让线程之间遵循这些原则,编译器还会优化我们的语句,所以等于是给了编译器优化的约束,不能让其优化的不知道东南西北了!
简单来说happens-before:前一个操作的结果可以被后续的操作会获取。前面一个操作将a赋值,那后一个操作肯定能知道a已经变成1了。
- happens-before规则
- 程序顺序规则(单线程规则)
解释:同一个线程中前面的操作对后面的操作可见 - 锁规则(synchronized, lock)
解释:对一个线程的解锁,可见于随后的这个锁的加锁
简单来说:就是想如果线程1解锁了monitor a,接着线程2锁定了a,那么线程1解锁a之前的操作都对线程2可见(线程1和线程2是同一个线程) - volatile变量规则
解释:对一个volatile域的写,可见于的后续对这个域的读 - 传递性
解释:如果A可见于B,B可见于C,那么A可见于C - start()规则
解释:假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来的线程B开始执行前对线程B可见。注意:线程B启动后,线程A对变量修改线程B未必可见 - join()规则
解释:如果线程A执行才啊哦做ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回
- volatile写读建立的happens-before原则
package com.JUC编程.volatile0;
public class HappBefore {
private int a = 1;
private volatile int b = 3;
public void write() {
a = 3;
b = a;
}
/*
* 我们发现会出现a=1 b=3 这就是由于前一个线程的写操作对于后一个线程的读操作不可见造成的。
* 所以我们可以利用volatile写读建立的happens-before关系来实现。
*我们可以给b加volatile,前一个线程下对b写入后,后一个线程在读取b时,那么b之前那所有的操作都对后一个线程可见,
* 就不会出现a=1的情况了。
* */
public void read() {
System.out.println("a=" + a + ",b=" + b);
}
public static void main(String[] args) {
HappBefore before = new HappBefore();
new Thread(() -> {
before.write();
}).start();
new Thread(() -> {
before.read();
}).start();
}
}
单例模式
- 饿汉式的两种写法
饿汉式的特点:在获取单例对象之前对象就已经创建完成了
package com.JUC编程.volatile0;
public class SingleEh {
//饿汉式单例模式
//1.静态常量
/* private static final SingleEh singleEh=new SingleEh();
private SingleEh(){
}
public static SingleEh getSingleEh(){
return singleEh;
}*/
//2.静态代码块
private static final SingleEh singleEh;
private SingleEh(){
}
static {
singleEh=new SingleEh();
}
public static SingleEh getSingleEh(){
return singleEh;
}
}
class test{
public static void main(String[] args) {
SingleEh singleEh = SingleEh.getSingleEh();
SingleEh singleEh1 = SingleEh.getSingleEh();
System.out.println(singleEh1 == singleEh);//true
}
}
- 懒汉单例的四种写法
懒汉单例的特点:在真正需要单例的时候才撞见出该对象。
package com.JUC编程.volatile0;
public class SingleLh {
//懒汉式单例模式
//1.线程不安全
private SingleLh(){
}
private static SingleLh singleLh;//定义的时候不初始化
public static SingleLh getSingleLh(){
//当有两个线程同时进入到这个方法中时,就会发生线程安全问题
if (singleLh==null) {
return new SingleLh();
}
return singleLh;
}
}
package com.JUC编程.volatile0;
public class SingleLh {
//2.使用synchronized修饰方法,但是性能太低
private SingleLh(){
}
private static SingleLh singleLh;
public synchronized static SingleLh getSingleLh(){
if (singleLh==null) {
return new SingleLh();
}
return singleLh;
}
}
package com.JUC编程.volatile0;
public class SingleLh {
//3.可以缩小synchronized的使用范围,但是我们发现却保证不了线程安全
private SingleLh(){
}
private static SingleLh singleLh;
public synchronized static SingleLh getSingleLh(){
if (singleLh==null) {
synchronized (SingleLh.class) {
return new SingleLh();
}
}
return singleLh;
}
}
volatile双重检查机制(推荐方式)
package com.JUC编程.volatile0;
public class SingleLh {
//4.volatile双重检查模式(推荐)
private SingleLh(){
}
//volatile在此处主要的作用是保证可见性和禁止指令重排序
private volatile static SingleLh singleLh;
public static SingleLh getSingleLh(){
/*双重检查既保证了效率又保证了线程安全*/
if (singleLh==null) {
synchronized (SingleLh.class) {
if(singleLh==null) {
//因为new SingleLh()并不是一个原子操作,可能会发生指令重排。
return new SingleLh();
}
}
}
return singleLh;
}
}
- 静态内部类单例方式
静态内部类是在被调用时才会被加载,这种方案实现懒汉单例的一种思想,需要用到的时候才回去创建单例,加上JVM的特性,这种方式又实现了线程安全的创建单例对象
package com.JUC编程.volatile0;
public class SingleStatic {
/*步骤:
* 1.提供一个静态内部类来,里面提供一个常量用来存储单例对象
* 2.提供一个静态方法返回静态内部类中的单例对象
* */
private SingleStatic(){
}
private static class InnerClass{
private static final SingleStatic single=new SingleStatic();
}
public static SingleStatic getSingleStatic(){
return InnerClass.single;
}
}
- 枚举实现单例
package com.JUC编程.volatile0;
/*
* 枚举实际上是一种多例的模式,如果我们直接定义了一个实例就相当于是单例了
* */
public enum SingleEnum {
//但是枚举的主要作用是用于信息的标志和分类,不足以应付特殊的应用场景,单例中用的较少
SINGLE_ENUM;
}
单例模式的使用场景
- 纯赋值操作
package com.JUC编程.volatile0;
import java.util.concurrent.atomic.AtomicInteger;
public class SingleUse1 {
public static void main(String[] args) throws InterruptedException {
Resources resources = new Resources();
Thread thread = new Thread(() -> {
resources.add();
});
Thread thread1 = new Thread(() -> {
resources.add();
});
thread.start();
thread1.start();
thread.join();
thread1.join();
System.out.println(resources.flag);
System.out.println(resources.atomicInteger.get());
}
}
class Resources{
public volatile Boolean flag=false;
public AtomicInteger atomicInteger= new AtomicInteger();
public void add(){
for (int i = 0; i < 10000; i++) {
flag=true;
//flag=!flag;
atomicInteger.incrementAndGet();
}
}
}
小结:
volatile可以适合做多线程中的纯赋值操作,如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全
- 触发器(happens-before)
package com.JUC编程.volatile0;
public class SingleUse2 {
public static void main(String[] args) {
//可以作为刷新之前操作的触发器
resource re = new resource();
new Thread(()->{
re.write();
}).start();
new Thread(()->{
re.read();
}).start();
}
}
class resource {
private int a = 1;
private int b = 2;
private int c = 3;
private volatile boolean flag = true;
public void write() {
a = 10;
b = 20;
c = 30;
flag = true;
}
public void read() {
//flag被volatile修饰,充当了触发器,一旦值为被修改为true,那么此处对变量之前的操作对于后面的线程都是可见的。
while (flag) {
System.out.println("a=" + a + ",b=" + b + ",c=" + c);
}
}
}
小结:
volatile可以作为刷新之前变量的触发器,我们可以将某个变量设置为volatile修饰,其他线程一旦发现该变量修改的值后,触发获取到的该变量之前的最新且可见的。
volatile和synchronized的区别
- volatile只能修饰实例变量和类变量;而synchronized可以修饰方法,以及代码块
- volatile保证数据可见性,但是不保证原子性,不保证线程安全;而synchronized是一种互斥锁(排他)机制,可以保证线程安全
- volatile用于禁止指令重排序,可以解决单例双重检查对象初始化代码执行乱序问题
- volatile可以看做是轻量版的synchronized,volatile不保证原子性,但是如果对一个共享变量进行多个线程的赋值,而没有其他操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就可以保证线程安全了