并发三大特性

并发和并行

目标都是最大化CPU的使用率
并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。

并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。


并行在多处理器系统中存在,而并发可以在单处理器和多处理器系统中都存在,并发能够在单处理器系统中存在是因为并发是并行的假象,并行要求程序能够同时执行多个操作,而并发只是要求程序假装同时执行多个操作(每个小时间片执行一个操作,多个操作快速切换执行)
并发主要是用来解决多线程之间的同步、互斥、分工的问题。分工就是指,比如我们需要计算一个10亿数组的和,这个时候挨个去计算效率太低,就可以把他拆分成n个数组,把小数组的和计算出来之后再进行合并。

并发三大特性

并发过程中可能会出现很多的问题,比如可见性、死锁、超卖的场景。。。。
并发编程Bug的源头:可见性、原子性和有序性问题

原子性

一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。在 Java 中,对基本数据类型的变量的读取和赋值操作是原子性操作(64位处理器)。不采取任何的原子性保障措施的自增操作并不是原子性的。

1.X=10 写 原子性 如果是私有数据具有原子性,如果是共享数据没原子性(读写)
2.Y=x 没有原子性

  1. 把数据X读到工作空间(原子性)
  2. 把X的值写到Y(原子性)

3.I++ 没有原子性

  1. 读i到工作空间
  2. +1;
  3. 刷新结果到内存

4.Z=z+1 没有原子性

  1. 读z到工作空间
  2. +1;
  3. 刷新结果到内存

多个原子性的操作合并到一起没有原子性

如何保证原子性
通过 synchronized 关键字保证原子性。
通过 Lock保证原子性。
通过 CAS保证原子性。

有序性

处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。即程序执行的顺序按照代码的先后顺序执行。

int a = 10;    //语句1
int r = 1;     //语句2
a = a + 9;     //语句3
r = a*a;       //语句4

因为重排序,他还可能执行顺序为 2-1-3-4,1-3-2-4,但绝不可能是 2-1-4-3,因为这打破了依赖关系。
as-if-serial :排序后运行的结果和程序的结果一致
不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序。
显然重排序对单线程运行是不会有任何问题,而多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。

线程T1执行:
a = 1; //共享变量 int a
b = true; //共享变量 boolean b

线程T2执行:
if (b){
    int c = a;
    System.out.println(c);
}

假如某个并发时刻,T2检测到b变量已经是true值了,并且变量都对T2可见。c 赋值得到的不一定是1。
b有可能先执行,a还没来的及执行,此时线程T2已经看到b变更,然后去获取a的值,自然不等于1

因为有以上重排序问题,会导致并发执行的问题,那么有没有方法解决呢?
happen-before原则,就是用来解决这个问题的一个原则说明

  1. 程序次序原则,一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  2. 锁定原则 :后一次加锁必须等前一次解锁
  3. Volatile原则:霸道原则,对一个变量的写操作先行发生于后面对这个变量的读操作;
  4. 传递原则:A—B —C ,那么 A–C

如何保证有序性
通过 volatile 关键字保证可见性。
通过 内存屏障保证可见性。
通过 synchronized关键字保证有序性。
通过 Lock保证有序性。

可见性

当一个线程修改了共享变量的值,其他线程能够看到修改的值。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的。

我们通过下面的Java小程序来分析Java的多线程可见性的问题

public class VisibilityTest {

    private boolean flag = true;

    public void refresh() {
        flag = false;
        System.out.println(Thread.currentThread().getName() + "修改flag");
    }

    public void load() {
        System.out.println(Thread.currentThread().getName() + "开始执行.....");
        int i = 0;
        while (flag) {
            i++;
            //TODO  业务逻辑

        }
        System.out.println(Thread.currentThread().getName() + "跳出循环: i=" + i);
    }

    public static void main(String[] args) throws InterruptedException {
        VisibilityTest test = new VisibilityTest();

        // 线程threadA模拟数据加载场景
        Thread threadA = new Thread(() -> test.load(), "threadA");
        threadA.start();

        // 让threadA执行一会儿
        Thread.sleep(1000);
        // 线程threadB通过flag控制threadA的执行时间
        Thread threadB = new Thread(() -> test.refresh(), "threadB");
        threadB.start();

    }


    public static void shortWait(long interval) {
        long start = System.nanoTime();
        long end;
        do {
            end = System.nanoTime();
        } while (start + interval >= end);
    }
}

思考:上面例子中为什么多线程对共享变量的操作存在可见性问题?

需要从java内存模型去理解
image.png
JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。

JMM规定:所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。

image.png

从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:

  1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
  2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。

总之,就是当while的缓存淘汰之后,才会从主存去重新加载,修改的flag值才会被更新到工作内存中去。

保证可见性的方式
通过 volatile 关键字保证可见性。
通过 内存屏障保证可见性。
通过 synchronized 关键字保证可见性。
通过 Lock保证可见性。
通过 final 关键字保证可见性

比如:
1.给 flag 加关键字 volatile
2.加内存屏障,UnsafeFactory.getUnsafe().storeFence();
3.释放时间片,Thread.yield();与上下文切换有关,会重新从主存加载,加载最新的值
4.加一个打印语句,sout
5.while循环里面加 停顿时间 shortWait(1000000); //1ms 也会退出循环,while的缓存 flag不会存在太久
如果是shortWait(1000); //1微秒时间太短,flag的缓存不会被清理

sleep方法最终也是调内存屏障

什么是内存屏障?
内存屏障是一个CPU指令。基本上,它是这样一条指令:
a) 确保一些特定操作执行的顺序;
b) 影响一些数据的可见性(可能是某些指令执行后的结果)。
编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。

为什么加锁后就保证了变量的内存可见性了?
因为当一个线程进入 synchronizer 代码块后,线程获取到锁,会清空本地内存,然后从主内存中拷贝共享变量的最新值到本地内存作为副本,执行代码,又将修改后的副本值刷新到主内存中,最后线程释放锁。
这里除了 synchronizer 外,其它锁也能保证变量的内存可见性。

当然,我们可以用synchronized来保证这个过程。但是Java1.5以后提供了更加轻量的锁volatile

volatile

volatile关键字的语义分析

volatile作用:让其他线程能够马上感知到某一线程对某个变量的修改

保证可见性

对共享变量的修改,其他的线程马上能感知到
image.png
当一个线程要对共享变量进行修改时,告诉其他线程我要去修改他,将这个变量的cache line置为无效状态,对应到硬件层面就叫做JMM控制,volatile。其他线程就可以及时的到主存去读取数据。

保证有序性

重排序(编译阶段、指令优化阶段)
输入程序的代码顺序并不是实际执行的顺序
重排序后对单线程没有影响,对多线程有影响

int i = 0;
int j = 3;
int m = i+j;
i++;
j++;

加Volatile
Happens-before原则规则中,有一个volatile规则:
对于volatile修饰的变量:

  1. volatile之前的代码不能调整到他的后面
  2. volatile之后的代码不能调整到他的前面(as if seria)
  3. 霸道(位置不变化)

Int i=0;
Int a=3;
Int b=5;
Volatile Int j=3;
Int i=0;
Int a=3;
Int b=5;

volatile的原理和实现机制

锁、轻量级
HSDIS --反编译—汇编
Java --class—JVM—ASM文件(汇编语言的文件)

public static volatile boolean finishFlag = false;

对应到汇编语言会有一个Lock :a,相当于给这个变量加了一个锁, lock前缀指令实际上相当于一个内存屏障,锁定起来之后如果别的线程要使用这个变量,需要重新去内存中取。
image.png

volatile使用场景

1、状态标志(开关模式)

public class ShutDowsnDemmo extends Thread{
    private volatile boolean started=false;
    @Override
    public void run() {
        while(started){
            dowork();
        }
    }
    public void shutdown(){
        started=false;
    }
}

2、双重检查锁定(double-checked-locking)
DCL

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

3、需要利用顺序性
涉及到特定的变量不能去调整他的位置的时候,就去使用volatile。

Volatile与Synchronized区别

1、使用上的区别
volatile只能修饰变量,sync只能修饰方法和语句块。
2、对原子性的保证
sync可以保证原子性,volatile不能保证原子性。
3、对可见性的保证
都可以保证,但是实现原理不同,volatile对变量加了lock,sync使用monitorenter和monitoeexit。
4、对有序性的保证
volatile能保证有序,sync可以保证有序性,但是代价并发返回到串行(重量级)。
5、性能方面
synchronized关键字是防止多个线程同时执行一段代码,就会影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized。但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

瓜尔佳敏敏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值