掌握之并发编程-3.锁

掌握高并发、高可用架构

第二课 并发编程

从本课开始学习并发编程的内容。主要介绍并发编程的基础知识、锁、内存模型、线程池、各种并发容器的使用。

创新互联公司坚持“要么做到,要么别承诺”的工作理念,服务领域包括:成都网站设计、网站制作、企业官网、英文网站、手机端网站、网站推广等服务,满足客户于互联网时代的平鲁网站设计、移动媒体设计的需求,帮助企业找到有效的互联网解决方案。努力成为您成熟可靠的网络建设合作伙伴!

第三节 锁

并发编程 并发基础 AQS Synchronized Lock

这小节咱们来学习并发编程中锁的知识。主要包括关键字synchronized、各种LockAQS的原理、以及各自的应用。

synchronized

可以修饰方法或者代码块

表示多个线程访问该方法或者代码块时要进行排队,串行的执行该方法或者代码块

执行效率低,但是它是并发编程容器的基础

分类具体分类被锁的对象示例代码说明
方法 实例方法 类的实例对象 synchronized void methodA() {};
void methodB() {};
synchronized void methodC() {};
线程调用了同步方法,
别的线程可以调用非同步方法,
对于其他同步方法,必须该方法在执行完成后才能调用
不影响静态方法的调用(包括同步,非同步)
静态方法 类对象 static synchronized void methodA() {};
static void methodB() {};
static synchronized void methodC() {};
线程调用了同步方法,
别的线程可以调用非同步方法,
对于其他同步方法,必须该方法在执行完成后才能调用
不影响对象方法的调用(包括同步,非同步)
代码块 实例对象 类的实例对象 synchronized(this) {} 同上
class对象 类对象 synchronized(SynchronizedTest.class) {} 同上
任意实例对象 实例对象Object Object lock = new Object();
synchronized(lock) {}
只影响锁住的对象,而不影响类和类的实例对象
synchronized的实现机制

JAVA对象头Monitor是实现synchronized的基础。

  1. JAVA对象头,对于Hotspot虚拟机的对象头主要包含两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)

    • Mark Word,用于存储对象自身的运行时数据,如哈希码(Hash Code)、GC分代年龄、锁状态标识、线程持有的锁、偏向线程ID、偏向时间戳等
    • Klass Pointer,是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
  2. Monitor,是一种同步机制,即同一时刻只允许一个线程进入Monitor的临界区,从而达到互斥的效果。synchronized的对象锁,其指针指向的是一个Monitor对象的起始地址。每个对象实例都有一个monitor。由C++实现,其数据结构如下。

    ```C++
    ObjectMonitor() {
    _count = 0;
    _owner = NULL;
    _waitSet = NULL;
    _waitSetLock = 0;
    _EntryList = NULL;
    }

    
    
    其中,**_owner**指向持有ObjectMonitor对象的线程。当多个线程同时访问一段同步代码时,会把线程存放在锁的对象的**_EntryList**中。当某个线程获得对象的Monitor时,就会把*_owner*的值设置为当前线程,同时*_count*加1。如果线程调用**wait()**方法,就会释放当前持有的Monitor,*_owner*置为null,*_count*减1,并将该线程放入**_waitSet**中。当然,如果持有monitor的线程正常执行完毕,也会释放monitor,*_owner*置为null,*_count*减1。

对于加在代码块上的synchronized,其字节码是:一次monitorenter、两次monitorexit(含有一次编译器自动生成的异常处理的monitorexit);

对于加在方法上的synchronized,其字节码是:标识方法为ACC_SYNCHRONIZED

新特性

synchronized是一个重量级锁,相较Lock,比较笨重,不高效。在JDK1.6中,其实现过程引入了大量的优化,如自旋锁自适应自旋锁锁消除锁粗化偏向锁轻量级锁等技术来减少锁的开销。

  • 自旋锁,是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断判断锁是否能够成功获取,直到获取到锁才退出循环。

    优点:自旋锁不会使线程发生状态切换,而是一直处于活动状态,不会进入阻塞状态,减少了不必要的上下文切换,执行速度快

    缺点:如果某个线程持有锁的时间过长,就会导致其他等待获取锁的线程进入循环等待,消耗CPU。如果使用不当会导致CPU使用率极高;不公平的锁会导致“线程饥饿”问题

  • 自适应自旋锁,JDK1.6引入的更聪明的自旋锁。就是说自旋的次数不是固定的,它是由前一次在同一个锁上的自旋时间以及锁的持有者的状态决定的。JVM自适应的调整自旋次数,使能更有效的获取到锁,避免浪费资源

  • 锁消除,当JVM检测到不可能存在共享数据竞争,此时会对这些锁进行锁消除

  • 锁粗化,在使用锁时,需要让同步代码块的作用范围尽可能小。所谓锁粗化,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁

  • 偏向锁,是指同一段代码一直被同一个线程访问,那么该线程会自动获取到锁,从而降低获取锁的代价

  • 轻量级锁,当锁是偏向锁的时候,此时被另一个线程访问,偏向锁会升级为轻量级锁。其他线程会通过自旋的形式尝试获取锁,不会阻塞

  • 重量级锁,当锁是轻量级锁时,另一个线程虽然在自旋等待获取锁,但是自旋不会一直持续,当自旋一定次数后,还没有获取到锁,就会进入阻塞,该锁也会膨胀为重量级锁。重量级锁会让其他线程进入阻塞,性能降低。此时就成为了原始的synchronized的实现
锁的等级

依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态

Lock

Lock是一个接口,有以下方法。

public interface Lock {
    void lock();
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    void lockInterruptibly();
    Condition newCondition();
}

这里说下方法void lockInterruptibly()一个线程获得锁之后是不可以被interrupt()方法中断的,是不能中断正在执行中的线程的,只能中断阻塞过程中的线程lockInterruptibly方法允许当线程等待获取锁的过程中由其他线程来中断等待。

Lock 和 synchronized的区别与相同点

区别:

  • Lock是一个接口,synchronized是内置关键字;Lock只能在代码块中,而synchronized可以在方法上和代码块中
  • synchronized不管正常退出还是异常退出,都会自动释放锁资源,不会导致死锁的发生;Lock需要手动加锁和解锁,所以如果解锁操作未执行就会导致死锁
  • Lock可以让等待锁的线程响应中断,而synchronized不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断
  • 通过Lock可以知道有没有获取到锁,而synchronized无法知道
  • Lock可以选择公平锁和非公平锁,而synchronized只有非公平锁
  • Lock是乐观锁,通过CAS(compare and swap 比较交换)来实现,底层是AbstractQueuedSynchronizer(AQS),而synchronized是悲观锁,是由JVM来控制的
  • Lock的锁是针对lock对象,而synchronized的锁定对象是类或者指定对象
  • Lock可以提高多个线程进行读操作的效率

相同点:

  • 都是可重入锁
ReentrantLock

可重入锁,是Lock接口的唯一实现类。

可重入锁:是指如果一个线程获得了一个对象的锁,那么它不需要再获取该对象的锁而可以直接执行方法。也就是锁的分配机制是基于线程来分配的,而不是基于方法调用的分配。

可中断锁:可以响应中断的锁。只有lockInterruptibly()方法的锁是可中断锁,lock()还是不可中断的

公平锁:指尽量以请求锁的顺序来获取锁。比如有多个线程在等待一个锁,当锁被其他线程释放时,最先请求锁的线程会获得该锁

非公平锁:无法保证锁的获取是按照请求锁的顺序进行的。可能导致某个或者一些线程永远获取不到锁

对于ReentrantLock,默认是非公平锁,但可指定为公平锁。

ReentrantLock lock = new ReentrantLock(true)

ReentrantLock中定义了两个内部类,一个是NotFairSync,一个是FairSync。当构造器参数为true时,表示创建公平锁,参数为false或者无参时,表示非公平锁。

ReadWriteLock
public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

一个用来获取读锁,一个用来获取写锁。

ReentrantReadWriteLock

可重入读写锁,实现了ReadWriteLock接口。

多个线程同时进行读操作时,会使多个线程交替进行,从而提高读操作的效率。但是,如果有线程占用读锁,此时其他要获取写锁的话,就必须等待读锁释放后才可执行;如果有线程占用写锁,此时其他线程不管是要获取读锁或者写锁的话,都必须等待写锁释放。

Lock的实现原理

ReentrantLockFairSyncNotFairSync都继承了AbstractQueuedSynchronizer,并且真正lock()unlock()的实现过程都是在AQS中。

首先,AQS的数据结构是:一个表示锁状态的变量volatile int state,取值范围是 0 无锁、1 有锁;一个用于存储等待获取锁的线程的双向链表transient volatile Node headtransient volatile Node tail

其次,加锁流程NotFairSync.lock()是:

  1. 通过CAS去尝试获取锁:判断当前state是0的话表示无锁,然后把当前线程设置为独占执行线程,再修改state为1表示有锁
    掌握之并发编程-3.锁

  2. 如果第一步获取锁失败,那么执行acquire(1)(这是AQS的方法)

掌握之并发编程-3.锁

主要是三个方法:

  • tryAcquire,再次尝试通过CAS获取一次锁(子类NotFairSync的方法)
    • 判断state,0则可以获取到锁,非0则判断执行线程是否是当前线程,如果是,则把重入次数加1,都不是则设置获取锁失败

掌握之并发编程-3.锁

  • addWaiter,把当前线程放入等待队列的双向链表Node中(通过无限循环-自旋,找到链表尾,接到尾部)
  • acquireQueued,通过自旋,判断当前线程是否到达链表头部,当到达头部时,就尝试获取锁。如果获取到锁就把其从头部移除
    • 在自旋过程中,通过shouldParkAfterFailedAcquire判断当前线程的状态是CancelledSignal等,从而在parkAndCheckInterrupt中对线程进行剔除(Cancelled)、阻塞(Signal)操作

掌握之并发编程-3.锁
下面借两张图(

掌握之并发编程-3.锁cdn.com/71e8b71038243dfaf21ebcf6f9fcc5fbaa659b08.png">

获取锁的流程:

掌握之并发编程-3.锁

然后,解锁的流程是调用NotFairSync.release(),主要是对重入数量的调整。每次释放锁都只会对数量减1,直到state为0时表示锁释放完成。
掌握之并发编程-3.锁

Lock和synchronized的选择

根据CAS的特性,建议在低锁冲突的情况下使用Lock

在JDK1.6后,官方对synchronized做了大量的优化(偏向锁、自旋、轻量级锁等),因此在非必要的情况下,建议都使用synchronized做同步操作


本文名称:掌握之并发编程-3.锁
标题路径:http://myzitong.com/article/jshpjg.html