锁的种类有:乐观锁与悲观锁、公平锁与非公平锁、独占锁与共享锁、可重入锁、自旋锁等。
乐观锁与悲观锁
乐观锁与悲观锁原本是数据库的概念,Java中借用了这个思想。
悲观锁:指多线程的情况下,每次访问数据一定会被其他线程修改,因此每次访问都要加锁,并在整个数据处理过程中,都要锁定数据,阻塞其他线程。数据库的悲观锁一般借助数据库的锁机制,即加上排他锁。
乐观锁:相对于悲观锁认为每次访问数据都要加锁,乐观锁认为一般数据不会发生冲突,因此访问数据不加排他锁,而是处理完数据,更新数据时检测是否发生了数据冲突。
乐观锁的实现可以借助“版本version”这一概念,更新数据时判断当前的数据版本是否是version,如果是则version+1,否则表示数据已经被其他线程更新。
公平锁与非公平锁
根据线程获取锁的抢占机制,分为公平锁与非公平锁。
公平锁指线程获取锁的顺序由线程请求锁的顺序决定,即先到先得。
非公平锁指线程获取锁的顺序由线程的优先级决定,即先到不一定先得。
Java的ReentrantLock提供了公平锁与非公平锁的实现:
公平锁:ReentrantLock fairLock = new ReentrantLock(true);
非公平锁:ReentrantLock unfairLock = new ReentrantLock(false) 或
ReentrantLock unfairLock = new ReentrantLock();
一般情况下使用非公平锁,公平锁会造成性能损耗。造成这个差异的原因是:非公平锁按优先级抢占,后请求的锁可能在先请求锁恢复休眠的时间内抢到锁,提高并发性能。
独占锁与共享锁
按锁被单个线程持有还是多个线程共享将锁分成独占锁与共享锁。
独占锁:指任何时候只有一个线程持有的锁,其他线程获取将被阻塞,ReentrantLock就是独占锁。独占锁属于悲观锁,每次访问资源都要加上互斥锁,影响并发性,但是也保证了数据的一致性。
共享锁:指可以被多个线程共同持有,如ReadWriteLock读写锁的读锁。而共享锁属于乐观锁,一般用于数据的读取操作,不用于修改。
可重入锁
可重入锁:如果一个线程想获取它已经获取但还没有释放的锁时,可以再次请求通过。synchronized就是可重入锁,实现原理是内部有一个标记表示当前线程,然后关联一个计数器,用于统计线程的进入次数,每次进入+1,退出-1。
自旋锁
Java线程与操作系统的线程一一对应,因此每次切换都涉及到内核态与用户态的切换,切换操作开销较大,影响性能。自旋锁的意思是当线程尝试获取锁失败后不立刻阻塞自己,而是在不放弃CPU的情况多试几次(默认10次),很有可能在后面的几次尝试中获取到锁,如果超过这个次数仍没有得到锁则将自己挂起。
自旋锁减少了内核状态切换造成的开销,但是也可能会浪费CPU时间。