JAVA锁
Posted 卢亮的博客
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA锁相关的知识,希望对你有一定的参考价值。
Synchronized、ReentrantLock、ReentrantReadWriteLock
一、公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解?请手写一个自旋锁
1. 公平锁和非公平锁:
1.1 是什么?
公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。
非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获得锁;在高并发的情况下,有可能会造成优先级反转或者饥饿现象;
1.2 两者区别?
公平锁/非公平锁:
并发包ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认是非公平锁;
关于两者的区别:
公平锁,就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从从队列中取到自己;
非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就在采用类似公平锁的那种方式;
1.3 题外话?
Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量大比公平锁大;对于Sysnchronized而言,也是一种非公平锁;
2. 可重入锁(又名递归锁):【ReentrantLock】
2.1 是什么?
可重入锁也叫递归锁,指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,同一个线程在外层方法获取锁的时候,在进入内层方法的时候会自动获取锁。也就是说,线程可以进入任何一个它已经拥有锁所同步着的代码块。
2.2 ReentrantLock/Synchronized就是一个典型的可重入锁
2.3 可重入锁最大的作用是避免死锁
参考代码1:Synchronized
package com.example.code.lock; class Phone { public synchronized void sendSMS() { System.out.println(Thread.currentThread().getName() + "\\t invoked sendSMS()"); this.sendEmail(); } public synchronized void sendEmail() { System.out.println(Thread.currentThread().getName() + "\\t invoked sendEmail()"); } } /** * 同步方法可以调用另一个同步方法 * 0 invoked sendSMS() //0线程在外层方法获取锁的时候 * 0 invoked sendEmail() //0在进入内层方法的时候会自动获取锁 * 1 invoked sendSMS() * 1 invoked sendEmail() */ public class LockDemo { public static void main(String[] args) { Phone phone = new Phone(); for (int i = 0; i < 2; i++) { new Thread(() -> {phone.sendSMS();},String.valueOf(i)).start(); } } }
参考代码2:ReentrantLock
package com.example.code.lock; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class Phone { public void sendSMS() { System.out.println(Thread.currentThread().getName() + "\\t invoked sendSMS()"); this.sendEmail(); } public void sendEmail() { System.out.println(Thread.currentThread().getName() + "\\t invoked sendEmail()"); } } /** * ReentrantLock是典型的可重入锁 * 注意:锁几次释放几次 */ public class LockDemo { public static void main(String[] args) { Phone phone = new Phone(); Lock lock = new ReentrantLock(); for (int i = 0; i < 2; i++) { new Thread(() -> { lock.lock(); lock.lock(); try { phone.sendSMS(); }finally { lock.unlock(); lock.unlock(); } }, String.valueOf(i)).start(); } } }
3. 自旋锁:【循环,不断尝试】
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式尝试去获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU;
案例:手写自旋锁
package com.example.code.lock; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; /** * 题目:实现一个自旋锁 * 自旋锁的好处:循环比较获取直到成功为止,没有类似wait的阻塞。 * * 通过CAS完成自旋锁,A线程先进来调用myLock方法自己持有5秒钟,B随后进来后发现当前线程拥有锁,表示null,所以只能通过自选等待,直到A释放锁后B随后抢到 */ public class SpinLockDemo { AtomicReference<Thread> atomicReference = new AtomicReference<>(); public void myLock() { Thread thread = Thread.currentThread(); System.out.println(thread.getName() + "\\t come in o(n_n)o"); while (!atomicReference.compareAndSet(null, thread)) {} } public void myUnlock() { Thread thread = Thread.currentThread(); atomicReference.compareAndSet(thread, null); System.out.println(thread.getName() + "\\t invoked myUnlock()"); } public static void main(String[] args) { SpinLockDemo spinLockDemo = new SpinLockDemo(); for (int i = 0; i < 2; i++) { new Thread(() -> { spinLockDemo.myLock(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } spinLockDemo.myUnlock(); },String.valueOf(i)).start(); } } }
4. 独占锁(写锁) / 共享锁(读锁) / 互斥锁:【ReentrantReadWriteLock】
独占锁:指该锁一次只能被一个线程持有。对ReentrantLock和Synchronized而言都是独占锁;
共享锁:该锁可以被多个线程持有;
对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁;该锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。
代码验证:
package com.example.code.lock; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; class MyCache { private volatile Map<String, Object> map = new HashMap<>(16); ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); public void put(String key, Object value) { readWriteLock.writeLock().lock(); try { System.out.println(Thread.currentThread().getName() + "\\t线程正在写入:" + key); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } map.put(key, value); System.out.println(Thread.currentThread().getName() + "\\t线程正在写入完成。"); }finally { readWriteLock.writeLock().unlock(); } } public void get(String key) { readWriteLock.readLock().lock(); try { System.out.println(Thread.currentThread().getName() + "\\t线程正在读取:"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\\t线程读取完成。" + map.get(key)); }finally { readWriteLock.readLock().unlock(); } } } /** * 多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行 * 但是,如果有一个线程想去写共享资源,就不应该有其他线程可以对该资源进行读或写 * 小总结: * 读 - 读能共存 * 写 - 读不能共存 * 写 - 写不能共存 * 写操作:原子 + 独占,整个过程必须是完整的统一体,中间不许被分割,被打断 */ public class ReadWriterLockDemo { public static void main(String[] args) { MyCache myCache = new MyCache(); for (int i = 0; i < 2; i++) { final int temp = i ; new Thread(() -> { myCache.put(String.valueOf(temp),temp); },String.valueOf(i)).start(); } for (int i = 0; i < 2; i++) { final int temp = i ; new Thread(() -> { myCache.get(String.valueOf(temp)); },String.valueOf(i + i)).start(); } } }
二、CountDownLatch/CyclicBarrier/Semaphore使用过吗?
1. CountDownLatch:倒计时锁(人走完,才能关门)
让一些线程阻塞直到另一个线程完成操作后才被唤醒;CountDownLatch主要有两个方法,当一个过多个线程调用await方法时,调用线程会被阻塞。其他线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),当计数器变为0时,因调用await方法被阻塞的线程会被唤醒,继续执行。
案例一:教室关门
/** * @Author luliang * @Date 2020-01-02 16:00 * countDownLatch:倒计时锁 */ public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(6); for (int i = 0; i < 6; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\\t 上完自习,离开教室。。。"); countDownLatch.countDown(); },String.valueOf(i)).start(); } countDownLatch.await(); System.out.println(Thread.currentThread().getName() + "\\t ****班长最后关门****"); } }
案例二:枚举在企业中的正常用法
package com.example.code2019.Lock; import java.util.concurrent.CountDownLatch; enum CountryEnum { ONE(1,"齐"),TOW(2,"楚"),THREE(3,"燕"),FOUR(4,"赵"),FIVE(5,"魏"),SIX(6,"韩"); @Getter private Integer retCode; @Getter private String retMessage; CountryEnum(Integer retCode, String retMessage) { this.retCode = retCode; this.retMessage = retMessage; } public static CountryEnum forEach_CountryEnum(int index) { CountryEnum[] values = CountryEnum.values(); for (CountryEnum element : values) { return element; } return null; } } /** * @Author luliang * @Date 2020-01-02 16:00 * countDownLatch:倒计时锁 */ public class CountDownLatchDemo { public static void main(String[] args) { CountDownLatch countDownLatch = new CountDownLatch(6); for (int i = 0; i < 6; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\\t 国,被灭"); countDownLatch.countDown(); },CountryEnum.forEach_CountryEnum(i).getRetMessage()).start(); } try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\\t ***秦统一***"); } }
2. CyclicBarrier:循环屏障(人到齐,才能开会)
CylicBarrier的字面意思是可循环(Cylic)使用的屏障(Barrier)。它要做的事情是,让一组线程达到一个屏障(也叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CylicBarrier的await()方法。
案例:开会
public class CylicBarrierDemo { public static void main(String[] args) { CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> { System.out.println("***开始开会***"); }); for (int i = 0; i < 7; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\\t号签到"); try { cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } },String.valueOf(i)).start(); } } }
3. Semaphore:信号量/信号灯
信号量主要有两个目的,一个用于多个共享资源互斥使用,另一个用于并发线程数的控制;
案例:抢车位
public class SemaphoreDemo { public static void main(String[] args) { Semaphore semaphore = new Semaphore(5);//模拟5个车位 for (int i = 0; i < 10; i++) { final int temp = i; new Thread(() -> { try { semaphore.acquire(); System.out.println(Thread.currentThread().getName() + "\\t 号抢到车位"); TimeUnit.SECONDS.sleep(temp); System.out.println(Thread.currentThread().getName() + "号,停车" + temp + "秒后离开车位"); } catch (InterruptedException e) { e.printStackTrace(); }finally { semaphore.release(); } },String.valueOf(i)).start(); } } }
三、阻塞队列知道吗?
队列:先进先出、栈:先进后出
1. 队列 + 阻塞队列:
阻塞队列,顾名思义,首先它是一个队列,而一个队列在数据结构中所起的作用大致如下图所示:
当阻塞队列是空时,从队列中获取元素的操作将会被阻塞;当阻塞队列是满时,往队列里添加元素的操作将会被阻塞;
试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。同样,试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程从队列中移除一个或多个元素或者清空队列后使队列重新变得空闲起来并后续新增;
2. 为什么用?有什么好处?
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒
为什么需要BlockingQueue:
好处是我们不需要关系什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了。在concurrent包发布以前,在多线程环境下,我们每个程序员都必须自己去控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。
3. BlockingQueue的核心方法:
4. 架构梳理 + 种类分析
ArrayBlockingQueue:由数组组成的有界阻塞队列;
LinkedBlockingQueue:由链表结构组成的有界阻塞队列;(默认大小Integer.Max_VALUE)
PriorityBlockingQueue:支持优先级排序的无界阻塞队列;
DelayQueue:使用优先级队列实现的延迟无界阻塞队列;
SychronousQueue:不存储元素的队列,也即单个元素的队列;
LinkedTransferQueue:由链表结构组成的无界阻塞队列;
LinkedBlockingDeque:由链表结构组成的双向阻塞队列;
5. 用在哪里
5.1 生产者消费者模式:
sync --> wait --> notify
lock --> await --> single
案例一:传统版:ReentrantLock、单个Condition
class ShareDate { private int number = 0; private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void increment() throws Exception { lock.lock(); try { //1.判断 while (number != 0) { //等待,不能生产 condition.await(); } //2.干活 number++; System.out.println(Thread.currentThread().getName() + "\\t " + number); //3.通知唤醒 condition.signalAll(); }catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } public void decrement() throws Exception { lock.lock(); try { //1.判断 while (number == 0) { //等待,不能生产 condition.await(); } //2.干活 number--; System.out.println(Thread.currentThread().getName() + "\\t " + number); //3.通知唤醒 condition.signalAll(); }catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } } /** * 题目:一个初始值为0的变量,两个线程对其交替操作,一个人加1一个人减1,来5轮 * * 高内聚、低耦合的前提下,线程操作资源类 * 判断、干活、唤醒、通知 * 严防多线程环境下的虚假唤醒 * * 1. 线程 操作 资源类 * 2. 判断 干活 通知 * 3. 防止多线程虚假唤醒机制 */ public class ProdConsumer_TraditionDemo { public static void main(String[] args) { ShareDate shareDate = new ShareDate(); for (int i = 0; i < 5; i++) { new Thread(() -> { for (int j = 0; j < 5; j++) { try { shareDate.increment(); } catch (Exception e) { e.printStackTrace(); } } },"A" + i).start(); new Thread(() -> { for (int j = 0; j < 5; j++) { try { shareDate.decrement(); } catch (Exception e) { e.printStackTrace(); } } },"B" + i).start(); } } }
案例二:传统版:ReentrantLock、多个Condition
package com.example.code2019.Lock; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 题目:synchronized和lock有什么区别?用新的lock有什么好处?你具体说说 * 1. 原始构成: * synchronized是关键字属于JVM层面锁,底层通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象,只有在同步块或方法中才能调用wait/notify等方法 * Lock是具体类(java.util.concurrent.locks.Lock)是api层面的锁; * 2. 使用方法: * synchronized不需要用户去手动释放锁,当synchronized代码执行完毕后系统会自动让线程释放对锁的占用 * ReentrantLock则需要用户手动去释放锁,若没有主动释放锁,就有可能导致出现死锁现象,需要lock()和unlock()方法配合try/finally语句块来完成。 * 3. 等待是否可中断: * synchronized不可中断,除非抛出异常或者正常运行完成 * ReentrantLock可中断,1.设置超时时间 tryLock(long timeout, TimeUnit unit) * 2.lockInterruptibly()放代码块中,调用interrupt()方法可中断 * 4. 加锁是否公平: * synchronized非公平锁 * ReentrantLock两者都可以,默认公平锁,构造方法可传入Boolean值,true为公平锁,false为非公平锁 * * 5. 绑定多个条件Condition * synchronized没有 * ReentrantLock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个线程,要么唤醒全部线程。 * *================================================================================================================== * 题目:多线程之间按顺序调用,实现A->B->C三个线程启动,要求如下: * AA打印5次,BB打印10次,cc打印15次,...... 来10轮 */ class ShareResource { private int number = 1; //A:1 B:2 C:3 private Lock lock = new ReentrantLock(); private Condition c1 = lock.newCondition(); private Condition c2 = lock.newCondition(); private Condition c3 = lock.newCondition(); public void print5() { lock.lock(); try { //1. 判断 while (number != 1) { c1.await(); } //2. 干活 for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + "\\t" + i); } //3. 通知 number = 2 ; c2.signal(); }catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } public void print10() { lock.lock(); try { //1. 判断 while (number != 2) { c2.await(); } //2. 干活 for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "\\t" + i); } //3. 通知 number = 3 ; c3.signal(); }catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } public void print15() { lock.lock(); try { //1. 判断 while (number != 3) { c3.await(); } //2. 干活 for (int i = 0; i < 15; i++) { System.out.println(Thread.currentThread().getName() + "\\t" + i); } //3. 通知 number = 1 ; c1.signal(); }catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } } public class SyncAndReentrantLockDemo { public static void main(String[] args) { ShareResource shareResource = new ShareResource(); new Thread(() -> { for (int i = 0; i < 10; i++) { shareResource.print5(); } },"A").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { shareResource.print10(); } },"B").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { shareResource.print15(); } },"C").start(); } }
案例三:阻塞队列版(不用手动控制阻塞、唤醒)
package com.example.code2019.Lock; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; class MyResource { private volatile boolean FLAG = true; //开启/关闭,生产者与消费者 private AtomicInteger atomicInteger = new AtomicInteger(); BlockingQueue<String> blockingQueue = null; public MyResource(BlockingQueue<String> blockingQueue) { this.blockingQueue = blockingQueue; System.out.println(blockingQueue.getClass().getName()); } public void myProd() throws Exception{ String data = null; boolean retValue ; while (FLAG) { data = atomicInteger.incrementAndGet() +""; retValue = blockingQueue.offer(data, 2, TimeUnit.SECONDS); if (retValue) { System.out.println(Thread.currentThread().getName() + "\\t 插入队列" + data + "成功。"); }else { System.out.println(Thread.currentThread().getName() + "\\t 插入队列" + data + "失败。"); } TimeUnit.MILLISECONDS.sleep(100); } System.out.println(Thread.currentThread().getName() + "\\t\\t 生产者停止,FLAG = false"); } public void myConsumer() throws Exception{ String result = null ; while (FLAG) { result = blockingQueue.poll(2, TimeUnit.SECONDS); if (result == null || result.equalsIgnoreCase("")) { FLAG = false; System.out.println(Thread.currentThread().getName() + "\\t\\t 超过2秒没有取得数据,消费者退出。"); return; } System.out.println(Thread.currentThread().getName() + "\\t 消费队列" + result + "成功。"); } } public void stop() { this.FLAG = false; } } /** * @Author luliang * @Date 2020-01-03 16:32 * volatile、CAS、atomicInteger、BlockQueue、线程交互、原子引用 */ public class ProdConsumer_BlockQueueDemo { public static void main(String[] args) { MyResource myResource = new MyResource(new ArrayBlockingQueue<>(5)); new Thread(() -> { System.out.println(Thread.currentThread().getName() + "生产者启动"); try { myResource.myProd(); } catch (Exception e) { e.printStackTrace(); } },"Prod").start(); new Thread(() -> { System.out.println(Thread.currentThread().getName() + "消费者启动"); try { myResource.myConsumer(); } catch (Exception e) { e.printStackTrace(); } },"Consumer").start(); //线程暂停一会 try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } myResource.stop(); System.out.println("\\n2秒钟时间到,活动结束"); } }
5.2 线程池:构造注入,传接口
获得线程的4种方法:Thread、Runnable、Callable(FutureTask中间人)、线程池
案例一:Callable获取线程
package com.example.code2019.thread; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; class MyThread implements Callable<Integer> { @Override public Integer call() throws Exception { System.out.println("***come in call***"); TimeUnit.SECONDS.sleep(5); return 1024; } } /** * @Author luliang * @Date 2020-01-03 19:08 * 1. FutureTask类实现了Runnable接口,构造可以传入Callable,相当于中间人,Thead可以接收Runnable,所以也能接收FutureTask * 2. 建议将futureTask.get(),放在最后,如果放在前面,线程没有执行完毕,还没有返回值的话,会造成线程阻塞; * 3. 如果非常需要线程执行结果,可以使用while (!futureTask.isDone()){},来组合使用,相当于“自旋锁” * 4. 如果多个个线程传入同一个FutureTask,Callable接口的call()方法只会执行一次;如果要想call()多次执行,传入不同的FutureTask */ public class CallableDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { FutureTask<Integer> futureTask = new FutureTask<>(new MyThread()); new Thread(futureTask, "AA").start(); new Thread(futureTask, "BB").start(); System.out.println(Thread.currentThread().getName() + "正在执行"); int result01 = 10 ; 以上是关于JAVA锁的主要内容,如果未能解决你的问题,请参考以下文章java中ReentrantReadWriteLock读写锁的使用
JUC并发编程 共享模式之工具 JUC CountdownLatch(倒计时锁) -- CountdownLatch应用(等待多个线程准备完毕( 可以覆盖上次的打印内)等待多个远程调用结束)(代码片段