1、锁的分类
你是不是听说过什么乐观锁、悲观锁、可重入锁、不可重入锁、共享锁、独占锁等等一堆锁,听到头皮发麻?
其实 Java 的锁也是按照不同的标准来分类的。
2、乐观锁和悲观锁
悲观锁很悲观,怕出错,总考虑最坏的情况,线程来访问就锁死,不让其他线程来访问,直到锁释放。
乐观锁很乐观,不怕出错,不担心别人修改,所有线程一起上。
两者的流程:
两者的区别:
3、公平锁和非公平锁
公平锁很公平,当一个线程获取了资源并上锁,后面接着来的线程依次排队,当资源的锁释放时,后面的线程依次访问,很公平。
非公平锁很不公平,排队的线程看谁执行的效率高就插队去访问资源,不管它是不是先来的。
4、可重入锁和非可重入锁
可重入锁,也叫递归锁,是指在同一个线程的最外层方法获取锁的时候,再进入该线程的内层方法时会自动获取锁。(前提是锁对象是同一个或者是class对象)
ReentrantLock
和synchronized
都是可重入锁。
class MyClass {
public synchronized void method1() {
doSth();
}
public synchronized void method2() {
doAnother();
}
}
当一个线程执行到某个synchronized
方法时,比如说method1
,在method1
中又会调用另外一个synchronized
方法method2
,此时线程不必重新去申请锁,而是可以直接执行方法method2
。
如果不是可重入锁的话,method2
可能不会被当前线程执行,可能造成死锁。
用自旋锁来模拟,代码如下:
public class UnreentrantLock {
private AtomicReference<Thread> owner = new AtomicReference<Thread>();
public void lock() {
Thread current = Thread.currentThread();
//这句是很经典的“自旋”语法,AtomicInteger中也有
for (;;) {
if (owner.compareAndSet(null, current)) {
return;
}
}
}
public void unlock() {
Thread current = Thread.currentThread();
owner.compareAndSet(current, null);
}
}
同一线程两次调用lock()
方法,如果不执行unlock()
释放锁的话,第二次调用自旋的时候就会产生死锁。
5、自旋锁
上面举例非可重入锁时拿自旋锁来当例子,那什么是自旋锁呢?
所谓自旋锁,就是一个线程拿不到锁时,不会进入阻塞状态,而是原地空转等待,直到锁释放。如果等待超过一定时间,则进入阻塞状态。循环多少次可以认为指定,也可以自适应,自适应的也叫做自适应旋转锁。
下面是自旋锁的例子:
public class SpinLock {
private AtomicReference<Thread> cas = new AtomicReference<Thread>();
public void lock() {
Thread current = Thread.currentThread();
// 利用CAS
while (!cas.compareAndSet(null, current)) {
// DO nothing
}
}
public void unlock() {
Thread current = Thread.currentThread();
cas.compareAndSet(current, null);
}
}
当线程 A 进入 lock()
方法后,while 语句里设置成功,加上取反符号,因此不会进入 while 内部,直接结束 lock()
方法。此时cas
的值为线程 A。
然后线程 B 进入lock()
方法,while 判断为 true,导致自己直接在这里不断判断循环。
当线程 A 调用unlock()
方法,把cas
的值又变为了 null。此时,在那里空转的线程 B 突然判断出cas.compareAndSet(null, current)
为 true,加上取反,于是跳出 while 循环,此时cas
的值变为线程 B,我们也叫做:线程 B 拿到了锁。
这就是完整的自选锁原理,利用的是 CAS 机制,即比较和交换(Compare And Swap)。
CAS 锁主要存在以下问题:
1、如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
2、本身无法保证公平性,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。
3、无法保证可重入性。基于自旋锁,可以实现具备公平性和可重入性质的锁。
6、阻塞锁
上面说的自旋锁,就是等待锁释放的线程不会进入阻塞状态,而是在原地空转。
阻塞锁在等待锁释放期间会进入阻塞状态,等待被唤醒。
二者的区别:
7、只升不降的锁
锁主要存在四种状态:“无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态”。
其实这四种状态都不是Java语言中的锁,而是 Jvm 为了提高锁的获取与释放效率而做的优化(使用synchronized时)。
它们会随着竞争的激烈而逐渐升级,并且是不可逆的升级。
升级过程是这样的:偏向锁 -> 轻量级锁 -> 重量级锁。
关于无锁:如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的。它没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。CAS算法 即compare and swap(比较与交换),就是有名的无锁算法。
8、独享锁和占有锁
可以参考:读写锁