锁类型
锁按照不同的标准可以定义出不同类型。
- 可重入锁/不可重入锁
- 公平锁/非公平锁
- 独占锁/共享锁
- 乐观锁/悲观锁
- 偏向锁/轻量级锁/重量级锁
可重入锁/不可重入锁
按照锁是否可以嵌套获取,我们可以将锁分为可重入锁和不可重入锁。Java中的synchronized和ReentrantLock都是可重入锁,现在基本上很少见不可重入锁,不可重入锁在嵌套获取锁的时候会阻塞出现死锁。如下代码就是一个不可重入锁,永远不会打印lock second。
public static void main(String[] args) {
NonReentrantLock lock = new NonReentrantLock();
lock.lock();
System.out.println("lock first");
lock.lock();
System.out.println("lock second");
}
公平锁/非公平锁
按照锁的公平性可以分为公平锁和非公平锁。
公平锁:线程获取锁的顺序和线程得到锁的顺序是严格一致的,也就是A和B线程,如果A先来,那么A就必然先获取到锁。
非公平锁:和公平锁不同,如果A和B线程都来获取同一锁并阻塞等待,A和B都可能先获取到锁。
Java中ReentrantLock的空构造函数和synchronized都是非公平锁,因为要保证公平性必然会浪费一定的资源。(试想一下,对于公平锁,在锁释放时,如果线程A还没有获取到CPU资源,锁就得等待线程A获取到CPU资源然后将锁给A使用;而如果是非公平锁他发现A线程没有获取到CPU资源,他可以把锁交给已经获取到CPU资源的B线程,对锁的利用率更高)。
如果需要使用非公平锁,可以使用ReentrantLock的一个boolean型构造函数,传入true,进行创建。
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
独占锁/共享锁
按照锁是否可以被多个线程获取,可以分为独占锁和共享锁。Java中,ReentrantReadWriteLock读写锁中有两个锁,读锁和写锁,其中读锁就是共享锁,可以被多个线程获取;而写锁就是独占锁,在写锁被占用的时候,读锁也不能获取。
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
写锁和其他锁(读锁和写锁)属于互斥关系,读锁和读锁可以共享,但是读锁和写锁也属于互斥关系,下面写了个例子,有兴趣的人可以跑一下。
SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd HH:mm:ss");
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " try get readLock");
lock.readLock().lock();
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " get readLock");
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " try get readLock");
lock.readLock().lock();
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " get readLock");
Thread.sleep(2000);
lock.readLock().unlock();
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " release readLock");
Thread.sleep(1000);
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " try get readLock");
lock.readLock().lock();
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " get readLock");
lock.readLock().unlock();
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " release readLock");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
Thread.sleep(1000);
lock.readLock().unlock();
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " release readLock");
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " try get writeLock");
lock.writeLock().lock();
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " get writeLock");
Thread.sleep(2000);
lock.writeLock().unlock();
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " release writeLock");
Thread.sleep(10000);
运行结果:
2018-05-10 11:07:11 main try get readLock
2018-05-10 11:07:11 main get readLock
2018-05-10 11:07:11 Thread-1 try get readLock
2018-05-10 11:07:11 Thread-1 get readLock
2018-05-10 11:07:12 main release readLock
2018-05-10 11:07:12 main try get writeLock
2018-05-10 11:07:13 Thread-1 release readLock
2018-05-10 11:07:13 main get writeLock
2018-05-10 11:07:14 Thread-1 try get readLock
2018-05-10 11:07:15 main release writeLock
2018-05-10 11:07:15 Thread-1 get readLock
2018-05-10 11:07:15 Thread-1 release readLock
乐观锁/悲观锁
使用乐观锁和悲观锁,我个人理解其实关注点是对待一个可能出现竞争的问题的态度,如果我们认为这个操作只有少数情况下才会出现并发问题,那么使用一个乐观锁会更优。如果并发比较高,使用悲观锁就是更好。
乐观锁:乐观锁态度认为不加锁通常都不会有竞争,如果有竞争采用失败重试的方式循环操作直到成功,通常乐观锁采用一种原子更新+重试的方式,比如数据库增加版本号,通过update + where 版本号信息进行更新,如果update返回行数为0,则进行重试。
悲观锁:悲观锁态度认为竞争总是存在,所以每次都直接加锁进行操作。
偏向锁/轻量级锁/重量级锁
偏向锁/轻量级锁/重量级锁属于JVM锁优化的东西,和上面的锁不属于一个维度,我们在编程的时候无法察觉。
偏向锁:目的是在消除数据在无竞争的情况下的同步,从而提高系统的性能。在第一个线程获得到一个对象的锁时,在没有其他线程访问来获取锁之前,该锁是偏向锁,该线程不会同步数据到主存,减少了同步(加锁)的开销。在JVM启动参数增加-XX:+UseBiasedLocking会JVM会启动偏向锁,JDK1.6之后默认是开启的。
轻量级锁:目的是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。偏向锁是当只有一个线程使用一个锁的时候JVM进行的锁优化,但是当有另一个线程来获取锁的时候,偏向锁就不再有效,会膨胀成轻量级锁。轻量级锁是采用CAS将当前线程的栈帧中的信息和锁对象头中的信息交换来实现加锁,减少了互斥带来的开销。
重量级锁:偏向锁和轻量级锁都属于没有锁竞争的情况下的锁优化,一旦出现两个线程都需要同一个锁(发生了锁竞争),锁就会膨胀成重量级锁。
关于JVM的锁优化知识这里不详细介绍,还有自旋锁/锁消除/锁粗化等。关于锁优化的知识,有兴趣的可以阅读《深入理解Java虚拟机》周志明版的高效并发章节。