《Java并发编程的艺术》读书笔记
《Java并发编程的艺术》读书笔记
爱宝贝丶 发表于6个月前
《Java并发编程的艺术》读书笔记
  • 发表于 6个月前
  • 阅读 27
  • 收藏 1
  • 点赞 0
  • 评论 0

腾讯云 新注册用户 域名抢购1元起>>>   

摘要: 本文主要记录本人在阅读《Java并发编程的艺术》时的一些笔记,特此记录

1.减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。

无所并发编程:多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据;

CAS算法:Java的Atomic包使用CAS算法来更新数据,而不需要加锁;

使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程处于等待状态;

协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

2.避免死锁的几个常见方法:

  • 避免一个线程同时获取多个锁;
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源;
  • 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制;
  • 对于数据库锁,加锁和解锁必须在一个数据库链接里,否则会出现解锁失败的情况。

3.在Java中,锁一共有四种,级别从低到高一次为:无所状态,偏向锁状态,轻量级锁状态和重量级锁状态。这几个状态会随着竞争情况逐渐升级,锁可以升级但不能降级,意味着偏向锁升级为轻量级锁后不能降级为偏向锁。

4.在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步代码块获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成了1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

5.偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行了字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

6.因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程会进行新一轮的夺锁之争。

7.原子操作意为“不可被中断的一个或一系列操作”。

8.术语定义: 缓存行(Cache line):缓存的最小操作单位; 比较并交换(Compare and Swap, CAS):CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换; CPU流水线(CPU pipeline):CPU流水线的工作方式就像工业生产上的装配流水线,在CPU中由5~6个不同功能的电路单元组成一条指令处理流水线,然后将一条X86指令分成5~6步后再由这些电路单元分别执行,这样就能实现在一个CPU时钟周期完成一条指令,因此提高CPU的运算速度。 内存顺序冲突(Memory order violation):内存顺序冲突一般是由假共享引起的,假共享是指多个CPU同时修改同一个缓存行的不同部分而引起其中一个CPU的操作无效,当出现这个内存顺序冲突时,CPU必须清空流水线。

9.处理器实现原子操作的方式:

  • 通过总线锁保证原子性。总线锁就是使用处理器提供的一个LOCK #信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞,那么该处理器可以独占共享内存。
  • 通过缓存锁定来保证原子性。频繁使用的内存会缓存再处理器的L1、L2和L3高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁。所谓“缓存锁定”是指内存区域如果被缓存再处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作会写到内存时,处理器不在总线上声言LOCK #信号,而是修改内部内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效。

10.如果一个操作执行的结果需要对另一个操作可见,那么两个操作之间必须要存在happens-before关系。

11.happens-before规则如下:

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作;
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁;
  • volatile变量规则:对一个volatile变量的写,happens-before于任意后续对这个volatile变量的读;
  • 传递性:如果A happens-before B,B happens-before C,那么A happens-before C;

12.happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。

13.重排序:重排序是指编译器和处理器为了优化程序性能而对指令序列而进行重新排序的一种手段。

14.编译器和处理器会对操作进行重排序,编译器和处理在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序;这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。

15.as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变,编译器,runtime和处理器都必须遵守as-if-serial语义。

16.在计算机中,软件技术和硬件技术有一个共同的目标:在不改变程序执行结果的前提下,尽可能提高并行度。

17.当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜测执行来客户控制相关性对并行度的影响。

18.在单线程程序中,对存在控制依赖的程序重排序,不会改变执行结果(这也是as-if-serial语义允许对存在控制依赖的操作进行重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。

19.数据竞争的定义:在一个线程中写一个变量,在另一个线程中读同一个变量,而且写和读没有通过同步来排序。

20.如果程序是正确同步的,程序的执行将具有顺序一致性--即该程序的执行结果与程序在顺序一致性模型中的执行结果相同。

21.顺序一致性内存模型的两大特性:

  • 一个线程中的所有操作必须按照程序的顺序来执行;
  • (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。

22.在Java内存模型中,临界区内的代码可以重排序(但Java内存模型不允许临界区内的代码逸出到临界区之外,那样会破坏监视器的语义)。Java内存模型会在退出临界区和进入临界区这两个关键时间点做一些特殊处理,使得线程在这两个时间点具有与顺序一致性模型相同的内存视图。

23.Java内存模型与顺序一致性模型的差别:

  • 顺序一致性模型保证单线程内的操作会按程序的顺序执行,而Java内存模型不保证单线程内的操作按照程序顺序执行;
  • 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而Java内存模型不保证所有线程能看到一致的操作执行顺序;
  • Java内存模型不保证对64位的long和double类型变量写操作具有原子性,而顺序一致性模型保证对所有的内存读写操作都具有原子性。

24.64位long和double类型变量的写操作不具有原子性的原因是:在一些32位的机器上,对于内存的写入操作,每次只能是原子的执行一段32位内存的写入,对于64位的变量,需要分两次原子的写入操作进行,因而整体来说是不具备原子性的。

25.在jdk1.5版本以前,一个64位long/double类型变量的读/写操作可以拆分为两个32位的读/写操作来进行;在jdk1.5版本(含)之后,64位的long/double类型变量的写入操作能够拆分为两个32位的写操作来进行,而读操作则通过jvm保证其具有原子性。

26.volatile变量的特性:

  • 可见性。一个volatile变量的读,总是能够看到最后一个线程对volatile变量写入的值;
  • 原子性。对任意单个volatile变量的读/写具有原子性,但是类似于volatile++这种复合操作不具备原子性。

27.当读一个volatile变量时,Java内存模型会把该线程对应的本地内存置为无效,而从主内存中读取共享变量。

28.volatile写和volatile读的内存语义:

  • 线程A写一个volatile变量,实质上是线程A向接下来所要读这个volatile变量的线程发出了(其对共享变量所作修改的)信息;
  • 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)信息;
  • 线程A写一个volatile变量,随后线程B读一个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。

29.volatile变量读写操作重排序的规则:

  • 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被重排序到volatile写之后。
  • 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被重排序到volatile读之前。
  • 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

30.Java内存模型对于volatile变量插入内存屏障的策略:

  • 在每个volatile写操作之前插入一个StoreStore屏障;
  • 在每个volatile写操作后面插入一个StoreLoad屏障;
  • 在每个volatile读操作后面插入一个LoadLoad屏障;
  • 在每个volatile读操作后面插入一个LoadStore屏障;

31.final域的重排序规则:

  • 在构造函数中对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序;
  • 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

32.通过为final域增加写和读重排序规则,可以为Java程序员提供初始化安全保证:只要对象是正确构造的(被构造对象的引用在对象构造过程中没有逸出),那么不需要使用同步(指lock和volatile的使用)就可以保证任意线程都可以看到这个final域在构造函数中被初始化之后的值。

33.对于会改变程序执行结果的重排序,Java内存模型要求编译器和处理器必须禁止这种重排序;对于不会改变程序执行结果的重排序,Java内存模型对编译器和处理器不做要求(Java内存模型允许这种重排序)。

34.Java内存模型对编译器和处理器已经尽可能少。从上面的分析可以看出,Java内存模型其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。

35.如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前;两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说Java内存模型允许这种重排序)。

36.jsr-133中定义的happens-before规则:

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作;
  • 监视器锁规则:对一个锁的解锁,happens-before于后续对该锁的加锁;
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读;
  • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C;
  • start规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程中的ThreadB.start()操作happens-before于线程B中的任意操作;
  • join规则:如果线程A执行ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。

37.当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。

38.Daemon属性需要在启动线程之前设置,不能再启动线程之后设置。

39.Daemon线程被用作完成支持性工作,但是在Java虚拟机退出时Daemon线程中的finally块并不一定会执行,因而在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。

40.线程start()方法的含义是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。

41.在启动一个线程时,最好为这个线程设置一个名称,这样在进行jstack分析时可方便问题排查。

42.调用wait()、notify()和notifyAll()需要注意的细节:

  • 使用wait()、notify()和notifyAll()时需要对调用对象加锁;
  • 调用wait()方法后,线程状态由RUNNING变成WAITING,并将当前线程放置到对象的等待队列;
  • notify()和notifyAll()调用之后,线程依旧不会从wait()返回,需要调用notify()或notifyAll()的线程释放锁之后,等待线程才有机会从wait()返回;
  • notify()将等待队列中的一个线程从等待队列移到同步队列中,而notifyAll()则是将等待队列中所有线程从等待队列移到同步队列中,被移动的线程状态由WAITING变为BLOCKED;
  • 从wait()方法返回的前提是获得了调用对象的锁。

43.Wait Notify的经典范式: ①等待方遵循如下规则:

  • 获取对象的锁;
  • 如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件;
  • 条件满足则执行对应的逻辑; 对应伪代码如下:
synchronized (对象) {
    while (条件不满足) {
        对象.wait();        
    }
    对象的处理逻辑
}

②通知方遵循如下原则:

  • 获得对象的锁;
  • 改变条件;
  • 通知所有等待在对象上的线程; 对应伪代码如下:
synchronized (对象) {
    改变条件;
    对象.notifyAll();
}

44.等待超时模式的伪代码:

public synchronized Object get(long mills) throws InterruptedException {
    long future = System.currentTimeMillis() + mills;
    long remaining = mills;
    
    // 当超时大于0并且result返回值不满足要求
    while ((result == null) && remaining > 0) {
        wait(remaining);
        remaining = future - System.currentTimeMillis();
    }

    return result;
}

45.如果在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的,反之,是不公平的。事实上,公平锁的效率没有非公平锁的效率高,但是公平锁能够减少“饥饿”发生的概率。

46.实现锁的重进入需要解决的问题:

  • 线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取;
  • 锁的最终释放。线程重复n此获取了锁,随后在第n此释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时,表示锁已经成功释放。

47.读写锁在同一时刻可以允许多个线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁:一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

48.读写锁将变量切分成了两个部分,高16位表示读,低16位表示写。

49.ReentrantReadWriteLock使用位来表征读写锁状态的方式:假设当前同步状态值为S,写状态等于S&0x0000FFFF(将高16位全部抹去),读状态等于S>>>16(无符号补0右移16位)。当写状态增加1时,等于S+1,当读状态增加1时,等于S+(1<<16),也就是S+0x00010000

50.根据上述状态划分得出一个结论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。

51.对于ReentrantReadWriteLock,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。

52.锁降级指的是把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。

53.LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark(Thread thread)方法来唤醒一个被阻塞的线程。

54.一般都会将Condition对象作为成员变量。当调用await()方法后,当前线程会释放锁并在此等待,而其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程才从await()方法返回,并在返回前获取到了锁。

55.在Condition的等待队列中并没有使用CAS算法来保证新加入的节点是加入到节点尾部,因为调用Condition.await()方法的线程必定是获取了锁的线程,其是通过锁来保证线程安全的。

56.多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点将永不为空,就会产生死循环获取Entry。

57.HashTable容器使用synchronized来保证线程安全,当一个线程访问HashTable的同步方法时,其他线程访问HashTable的任何方法都将进入阻塞或轮询状态。

58.ConcurrentHashMap容器中有多把锁,每一把锁只锁定一部分数据,多线程访问时,对于不同段数据的访问不需要竞争同一把锁,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap的锁分段技术。

59.ConcurrentHashMap在为Segament扩容的时候,首先会创建一个长度是原来两倍的数组,然后通过再散列将原数组中的数据散列到新的数组中。为了高效,ConcurrentHashMap不会对Segament进行扩容。

60.ConcurrentHashMap计算size的方式如下:在Segament对象中有一个字段count,该字段记录了当前Segament中所有的键值对的数目,在调用size()方法时通过累加每个Segament中的count,从而得到总的count,但是由于在累加的过程中可能之前累加的count发生了变化,因而ConcurrentHashMap通过两次累加每个Segament中的count,并比较两次得到的数值是否相同,如果相同则返回,否则将所有对表进行结构性修改的操作都锁住,然后进行一次累加得到结果。

61.阻塞队列中方法说明: 阻塞队列方法说明

62.各个阻塞队列说明: ①ArrayBlockingQueue:其是用一个数组实现的有界阻塞队列,按照先进先出的方式排序; ②LinkedBlockingQueue:其是一个用链表实现的有界阻塞队列,此队列的默认和最大长度为Integer.MAX_VALUE; ③PriorityBlockingQueue:其是一个支持优先级的无界阻塞队列,默认情况下按照自然顺序升序排序,如果指定了Comparator则按照指定的排序方式排序,需要注意的是其不能保证同优先级元素的顺序; ④DelayedQueue:其是一个支持延迟获取元素的无界阻塞队列,队列使用PriorityQueue实现,队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取到元素,只有延迟期满时才能从队列中提取元素; ⑤SynchronousQueue:其是一个不存储元素的阻塞队列,每个put操作必须等待一个take操作,否则不能添加元素,其也支持公平和非公平策略访问队列(该队列的吞吐量高于LinkedBlockingQueue和ArrayBlockingQueue); ⑥LinkedTransferQueue:其是一个有链表结构组成的无界阻塞TransferQueue队列,相比于其他阻塞队列,其多了tryTransfer和transfer方法,对于transfer方法,如果当前有正在等待的消费者,transfer方法会将生产者传入的元素立即transfer到消费者,如果没有等待的消费者,其会将元素放入到队列的tail节点,对于tryTransfer方法,其是用来试探生产者传入的元素能否直接传给消费者,如果有则返回true,否则返回false,对于有时间限的tryTransfer方法,如果有正在等待的消费者,该方法直接返回true,如果没有,则等待设定的时间,还是没有则返回false; ⑦LinkedBlockingDeque:其是一个由链表结构组成的双向阻塞队列,即可以从队列头和队列尾两端插入和移除元素,双向队列多了一个队列入队和出队的口,因而在多线程入队和出队时也就减少了一半的竞争。

63.DelayedQueue的两个应用场景: ①缓存系统的设计:可以用DelayedQueue设计缓存系统,将元素放置到DelayedQueue中后,如果在指定时间之后能从DelayedQueue中获取到元素,那么说明元素过期了; ②定时任务调度:可以将一些定时任务放到DelayedQueue中,通过一个线程轮询队列,当从队列中取到元素之后则执行该任务。

64.实现Delayed接口的方式(分三步): ①在创建对象时,初始化基本数据,使用time记录当前对象延迟到什么时候使用,使用sequenceNumber来标识元素在队列中的先后顺序; ②实现getDelay方法,该方法返回当前元素还需要延时多长时间,单位是纳秒; ③实现compareTo方法来指定元素的顺序,让延时时间最长的放在队列的末尾。

65.park方法会阻塞当前线程,其在以下四种情况下会返回:

  • 与park对应的unpark方法执行或已经执行,“已经执行”是指unpark先执行,然后执行park方法;
  • 线程被中断;
  • 等待完time参数指定的毫秒数;
  • 异常现象发生时,这个异常现象没有任何原因。

66.Fork/Join框架是一个把大任务分隔成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。

67.工作窃取算法:工作窃取算法是指某个线程从其他队列里窃取任务来执行,为了减少与正常执行线程之间的竞争,其一般是使用双端队列来完成。在Fork/Join框架中,一般fork出来的子任务是放在不同的队列中的,并且每个队列有一个线程执行其中的任务,如果某个线程执行得较快,任务已做完,其不能一直等待着其他的队列线程执行完任务之后才合并结果,因而需要使用工作窃取算法使其同步执行其他队列的任务。

68.工作窃取算法的优点:充分利用线程进行并行计算,减少线程间的竞争;缺点:在某些情况下还是存在竞争,比如双端队列只有一个任务时。并且该算法消耗了更多的系统资源,比如创建了多个线程和多个双端队列。

69.Fork/Join框架的设计:

  • 步骤一:分割任务。首先需要一个fork类来吧大任务分割成子任务,有可能子任务还是很大,所以还需要不停的分割,直到分割出的子任务足够小;
  • 步骤二:执行任务并合并结果:分割的子任务分别放在双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据。

70.Fork/Join框架的主要类:

  • ForkJoinTask:该类表示要执行的任务,使用时我们主要继承其两个子类:RecursiveAction和RecursiveTask。RecursiveAction表示没有返回值的任务,RecursiveTask表示有返回值的任务;
  • ForkJoinPool:执行ForkJoinTask的类。

71.ForkJoinTask在执行时,如果抛出异常,那么主线程是无法捕获到该异常的,因而该类提供了一个isCompletedAbnormally方法,用于检测是否正常完成,并且提供了一个getException方法,如果当前任务被取消了,那么该方法将返回CancellationException,如果任务没有完成或抛出异常,则返回null。

72.AtomicIntegerArray的构造函数可以传入一个整形数组,也可以传入一个length,其会在内部将传入的数组复制一份或者新建一个length长度的数组,因而对AtomicIntegerArray的操作不会影响原数组的值。

73.原子更新字段类AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicStampedReference使用时需要注意两点:

  • 因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性;
  • 更新类的字段(属性)必须使用public volatile修饰符。

74.AtomicStampedReference是原子更新带有版本号的引用类型,该类将整数值与引用关联起来,更新引用的时候也会原子的更新版本号,从而解决了使用CAS进行原子更新时可能出现的ABA问题。

75.CountDownLatch允许一个或多个线程等待其他线程完成任务之后才会继续往下执行,其内部维护了一个整数变量,每次调用countDown()方法,该整数变量都会减一,而另外的调用await()方法的线程将会被阻塞,直到多次调用countDown()方法后内部维护的整数变量值减为0了。

76.CountDownLatch在构造时需要传入一个整数,每次调用该类对象的countDown()方法时内部维护的该整数就会减一,而CountDownLatch::await()方法会阻塞当前线程,直到其内部维护的整数值降为0。这里countDown()方法的调用可以是一个线程调用多次,也可以是多个线程每个调用一次,只要调用该整数值次即可。

77.CountDownLatch构造函数传入的整数值必须大于等于0.

78.CyclicBarrier的作用是让一组线程到达一个屏障(也可以说是一个同步点)时阻塞,直到所有的线程都到达了该屏障,然后才让所有线程继续往下执行。

79.CyclicBarrier另外提供了一个如下构造函数CyclicBarrier(int parties, Runnable barrier-action),第一个参数还是指将要阻塞的线程数量,而第二个参数指定了一个任务,该任务会由第一个到达屏障的线程执行,但是其是在所有阻塞的线程执行当前任务到达屏障之后准备继续执行后续任务时才执行,也即所有阻塞的线程从await()方法中返回的时候。

80.Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。

81.线程池的优点:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗;
  • 提高响应速度。当任务到达时,任务可以部需要等到线程创建就能立即执行;
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

82.提交任务时线程池的处理流程如下: ①线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的线程来执行任务,如果核心线程池里的线程都在执行任务,则进入下一个流程; ②线程池判断工作队列是否已满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里,如果工作队列满了,则进入下一个流程; ③线程池判断线程池的线程是否都处于工作状态,如果没有,则创建一个新的线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

83.CachedThreadPool是大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。

84.FixedThreadPool适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的服务器。

85.SingleThreadExecutor适用于需要保证顺序执行各个任务,并且在任意时间点,不会有多个线程是活动的应用场景。

86.使用无界队列对FixedThreadPool带来了如下影响:

  • 当线程池中的线程达到corePoolSize之后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize,因而设置的maximumPoolSize将是一个无效参数;
  • 由于任务会一直添加到无界队列中,并且除了corePoolSize个线程之后不会创建新的线程,因而设置的keepAliveTime和RejectedExecutionHandler将不会产生影响。

87.CachedThreadPool底层是创建了一个ThreadPoolExecutor,其corePoolSize传入的是0,maxPoolSize是Integer.MAX_VALUE,空闲等待时间是60s,并且其使用的SynchronousQueue来处理任务。这里SynchronousQueue内部是没有容量保存任何任务的,每个线程提交了一个任务之后必须有一个线程取出任务。从这些参数可以看出,初始状态下CachedThreadPool中是没有任何线程的,当提交一个新的任务之后如果当前有空闲的线程,则该线程取出任务执行,如果没有线程空闲,那么就会创建一个新的线程,也就是说如果需要执行的任务非常多,那么创建的线程数将会急剧上升。因而CachedThreadPool适用于多任务数,并且执行时间较短的场景。

88.FutureTask表示一个将要执行的任务,其不仅实现了Future接口,还实现了Runnable接口,因而可以将其当做一个任务提交给Executor执行。FutureTask的任务有三种状态:未启动、已启动和已完成。对于已完成状态,其可能有三种状态:FutureTask::run()方法正常结束,run()方法被取消而结束,run()方法抛出异常结束。

89.当FutureTask处于未启动或已启动状态时,FutureTask::get()方法将导致线程阻塞;当FutureTask处于已完成状态时,FutureTask::get()方法将导致线程立即返回或抛出异常。

90.当FutureTask处于未启动状态时,FutureTask.cancel()方法将直接取消该任务的执行;当其处于已启动状态时,如果执行FutureTask::cancel(true),那么将以中断此任务执行线程的方式停止任务执行,如果执行FutureTask::cancel(false),那么其不会对正在执行此任务的线程产生影响;当FutureTask处于已完成状态时,执行FutureTask::cancel()方法将返回false。

91.在实际应用中,如果需要继承某个已有的类或抽象类,而基于“组合优先于继承”的原则,我们可以在要创建的类中声明一个内部类来继承需要继承的类,然后将当前类与内部类使用组合来解耦。

92.生产者和消费者是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通信,而是通过阻塞队列来进行通信,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

共有 人打赏支持
粉丝 32
博文 45
码字总数 152085
×
爱宝贝丶
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: