java进阶篇06、深入理解并发编程--AQS、synchronized

165 阅读8分钟

一、AQS:AbstractQueuedSynchronizer抽象队列同步器

1、AQS是什么:

AQS是用来构建锁或者其他同步组件的基础框架,其内部通过维护了一个int类型的state变量表示同步状态,通过内置的FIFO队列完成线程的排队工作;AQS的主要使用方法是继承,一般在锁或者同步组件的内部实现一个静态内部类,然后继承AQS,实现自己需要实现的特定方法,然后调用AQS中的开放接口实现锁或者同步组件的功能,在AQS内部通过模板方法设计模式对功能进行了封装,我们只需要在自己的类中对需要的方法进行实现就可以;以内部类实现AQS的组件有CountDownLatch、ThreadPoolExecutor、ReentrantLock和ReentrantReadWriteLock等;

2、模板方法设计模式:

定义一个操作内部执行的方法框架,而将方法的具体实现延迟到其子类中去,模板方法使得子类可以在不改变一个算法的结构前提下重定义该算法的某些特定步骤;

举个例子:

蛋糕的制作方法可分为造型、涂抹、烘培;我们在抽象类中定义制作蛋糕的操作步骤为造型、涂抹和烘培,然后因为不同类型的蛋糕具体操作是不一样的,比如我们制作芝士蛋糕时,可以在芝士蛋糕类中重定义三个具体的步骤;此时制造蛋糕的方法框架并没有改变,而方法内的具体操作由子类实现的;

3、AQS中的常用方法:

模板方法:

acquire:独占式获取同步状态;

acquireShared:共享式获取同步状态;

release:独占式的释放同步状态;

releaseShared:共享式的释放同步状态;

getQueuedThreads:获取等待在同步队列上的线程集合;

一般重写方法:

tryAcquire:独占式获取同步状态的具体实现,在acquire中会调用tryAcquire;

tryRelease:独占式释放同步状态的具体实现;

tryAcquireShared:共享式获取同步状态的实现;

tryReleaseShared:共享式释放同步状态的实现;

访问及修改同步状态的相关方法:

state变量是AQS中表示同步状态的int型变量;

setState:设置同步状态;

getState:获取同步状态;

compareAndSetState:使用CAS设置同步状态,保证设置操作的原子性;

4、CLH队列锁:AQS的基本思想

队列锁是一种自旋锁,不断自旋轮询前驱的状态,假设发现前驱释放了锁就结束自旋并获得锁;

执行逻辑:通过一个个线程节点组成一个链表,想获取锁的线程会把自己封装成一个线程节点,节点内部维护一个指向等待获取锁的线程的链表尾部的指针,还维护一个locked变量,表示是否释放锁。

当线程A想要获取锁时,首先将指针指向链表尾部,然后将locaked置为true,表示等待获取锁,线程B想要获取锁时执行同样的操作;当线程A前一个节点释放锁后,会将其locked变量置为false,然后线程A停止轮询上一个节点的locked变量,并获得锁,当线程A操作完成之后释放锁,线程B再通过同样的方式获得锁;

5、ReentrantLock的实现:

可重入锁的基本实现:

可重入锁是指一个线程在获得锁的状态下再次获取该锁而不会被阻塞,再次获得锁的前提是先判断当前线程是否是获得锁的线程,当获得锁时,会将state状态变量+1,每重入一次就加一次1,释放一次锁将state变量-1,在state变量减为0的时候才算完全释放掉锁;

公平锁和非公平锁:

在构造ReentrantLock时通过传入true可以将其设置为公平锁;

对于非公平锁,只要当前CAS设置同步状态成功,就表示获取到锁;但是对于公平锁,他在获取同步状态的时候多一个判断条件,hasQueuedPredesessor,如果该方法返回true说明有更早的线程申请锁,因此需要等待前驱节点释放锁之后才能竞争获取锁;

二、synchronized的实现原理

1、synchronized同步代码块

反编译class文件发现,同步代码块的实现原理是通过一对MonitorEnter和MonitorExit指令实现的,在进入同步代码块之前需要执行monitorEnter指令,用于获取monitor对象的锁状态,在同步代码块执行完成或者方法执行完成或者抛出异常都会执行monitorExit指令,用于释放monitor对象的锁状态;

如果monitor对象的进入数为0,则请求线程直接获得锁并且将进入数置为1;如果该线程已经持有锁,再次获得锁会将进入数加1,退出一次同步代码块将monitor对象的进入数减1,只有monitor进入数为0时其他线程才可以竞争获得锁;

2、synchronized同步方法

反编译class文件发现同步方法没有monitorEnter和monitorExit指令,而是在常量池中多了一个ACC_SYNCHRONIZED标识符,JVM再执行方法时如果发现有此同步标识符,内部也会通过monitor对象的进入数来获取和释放锁;

3、synchronized使用的锁

synchronized使用的锁是存放在java对象头里面,对象头有两部分组成,一个是MarkWord,用于存储锁的状态、hashcode、GC状态等;另一个是KlassPointer,指向对象所属类的类型的指针;如果是数组类型还会多一部分用于执行数组的长度;

锁信息存放在MarkWord部分中,此部分一般用来存储hashcode和GC信息等;但会随着对象的运行改变而发生变化,变成用来存储各种锁状态

三、java内存模型JMM与并发问题联系

1、工作内存和主内存

每个线程会对应自己的工作内存,对应物理设备一般为cpu寄存器或者高速缓存,而主内存一般代表内存条;可以简单类比JVM中的内存模型,线程独享的栈内存比作工作内存,线程共享的堆内存比作主内存;

2、JMM导致的并发安全问题

count++操作

表面上看count++操作很简单,其实内部的实现过程需要取数据,相加,写回主内存等,当不同的线程去执行这个操作时,就会产生并发安全问题;

3、volatile详解:最轻量级的同步操作

volatile保证可见性、有序性,但不保证原子性;例如上面的count++,即使我们将count设置为volatile,仍然存在并发安全问题,因为++操作不是原子操作;

volatile的底层原理:有volatile修饰的变量进行写操作的时候会使用cpu提供的lock前缀指令;将当前线程缓存行的数据写回到系统内存,这个写回操作会使其他线程中缓存的变量失效,如果要使用需要重新从主内存中获取;

4、volatile关键字在DCL上的作用:

在单例模式下的双重加锁检查中,一般用volatile关键字修饰单例对象,主要使用volatile的可见性和有序性,一是为了用于对变量的改变能够立即对其他线程可见;二是使用有序性禁止指令重排,防止先返回null又指向对象的情况发生;

四、锁的四种状态

存在四种锁状态,无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,锁的状态会随着竞争状态的加剧而升级,状态只能升级不能降级,多种状态切换是为了提高获得锁和释放锁的效率;

1、无锁状态:

顾名思义,没加锁,不存在锁的竞争和释放问题;

不锁住资源,多个线程只有一个能修改资源成功,其他线程会重试;类似CAS机理;

2、偏向锁状态:

程序在大多数情况下一个锁总是由一个线程持有,这个时候就可以运行在偏向锁状态,此时在对象头的Markwork部分会保存状态为偏向锁状态,并记录线程id,当某个线程执行代码时,检查当前线程id是否相等,相等则直接运行,如果不相等并且线程间存在竞争关系,此时偏向锁失效,膨胀为轻量级锁;

3、轻量级锁状态:

当多个线程执行时,当某个线程获得锁时其它线程不会阻塞,而是会执行CAS自旋操作,循环执行等待线程释放锁,如果等待线程执行CAS自旋次数超过设定次数时(一般设定为一次上下文切换的时间,因为重量级锁的消耗时间就是切换上下文的时间,java1.6以后通过虚拟机自行控制);轻量级锁会膨胀为重量级锁;

4、重量级锁状态:

当竞争条件激烈或者线程执行同步程序的时间过长时,轻量级锁一般会膨胀为重量级锁,此时就按照线程竞争锁,并将没有获得锁的线程阻塞,在线程间切换时同时执行上下文切换;