Java基础06——并发编程与volatile

112 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

并发编程3大特性

原子性

一个操作或者多个操作,要么全部执行成功,要么全部执行失败。线程切换可导致原子性问题。

【i++;】的自增操作,在多线程时无法保证原子性,可使用原子类CAS)。

可见性

多个线程共同访问共享变量时,某个线程修改了此变量,其他线程能立即看到修改后的值

Java内存模型JMM)规定程序中变量的访问规则,所有变量都存储在主存,线程均有自己的工作内存。工作内存中保存主内存变量的副本,线程只能在工作内存对变量操作,再通过缓存一致性协议将数据刷回主存。

对于普通共享变量,线程A将变量修改后,线程尚未将变量同步到主内存时,若线程B使用了此变量,从主内存中获取到的是修改前的值,便出现了线程的可见性问题image.png

有序性

程序执行的顺序按照代码的先后顺序执行。

由于JMM模型允许编译器处理器为了效率,进行指令重排序的优化。指令重排序在单线程内表现为串行语义,在多线程中会表现为无序语义image-20220410113435105

例如,创建对象大致分为三个步骤:申请内存空间变量初始化将内存空间引用赋值给变量

顺序:分配空间 -> 赋值地址给instence -> 初始化对象。

若该线程在赋值地址后被中断,其他线程可能获得未初始化的对象。

如果不禁止重排序,其他线程有可能得到未经初始化的变量。那么多线程并发编程中,就要考虑如何在多线程环境下可以允许部分指令重排,又要保证有序性。

原子性保证

监视器

加锁

加锁->临界区->解锁;

加锁:锁应加在临界区中受保护的资源上,并选择合适的粒度; image-20220410114408298

死锁

发生条件

1.互斥:共享资源X和Y只能被一个线程占用: 2.占有且等待:线程1已取得资源X,在等待资源Y的时候不释放资源X; 3.不可抢占:其他线程不能抢占线程1占有的资源; 4.循环等待:线程1等待线程2占有的资源,线程2等待线程1占有的资源;

破坏条件

1.互斥:无法破坏该条件,否则加锁就失去了意义; 2.占用且等待:一次性申请所有的资源: 3.不可拾占:如果申请不到资源,则释放已占有的资源(java.util.concurrent.Lock) ; 4.循环等待:保障申请资源的顺序,依次加锁;

实现

synchronized

优势:简单(自动加锁和解锁) ;

同步(Monitor) : wait(), notify(), notifyAll()

Lock

优势: 1.支持响应中断; 2.支持超时机制; 3.支持非阻塞获取锁(失败则报错); 4.支持多个条件变量;

同步(Condition) : await), signal), signalAll)

信号量

  • 本质:计数器+等待队列;

  • 操作:

    • 1.初始化;
    • 2.减1:若此时计数器的值 < 0,则当前线程被阻塞,否则就继续执行;
    • 3.加1:若此时计数器的值 <= 0,则唤醒队列中的一个线程,并将其从队列中移除;
  • 优势:可以允许多个线程访问一个临界区,常用于实现线程池、连接池限流器;

  • 实现: Semaphore

synchronized保证并发特性

synchronized关键字同时保证上述三种特性。

  • synchronized同步锁,同步块内的代码相当于同一时刻单线程执行,故不存在原子性有序性的问题

  • synchronizedJMM规定,保证其实现内存可见性

    • 线程加锁前将清空工作内存中共享变量的值从主内存中取值
    • 线程解锁前必须把共享变量的最新值刷新到主内存中

volatile保证并发特性

volatile保证可见性有序性

volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性

volatile强制刷新保证可见性

当对volatile变量执行写操作后,JMM会把工作内存中的最新变量值强制刷新到主内存,且会导致其他线程中的缓存无效.

这样,其他线程使用缓存时,发现本地工作内存中此变量无效,便从主内存中获取,这样获取到的变量便是最新的值,实现了线程的可见性

volatile禁止指令重排保证有序性

编译器生成字节码时,向指令序列中添加“内存屏障”来禁止指令重排序。

内存屏障是一组处理器指令用于实现对内存操作顺序限制