3分钟速读原著《Java并发编程的艺术》(三)

第5章 Java中的锁

本章主要介绍Java并发包当中与锁相关的API和组件

1.Lock接口

  • 1.1 锁就是用来控制多个线程访问共享资源的方式,简单来说,一个锁能够防止多个线程同时访问共享资源,Lock接口出现的比synchronized要晚一些,java5之后才开始出现的,使用的时候是属于显示的获取锁和释放锁,简单来说就是需要手动的去加锁和解锁,相比之下synchronized是隐式锁,synchronized简化了同步锁的管理,但是拓展性并没有显示的锁获取和释放来得好
  • 1.2 Lock使用一般都是搭配try-finally来进行使用的,在finally代码块当中去释放锁来避免出现死锁的现象
  • 1.3 Lock接口的实现基本都是通过了聚合饿了一个同步器的子类来完成线程访问控制的

2.队列同步器

  • 2.1 AbstractQueuedSynchronizer (队列同步器),是用来构建锁和其他同步组件的基础框架,使用int成员变量来表示同步状态,内置的FIFO队列来完成资源获取线程的同步工作
  • 2.2 同步器的主要使用方式是你集成,子类通过继承同步器并实现它的抽象方法来管理同步状态,本质使用的是乐观锁的形式进行的
  • 2.3 同步器的设计是 基于模板方法模式的,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中

3.重入锁

  • 3.1 支持重进入的锁,它表示锁能够支持一个线程对资源的重复加锁.除此之外,该锁还支持获取锁时的公平和非公平性的选择
  • 3.2 锁的公平性问题:如果在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁就是公平的,反之,是不公平的,事实上,公平的锁机制旺旺没有非公平的效率高,但是公平锁能够减少饥饿发生的概率,等待越久的请求越是能够优先的得到满足
  • 3.3 实现冲进入
    • ①重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞.该特性的实现需要解决两个问题
      • a) 线程再次获取锁.锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取锁
      • b) 锁的最终释放,线程重复N次获取了锁,随后在第N次释放锁后,其他线程能够获取到该锁,锁的最终释放要求锁对于获取进行计数自增,计数表示当前被重复获取的次数,而锁被释放时,计数自减,当计数为0的时候表示锁已经成功释放
    • ②重进入的本质就是线程执行的时候加上一条判断,判断是否该线程是当前获取锁的线程,如果是就再次获取,并且需要对最终释放上进行处理
  • 3.4 公平锁和非公平锁

在公平性锁和非公平性锁相比,总耗时是其94.3倍,总切换次数是133倍,可以看出公平锁为了保证锁的获取按照FIFO原则,而代价是进行了大量的线程切换,非公平性锁虽然可能造成线程的饥饿,但是极少的线程切换,保证了其更大的吞吐量

4.读写锁

  • 4.1 重入锁和队列同步器都是排它锁,这些锁的特性就是同一个时刻只允许一个线程进行访问,而读写锁是在同一个时刻允许多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞,读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大的提升
  • 4.2 读写锁的实现分析:
    • ①读写状态的设计
    • ②写锁的获取与释放
    • ③读锁的获取和释放以及锁降级
  • 4.3 锁降级:指的是写锁降级成为读锁
  • 4.4 锁降级的必要性:主要是为了保证数据的可见性,如果一个写锁在修改完数据之后,不是获取读锁,而是直接释放写锁,那就可能导致其它的写锁没法感知到当前数据被修改,从而继续去进行修改,造成了脏数据

5.LockSupport工具

  • 5.1 当需要阻塞或者唤醒一个线程的时候,都会使用LockSupport工具类来完成相对应的工作.LockSupport定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能
  • 5.2 LockSupport定义了一组以park开头的方法用来阻塞当前线程,park即是停车的意思,unpark方法就是唤醒当前线程,用于启动的方法

第6章 Java并发容器和框架

1.为何使用ConcurrentHashMap

  • 1.1 线程不安全的HashMap
    • ①多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发的情况下不能使用HashMap
    • ②HashMap在并发知心put操作时会引起死循环,因为多线程会导致会导致HashMap的Entry链表戏更环形数据结构,一旦性能环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry的next节点就永远不会为空,就会产生死循环获取Entry.
  • 1.2 效率低下的HashTable

HashTable使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下,因为当一个线程访问HashTable的同步方法时,会进入阻塞或者轮询的状态,如线程1使用put进行元素添加,线程2补单不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低下

  • 1.3 ConcurrentHashMap的锁分段技术

HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器中一部分数据,那么当多线程访问容器里的不同数据段的数据的时候,线程之间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的储存,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问

  • 1.4 ConcurrentHashMap的数据结构

ConcurrentHashMap是由Segment数组结构组成.Segment数组结构和HashEntry数组结构组成.Segment是一种可重入锁,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据

  • 1.5 ConcurrentHashMap的初始化
    • ①Segments锁的个数一定是2的N次方个,例如此时数据时14或15,都会选择最近的2的N次方来作为分段锁的个数,即是16,并且分段锁的个数最大值为65535,即是2的16次方-1
    • ②InitialCapacity是初始化的容量,loadFactor是加载因子,初始化ConcurrentHashMap的时候需要传入这三个参数,如果没有则会使用默认参数
    • ③默认情况下initialCapacity是16,loadfactor为0.75
    • ④散列函数:Hash算法,简单来说就是把一个不确定的任意值转换成为一个固定的值,我们暂且将传入的不确定的值作为key值,将输出的值作为value值,所以就可能会出现key值不相同,但是value值相同的情况,这种情况称之为碰撞,这种情况在以Hash作为数据结构的集合当中是允许存在的,但是key值是不能相同的
  • 1.6 ConcurrentHashMap的操作
    • ①Get操作
      • a) Segment的get操作实现是先经过一次再散列,获得一个固定值,然后使用这个固定的散列值通过散列运算定位到Segment(对应数据的分段锁),从而再通过散列算法定位到元素
      • b) Get操作的高效在于get过程不需要加锁,除非读到的值是空,就会加锁重读.HashTable的慢就是在任意get情况下都要加锁读,而不是读取null值时候才加锁读
    • ②Put操作
      • a) Put操作是需要对共享变量进行写入操作的,所以在操作共享变量的时候,一定会加锁,put方法首先堤内懂啊Segment,然后在Segment中进行插入操作,此时需要进行两步操作,第一:判断是否需要对HashEntry数组进行扩容,第二:定位添加元素的位置,然后将其放在HashEntry数组中
      • b) 在HashMap当中的自动扩容相对处理的就比ConcurrentHashMap要差一些,因为,HashMap扩容的是整个容器,所以可能在判断插入的元素达到阈值后的扩容,之后就再也没有元素插入了,但是ConcurrentHashMap只是对部分的Segmenet进行扩容,对整体的影响相对较小
      • c) 对于整个ConcurrentHashMap而言,整个容器的数据量会是所有Segment的总和

2.ConcurrentLinkedQueue

  • 2.1 在并发编程中,有时候需要使用线程安全的队列.如果要实现一个线程安全的队列有两种方式:
    • ①阻塞算法:使用同一个锁,入队和出队都使用同一个,或者是两个锁
    • ②非阻塞可以使用非循环的CAS算法来实现
  • 2.2 ConcurrentLinkedQueue是一个基于链接节点的无界限线程安全队列,它采用了FIFO的规则排序
  • 2.3 ConcurrentLinkedQueue的入队方法返回的永远都是true,所以要记得不要通过返回值来判断是否入队成功

3.阻塞队列

阻塞队列上一个支持两个附加操作的队列.这两个附加操作支持阻塞的插入和移除方法
  • 3.1 支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程,直到队列不满
  • 3.2 支持阻塞的移除方法:当队列为空时,队列会阻塞读取元素的线程,直到队列不为null
  • 3.3 阻塞队列的模型类似于生产者和消费者的模型
    • ①一直阻塞:当阻塞队列满时,如果生产者线程往队列里面put元素,队列会一直阻塞生产者线程,直到队列可以用或者响应中断退出,当队列空时,如果消费者线程从队列中获取元素,队列会阻塞消费者线程,直到队列不为空
    • ②超时退出:当阻塞队列满时,如果生产者线程往队列里面插入元素,队列会阻塞生产者线程一段实际,如果超过了指定的时间,生产者线程就会退出
  • 3.4 如果使用的是无界阻塞队列,那么队列不可能会出现满的情况
  • 3.5 Java中的7个阻塞队列
    • ①ArrayBlockingQueue:数组结构有界阻塞队列
    • ②LinkedBlockingQueue:链表结构有界阻塞队列
    • ③PriorityBlockingQueue:支持优先级排序无界阻塞队列
    • ④DelayQueue:使用优先级队列实现的无界阻塞队列
    • ⑤SynchronousQueue:不储存阻塞队列
    • ⑥LinkedBlockingDeque:链表结构组成的双向阻塞队列
    • ⑦LinkedTransferQueue:链表结构无界阻塞队列
  • 3.6 阻塞队列的实现原理

如果队列是空的,消费者会一直等待,当生产者添加元素时,阻塞队列当中的通知模式会通知消费者来进行消费

4.Fork/Join框架

Fork/Join框架是Java7开始提供的一个用于并行知悉该任务的框架,是一个把大任务分割成若干个小任务,最终汇总成为每个小任务结果够得到大任务结果的框架,从字面意义上面来进行理解,Fork就是将任务切割,Join就是将任务合并

  • 4.1 工作窃取算法:指的是某个线程从其他队列里窃取任务来执行.为了减少线程之间的竞争,把这些子任务分割为若干个子任务分别放到不同的队列当中,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列尾部拿任务执行
  • 4.2 工作窃取算法的优点:充分利用线程进行并行计算,减少了线程之间的竞争
  • 4.3 工作窃取算法的缺点:在某些情况下仍然存在竞争,比如双端队列里只有一个任务时,并且该算法会消耗更多的系统资源

5.Fork/Join框架的设计

  • 5.1 分割任务.我们需要一个fork类来把大任务分割成子任务,可能分割出来的子任务依旧很大,所以需要不停的分割,直到分割出的子任务足够的小
  • 5.2 执行任务并且合并结果.分割的子任务分别放在双端队列里,然后几个启动线程分别从双端队列里获取任务执行.子任务执行完的结果都同意放在一个队列里,启动一个线程从队列当中拿数据,然后合并这些数据
    • ①ForkJoinTask,要使用ForkJoin框架,必须首先创建一个ForkJoin任务,它提供在任务执行fork()和Join()操作的机制.通常情况下,我们不需要直接集成ForkJoinTask类,只需要集成它的子类
      • a) RecursiveAction:用于没有返回结果的任务
      • b) RecursiveTask:用于有返回结果的任务
      • c) ForkJoinPool:ForkJoinTask需要通过ForkJoinPool来执行

任务分割出的子任务会添加到当前工作线程所魏二虎的双端队列中,进入队列的头部.当一个工作线程当中暂时没有任务时,它会随机从其他工作线程的队列尾部获取一个任务

6.Fork/Join框架的实现原理

  • 6.1 当我们调用ForkJoinTask的fork方法时,程序会调用ForkJoinWorkerThread的pushTask方法异步执行这个任务
  • 6.2 Join方法主要作用就是阻塞当前线程并等待获取结果

转载于:https://www.jianshu.com/p/dd06f08a588f

猜你喜欢

转载自blog.csdn.net/weixin_33895695/article/details/91103723