拓之朴 做网站多少钱,昆明旅行社网站开发,域名注册好了怎么了做网站,网络营销国外研究现状J.U.C 简介
Java.util.concurrent 是在并发编程中比较常用的工具类#xff0c;里面包含很多用来在并发场景中使用的组件。比如线程池、阻塞队列、计时器、同步器、并发集合等等。并发包的作者是大名鼎鼎的 Doug Lea。我们在接下来的课程中#xff0c;回去剖析一些经典的比较…J.U.C 简介
Java.util.concurrent 是在并发编程中比较常用的工具类里面包含很多用来在并发场景中使用的组件。比如线程池、阻塞队列、计时器、同步器、并发集合等等。并发包的作者是大名鼎鼎的 Doug Lea。我们在接下来的课程中回去剖析一些经典的比较常用的组件的设计思想Lock
Lock 在 J.U.C 中是最核心的组件前面我们讲 synchronized 的时候说过锁最重要的特性就是解决并发安全问题。为什么要以 Lock 作为切入点呢如果有同学看过 J.U.C 包中的所有组件一定会发现绝大部分的组件都有用到了 Lock。所以通过 Lock 作为切入点使得在后续的学习过程中会更加轻松。Lock 简介
在 Lock 接口出现之前Java 中的应用程序对于多线程的并发安全处理只能基于synchronized 关键字来解决。但是 synchronized 在有些场景中会存在一些短板也就是它并不适合于所有的并发场景。但是在 Java5 以后Lock 的出现可以解决synchronized 在某些场景中的短板它比 synchronized 更加灵活。Lock 的实现
Lock 本质上是一个接口它定义了释放锁和获得锁的抽象方法定义成接口就意味着它定义了锁的一个标准规范也同时意味着锁的不同实现。实现 Lock 接口的类有很多以下为几个常见的锁实现
ReentrantLock表示重入锁它是唯一一个实现了 Lock 接口的类。重入锁指的是线程在获得锁之后再次获取该锁不需要阻塞而是直接关联一次计数器增加重入次数
ReentrantReadWriteLock重入读写锁它实现了 ReadWriteLock 接口在这个类中维护了两个锁一个是 ReadLock一个是 WriteLock他们都分别实现了 Lock接口。读写锁是一种适合读多写少的场景下解决线程安全问题的工具基本原则是 读和读不互斥、读和写互斥、写和写互斥。也就是说涉及到影响数据变化的操作都会存在互斥。
StampedLock stampedLock 是 JDK8 引入的新的锁机制可以简单认为是读写锁的一个改进版本读写锁虽然通过分离读和写的功能使得读和读之间可以完全并发但是读和写是有冲突的如果大量的读线程存在可能会引起写线程的饥饿。stampedLock 是一种乐观的读策略使得乐观锁完全不会阻塞写线程。Lock 的类关系图
Lock 有很多的锁的实现但是直观的实现是 ReentrantLock 重入锁void lock() // 如果锁可用就获得锁如果锁不可用就阻塞直到锁释放 void lockInterruptibly() // 和lock()方法相似, 但阻塞的线程 可 中 断 抛 出java.lang.InterruptedException 异常 boolean tryLock() // 非阻塞获取锁;尝试获取锁如果成功返回 true boolean tryLock(long timeout, TimeUnit timeUnit) //带有超时时间的获取锁方法 void unlock() // 释放锁
ReentrantLock 重入锁
重入锁表示支持重新进入的锁也就是说如果当前线程 t1 通过调用 lock 方法获取了锁之后再次调用 lock是不会再阻塞去获取锁的直接增加重试次数就行了。synchronized 和 ReentrantLock 都是可重入锁。锁会存在重入的特性那是因为对于同步锁的理解程度还不够比如在下面这类的场景中存在多个加锁的方法的相互调用其实就是一种重入特性的场景。重入锁的设计目的
比如调用 demo 方法获得了当前的对象锁然后在这个方法中再去调用demo2demo2 中的存在同一个实例锁这个时候当前线程会因为无法获得demo2 的对象锁而阻塞就会产生死锁。重入锁的设计目的是避免线程的死锁。public class ReentrantDemo{public synchronized void demo(){System.out.println(begin:demo);demo2();}public void demo2(){System.out.println(begin:demo1);synchronized (this){}}public static void main(String[] args) {ReentrantDemo rdnew ReentrantDemo();new Thread(rd::demo).start();}
}
ReentrantLock 的使用案例
public class AtomicDemo {private static int count0;static Lock locknew ReentrantLock();public static void inc(){lock.lock();try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}count;lock.unlock();}public static void main(String[] args) throws
InterruptedException {for(int i0;i1000;i){new Thread(()-{AtomicDemo.inc();}).start();;}Thread.sleep(3000);System.out.println(result:count);}
}ReentrantReadWriteLock
我们以前理解的锁基本都是排他锁也就是这些锁在同一时刻只允许一个线程进行访问而读写所在同一时刻可以允许多个线程访问但是在写线程访问时所有的读线程和其他写线程都会被阻塞。读写锁维护了一对锁一个读锁、一个写锁; 一般情况下读写锁的性能都会比排它锁好因为大多数场景读是多于写的。在读多于写的情况下读写锁能够提供比排它锁更好的并发性和吞吐量.public class LockDemo {static MapString,Object cacheMapnew HashMap();static ReentrantReadWriteLock rwlnew
ReentrantReadWriteLock();static Lock readrwl.readLock();static Lock writerwl.writeLock();public static final Object get(String key) {System.out.println(开始读取数据);read.lock(); //读锁try {return cacheMap.get(key);}finally {read.unlock();}}public static final Object put(String key,Object value){write.lock();System.out.println(开始写数据);try{return cacheMap.put(key,value);}finally {write.unlock();}}
}在这个案例中通过 hashmap 来模拟了一个内存缓存然后使用读写所来保证这个内存缓存的线程安全性。当执行读操作的时候需要获取读锁在并发访问的时候读锁不会被阻塞因为读操作不会影响执行结果。
在执行写操作是线程必须要获取写锁当已经有线程持有写锁的情况下当前线程会被阻塞只有当写锁释放以后其他读写操作才能继续执行。使用读写锁提升读操作的并发性也保证每次写操作对所有的读写操作的可见性
⚫ 读锁与读锁可以共享
⚫ 读锁与写锁不可以共享排他
⚫ 写锁与写锁不可以共享排他ReentrantLock 的实现原理
我们知道锁的基本原理是基于将多线程并行任务通过某一种机制实现线程的串行执行从而达到线程安全性的目的。在 synchronized 中我们分析了偏向锁、轻量级锁、乐观锁。基于乐观锁以及自旋锁来优化了 synchronized 的加锁开销同时在重量级锁阶段通过线程的阻塞以及唤醒来达到线程竞争和同步的目的。那么在 ReentrantLock 中也一定会存在这样的需要去解决的问题。就是在多线程竞争重入锁时竞争失败的线程是如何实现阻塞以及被唤醒的呢
AQS 是什么
在 Lock 中用到了一个同步队列 AQS全称 AbstractQueuedSynchronizer它是一个同步工具也是 Lock 用来实现线程同步的核心组件。如果你搞懂了 AQS那么 J.U.C 中绝大部分的工具都能轻松掌握。
AQS 的两种功能 从使用层面来说AQS 的功能分为两种独占和共享
独占锁每次只能有一个线程持有锁比如前面给大家演示的 ReentrantLock 就是以独占方式实现的互斥锁
共 享 锁 允 许 多 个 线 程 同 时 获 取 锁 并 发 访 问 共 享 资 源 比 如ReentrantReadWriteLock。
AQS 的内部实现
AQS 队列内部维护的是一个 FIFO 的双向链表这种结构的特点是每个数据结构都有两个指针分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。每个 Node 其实是由线程封装当线程争抢锁失败后会封装成 Node 加入到 ASQ 队列中去当获取锁的线程释放锁以后会从队列中唤醒一个阻塞的节点(线程)。Node 的组成 释放锁以及添加线程对于队列的变化 当出现锁竞争以及释放锁的时候AQS 同步队列中的节点会发生变化首先看一下添加节点的场景。 里会涉及到两个变化 1. 新的线程封装成 Node 节点追加到同步队列中设置 prev 节点以及修改当前节点的前置节点的 next 节点指向自己 2. 通过 CAS 讲 tail 重新指向新的尾部节点head 节点表示获取锁成功的节点当头结点在释放同步状态时会唤醒后继节点如果后继节点获得锁成功会把自己设置为头结点节点的变化过程如下 这个过程也是涉及到两个变化 1. 修改 head 节点指向下一个获得锁的节点 2. 新的获得锁的节点将 prev 的指针指向 null 设置 head 节点不需要用 CAS原因是设置 head 节点是由获得锁的线程来完成的而同步锁只能由一个线程获得所以不需要 CAS 保证只需要把 head 节点设置为原首节点的后继节点并且断开原 head 节点的 next 引用即可