一、锁的一些概念:锁机制:包括互斥锁、条件变量、读写锁
*互斥锁提供了以排他方式防止数据结构被并发修改的方法。
*读写锁允许多个线程同时读共享数据,而对写操作是互斥的。
*条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
死锁:多个线程(进程)互相握有对方需要的资源且不放弃自身的资源,进入无限期地阻塞、相互等待的一种状态。
产生死锁的必要条件:
1.互斥条件:一个资源每次只能被一个进程使用
2.不可剥夺条件:进程已获得资源,在未使用完之前,不能被其他进程强行剥夺,只能主动释放
3.请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
4.循环等待条件:即进程集合{p0,p1,p2,p3……pn};p0正在等待p1占用的资源,p1正在等待p2占用的资源,pn正在等待p0占用的资源。
注释:只要上述一个条件不成立,就不会发生死锁。预防策略:鸵鸟策略、预防策略、避免策略、检测与恢复策略
互斥:指某一个资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的
同步:是指在互斥的基础上(大多数情况下),通过其它机制实现访问者对资源的有序访问。大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
同步机制遵循的原则:
1.空闲让进;
2.忙则等待;
3.有限等待;
4.让权等待;
线程同步是指多个线程同时访问某资源时,采用一系列的机制以保证最多只能一个线程访问该资源。线程同步是多线程中必须考虑和解决的问题,以为很有可能发生多个线程同时访问(主要是写操作)同一资源,如果不进行线程同步,很可能会引起数据混乱,造成线程死锁等问题。
进程间同步的主要方法有原子操作、信号量机制、自旋锁、管程、会合、分布式系统等
进程的调度算法有哪些?
1.先来先服务(FCFS):此算法的原则是按照作业到达后备作业队列(或进程进入就绪队列)的先后次序选择作业(或进程)
2.短作业优先(SJF:Shortest Process First):这种算法主要用于作业调度,它从作业后备序列中挑选所需运行时间最短的作业进入主存运行。
3.时间片轮转调度算法:当某个进程执行的时间片用完时,调度程序便终止该进程的执行,并将它送到就绪队列的末尾,等待分配下一时间片再执行。然后把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。这样就可以保证队列中的所有进程,在已给定的时间内,均能获得一时间片处理机执行时间。
4.高响应比优先:按照高响应比(已等待时间+要求运行时间)/要求运行时间 优先的原则,在每次选择作业投入运行时,先计算此时后备作业队列中每个作业的响应比RP。选择最大的作业投入运行。
5.优先权调度算法:按照进程的优先权大小来调度。使高优先权进程得到优先处理的调度策略称为优先权调度算法。注意:优先数越多,优先权越小。
6.多级队列调度算法:多队列调度是根据作业的性质和类型的不同,将就绪队列再分为若干个队列,所有的作业(进程)按其性质排入相应的队列中,而不同的就绪队列采用不同的调度算法。
锁的优化:
1、锁粗化
按理来说,同步块的作用范围应该尽可能小,仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
但是加锁解锁也需要消耗资源,如果存在一系列的连续加锁解锁操作,可能会导致不必要的性能损耗。
锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。
2、锁消除
Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间
二、java中的常用的锁,synchronized 与RentrantLock、CAS
加锁后发生了什么:
某一个线程进入synchronized代码块前后,线程会获得锁,JMM 会把该线程对应的本地内存置为无效,,从而使得被监视器保护的临界区代码,必须要从主内存中去读取最新的共享变量到工作内存,保存成为副本,执行代码。当线程释放锁时,JMM 会把该线程对应的本地内存中的共享变量刷新到主内存中,线程释放锁。
注释1:对比锁释放 - 获取的内存语义与 volatile 写 - 读的内存语义,可以看出:锁释放与 volatile 写有相同的内存语义;锁获取与 volatile 读有相同的内存语义。
注释2:(1)线程 A 释放一个锁,实质上是线程 A 向接下来将要获取这个锁的某个线程发出了(线程 A 对共享变量所做修改的)消息。
(2)线程 B 获取一个锁,实质上是线程 B 接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。
(3)线程 A 释放锁,随后线程 B 获取这个锁,这个过程实质上是线程 A 通过主内存向线程 B 发送消息。
锁的类型
Synchronized ;非公平锁,重量级锁(JDK后续对其进行优化,锁升级),悲观锁,可以保证可见性、原子性、有序性。
1、JDK1.6之前,Synchronized为重量级锁,“当系统检查到锁是Synchronized锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。” 为了减少使用Synchronized获得锁和释放锁带来的性能消耗,JDK1.6之后对Synchronized锁进行了锁升级改进。
当使用Synchronized锁时,锁有4种状态(级别从低到高依次升级,不能降级):
无锁状态--->偏向锁状态--->轻量级锁状态--->重量级锁状态
注释1:其中偏向锁为线程获取到锁后默认设置的状态(默认开启),另外两种锁状态为多个线程竞争时,未获取到锁的线程等待状态。
注释2:偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,如果不想有这个延迟,那么可以使用-XX:BiasedLockingStartUpDelay=0;
注释3:如果不想要偏向锁,那么可以通过-XX:-UseBiasedLocking = false来设置;
2、锁升级的具体实现
无锁---->CAS(乐观锁)获取---->未获取到锁---->自旋(cpu空转,防止被挂起,默认10次)---->仍未获取到---->重量级锁(线程挂起阻塞)
注释:CAS(乐观锁)获取------>未获取到锁------->自旋(cpu空转,防止被挂起,默认10次)
这一部分整体实现了轻量级锁。
CAS(Compare And Swap)乐观锁:
定义:”假设对资源的访问时没有冲突的,既然没有冲突就不需要等待,线程不需要阻塞“,故叫它乐观锁。线程在读取数据时不进行加锁,在准备写回数据时,先去查询原值,操作的时候比较原值是否修改,若未被其他线程修改则写回;若已被修改,则重新执行读取流程。
Java使用CAS实现自旋锁,一般默认自旋10次。10次不成功(cpu空转消耗性能)升级为重量级锁。自旋过程可以防止线程被挂起。用户可以通过-XX:PreBlockSpin来进行更改
“何时使用自旋“自旋锁(spin lock)与互斥量(mutex)的比较:
1、自旋锁是一种非阻塞锁,也就是说,如果某线程需要获取自旋锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取自旋锁。
2、互斥量是阻塞锁,当某线程无法获取互斥量时,该线程会被直接挂起,该线程不再消耗CPU时间,当其他线程释放互斥量后,操作系统会激活那个被挂起的线程,让其投入运行。
两种锁适用于不同场景:如果是多核处理器,如果预计线程等待锁的时间很短,短到比线程两次上下文切换时间要少的情况下,使用自旋锁是划算的。如果是多核处理器,如果预计线程等待锁的时间较长,至少比两次线程上下文切换的时间要长,建议使用互斥量。”
引用:https://www.zhihu.com/question/38857029/answer/78480263
引用:https://mp.weixin.qq.com/s/WtAdXvaRuBZ-SXayIKu1mA
注释:CAS操作,以原子操作的方式更新变量
**RentrantLock:**实现锁的接口、可以自定义为公平锁或非公平锁(默认为非公平锁),悲观锁。
使用lock()方法上锁,unlock()方法解锁。线程结束需手动释放解锁。且可以使用lockInterruptibly()中断锁等待。
RentrantLock底层依赖于 java 同步器框架 AbstractQueuedSynchronizer(简称为 AQS)。AQS 使用一个整型的 volatile 变量(命名为 state)来维护同步状态。
如何维护:
1、公平锁获取时,首先会使用getState()方法去读这个 volatile 变量。
非公平锁获取时,首先会用 CAS 更新这个 volatile 变量, 这个操作同时具有 volatile 读和 volatile 写的内存语义。
2、公平锁和非公平锁释放时,最后都要用setState()写一个 volatile 变量 state。
注释1:编译器不会对 volatile 读与 volatile 读后面的任意内存操作重排序;编译器不会对 volatile 写与 volatile 写前面的任意内存操作重排序。
volatile关键字:保证可见性与有序性
volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。禁止指令重排序。
JMM(JavaMemoryModel)定义的八种原子操作。
happends-before规则
引用:1、 三大性质总结:原子性,有序性,可见性
2、深入理解 Java 内存模型(五)——锁
3、彻底搞懂synchronized(从偏向锁到重量级锁)
4、Java并发——Synchronized关键字和锁升级,详细分析偏向锁和轻量级锁的升级
5、【Java线程】锁机制:synchronized、Lock、Condition
6、找工作笔试面试那些事儿(13)—操作系统常考知识点总结