乐观锁与悲观锁
悲观锁
乐观锁和悲观锁的概念出自数据库,但在java并发包中也引入和类似的概念(乐观锁/悲观锁是一个概念而不是特指某一种锁,所以你可以说CAS是乐观锁的实现,Synchronized是悲观锁的实现,而不能说CAS就乐观锁)
悲观锁指对数据被外界修改持保守态度,认为数据很容易被其他线程修改,所以在数据被处理前就对数据进行加锁,并在整个处理过程中使数据始终处于锁定状态。
sql语句中的“select ...... for update”即为悲观锁的实现,只有该语句的事务执行完成,才会释放该语句所锁定的行,如以下示例
public int updateEntry(int id){
(1)
EntryObject entry = query("select * from table where id = #{id} for update",id);
(2)
String name = generatorName(entry);
entry.set(name);
.....
(3)
int count = update("update table set name=#{name} where id = #{id}",entry)
return count
}
程序调用updateEntry开启一个事务,执行query查询一条记录,事务传播特性为requried,所以执行entry没有开启新事务,而是加入updateEntry事务,同理2,3均为同一事务,query的查询到update执行完才被提交。
这时如果有多线程调用updateEntry方法,且为同一id,只有一个线程执行代码1会成功,其他线程被阻塞,这是因为同一时间只有一个线程可以获得记录的锁,在该线程的方法执行结束前,其他线程均被阻塞在query方法中。
这是数据库使用for update悲观锁的示例。
在java中通过Synchronized(this.name){}这种操作和ReentrantLock.lock这种独占锁就见得多了,不举例了。所有独占锁都是悲观锁。
乐观锁
乐观锁是相对于悲观锁而言,它认为数据一般情况下不会造成冲突,所以在访问记录前不会加独占锁,而是在数据进行正式更新的时候才会对数据是否冲突进行检测(正如CAS操作中,在更新的前一步对内存中的数据进行比较,如果是预期的值,说明其他线程没有对其更改,更新就行,如果不是预期值,通过while循环再次获得最新的内存值,再次CAS更新,retry)。
乐观锁没有加锁,自然没有锁的开销,对冲突不高的场景效率很高。
在数据库中的乐观锁,并没有通过sql语句实现的方式,通常是通过在表中添加version字段,将version作为判断数据是否被修改的标准,其实思路和CAS相同。
数据库中的乐观锁使用如下
public int updateEntry(int id){
(1)
EntryObject entry = query("select * from table where id = #{id} ",id);
(2)
String name = generatorName(entry);
entry.set(name);
.....
(3)
int count = update("update table set name=#{name} version=${version}+1 where id = #{id} and version=#{version}",entry)
return count;
}
当多线程同时执行updateEntry方法时,多个线程可以同时执行(1)(2)不会产生阻塞。但由于(3)中增加了一条判定条件version=#{version},假如t1线程执行(3)成功,则数据库中version+1,其他线程在随后执行(3)的时候,发现version判定失效,那么执行(3)失败。
公平锁和非公平锁
根据线程获得锁的抢占机制,锁可以分为公平锁和非公平锁,公平锁表示线程获取锁的顺序是按线程请求时间的早晚决定的,也就是先来先得,后来排队。非公平锁是运行时闯入,先来不一定先得,看谁抢的快。
ReentrantLock默认时非公平锁,可通过传入参数设置
ReentrantLock lock = new ReentrantLock(true);//公平锁
ps:在没有公平性要求的情况下,尽量使用非公平锁,因为公平锁会带来性能上的开销。
独占锁和共享锁
根据锁是否可以被多个线程持有,将锁分为独占锁和共享锁,ReentrantLock是独占锁
CountDownLatch是共享锁。独占锁是悲观的,共享锁是乐观的。
可重入锁
当一个线程要获取其他线程持有的独占锁时,会被阻塞,但如果线程想获得的是自己已经获得的锁呢?如果这种情况是可以的,称为可重入,否则称为不可重入。
例子
public class hello{
public synchronized void helloA(){
System.out.println("helloA");
}
public synchronized void helloB(){
System.out.println("helloB");
helloA();
}
}
如上代码中调用hello方法会获取内置锁,然后打印语句,然后去helloA再次获得内置锁,如果内置锁是不可重入的,则程序会一直阻塞,因为该锁还被helloB占据。而经测试,程序执行未阻塞,则synchronized内部锁为可重入锁。
自旋锁
由于java中线程是与操作系统中的线程是一一对应的,所以线程在获得锁失败后,会被切换到内核态挂起。当线程获得锁时又要切换到用户态,这种内核/用户态的切换开销比较大,影响并发性能。
自旋锁可以一定程度减缓这个问题,如果当前线程发现锁被其他线程持有,不会立即切换到内核态阻塞自己,而是多次尝试获取该锁(默认为10次),如果还不能获得该锁,再切换状态。
自旋锁是使用cpu时间换取线程阻塞和内核调度的开销。
网友评论