乐观锁和悲观锁
- 悲观锁:对于同一个数据的并发操作,悲观锁认为在使用数据时,一定会有别的线程来修改数据,因此在获取数据时,会先加锁,确保数据不会被别的线程修改。synchronized 关键字和Lock 的实现类都是悲观锁。
- 乐观锁:对于同一个数据的并发操作,乐观锁认为在使用数据时,不会有别的线程来修改数据,所以不会添加锁。只是在更新数据时,去判断之前有没有其他线程更新这个数据。有,则报异常或自动重试;没有,则正常写入数据。CAS算法(Java原子类中的递增操作就是CAS 自旋实现的)。
自旋锁
- 线程阻塞或唤醒,需要操作系统切换CPU 状态来完成,这种状态转换需要耗费处理时间。在许多场景中,同步资源的锁定时间很短,为了这小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。
- 自旋锁
- 为了让线程“稍等一下“,需要让当前线程进行自旋,如果在自旋完成后,前面锁定的同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销,这种处理方式就叫 自旋锁。
- 自旋锁虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用时间很短,自旋效果好;反之,自旋的线程会一直占用CPU,消耗处理资源。所以自旋等待的时间必须要有一定限度,如果自旋超过了限定次数没有成功获得锁,就应当挂起线程。
- 自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe 进行自增操作的源码实现do-while 就是一个自旋操作。
自适应自旋锁(适用性自旋锁)
- 自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
- 如果在同一个锁对象上,自旋等待刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。
- 如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时,将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
可重入锁
- 又名递归锁,是指同一个线程在外层方法获取锁时,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者类),不会因为之前已经获取过还没释放而阻塞。
- Java中ReentrantLock 和synchronized 都是可重入锁。
- 优点是可一定程度避免死锁。
- ReentrantLock的FairSync和NonfairSync 都继承父类AQS,其父类AQS 中维护了一个同步状态state字段(private volatile int state;)来计数重入次数,初始值为0;
- 当线程尝试获取锁时,可重入锁先尝试获取并更新state 值,如果state=0,表示没有其他线程在执行同步代码,则把state 置为1,当前线程开始执行;
- 如果state !=0,则判断当前线程是否是获取到这个锁的线程,如果是,则执行 state +1, 且当前线程可再次获取锁;
- 而非可重入锁是直接去获取并尝试更新当前state 的值,如果state != 0的话,会导致其获取锁失败,当前线程阻塞。
公平锁和非公平锁
- 公平锁:
- 指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。
- 优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
- 非公平锁:
- 非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。
- 优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获取锁,CPU 不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
- ReentrantLock类默认是采用非公平锁,但可通过构造函数,初始化指定使用公平锁。
- ReentrantLock类中有一个内部类Sync,Sync 继承于AbstractQueuedSynchronizer类,添加锁和释放锁的大部分操作实际上都是在Sync 类中实现的。Sync 有公平锁FairSync 和非公平锁NonfairSync 两个子类。