显式锁和AQS
首先我们需要知道的是:锁可以分为公平锁和不公平锁,重入锁和非重入锁;
一、Lock接口
Lock是java 1.5中引入的线程同步工具,它主要用于多线程下共享资源的控制。本质上Lock仅仅是一个接口(位于源码包中的java\util\concurrent\locks中),
Lock有三个实现类,一个是ReentrantLock,另两个是ReentrantReadWriteLock类中的两个静态内部类ReadLock和WriteLock。
Lock它包含以下方法
//尝试获取锁,获取成功则返回,否则阻塞当前线程 void lock(); //尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常 void lockInterruptibly() throws InterruptedException; //尝试获取锁,获取锁成功则返回true,否则返回false boolean tryLock(); //尝试获取锁,若在规定时间内获取到锁,则返回true,否则返回false,未获取锁之前被中断,则抛出异常 boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //释放锁 void unlock(); //返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能,一个锁可以有多个条件变量 Condition newCondition();
下面我们简单的写一个关于Lock的demo入门;
package com.youyou.ch4; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class LockDemo { private Lock lock = new ReentrantLock(); private int count; public void increament(){ lock.lock(); try { if(count > 30){ return; } count++; increament(); }finally { lock.unlock(); } } public synchronized void incre(){ count++; if(count > 30){ return; } incre(); } }
二、Lock与synchronized的不同:
1.synchronized的缺点
1)当一个代码块被synchronized修饰的时候,一个线程获取到了锁,并且执行代码块,那么其他的线程需要等待正在使用的线程释放掉这个锁,那么释放锁的方法只有两种,一种是代码执行完毕自动释放,一种是发生异常以后jvm会让线程去释放锁。那么如果这个正在执行的线程遇到什么问题,比如等待IO或者调用sleep方法等等被阻塞了,无法释放锁,而这时候其他线程只能一直等待,将会特别影响效率。那么有没有一种办法让其他线程不必一直傻乎乎的等在这里吗?
2)当一个文件,同时被多个线程操作时,读操作和写操作会发生冲突,写操作和写操作会发生冲突,而读操作和读操作并不会冲突,但是如果我们用synchronized的话,会导致一个线程在读的时候,其他线程想要读的话只能等待,那么有什么办法能不锁读操作吗?
3)在使用synchronized时,我们无法得知线程是否成功获取到锁,那么有什么办法能知道是否获取到锁吗?
2.两者之间的使用和区别:
synchronized 代码简洁,Lock:获取锁可以被中断,超时获取锁,尝试获取锁,读多写少用读写锁
1.synchronized是Java的关键字,是内置特性,而Lock是一个接口,可以用它来实现同步。
2.synchronized同步的时候,其中一条线程用完会自动释放锁,而Lock需要手动释放,如果不手动释放,可能会造成死锁。
3.使用synchronized如果其中一个线程不释放锁,那么其他需要获取锁的线程会一直等待下去,知道使用完释放或者出现异常,而Lock可以使用可以响应中断的锁或者使用规定等待时间的锁
4.synchronized无法得知是否获取到锁,而Lcok可以做到。
5.用ReadWriteLock可以提高多个线程进行读操作的效率
三、可重入锁
Lcok在Java中是一个接口,一般在面试问题中问到的可能是ReentrantLock与synchronized的区别。ReentrantLock是Lock的一个实现类,字面意思的话就是可重入锁,那么什么是可重入锁呢。
可重入锁是锁的一个相关概念,并不是特指我们的ReentrantLock,而是如果一个锁具备可重入性,那我们就说这是一个可重入锁。ReentrantLock和synchronized都是可重入锁。至于什么是可重入性,这里举个简单的例子,现在在一个类里我们有两个方法(代码如下),一个叫做去北京,一个叫做买票,那我们在去北京的方法里可以直接调用买票方法,假如两个方法全都用synchronized修饰的话,在执行去北京的方法,线程获取了对象的锁,接着执行买票方法,如果synchronized不具备可重入性,那么线程已经有这个对象的锁了,现在又来申请,就会导致线程永远等待无法获取到锁。而synchronized和ReentrantLock都是可重入锁,就不会出现上述的问题。
举个简单的例子:
class Trip { public synchronized void goToBeiJing() { // 去北京 buyATicket(); } public synchronized void buyATicket() { // 买票 } }
四、公平锁和非公平锁
公平锁严格按照先来后到的顺去获取锁,而非公平锁允许插队获取锁。
公平锁获取锁的过程上有些不同,在使用公平锁时,某线程想要获取锁,不仅需要判断当前表示状态的变量的值是否为0,还要判断队列里是否还有其他线程,若队列中还有线程则说明当前线程需要排队,进行入列操作,并将自身阻塞;若队列为空,才能尝试去获取锁。而对于非公平锁,当表示状态的变量的值是为0,就可以尝试获取锁,不必理会队列是否为空,这样就实现了插队获取锁的特点。通常来说非公平锁的吞吐率比公平锁要高,我们一般常用非公平锁。
其中ReentrantLock,默认是非公平锁,但是构造函数可以传入值,传入true时候,就是公平锁;
五、读写锁
读写锁:同一时刻允许多个读线程同时访问,但是写线程访问的时候,所有的读和写都被阻塞,最适宜与读多写少的情况。
代码看效果:
public class GoodsInfo { private final String name; private double totalMoney;//总销售额 private int storeNumber;//库存数 public GoodsInfo(String name, int totalMoney, int storeNumber) { this.name = name; this.totalMoney = totalMoney; this.storeNumber = storeNumber; } public double getTotalMoney() { return totalMoney; } public int getStoreNumber() { return storeNumber; } public void changeNumber(int sellNumber){ this.totalMoney += sellNumber*25; this.storeNumber -= sellNumber; } } public class UseSyn implements GoodsService { private GoodsInfo goodsInfo; public UseSyn(GoodsInfo goodsInfo) { this.goodsInfo = goodsInfo; } @Override public synchronized GoodsInfo getNum() { try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } return this.goodsInfo; } @Override public synchronized void setNum(int number) { try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } goodsInfo.changeNumber(number); } } public class UseRwLock implements GoodsService { private GoodsInfo goodsInfo; private final ReadWriteLock lock = new ReentrantReadWriteLock(); private final Lock getLock = lock.readLock();//读锁 private final Lock setLock = lock.writeLock();//写锁 public UseRwLock(GoodsInfo goodsInfo) { this.goodsInfo = goodsInfo; } @Override public GoodsInfo getNum() { getLock.lock(); try { try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } return this.goodsInfo; }finally { getLock.unlock(); } } @Override public void setNum(int number) { setLock.lock(); try { try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } goodsInfo.changeNumber(number); }finally { setLock.unlock(); } } } public interface GoodsService { public GoodsInfo getNum();//获得商品的信息 public void setNum(int number);//设置商品的数量 } public class BusiApp { static final int readWriteRatio = 10;//读写线程的比例 static final int minthreadCount = 3;//最少线程数 //static CountDownLatch latch= new CountDownLatch(1); //读操作 private static class GetThread implements Runnable{ private GoodsService goodsService; public GetThread(GoodsService goodsService) { this.goodsService = goodsService; } @Override public void run() { // try { // latch.await();//让读写线程同时运行 // } catch (InterruptedException e) { // } long start = System.currentTimeMillis(); for(int i=0;i<100;i++){//操作100次 goodsService.getNum(); } System.out.println(Thread.currentThread().getName()+"读取商品数据耗时:" +(System.currentTimeMillis()-start)+"ms"); } } //写操做 private static class SetThread implements Runnable{ private GoodsService goodsService; public SetThread(GoodsService goodsService) { this.goodsService = goodsService; } @Override public void run() { // try { // latch.await();//让读写线程同时运行 // } catch (InterruptedException e) { // } long start = System.currentTimeMillis(); Random r = new Random(); for(int i=0;i<10;i++){//操作10次 SleepTools.ms(50); goodsService.setNum(r.nextInt(10)); } System.out.println(Thread.currentThread().getName() +"写商品数据耗时:"+(System.currentTimeMillis()-start)+"ms---------"); } } public static void main(String[] args) throws InterruptedException { GoodsInfo goodsInfo = new GoodsInfo("Cup",100000,10000); GoodsService goodsService = /*new UseRwLock(goodsInfo);*/new UseSyn(goodsInfo); for(int i = 0;i<minthreadCount;i++){ Thread setT = new Thread(new SetThread(goodsService)); for(int j=0;j<readWriteRatio;j++) { Thread getT = new Thread(new GetThread(goodsService)); getT.start(); } SleepTools.ms(100); setT.start(); } //latch.countDown(); } }
六、用Lock和Condition实现等待通知
- Condition类有很好的灵活性,可以实现多路通知功能,一个Lock对象中可以创建多个Condition对象实例,线程对象可以注册在指定的Condition中,进而有选择的进行线程通知,在调度线程上更加灵活
- wait与notify/notifyAll进行等待通知时,被通知的线程是随机的,但是Condition与Lock结合的通知是有选择性的通知
- synchronized就相当于整个Lock对象中只有一个单一的Condition对象,所有线程对象都注册在一个Condition对象身上,线程开始notifyAll时,需要通知所有的WAITING线程,没有选择性,会出现很大的效率问题
看代码事例:
package com.xiangxue.ch4.condition; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * *@author Mark老师 享学课堂 https://enjoy.ke.qq.com * *类说明: */ 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.signalAll(); }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("check km thread["+Thread.currentThread().getId() +"] is be notifed."); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }finally { lock.unlock(); } System.out.println("the Km is "+this.km+",I will change db"); } /*当快递到达目的地时通知用户*/ public void waitSite(){ lock.lock(); try { while(CITY.equals(this.site)) { try { siteCond.await(); System.out.println("check site thread["+Thread.currentThread().getId() +"] is be notifed."); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }finally { lock.unlock(); } System.out.println("the site is "+this.site+",I will call user"); } } package com.xiangxue.ch4.condition; /** *@author Mark老师 享学课堂 https://enjoy.ke.qq.com * *类说明:测试Lock和Condition实现等待通知 */ 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();//快递里程变化 } }
总结信息:
ReentrantLock和Syn关键字,都是排他锁,即只能有一个线程可以访问
七、具体的AQS和Condition还有的是具体实现原理。包括的是数据结构和实现过程,还需要以后的时间在慢慢研究一下。