变法编程中的锁
悲观锁和乐观锁
悲观锁顾名思义就是持有悲观的态度,线程每次进入临界区处理数据时,都认为数据很容易被其他线程修改.所以,在线程进入临界区前,都会锁住临界区的资源,并且在处理数据的过程中一直保持锁住状态.其他线程无法获取相应的资源,就会阻塞等待,知道获取锁的线程释放锁,等待的线程才能获取资源.
在java中synchronized重量级锁就是悲观锁的代表
乐观锁就是持有乐观的态度,认为每次访问数据的时候其他线程都不会修改数据,所以在访问锁时,不会对数据进行加锁操作,当涉及对数据进行更新时,会检测数据是否被其他线程修改过,如果数据没有被修改过,则当前线程提交更新操作;如果数据被其他线程修改过,则当前线程会尝试再次读取数据,检测数据是否被其他线程修改过,如果再次检测的结果仍然为数据已经被其他线程修改过,则会再次尝试读取数据,如此反复,知道检测到的数据没有被修改过
乐观锁在具体实现时,一般会采用版本号机制,先读取数据的版本号,再写数据时比较版本号是否一致,如果版本号一致则更新数据,否则再次读取版本号,比较版本号是否一致,知道版本号一致时更新数据
java中的乐观锁一般是基于CAS自旋实现的.在java中CAS是一种原子操作,底层调用的是硬件层面的比较并交换的逻辑.在实现时,会比较当前值与传入的期望值是否相同,如果相同,则把当前值修改为目标值,否则不修改.
java中的synchronized轻量级锁属于乐观锁,是基于抽象队列同步器(AQS)实现的锁,如reentrantlock等
公平锁与非公平锁
公平锁的核心思想就是公平,能够保证各个线程按照顺序获取锁,也就是先来先获取的原则
非公平锁的核心思想就是每个线程获取锁的机会是不平等的,也是不公平的.先抢占锁的线程不一定能够获取锁.
在Java中,ReentrantLock默认的实现为非公平锁,也可以在构造方法中传入true来创建公平锁对象
独占锁和共享锁
独占锁也叫排他锁,在多个线程抢占锁的过程中,无论是读操作还是写操作,只能有一个线程获取锁,其他线程阻塞等待,独占锁采取的是悲观保守策略.
独占锁的缺点是无论对于读操作还是写操作,都只有一个线程获取锁.但是读操作不会修改数据,如果当前读操作线程获取锁时其他线程被阻塞,就会大大降低系统的读性能.
共享锁允许多个线程同时获取临界区的资源,他采取的是乐观锁的机制.共享锁会限制写操作与写操作之间的竞争,也会限制写操作与读操作之间的竞争,但是不会限制读操作与读操作之间的竞争.
在Java中,ReentrantLock是一种独占锁,而ReentrantLockReadWriteLock可以实现读/写锁分离,允许多个读操作同时获取读锁.
可中断锁与不可中断锁
可中断锁与不可中断锁主要指线程在阻塞等待的过程中,能否中断自己阻塞等待的状态.
可中断锁指锁在被其他线程获取后,某个线程在阻塞等待的过程中,可能由于等待的时间过程过长而中断阻塞等待的状态,去执行其他的过程.
不可中断锁指锁在被其他线程获取后,某个线程如果也想获取这个锁,就只能阻塞等待.如果占有锁的线程一直不释放锁,其他想获取锁的线程就会一直阻塞等待.
在Java中ReentrantLock是一种可中断锁,synchronized则时一种不可中断锁
可重入锁与不可重入锁
重入锁也叫递归锁,指同一个线程可以多次占用同一个锁,但是在解锁时,需要执行相同次数的解锁操作
不可重入锁与可重入锁在逻辑上是相反的,指一个线程不能多次占用同一个锁
在Java中ReentrantLock就是一种可重入锁
读/写锁
读/写锁分为读锁和写锁,当持有读锁时,能够对共享资源进行读操作,当持有写锁时,能够对共享资源进行写操作.写锁具有排他性.在同一时刻,一个读/写锁只允许一个线程进行写操作,可以允许多个线程进行读操作
当某个线程试图获取写锁时,如果发现其他线程已经获取到写锁或者读锁时,则当前线程会阻塞等待,知道任何线程不再持有写锁或读锁.当某个线程试图获取读锁时,当发现其他线程获取到读锁,则这个线程会直接获取到读锁.当某个线程获取读锁时,如果发现其他线程获取到写锁时,则这个线程会阻塞等待,直到占有写锁的线程释放锁.
在读/写锁中,读操作与读操作是可以共存的,但是读操作与写操作,写操作与写操作不能共存
在Java中,ReentrantLock就是一种读/写锁
自旋锁
自旋锁指某个线程在没有获取到锁时,不会立即进入阻塞等待的状态,而是不断尝试获取锁,直到占用锁的线程释放锁.
自旋锁可能引起死锁和占用CPU时间过长的问题
程序不能在占有自旋锁时调用自己,也不能递归调用时获取相同的自旋锁,可以在一定程度上避免死锁
当某个线程进入不断获取锁的循环时,可以设定一个循环事件或循环次数,超过这个时间或者次数,就让线程进入阻塞等待的状态,在一定程度上可以有效地避免长时间占用CPU的时间
在java中,CAS是一种自旋锁
死锁/饥饿与活锁
死锁指两个或者多个线程互相持有对方所需要的资源,导致多个线程互相等待,无法继续后续任务的现象.
死锁产生的4个必要条件,分别是挥斥,不可剥夺,请求与保持和循环等待
饥饿指一个或多个线程由于一直无法获取到需要的资源而无法继续执行的现象
导致饥饿问题的原因:
高优先级线程不断抢占资源,导致低线程无法获取资源
某个线程一直不释放资源,导致其他线程无法获取资源
解决方案:
从程序运行过程中,尽量公平的分配资源,可以尝试使用公平锁
为程序分配充足的系统资源
尽量避免持有锁的线程长时间占用锁
活锁指两个或者多个线程在同事抢占同一资源时,主动将资源让给其他线程使用,导致这个资源在多个线程之间来回跳动,这些线程因无法获取所有资源而无法继续执行的现象.活锁是两个或多个线程抢占同一资源的一种冲突.
当两个或多个线程抢占同一资源发生冲突时,可以让每个线程随即等待一段时间后再次抢占资源,这样会大大减少线程抢占资源的冲突次数,有效避免活锁的发生.