悲观锁:悲观锁是指心态十分的悲观,认为每次去读数据时,别人都有可能会对数据进行修改,所以悲观锁每次读数据时都会对数据进行上锁操作,所以synchronized和ReentrantLock都是悲观锁,另外对于MySQL数据库,“SELECT * FROM xxx FOR UPDATE”,其实利用了MySQL的行锁,会对该行内容上锁,上锁期间别的线程无法进行操作,另外MySQL还有表锁。
乐观锁:乐观锁是指心态十分的乐观,认为每次去读数据时,别人都不会对数据进行修改,乐观锁不会加锁,因为加锁是一种十分影响并发性能的操作,乐观锁使用的是一种将版本号+1的行为,比如在数据库的表中增加一个version字段,同时有两个线程,线程A和线程B,线程A读取该行数据后,version值为1,此时线程A修改该行数据,然后将version值+1(此时为2),但是线程A还没有提交,这时候线程B读取该行数据,version仍为1,此时线程A提交,version变成了2,然后线程B也对该数据进行修改,但是发现version不在是1了,此时修改便会自动失败,因为数据已经被其他线程污染了。CAS(compare and swap,比较和交换)就是乐观锁,虽然它没有版本号,但是实现原理却是和版本号差不多,在Unsafe类中(一个很底层的类,里面的方法都是调用的本地方法)可以直接调用本地C++中的方法,它会在硬件层面完成原子性的操作,那么CAS是如何保证数据的安全的呢?
CAS会把期望的值和原值进行比较,如果相等,说明此时数据并未被别的线程修改,此时就可以对数据安全的修改,如果期望的值和原值不同,那么就会循环,然后直到相等。但是CAS会存在ABA的问题,比如线程A期待的值是1,B线程把1修改成了3,C线程又把3改成了1,那么此时A认为期待的1和现在的1相等,认为没被修改过,但是此时已经修改过了,非常的危险,举例:
此时引入版本号可以解决ABA问题。
那么乐观锁和悲观锁什么时候该用哪一个呢?
如果读的场景比较多,那么冲突会很少,使用乐观锁十分的合适,因为没有上锁,仅仅使用了版本号,十分的轻量级;如果写的场景比较多,那么冲突的可能性会很多,此时应该使用悲观锁。
公平锁:公平锁表示线程获取锁的顺序是按照请求锁的时间早晚来决定的,比如此时线程A获得了锁,线程B和C他们俩在等待A释放锁,但是B来的比C早,那么当A释放锁后,B会得到锁,这就是公平锁。
非公平锁:非公平锁是根据线程调度策略,A释放锁后,B和C相互竞争,谁抢到了就是谁的
ReentrantLock提供了公平锁和非公平锁的实现
ReentrantLock lock = new ReentrantLock(true); //公平锁
ReentrantLock lock = new ReentrantLock(false); //非公平锁
ReentrantLock lock = new ReentrantLock(); //默认就是非公平锁
那么什么时候使用公平锁,什么时候使用非公平锁呢?
在业务中没有公平性需求的情况下使用非公平锁,因为公平锁需要去实现公平,会带来性能开销
如果在业务中需要保证某个操作的公平性,那么就应该使用公平锁
独占锁:独占锁保证任何时候都只有一个线程能得到锁,ReentrantLock和synchronized都是独占锁
共享锁:共享锁则同时可以由多个线程持有,例如ReadWriteLock(读写锁),它允许一个资源可以被多个线程同时进行操作。
独占锁其实是一种悲观锁,由于每次访问资源都要先加上互斥锁,这限制了并发性,因为读操作并不会影响数据的一致性,而独占锁只允许在同一时间由一个线程读取数据,其他线程必须等待当前线程释放锁才能进行读取。
共享锁其实是一种乐观锁,它放宽了加锁的条件,允许多个线程同时进行读操作。
可重入锁:当一个线程获取其他线程持有的独占锁时,该线程会被阻塞,那么当一个线程再次获取它自己已经获取的锁时会否会被阻塞呢?如果不被阻塞,那该锁就是可冲入锁,也就是只要获得了该锁的线程,那么可以无限次数(严格来说其实有次数)地进入该锁锁住的代码。
例如下列代码:
public class Hello {
Object o = new Object();
public void reentrant() {
synchronized(o) {
System.out.println("线程已获得锁");
synchronized(o) {
System.out.println("线程再次获得了该锁");
}
}
}
}
实际上synchronized和ReentrantLock都是可重入锁。可重入锁的原理是在锁内部维护了一个线程标示,用来标示该锁目前被哪个线程所持有,然后关联一个计数器。一开始计数器为0(即该锁未被占用),当一个线程获得了该锁后,计数器的值会+1,这时其他线程再来获取该锁时,会发现锁的持有者不是自己而被阻塞挂起。但是当获得了该锁的线程再次获取锁时发现锁的拥有者是自己,就会把计数器值+1(变成2,3,4,5…),当释放锁后计数器-1,当计数器值为0时,锁里面的线程标示被重置为null,这时候被阻塞的线程会被唤醒来竞争获取该锁。
自旋锁:由于Java中的线程与操作系统中的线程一一对应的,所以当一个线程在获取锁(比如独占锁)失败后,会被切换到内核态而被挂起。当该线程获取到锁时有需要将其切换到用户态而唤醒该线程。而从用户状态切换到内核态的开销是比较大的,在一定程度上会影响并发性能。自旋锁则是,当前线程在获取锁时,如果发现锁已经被其他线程占有,它不会马上阻塞自己,在不放弃CPU使用权的前提下,多次尝试获取(默认次数为10),很有可能在后面几次尝试中其他线程已经释放了锁。如果尝试指定的次数后仍然没有获取到锁当前线程才会被阻塞挂起。由此看来自旋锁是使用了CPU时间来换取上下文切换和线程调度的开销,但是很有可能这些CPU时间白白浪费了。