如上代码使用retryNum设置更新失败后的重试次数,如果代码(3.1)执行后返回0,额说明代码(1.1)获取的记录已经被修改了,则循环一遍,重新通过代码(1.1)获取最新的数据,然后执行代码(3.1)尝试更新,这类似CAS的自旋操作,只是这里没有死循环,而是指定了尝试次数。
乐观锁并不会使用数据库提供的锁机制,一般在表中添加version字段或者使用业务状态来实现。乐观锁直到提交才锁定,所以不会产生任何死锁。
公平锁与非公平锁 根据线程获取锁的机制,锁可以分为公平锁和非公平锁,公平锁表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程获取到锁,而非公平锁在运行时闯入,也就是先来不一定先得。
ReentrantLock提供了公平与非公平锁的实现。
公平锁:ReentrantLock pairLock=new ReentrantLock(true);
非公平锁:ReentrantLock pairLock=new ReentrantLock(false);
如果构造函数不传入参数,就默认就是非公平锁。
假如线程A已经持有了锁,这时候线程B请求该锁会被挂起。当线程A释放锁后,假如当前也有线程C也需要获取到该锁,如果采用非公平锁方式,则根据线程调度策略,线程B和线程C两者之一可能获得该锁,这时候不需要任何其他干涉。而如果采用公平锁,则需要把C线程挂起,让线程B获取到该锁。在没有公平性需求的情况下尽量使用非公平锁,因为公平锁会带来性能消耗。
独占锁与共享锁 根据锁只能被单个线程持有还是多个线程共享哎,可以将锁分为独占锁和共享锁。
独占锁保证任何时候都只有一个线程得到锁,ReenTrantLock就是以独占的方式实现的,共享锁则可以同时由多个线程持有,,例如ReadWriteLock读写锁,它允许一个资源可以同时被多个线程进行读操作。
独占锁是一种悲观锁,由于每次访问资源都需要先加上互斥锁,这限制了并发性,因为读操作并不会影响数据的一致性,而独占锁只允许同一时间只能允许同一个线程读取数据,其他线程必须等待当前线程释放锁后才能释放。共享锁是一种乐观锁,它放松了加锁的条件,允许多个线程同时进行读操作。
什么是可重入锁 当一个线程要获取一个被其他线程持有的独占锁时,该线程会被阻塞,那么当一个线程再次获取它自己已经获取的锁时是否会被阻塞呢?如果不阻塞,那么我们说该锁是可重入的,也就是只要线程获取到了该锁,那么可以无限次数(在以后的文章中我们将知道严格来说是有限次)地进入被该锁锁住的代码。
下面看一个例子:
public class Hello{ public synchronized void helloA(){ System.out.println("helloA"); } public synchronized void helloB(){ System.out.println("helloB"); } } 在如上代码中,调用helloB方法前会先获取内置锁,然后打印输出,之后调用helloA方法,在调用前会先获取内置锁,如果内置锁是不可重入的,那么调用线程将会一直被阻塞。实际上,synchronized内部锁是可重入锁,可重入锁的原理是在锁内部维护一个线程标示,用来标示该锁被目前哪个线程占用,然后关联一个计数器,一开始计数器为0,说明该锁没有被任何线程占用,当一个线程获取到该锁时,计数器的值就会变为1,这时其他线程再次来获取到该锁时发现锁的持有者不是自己就会被阻塞挂起。
当时当获取到了该锁的线程再次发现锁的拥有者仍然是自己的时候,就会把计数器+1,当释放锁后计数器-1,当计数器为0的时候,锁里面的线程标示被指为null,之时候被阻塞的线程会被唤醒来竞争获取该锁。
自旋锁 由于Java中的线程与操作系统中的线程一一对应,所以当一个线程获取锁失败后,会被切换到内核状态而挂起。当该线程获取到锁的时候有需要将其切换到内核状态而唤醒该线程,而从用户状态切换到内核状态开销是比较大的,在一定程度上会影响并发性能。自旋锁的原则是:当前线程在获取锁时,如果发现锁已经被其他线程占有,它不马上阻塞自己,在不放弃CPU使用权的情况下,多次尝试获取(默认为10次,可以使用XX:PreBlockSpinsh参数设置该值),很有可能在后面几次尝试中其他线程已经释放了该锁。如果尝试了指定次数后仍然没有获取到该锁则当前线程才会被挂起,由此看来自旋锁就是利用CPU时间来获取线程阻塞与调度的开销,但是很有可能这些CPU时间白白浪费了。