显示锁和AQS
一、Lock和synchronized关键字的区别?
- synchronized会比lock消耗的性能会少一点。因为lock是一个类,使用时需要创建一个对象的实例。而lock又比synchronized使用起来更灵活。
- synchronized代码简洁。Lock:获取锁可以被中断,超时获取锁,尝试获取锁,读多写少用读写锁
- synchronized会自动释放锁。Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
- synchronized阻塞。tryLock非阻塞。
二、可重入锁
一个线程可以多次获得这个锁。比如递归调用时,可以继续进入这个锁。
synchronized就是可重入锁。ReentrantLock也是可重入锁,ReentrantLock是可以设置为是否可重入锁的。
// 递归调用
public synchronized void demo() {
demo();
}
可重入锁实现:当前线程去获取它就会在递增加一,当释放一次减一,当等于0时则会释放锁。
三、锁的公平和非公平
公平锁:公平就是排队等待获取锁,新线程加入到队列尾部等待。
非公平锁:非公平就是谁先抢到就是谁获取锁。(线程从等待到唤醒是需要上下文操作时间,如果有新的线程刚好来,可能直接会获取到锁。所以非公平锁比公平锁效率更高。Syn是非公平锁。)
ReentrantLock默认为非公平锁。
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
四、ReadWriteLock接口和读写锁ReentrantReadWriteLock,什么情况下用读写锁?
前面说到的synchronized关键字和ReentrantLock都属于排他锁,同时只能一个线程获取锁。读写锁可以同时多个读线程获取锁,但是当写锁获得锁时,所有读线程和写线程都会被阻塞。写独占,读共享,读锁和写锁是互斥的。
读多写少的情况下使用,可以提高效率。
公平性选择:支持公平与非公平(默认)的锁获取方式,吞吐量非公平优先于公平。
可重入:读线程获取读锁之后可以再次获取读锁,写线程获取写锁之后可以再次获取写锁
可降级:写线程获取写锁之后,其还可以再次获取读锁,然后释放掉写锁,那么此时该线程是读锁状态,也就是降级操作。读锁升级到写锁是不可以的,会导致死锁。
ReentrantReadWriteLock:读和写用的是一把锁,通过sync里的status值来记录的,因为status是int型,32位,高(16-32)16位用来记录读锁(通过ThreadLocal来实现记录重入次数),低(0-16)16位用来记录写锁。
synchronized和ReadWriteLock的效率比较:模拟一个商店销售总额和库存数量的计算。
public class StoreInfo {
private final String name;
// 销售总额
private double totalMoney;
// 库存数量
private int storeNumber;
public StoreInfo(String name, double totalMoney, int storeNumber) {
this.name = name;
this.totalMoney = totalMoney;
this.storeNumber = storeNumber;
}
// 根据设置商品数量计算总销售额和库存数量
public void change(int number) {
totalMoney += number * 20;
storeNumber -= number;
}
}
public interface StoreService {
// 获取商店信息
public StoreInfo getInfo();
// 设置商品数量
public void set(int number);
}
/**
* @Author nanjunkai
* @Description: synchronized内置锁
* @Date 2020/11/18 15:07:27
*/
public class Sys implements StoreService {
private StoreInfo storeInfo;
public Sys(StoreInfo storeInfo) {
this.storeInfo = storeInfo;
}
@Override
public synchronized StoreInfo getInfo() {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return storeInfo;
}
@Override
public synchronized void set(int number) {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
storeInfo.change(number);
}
}
/**
* @Author nanjunkai
* @Description: ReadWriteLock读写锁
* @Date 2020/11/18 15:07:10
*/
public class RwLock implements StoreService {
private StoreInfo storeInfo;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
// 读锁
private final Lock readLock = lock.readLock();
// 写锁
private final Lock writeLock = lock.writeLock();
public RwLock(StoreInfo storeInfo) {
this.storeInfo = storeInfo;
}
@Override
public StoreInfo getInfo() {
readLock.lock();
try {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return this.storeInfo;
} finally {
readLock.unlock();
}
}
@Override
public void set(int number) {
writeLock.lock();
try {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
storeInfo.change(number);
} finally {
writeLock.unlock();
}
}
}
/**
* @Author nanjunkai
* @Description: 比较synchronized和ReadWriteLock在读多写少情况下效率
* @Date 2020/11/18 14:43:04
*/
public class TestRwSyn {
// 读
static class Read implements Runnable {
private StoreService storeService;
private Read(StoreService storeService) {
this.storeService = storeService;
}
@Override
public void run() {
Long start = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
storeService.getInfo();
}
System.out.println(Thread.currentThread().getName() + "读时间:" + (System.currentTimeMillis() - start));
}
}
// 写
static class Write implements Runnable {
private StoreService storeService;
private Write(StoreService storeService) {
this.storeService = storeService;
}
@Override
public void run() {
Long start = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
storeService.set(10);
}
System.out.println(Thread.currentThread().getName() + "写时间:" + (System.currentTimeMillis() - start));
}
}
public static void main(String[] args) {
StoreInfo storeInfo = new StoreInfo("手机", 100000, 10000);
StoreService storeService = new Sys(storeInfo);//Sys为synchronized,RwLock为ReadWriteLock
// 设置读写比例10:1
for (int i = 0; i < 2; i++) {
new Thread(new Write(storeService)).start();
for (int j = 0; j < 10; j++) {
new Thread(new Read(storeService)).start();
}
}
}
}
这里首先用到的是Sys(synchronized)类,我们先看一下大概的时间。
Thread-0写时间:59
Thread-11写时间:1061
Thread-21读时间:6720
Thread-16读时间:7936
Thread-17读时间:7976
Thread-9读时间:8251
Thread-8读时间:8352
Thread-12读时间:9238
Thread-13读时间:9388
Thread-20读时间:9427
Thread-18读时间:9854
Thread-7读时间:10275
Thread-6读时间:10326
Thread-3读时间:10472
Thread-1读时间:10775
Thread-14读时间:10855
Thread-19读时间:10976
Thread-4读时间:10997
Thread-5读时间:11109
Thread-2读时间:11207
Thread-15读时间:11272
Thread-10读时间:11333
我们再用RwLock(ReadWriteLock)运行。
Thread-0写时间:63
Thread-11写时间:109
Thread-2读时间:677
Thread-7读时间:676
Thread-6读时间:677
Thread-10读时间:676
Thread-3读时间:677
Thread-9读时间:679
Thread-4读时间:695
Thread-13读时间:679
Thread-17读时间:679
Thread-5读时间:688
Thread-21读时间:679
Thread-16读时间:679
Thread-12读时间:679
Thread-20读时间:679
Thread-1读时间:696
Thread-8读时间:680
Thread-14读时间:682
Thread-15读时间:682
Thread-18读时间:682
Thread-19读时间:688
这里可以明显看到速度的提升,而且现在设置的读写比例还是10:1,数据量更大比例更大的时候,效果更明显。由此我们可以得到,在读多写少的情况下,使用ReadWriteLock可以提升效率。
记录一点,我是用JDK1.8跑的项目,最开始做的是改变一个常量number,然后读和写,发现两种锁的效果居然一样,甚至内置锁比读写锁还要快。后面想到常量放在常量池,对象放在堆里面,不知道是不是这个的影响,后面又用实体类重新写了一遍,才看到最终的效果。
五、Lock和condition实现等待通知
condition接口提供了一些方法:
- await():线程等待。
- awaitUninterruptibly():线程等待不可被中断
- signal():相当于notify()的功能。
- signalAll():相当于notifyAll()的功能。
在使用synchronized关键字wait()和notfiy()的时候,尽量用notifyAll(),而不是notify()。显示锁尽可能使用signal(),而不是signalAll()。wait()是Object下的方法,当有多个属性时,如果使用notify()可能休眠或唤醒的不是当前属性,所以使用notifyAll()。在使用condition时都会根据属性new出多个condition,无论是休眠还是唤醒,都已经精确到具体的锁,所以使用signal()。
public class ExpressCond {
public final static String CITY = "ShangHai";
private int km;/*快递运输里程数*/
private String site;/*快递到达地点*/
private Lock lock = new ReentrantLock();
private Condition keCond = lock.newCondition();
private Condition siteCond = lock.newCondition();
public ExpressCond() {
}
public ExpressCond(int km, String site) {
this.km = km;
this.site = site;
}
/* 变化公里数,然后通知处于wait状态并需要处理公里数的线程进行业务处理*/
public void changeKm(){
lock.lock();
try {
this.km = 101;
keCond.signal();
}finally {
lock.unlock();
}
}
/* 变化地点,然后通知处于wait状态并需要处理地点的线程进行业务处理*/
public void changeSite(){
lock.lock();
try {
this.site = "BeiJing";
siteCond.signal();
}finally {
lock.unlock();
}
}
/*当快递的里程数大于100时更新数据库*/
public void waitKm(){
lock.lock();
try {
while(this.km<=100) {
try {
keCond.await();
System.out.println("km已开启");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}finally {
lock.unlock();
}
}
/*当快递到达目的地时通知用户*/
public void waitSite(){
lock.lock();
try {
while(CITY.equals(this.site)) {
try {
siteCond.await();
System.out.println("site已开启");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}finally {
lock.unlock();
}
}
}
public class TestCond {
private static ExpressCond express = new ExpressCond(0,ExpressCond.CITY);
/*检查里程数变化的线程,不满足条件,线程一直等待*/
private static class CheckKm extends Thread{
@Override
public void run() {
express.waitKm();
}
}
/*检查地点变化的线程,不满足条件,线程一直等待*/
private static class CheckSite extends Thread{
@Override
public void run() {
express.waitSite();
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<3;i++){
new CheckSite().start();
}
for(int i=0;i<3;i++){
new CheckKm().start();
}
Thread.sleep(1000);
express.changeKm();//快递里程变化
}
}
km已启动。。。
一个Lock可以new出多个condition,当属性字段很多时,可以new出多个condition,分别按条件管理锁。不会影响性能。
六、LockSupport
LockSupport 工具可以帮助我们阻塞或唤醒一个线程,也是构建同步组件的基础工具。
AQS中 对于节点的阻塞和唤醒就是通过LockSupport的park和unpark实现的。
park():堆线程产生阻塞作用
unpark():对阻塞线程进行唤醒操作
七、AQS(AbstractQueuedSynchronizer)
AQS使用的设计模式是模板方法模式。
模板方法模式:由父类定义一个所谓的框架方法,这个方法会设置方法的运行流程。但是这些方法的具体实现交给子类进行实现。
/**
* @Author nanjunkai
* @Description: 模板方法的父类(定义一个发送邮件的模板)
* @Date 2020/11/18 18:03:53
*/
public abstract class SendCustom {
public abstract void to();
public abstract void from();
public abstract void content();
public void date() {
System.out.println(new Date());
}
public abstract void send();
//框架方法-模板方法
public void sendMessage() {
to();
from();
content();
date();
send();
}
}
/**
* @Author nanjunkai
* @Description: 模板方法的派生类
* @Date 2020/11/18 18:05:00
*/
public class SendSms extends SendCustom {
@Override
public void to() {
System.out.println("a");
}
@Override
public void from() {
System.out.println("b");
}
@Override
public void content() {
System.out.println("Hello world");
}
@Override
public void send() {
System.out.println("message");
}
public static void main(String[] args) {
SendCustom sendC = new SendSms();
sendC.sendMessage();
}
}
运行结果
a
b
Hello world
Wed Nov 18 18:16:56 CST 2020
message
AQS中的模板方法
独占式获取: accquire acquireInterruptibly tryAcquireNanos
共享式获取: acquireShared acquireSharedInterruptibly tryAcquireSharedNanos
独占式释放锁: release
共享式释放锁: releaseShared
需要子类覆盖的流程方法
独占式获取 tryAcquire 独占式释放 tryRelease 共享式获取 tryAcquireShared 共享式释放 tryReleaseShared
这个同步器是否处于独占模式: isHeldExclusively
同步状态state:
getState:获取当前的同步状态
setState:设置当前同步状态
compareAndSetState: 使用CAS设置状态,保证状态设置的原子性
八、实现一个类似于ReentrantLock的锁
先看看源码:首先继承Lock,然后创建一个内部类继承AbstractQueuedSynchronizer
* @since 1.5
* @author Doug Lea
*/
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
/**
* Base of synchronization control for this lock. Subclassed
* into fair and nonfair versions below. Uses AQS state to
* represent the number of holds on the lock.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
实现一个类似于ReentrantLock锁(不可重入):if(compareAndSetState(0,1)) state一直1,会造成死锁。
public class SelfLock implements Lock{
//state 表示获取到锁 state=1 获取到了锁,state=0,表示这个锁当前没有线程拿到
private static class Sync extends AbstractQueuedSynchronizer{
//是否占用
@Override
protected boolean isHeldExclusively() {
return getState()==1;
}
@Override
protected boolean tryAcquire(int arg) {
// 原子性改变状态,保证获取前没有被占用
if(compareAndSetState(0,1)) {
// 设置独占线程为当前线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
if(getState()==0) {
throw new UnsupportedOperationException();
}
setExclusiveOwnerThread(null);
// 获取锁已经确定原子性,这里可以直接修改状态
setState(0);
return true;
}
Condition newCondition() {
return new ConditionObject();
}
}
private final Sync sycn = new Sync();
@Override
public void lock() {
sycn.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sycn.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sycn.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sycn.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sycn.release(1);
}
@Override
public Condition newCondition() {
return sycn.newCondition();
}
}
九、AQS的结构以及流程
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
竞争失败的线程会打包成Node放到同步队列,Node可能的状态里:
CANCELLED:线程等待超时或者被中断了,需要从队列中移走
SIGNAL:后续的节点等待状态,当前节点,通知后面的节点去运行
CONDITION :当前节点处于等待队列
PROPAGATE:共享,表示状态要往后面的节点传播 0,表示初始状态
volatile int waitStatus;
volatile Node prev;
volatile Node next;
waitstatus:表示等待状态,Node的状态会赋值于waitstatus
prev:表示当前节点的前驱节点
next:表示当前节点的后继节点
AQS中的数据结构-节点和同步队列
AQS内部就是一个同步队列,数据结构底层是用双向链表进行储存的。比如当前有10个线程竞争同一个锁,第一个线程获取到锁,其他9个线程会被打包成一个Node放到同步队列。
当线程加入同步队列的尾部时用到了CAS,因为获取锁存在竞争关系,所以这里要确保队列的安全性,设置首节点时只有一个,所以不需要CAS。(先进先出)
当首节点获取到锁,会唤醒下一个节点并设置为头节点。
十、Condition(结合第五点实例分析)
AQS内部就是一个同步队列,数据结构底层是用双向链表进行储存。一个condition包含一个等待队列,数据结构底层是用单向链表进行存储。
项目实例:当newCondition时会新建一个等待队列,condition和AQS内部节点结构是一样的。
private Condition keCond = lock.newCondition();
private Condition siteCond = lock.newCondition();
根据上图转换成下图:
节点在队列之间的移动
await方法:当调用await方法时,会以节点的线程构造新的节点并加入等待队列
signal方法:当调用signal方法时,去唤醒condition同步节点,尝试竞争锁,但不一定竞争成功,以尾节点加入AQS同步队列
notfiy()和notifyAll(),signal()和signalAll()的选用?
第五点其实已经说到在使用synchronized关键字wait()和notfiy()的时候,尽量用notifyAll(),而不是notify()。显示锁尽可能使用signal(),而不是signalAll()。这里结合图再说明一下。
不用signalAll的原因:唤醒了所有的挂起线程,而锁同时只能被一个线程使用,后面唤醒的线程是无意义的。
不用notify的原因:一个同步队列,但是只有一个等待队列,确保不了哪一个线程处于当前等待队列的头结点,所以要唤醒所有节点,由线程判断是否满足。
十一:独占锁和共享锁
独占锁也就是悲观锁,在某一时间段只可以被一个线程占有,典型的有synchronized 关键字和ReentrantLock。而共享锁 简而言之就是可以被多个线程去共享,典型的有ReentrantReadWriteLock的读锁。
独占锁state初始是0,当有线程获取的时候变为1,表示已经被获取。
共享锁的state初始并不是0,而是一个可以被共享的量,比如是10,每个线程去获取它就会在初始值减去1,释放时会加1,超出的时候加入队列,当0的时候,共享锁共享完毕,其它线程就会被阻塞。