锁理论
一、概述
1. Java读写锁理论
锁的种类:
(1) 独享/共享锁 (广义)
独占锁:是指该锁一次只能被一个线程锁持有,eg:ReentrantLock、Sychronized;
共享锁:是指该锁可被多个线程持有,eg: ReentrantReadWriteLock,其读锁是共享锁,其写锁是独占锁;
(2) 互斥/读写锁 (具体)
互斥锁:在访问共享资源之前对其进行加锁操作,在访问完成之后进行解锁操作;
加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前线程解锁,eg: ReentrantLock;
读写锁:读写锁既是独占锁,又是共享锁,读时共享,写时互斥,eg: ReentrantReadWriteLock;
(3) 公平/非公平锁
公平锁:公平锁是指多个线程按照申请锁的顺序来获取锁;
非公平锁:非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,有可能会造成饥饿现象。
对于Java ReentrantLock和而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的控制线程对锁的获取, 所以并没有任何办法使其变成公平锁。
(4) 可重入/不可重入锁
可重入锁又名递归锁,是指同一个线程在外层方法获取锁之后,在进入内层方法会也可以获取锁,
即:在一个线程中可以多次获取同一把锁。eg: ReentrantLock、ReadWriteLock和Synchronized都是可重入锁。
可重入锁的一个好处是可一定程度避免死锁,可重入锁加锁和解锁的次数要相等。
(5) 乐观锁/悲观锁
乐观锁/悲观锁不是指具体类型的锁,而是看待并发的角度。
悲观锁: 悲观锁认为存在很多并发更新操作,采取加锁操作,如果不加锁一定会有问题
乐观锁: 乐观锁认为不存在很多的并发更新操作,不需要加锁。数据库中乐观锁的实现一般采用版本号,Java中可使用CAS实现乐观锁。
(6) 分段锁
分段锁是一种锁的设计,并不是一种具体的锁。对于ConcuttentHashMap就是通过分段锁实现高效的并发操作。
(7) 自旋锁
自旋锁是指尝试获取锁的线程不会阻塞,而是采用循环的方式尝试获取锁。好处是减少上下文切换,缺点是一直占用CPU资源。
(8) 偏向锁 / 轻量级锁 / 重量级锁
这三种锁是指锁的状态,并且是针对Synchronized
。在Java 5通过引入锁升级的机制来实现高效Synchronized
。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁: 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
轻量级锁: 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁: 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申
请的线程进入阻塞,性能降低。
多个线程同时读一个资源类的时候是没有任何问题,所以为了满足并发量,读取共享资源应该同时进行。但是,如果有一个线程想去写共享资源,就不应该有其他线程去读或者写。
写操作:原子+独占,整个过程必须是一个完整体,中间不能有任何中断。
读-读共存、读-写不共存、写-读不共存、写-写不共存
2. 原子性与原子操作
线程安全性:
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者着写进程如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
线程安全主要体现在三个方面:原子性、可见性、有序性。
2.1 原子性
原子性提供了互斥访问,同一时刻只能有一个线程对它进行操作。
所以解决原子性问题的重要条件是:同一时刻只能有一个线程对共享变量进行操作,即互斥。如果我们能够保证对共享变量的修改是互斥的,那么无聊单核CPU还是多核CPU,都能保证原子性。
以下三种方法都可以保证原子操作:
(1) Atomic
Atomic包下提供的类利用CAS保证操作的原子性。
(2) 原子性锁
实现锁的两种方式:
1)synchronized:在作用对象的作用范围内,依赖JVM实现操作的原子性;
2)Lock:依赖特殊的CPU指令,代码实现,如ReentrantLock(本章不做说明);
注:子类继承父类时,如果父类中有syncronized修饰的方法,syncronized关键字是不会继承过去的,因为syncronized关键字不属于方法声明的一部分。
(3) Atomic、Synchronized、Lock对比
syncronized:不可中断锁,适合竞争不激烈场景,可读性好;
Lock:可中断锁,多样化同步,竞争激烈时能维持常态,需要大量代码实现;
Atomic:竞争激烈时能维持常态,比Lock性能好;缺点是一次只能同步一个值,虽然提供了AtomicReference、AtomicReferenceFieldUpdater也只是一次同步一个对象。
此外,CAS操作也可以保证原子性。
2.2 可见性
一个线程对主内存的修改可以及时的被其他线程观察到。
导致共享变量在线程间不可见的原因:
a. 线程交叉执行;
b. 代码重排序结合线程交叉执行;
c. 共享变量更新后的值没有在工作内存与主内存之间及时更新;
解决:
(1) syncronized
JMM关于syncronized的两条规定:
1) 线程解锁前,必须把共享变量的最新值刷新到主内存中;
2) 线程加锁时,将清空工作内存中共享变量的值,从而使得使用共享变量时需要从主内存中重新读取最新的值(注意,加锁与解锁是同一把锁)
由于syncronized可以保证原子性及可见性,变量只要被syncronized修饰,就可以放心的使用.
(2) volatile
通过加入内存屏障和禁止重排序优化来实现可见性。
具体实现过程:
1)对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存;
2)对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量;
注:volatile不能保证操作的原子性,也就是不能保证线程安全性。
如果需要使用volatile必须满足以下两个条件:
1)对变量的写操作不依赖与变量当前的值。
2)该变量没有包含在具有其他变量的不变的式子中。
所以volatile修饰的变量适合作为状态标记量。
2.3 有序性
Java内存模型中,允许编译器和处理器对指令进行重排序,但重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
volatile、syncronized、Lock都可保证有序性。
一系列操作如果无法保证happens-before原则,就说这段操作无法保证有序性。
happens-before原则:
1)程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
2)锁定规则:一个unLock操作先行发生于后面对同一个锁的Lock()操作;
3)volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
4)传递规则:如果操作A先行发生与操作B,而操作B先行发生于操作C,则操作A先行发生于操作C;
5)线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作;
6)线程终端规则:对线程interrupt()方法的调用先行发生与被中断线程的代码检测到中断事件的发生(只有执行了interrupt()方法才可以检测到中断事件的发生);
7)线程终结规则:线程中所有操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值手段检测到线程已经终止执行;
8)对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
总结:
此外,在实际的业务过程中也经常用到原子性,只顾过和多线程的原子性有点区别:
原子性:
原子性是指一个或者多个操作要么全部执行,并且执行的过程中不会被任何因素打断,要么全部都不执行的特性。
例如账户之间的转账问题,就是一个很经典的原子性问题:A账户向B账户转入100元钱,A账户减去100和B账户加上100就必须具备原子性,这才不会有问题。
原子操作:
原子操作(atomic operation),是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何的环境切换。
eg: java中的事物管理
3. 锁模型
我们把一段需要互斥执行的代码称为临界区。线程在进入临界区之前,首先尝试加锁,如果成功,则进入临界区,此时我们称这个线程持有锁。否则就等待或者阻塞,直到持有锁的线程释放锁。持有锁的线程执行完临界区的代码后,执行解锁。
二、使用
1. ReentrantLock / ReentrantReadWriteLock
ReentrantLock:
ReentrantLock重入锁和synchronize关键字都是互斥排他锁,,作用是一样的,都是实现锁的功能,同一时间只能有一个线程在执行任务,虽然保证了线程的安全性,但是效率不高。但是ReentrantLock需要手写代码对锁进行获取和释放(一定要在finally里面释放),要不然就永远死锁了。ReentrantLock也可以用来做线程之间的挂起和通知,synchronize一般是使用object的wait和notify来实现,ReentrantLock使用Condition来实现线程之间的通信。
ReentrantReadWriteLock:
ReentrantReadWriteLock是接口ReadWriteLock的实现类,ReentrantReadWriteLock读写锁是一个读写分离的锁,其读锁是共享锁,其写锁是独占锁,读锁的共享性可保证并发读是非常高效的,读写、写读、写写的过程都是互斥的。
这种锁主要用于读多写少的业务场景,口诀就是:读读共享、写写互斥、读写互斥。