乐观锁

认为竞争不会经常发生。任务读多写的少,因此拿数据不上锁。但是在更新的时候会判断在此期间有没有人去更新这个数据,采取在写时先读取版本号,然后加锁操作,比较上一次版本号,如果一样则更新,如果失败则重复比较写的操作。适用于多读场景
java 中的乐观锁都是通过 CAS 操作的, CAS 是一种更新的原子操作,比较当前值跟穿入值是否一样,一样则更新,否则失败。

悲观锁

认为竞争经常发生,认为写的多,遇到并发的可能性高,所以每次拿数据都会上锁,这样别人读写这个数据的 Block ,直到拿到锁,
java 中的悲观锁就是 synchroinized , AQS 框架下的锁则是先尝试 CAS ,去获取锁,获取不到才转换为悲观锁,如ReentrantLock

Synchronized

会导致争不到锁的线程进入阻塞状态,是 java 中的一个重量级锁,为了性能,JDK1.5,引入轻量级锁与偏向锁,默认启用了自旋锁,他们都是乐观锁

  1. 如果一个对象多个 synchronized 方法,那么只要一个线程访问了其中一个 synchronized 方法,其他现在就不能访问这个对象的任意一个 synchronized 方法。
  2. 可以修饰方法(同步方法),或者代码块(同步代码块),作用域是当前对象。
  3. 不能继承,基类中的 synchronized 方法并不能能自动的转换到子类中
  4. 细分, synchronized 可以作用于 instance 变量, object reference 对象引用, static 函数和class literals(类名称字面量,例如String.class )身上
  5. 无论是修饰方法或者代码快,取得的锁都是对象,而不是一个方法或者代码块,而且同步的方法还很可能被其他线程的对象访问
  6. 每个对象只有一个锁 lock 与之关联
  7. 实现同步是需要很大的系统开销的,可能造成死锁,所以尽量避免无所谓的同步控制

同步方法实质上是把 synchronized 作用在对象引用上
死锁是线程间相互等待锁造成的,在实际中发生概率很小,真让写一个死锁,并不一定好使, 但是程序一旦发生死锁,必将死掉。

  • 一个线程获取了该对象的锁之后,其他线程来访问其他synchronized实例方法现象 ,则需要等待这个线程先把锁释放。因为实例方法锁对象都是这个实例。
  • 两个线程实例化两个不同的对象,但是访问的方法是静态的,两个线程发生了互斥(即一个线程访问,另一个线程只能等着),因为静态方法是依附于类而不是对象的,当synchronized修饰静态方法时,锁是class对象。

自旋锁

如果持有锁的线程在很短时间内能释放锁,那么等待锁的线程就不要进行内核态与用户态之间的切换进入阻塞状态,他们只需要等一等(自旋),持有锁的线程释放锁资源后就可以立即获得锁,这样就避免用户线程和内核线程之间的切换了

Lock

在java.util 包中,一共有三个实现类

  • ReentrantLock
  • ReentrantReadWriteLock.ReadLock
  • ReentrantReadWriteLock.WriteLock

主要目的和 synchronized 一样,都是解决同步问题,处理资源争端而产生的艺术。功能类似但是有一些区别

  1. Lock 更灵活,可以自由定义多把锁的加锁解锁顺序, synchronized 要按照先加后解顺序。
  2. 提供多种加锁方案,
    • lock 阻塞式,
    • tyrlock 无阻塞式,
    • lockInterruptily 可打断时,
    • trylock 带超时时间版本,
  3. Lock的性能更高
  4. 能力越大,责任越大,必须控制好加锁和解锁,否则会导致灾难。

ReentrantLock 可重入锁

使用方法

  1. 创建一个实例 ReentrantLock r=new ReentrantLock()
  2. 加锁 r.lock() 或者 r.lockInterruptily()
    • lockInterruptily() 可被打断,当线程 a lock 后,线程 b 阻塞,此时如果是 lockInterruptily ,那么调用b.interrupt(), b 线程退出阻塞,并放弃对资源的争抢,进入 catch 块,如果使用 lockInterruptily ,必须 throw interruptable exception 或catch
  3. 释放锁 r.unlock(),必须要做,要放到 finally 里面,防止异常跳出正常流程,导致灾难。

ReentrantReadWriteLock 可重入读写锁

ReentrantLock 是防止线程 A 在读数据,线程 B 写数据造成的数据不一致,那么两个线程都是读数据,就没必要加锁了,所以读写锁 ReadWriteLock 就产生了,而 ReentrantReadWriteLock 是 ReadWriteLock 的具体实现,实现了读写分离,读锁是共享,写锁是独占,读和读之间不会排斥,读和写,写和读,写和写之间才会排斥。提升了读写性能

ReentrantReadWriteLock lock = new ReentrantReadWriteLock()
ReadLock r = lock.readLock();
WriteLock w = lock.writeLock();

两个都有 lock , unlock 方法,

Condition

使用 await() , signal() , signalAll() 替换 Object 中的 wait() , notify() , notifyAll() ,传统的线程的通信方式, Condition 都可以实现,注意 Condition 绑定在 Lock 上的,要先创建一个 Lock 对象,然后创建 Condition 需要执行 Lock 的 newCondition() 方法,
Condition 的强大之处:

  • 可以为不同的线程创建多个 Condition ,而sychronized/wait()只有一个阻塞队列。 notifyAll() 唤醒所有阻塞队列的线程,而使用lock/Condition,可以实现多个阻塞队列, signalAll() 只会唤起某个阻塞队列下的线程。

搬运地址:

一道多线程面试题引起的自我救赎

java 中的锁 - 偏向锁、轻量级锁、自旋锁、重量级锁