显示锁和AQS,ReentrantReadWriteLock读写锁

显示锁和AQS

一、Lock和synchronized关键字的区别?

  1. synchronized会比lock消耗的性能会少一点。因为lock是一个类,使用时需要创建一个对象的实例。而lock又比synchronized使用起来更灵活。
  2. synchronized代码简洁。Lock:获取锁可以被中断,超时获取锁,尝试获取锁,读多写少用读写锁
  3. synchronized会自动释放锁。Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
  4. 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接口提供了一些方法:

  1. await():线程等待。
  2. awaitUninterruptibly():线程等待不可被中断
  3. signal():相当于notify()的功能。
  4. 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的时候,共享锁共享完毕,其它线程就会被阻塞。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值