目录

Java主流锁

转载自美团技术

乐观锁VS悲观锁

悲观锁

认为使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁

乐观锁

认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)

乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法

悲观锁适合写操作多的场景

乐观锁适合读操作多的场景

自旋锁VS适应性自旋锁

互斥同步对性能最大的影响就是阻塞的实现 因为 有关线程的挂起与恢复 涉及到了 内核的陷入 于是就给系统的并发性能带来了 一些压力 同时对于某些情况 共享数据的锁定 只会持续很小的一段时间 为了这一小段时间 而去采用挂起和恢复 实在是不划算 这时候就出现了 自旋锁: 让后面的请求的进程进行自旋 不放弃处理器的执行时间 正因为自旋是不会放弃处理器的运行时间

自旋锁

阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间 若同步代码块的内容过于简单 容易得不偿失 干脆让后面的请求的进程进行自旋 不放弃处理器的执行时间

缺点

不能代替阻塞,虽然避免了线程切换的开销,但它要占用处理器时间

如果被占用的时间很短,自旋等待的效果就会非常好,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源 限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改) 超过次数没有成功得到锁 则就要挂起线程

底层也是CAS

自适应锁

自适应说明自旋次数 不是固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定

  • 若自旋等待的锁成功获得 并且还在运行 虚拟机会认为很有可能再成功 进而允许自旋等待持续相对更长的时间
  • 那在以后尝试获取这个锁时将可能省略掉自旋过程直接阻塞线程,避免浪费处理器资源

无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁

四种锁是指锁的状态,专门针对synchronized的

无锁

没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功

偏向锁

一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价

轻量级锁

当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能

重量级锁

升级为重量级锁时,锁标志的状态值变为“10” ,此时等待锁的线程都会进入阻塞状态

公平锁VS非公平锁

公平锁

多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁

  • 优点: 等待的锁不会饥饿
  • 缺点: 整体吞吐效率相对非公平锁要低

非公平锁

多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待 有可能出现后申请锁的线程先获取锁的场景

可重入锁 VS 非可重入锁

可重入锁

又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞

非可重入锁

再进行该线程的内层方法会是释放原来的锁 等待获取当前的锁、

独享锁 VS 共享锁

独享锁

也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据

共享锁

指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据

附加

CAS算法

CAS(Compare-and-Swap),即比较并替换,是一种实现并发算法时常用到的技术,Java并发包中的很多类都使用了CAS技术

  public static void increase(){
        race.getAndIncrement();
    }
    public static AtomicInteger race = new AtomicInteger(0);

getAndIncrement方法调用getAndAddInt方法,最后调用的是compareAndSwapInt方法,即CAS

getAndAddInt方法解析:拿到内存位置的最新值v,使用CAS尝试修将内存位置的值修改为目标值v+delta,如果修改失败,则获取该内存位置的新值v,然后继续尝试,直至修改成功

CAS算法涉及到三个操作数:

  • 需要读写的内存值 V
  • 进行比较的值 A。
  • 要写入的新值 B。

CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作

源码分析:

mp是“os::is_MP()”的返回结果,“os::is_MP()”是一个内联函数,用来判断当前系统是否为多处理器。

  • 如果当前系统是多处理器,该函数返回1。
  • 否则,返回0。

LOCK_IF_MP(mp)会根据mp的值来决定是否为cmpxchg指令添加lock前缀。

  • 如果通过mp判断当前系统是多处理器(即mp值为1),则为cmpxchg指令添加lock前缀。
  • 否则,不加lock前缀。

这是一种优化手段,认为单处理器的环境没有必要添加lock前缀,只有在多核情况下才会添加lock前缀,因为lock会导致性能下降。cmpxchg是汇编指令,作用是比较并交换操作数。

对于lock:

  1. 确保对内存的读-改-写操作原子执行 带lock前缀的命令会锁住总线 volatile底层就是用lock实现
  2. 禁止该指令与之前和之后的读和写指令重排序
  3. 把写缓冲区中的所有数据刷新到内存中