本文已参与「新人创作礼」活动,一起开启掘金创作之路。
并发编程3大特性
原子性
一个操作或者多个操作,要么全部执行成功,要么全部执行失败。线程切换可导致原子性问题。
【i++;】的自增操作,在多线程时无法保证原子性,可使用原子类(CAS)。
可见性
多个线程共同访问共享变量时,某个线程修改了此变量,其他线程能立即看到修改后的值。
Java内存模型(JMM)规定程序中变量的访问规则,所有变量都存储在主存,线程均有自己的工作内存。工作内存中保存主内存变量的副本,线程只能在工作内存对变量操作,再通过缓存一致性协议将数据刷回主存。
对于普通共享变量,线程A将变量修改后,线程尚未将变量同步到主内存时,若线程B使用了此变量,从主内存中获取到的是修改前的值,便出现了线程的可见性问题。
有序性
程序执行的顺序按照代码的先后顺序执行。
由于JMM模型允许编译器和处理器为了效率,进行指令重排序的优化。指令重排序在单线程内表现为串行语义,在多线程中会表现为无序语义。
例如,创建对象大致分为三个步骤:申请内存空间,变量初始化,将内存空间引用赋值给变量。
顺序:分配空间 -> 赋值地址给instence -> 初始化对象。?
若该线程在赋值地址后被中断,其他线程可能获得未初始化的对象。?
如果不禁止重排序,其他线程有可能得到未经初始化的变量。那么多线程并发编程中,就要考虑如何在多线程环境下可以允许部分指令重排,又要保证有序性。
原子性保证
监视器
加锁
加锁->临界区->解锁;
加锁:锁应加在临界区中受保护的资源上,并选择合适的粒度;
死锁
发生条件
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是同步锁,同步块内的代码相当于同一时刻单线程执行,故不存在原子性和有序性的问题 -
synchronized,JMM规定,保证其实现内存可见性:- 线程加锁前,将清空工作内存中共享变量的值,从主内存中取值。
- 线程解锁前,必须把共享变量的最新值刷新到主内存中;
volatile保证并发特性
volatile保证可见性和有序性:
volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。
volatile强制刷新保证可见性
当对volatile变量执行写操作后,JMM会把工作内存中的最新变量值强制刷新到主内存,且会导致其他线程中的缓存无效.
这样,其他线程使用缓存时,发现本地工作内存中此变量无效,便从主内存中获取,这样获取到的变量便是最新的值,实现了线程的可见性。
volatile禁止指令重排保证有序性
在编译器生成字节码时,向指令序列中添加“内存屏障”来禁止指令重排序。
内存屏障是一组处理器指令,用于实现对内存操作的顺序限制。