1.乐观锁

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为
别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数
据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
java 中的乐观锁基本都是通过 CAS 操作实现的, CAS 是一种更新的原子操作, 比较当前值跟传入值是否一样,一样则更新,否则失败。

1.1 实现机制

CAS(Compare And Swap 比较并且替换)是乐观锁的一种实现方式,是一种轻量级锁,JUC 中很多工具类的实现就是基于 CAS 的。
线程在读取数据时不进行加锁,在准备写回数据时,先去查询原值,操作的时候比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程。

Java 锁 - 图2

1.2 缺陷

  • 你们看图发现没,要是结果一直就一直循环了,CUP开销是个问题,
  • ABA问题
  • 保证一个共享变量原子操作,多个变量改变无法实现

    1.3 改进

    加标志位,例如搞个自增的字段,操作一次就自增加一,或者搞个时间戳,比较时间戳的值。

    2.悲观锁

    悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人
    会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。
    java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,
    才会转换为悲观锁,如 RetreenLock。 数据库:行锁,表锁等,读锁,写锁等。

    2.1 synchronized与Lock的区别

    image.png
    1、Lock是java的一个interface接口,而synchronized是Java中的关键字,synchronized是由JDK实现的,不需要程序员编写代码去控制加锁和释放;Lock的接口如下:
  1. public interface Lock {
  2. void lock();
  3. void lockInterruptibly() throws InterruptedException;
  4. boolean tryLock();
  5. boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
  6. void unlock();
  7. Condition newCondition();
  8. }

2、synchronized修饰的代码在执行异常时,jdk会自动释放线程占有的锁,不需要程序员去控制释放锁,因此不会导致死锁现象发生;但是,当Lock发生异常时,如果程序没有通过unLock()去释放锁,则很可能造成死锁现象,因此Lock一般都是在finally块中释放锁;格式如下:

  1. Lock lock = new LockImpl; // new 一个Lock的实现类
  2. lock.lock(); // 加锁
  3. try{
  4. //todo
  5. }catch(Exception ex){
  6. // todo
  7. }finally{
  8. lock.unlock(); //释放锁
  9. }

3、Lock可以让等待锁的线程响应中断处理,如tryLock(long time, TimeUnit unit),而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够中断,程序员无法控制;
4、synchronized是非公平锁,Lock可以设置是否公平锁,默认是非公平锁;
5、Lock的实现类ReentrantReadWriteLock提供了readLock()和writeLock()用来获取读锁和写锁的两个方法,这样多个线程可以进行同时读操作;
6、Lock锁的范围有局限性,仅适用于代码块范围,而synchronized可以锁住代码块、对象实例、类;
7、Lock可以绑定条件,实现分组唤醒需要的线程;synchronized要么随机唤醒一个,要么唤醒全部线程。

2.2 公平锁和非公平锁

公平锁:
是指多个线程按照申请锁的顺序来获取锁,类似于排队买饭,先来后到,先来先服务,就是公平的,也就是队列
非公平锁:
是指多个线程获取锁的顺序,并不是按照申请锁的顺序,有可能申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转,或者饥饿的线程(也就是某个线程一直得不到锁)

优缺点:

非公平锁性能高于公平锁性能。首先,在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。而且,非公平锁能更充分的利用cpu的时间片,尽量的减少cpu空闲的状态时间。

3.自旋锁

自旋锁原理非常简单, 如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
线程自旋是需要消耗 cup 的,说白了就是让 cup 在做无用功,如果一直获取不到锁,那线程也不能一直占用 cup 自旋做无用功,所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。