并发编程
2021-08-02 17:45:41 8 举报
AI智能生成
并发编程
作者其他创作
大纲/内容
并发问题
可见性
概念
是指一个线程对一个共享变量进行修改,另一个线程立即获取到变量的最新值
现象
并发编程时,会出现可见性问题,当一个线程对共享变量进行修改,另一个线程无法获取到变量最新的值,就会出现问题、
原子性
概念
在一次或多次操作中,要么所有操作都执行,不会受其他因素干扰,要么所有操作都不执行
现象
并发编程时,当一个线程执行途中,另一个线程的执行会干扰到前一个线程,操作了共享变量
有序性
概念
是指程序中代码的执行顺序,java在编辑和运行时会对代码进行优化,导致程序最终的执行顺序不一致
现象
多线程的情况下,执行顺序改变影响了最终结果。
jvm内存模型(JMM)
概念
JMM规定了内存主要划分为主内存和工作内存两种。此处的主内存和工作内存跟JVM内存划分(堆、栈、方法区)是在不同的层次上进行的,如果非要对应起来,主内存对应的是Java堆中的对象实例部分,工作内存对应的是栈中的部分区域,从更底层的来说,主内存对应的是硬件的物理内存,工作内存对应的是寄存器和高速缓存。
构成
主内存
工作内存
大致过程
共享变量从主内存复制到工作内存,线程对工作内存的变量进行操作,然后再复制回主内存
实际操作过程
lock
锁定
read
读取
load
加载
use
使用
assign
分配
store
保存
write
写入
unlock
解锁
实现方式
synchronized
解决并发问题
保证原子性
只有一个线程能拿到锁
保证可见性
相当于lock和unlock原子操作,会在多线程对变量进行操作的时候从主内存重新获取最新的值到工作内存,这样就能保证可见性
保证有序性
cpu和编辑器会对代码重新排序
as-if-serial
即不管如何重新排序,必须保证单线程下结果是正确的
程序仍然会重新排序,但是sync会保证只有一个线程执行,所以重排序会保证了有序性
特性
可重入
概念:一个线程可以多次执行sync,重复获取同一把锁,也就是方法调用方法可以重复使用sync
原理:sync对象有一个计数器(recursions变量)会记录线程获得几次锁,执行完同步代码块计数器会减1,直到计数器数量为0即释放锁
好处:1,可以避免死锁。2,可以更好地封装代码
不可中断
概念:一个线程获得锁后,另一个线程想要获得锁,那么该线程必须处于阻塞或者等待状态,而且是不可中断的(interrupt()不能中断线程),会一直等待
ReentrantLock
方法lock()不可中断
方法trylock()可中断
原理
反编译的原理
修饰同步方法
会增加ACC_SYNCHRONIZED修饰,会隐式调用monitorenter和monitorexit,在方法执行前调用enter,在方法结束后调用exit
修饰同步代码块
montor才是真正的锁,是存在于创建在堆的对象头中,每一个锁对象都会关联一个monitor(监视器)
代码里含有以下两个成员变量
monitorenter
原理:对象头中包含owner(拥有锁的线程)和recursions(记录锁的次数)信息,其它线程执行的时候会检查当前锁的owner,如果不是自己,那么进入等待状态。
monitorexit
原理:判断recursions(记录锁的次数)信息,如果为0即释放锁
monitor
重量级锁
锁升级过程jdk1.6之后添加
无锁状态
偏向锁
原理:当锁对象第一次被线程获取的时候,虚拟机会把对象头的标识位标记为01,即偏向模式,同时使用CAS操作把获取到的线程锁id存储到Mark word中,持有偏向锁的线程之后每次进入同步块时,虚拟机都不会再进行任何同步操作,偏向锁单线程效率高
好处:适用于单线程进入同步代码块反复获得同一个锁的情况,可以提高带有同步但无竞争的程序效率,当存在竞争的时候就会升级为轻量级锁
乐观锁
轻量级锁
原理:线程在执行同步代码块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用 CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
好处:在多线程交替执行同步代码块的时候,可以避免重量级锁引起的性能消耗
(自旋锁)
在锁升级的过程中,为了避免线程的阻塞和唤醒对cpu的负载过大,会先尝试自旋锁,减少升级为重量级锁
自适应自旋
乐观锁
重量级锁
悲观锁
锁消除
jvm会在一段认为并不存在线程的共享数据安全问题的代码块中消除锁。如Stringbuffer
锁粗化
例如:在for循环中在外面加锁而不是在循环内加锁,即锁粗化。避免加锁解锁消耗资源
升级过程:
当创建一个锁对象的时候,对象会有一个mark word,存储了是否是偏向锁和标志位,标志位为0非偏向锁,1为偏向锁,初始为无锁状态,第一个线程进来的时候,会被标记为偏向锁,在之后的执行过程中如果没有出现线程竞争,那么这个持有偏向锁的线程永远不需要同步操作。如果有其它线程进入,那么就会释放偏向锁,在释放偏向锁的过程中,需要等到全局安全点,导致性能下降,所以如果一段程序有多个线程竞争的话,需要禁用偏向锁否则加锁和释放很消耗资源。偏向锁释放之后会升级为轻量级锁,轻量级锁会使用cas算法在mark word中竞争锁,如果在此期间一直没有获取到锁,那么就会尝试自旋锁循环尝试获取锁,最终没有获得锁就会锁膨胀升级为重量级锁
对象在jvm中的构成
对象头
锁信息
分代年龄
实例数据
对齐数据
优化
减少sync的范围
代码块里的代码尽量短
面试题
sync代码块遇到异常会释放锁吗
会,可以通过反编译文件看出,monitorexit会在代码执行异常时也会执行一次。
sync和lock区别
sync是关键字,lock是接口
sync会自动释放锁,lock手动释放锁
sync是不可中断的,lock的lock()不可中断trylock()可中断
sync不能知道线程是否有拿到锁,而lock通过返回值可以判断
sync用于方法和代码块,lock只能用于代码块
lock可以使用读锁提高读的效率
sync是非公平锁,lock可以控制是否公平锁
即多个线程处于等待状态的时候,唤醒时会随机抽取一个而不是先来后到,所以是非公平的
sync关键字
sync主要用于多线程访问资源的同步性问题,保证修饰的方法和代码块在任意时刻只有一个线程执行,在旧的jdk版本只有重量级锁,重量级锁效率低下,其底层使用的monitor需要频繁的切换线程用户到内核再到用户,在jdk1.6之后新增了锁升级策略减少重量级锁的开销。修饰代码块是使用monitorenter和monitorexit操作锁,修饰方法是使用ACC_SYNCHRONIZED修饰隐式调用。monitor信息存储在每一个java对象的对象头中,monitor里面有一个计数器标记,当计数器为0代表锁被释放。锁升级就是从偏向锁到轻量锁再到重量级锁的过程,偏向锁适用于单线程,当竞争激烈时就会升级为轻量锁,轻量锁没有多线程的消耗,加锁解锁只是通过CAS策略,轻量级锁会通过自旋锁,自适应自旋锁获取锁,如果一直没有获取成功那么就是通过锁膨胀的方式升级为重量级锁。
Volatile
作用:在jvm内存模型中每次使用变量都会从主内存取数据到工作内存,这样就会导致不一致性,如果把这个变量增加volatile修饰,就会每次都从主内存取。
特性
可见性
每次都从主内存取值
有序性
会禁止指令重排
不保证原子性
0 条评论
下一页