悲观锁使用数据时先加锁,如 java 中的 synchronized 和 Lock
乐观锁不加锁,在更新数据前判断是否有线程更新了这个数据,没有则更新,有则报错或重试。在 java 中,乐观锁是通过无锁编程来实现的,如 CAS 算法,在原子类中的递增操作就是通过 CAS 自旋实现的。
使用场景:
CAS
全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent
包中的原子类就是通过 CAS 来实现了乐观锁。
CAS算法涉及到三个操作数:
当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。
CAS 的问题:
(锁同步资源失败,是否阻塞)
自旋的必要性:对于获取锁失败时,如果先进行阻塞(休眠,让出时间片),等到资源释放时在唤醒,这个过程需要切换 CPU 的状态,这种状态的切换需要耗费处理器时间。对于同步代码块的内容过于简单的情况下,状态切换的时间可能比执行代码地时间还长。让线程自旋等待资源释放,异常来避免线程切换的开销,这就是自旋锁
同理,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源
自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。
适应性自旋锁: 自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,如果自旋刚刚成功并且持有锁的线程正在运行中,这准许自旋更长的瞬间。如果自旋很少成功获得过,则有可能直接阻塞线程,不做或者做少量自旋。
java对象头: 对象头主要由 Mark Word(标记字段)、Klass Pointer(类型指针)构成
Monitor: 可以理解为一个同步工具或一种同步机制,是线程私有的数据结构,每一个线程都有一个可用monitor record列表。每一个被锁住的对象都会和一个monitor关联
锁状态 | Mark Word 存储内容 | 标记 |
---|---|---|
无锁 | 对象的 hashCode、对象分代年龄、是否是偏向锁(0) | 01 |
偏向锁 | 偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1) | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 |
重量级锁 | 指向互斥量(重量级锁)的指针 | 10 |
偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。
公平锁是指多个线程按照申请锁的顺序来获取锁。缺点是吞吐效率低,CPU 唤醒阻塞线程的开销大
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。如果获取锁时刚好锁释放,那么这个线程无需等待直接获取到锁。可以减少线程唤醒的开销,提高吞吐率,但是有可能排队的锁一直等不到锁。
ReentrantLock 默认使用非公平锁。有公平锁FairSync和非公平锁NonfairSync两个子
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中 ReentrantLock
和synchronized
都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
javapublic class Widget {
public synchronized void doSomething() {
System.out.println("方法1执行...");
doOthers();
}
public synchronized void doOthers() {
System.out.println("方法2执行...");
}
}
类中的两个方法都是被内置锁synchronized修饰的,doSomething()方法中调用doOthers()方法。因为内置锁是可重入的,所以同一个线程在调用doOthers()时可以直接获得当前对象的锁,进入doOthers()进行操作。
如果是一个不可重入锁,那么当前线程在调用doOthers()之前需要将执行doSomething()时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。
独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。JDK中的synchronized和JUC中Lock的实现类就是互斥锁
共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
java中的大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于AbstractQueuedSynchronizer(简称为AQS)实现的。
AQS 是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架
ReentrantLock意思为可重入锁,指的是一个线程能够对一个临界资源重复加锁
ReentrantLock 和 Synchronized 的对比
ReentrantLock | Synchronized | |
---|---|---|
锁实现机制 | 依赖 AQS | 监视器模式 |
灵活性 | 支持响应中断、超时、尝试获取锁 | 不灵活 |
释放形式 | 必须显示调用 unlock() 释放锁 | 自动释放监视器 |
锁类型 | 公平锁&非公平锁 | 非公平锁 |
条件队列 | 可关联多个条件队列 | 关联一个条件队列 |
可重入性 | 可重入 | 可重入 |
java// **************************Synchronized的使用方式**************************
// 1.用于代码块
synchronized (this) {}
// 2.用于对象
synchronized (object) {}
// 3.用于方法
public synchronized void test () {}
// 4.可重入
for (int i = 0; i < 100; i++) {
synchronized (this) {}
}
// **************************ReentrantLock的使用方式**************************
public void test () throw Exception {
// 1.初始化选择公平锁、非公平锁
ReentrantLock lock = new ReentrantLock(true);
// 2.可用于代码块
lock.lock();
try {
try {
// 3.支持多种加锁方式,比较灵活; 具有可重入特性
if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ }
} finally {
// 4.手动释放锁
lock.unlock()
}
} finally {
lock.unlock();
}
}
本文作者:Yui_HTT
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!