Java笔记之并发编程

  1. volatile修饰的共享变量,经汇编后较普通的共享变量多出一个lock前缀,该前缀会引起处理器缓存行的数据写回到系统内存;其次这个写回操作会使得其他cpu里缓存了该内存地址的数据无效(实质为处理器的主动嗅探)。以上特性保证了JMM要求的可见性。
  2. JMM,即java内存模型,需要解决的三个问题:原子性,可见性,有序性。
  3. JMM提供的八个操作:lock-unlock,read-write,load-store,use-assign。
  4. synchronized在JVM里的实现原理:JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。
  5. synchronized编译后形成的monitenter和monitexit指令,隐式的使用了JMM提供的lock-unlock操作,保证了JMM要求的原子性;与unlock操作相关联的store、write操作保证了JMM要求的可见性;依据“一个变量同一时刻只允许一条线程对其进行lock操作”这一规则,保证了JMM要求的有序性。
  6. 被final关键字修饰的变量一旦在构造器中初始化完成,并且该过程没发生this引用逃逸,那么在其他线程中就能看见final字段的值,所以final关键字也满足可见性。
  7. java对象头:MarkWord,Class Metadata Adress,(Array Length)。
  8. Java对象头里的MarkWork默认存储对象的HashCode、分代年龄和锁标记位。
  9. 无锁-->偏向锁-->轻量级锁-->重量级锁,锁只能升级不能降级。
  10. 处理器实现原子操作的方式:总线锁、缓存锁。
  11. java实现原子操作的方式:锁机制、循环CAS。
  12. 线程之间通信的方式:显示通信-发送消息,隐式通信-写/读内存中的公共状态。java的并发采用的是共享内存模型,java线程之间的通信由java内存模型控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。
  13. 数据依赖性:如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。编译器和处理器在重排序时,会遵守数据依赖性。
  14. as-if-serial语义:不管怎么重排序,(单线程)程序的执行结果不能被改变。
  15. 指令重排序:编译器优化的重排序(编译器级别)、指令级并行的重排序(处理器级别)、内存系统的重排序(处理器级别)。JMM的编译器重排序规则会禁止特定类型的编译器重排序,对于处理器重排序,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障指令,来禁止特定类型的处理器重排序。
  16. 内存屏障类型:LoadLoad、StoreStore、LoadStore、StoreLoad。
  17. happens-before:程序次序规则(流控语句) 、管程锁定规则(Monitor)、volatile变量规则、线程启动规则、线程终止规则、线程中断规则、对象终结规则、传递性。
  18. volatile内存语义的实现:JMM采取保守策略,在每个volatile写操作前面插入一个StoreStore屏障、后面插入一个StoreLoad屏障;在每个volatile读操作后面插入一个LoadLoad屏障和LoadStore屏障。
  19. JSR-133增强volatile内存语义,严格限制编译器和处理器对volatile变量与普通变量的重排序。由于volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁互斥执行的特性可以确保对整个临界区代码的执行具有原子性,在功能上,锁比volatile更强大,在可伸缩性和执行性能上,volatile更有优势。
  20. 锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中;当线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
  21. 锁释放与volatile写有相同的内存语义,锁获取与volatile读具有相同的内存语义。
  22. AQS = volatile + CAS 。
  23. java的CAS会使用现代处理器上提供的高效机器级别的原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多核处理器中实现同步的关键。同时,volatile变量的读/写和CAS可以实现线程之间的通信。整合这些特性,形成concurrent包得以实现的基石。
  24. CAS以原子的方式更新状态变量(volatile变量),compareAndSetState()方法是对native方法unsafe.compareAndSwapInt()方法的调用,该native方法在OpenJdk中依次调用unsafe.cpp,atomic.cpp和atomic_windows_x86.inline.hpp(windows系统,X86处理器),程序会在多核处理器的情形下,在cmpxchg指令加上lock前缀(单核处理器自身会维护单核处理器内的顺序一致性)。lock前缀确保对内存读-改-写操作原子执行。
  25. 对final域,编译器和处理器要遵守两个重排序规则:在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序;初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
  26. 写final域重排序规则的实现: JMM禁止编译器把final域的写重排序到构造函数之外;编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。
  27. 读final域的重排序规则是,在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作,而编译器会遵守“初次对对象引用与初次读该对象包含的final域”这两个操作之间的依赖关系。编译器会在读final域操作的前面插入一个LoadLoad屏障。
  28. 对于引用类型,写final域的重排序规则对编译器和处理器增加了如下约束:在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
  29. JMM是一个语言级的内存模型,处理器内存模型是硬件级的内存模型,顺序一致性内存模型是一个理想型的理论上的内存模型。
  30. JVM在类的初始化阶段(即在Class被加载,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁,这个锁可以同步多个线程对同一个类的初始化。
  31. Java语言规范规定,对于每一个类或接口C,都有一个唯一的初始化锁LC与之对应。从C到LC的映射,由JVM的具体实现去自由实现。JVM在类初始化期间会获取这个初始化锁,并且每个线程至少获取一次锁来确保这个类已经被初始化过了。
  32. 类初始化过程:通过在Class对象上同步(即获取Class对象的初始化锁),来控制类或接口的初始化,这个获取锁的线程会一直等待,直到当前线程能够获取到这个初始化锁;线程A执行类的初始化,同时线程B在初始化锁对应的condition上等待;线程A设置state=initialized,然后唤醒在condition中等待的所有线程;线程B结束类的初始化处理;线程C执行类的初始化处理......
  33. 处理器的内存模型: Total Store Ordering(TSO),Partial Store Order(PSO),Relaxed Memory Order(RMO),PowerPC。处理器内存模型要比JMM弱,Java编译器在生成字节码时,会在执行指令序列的适当位置插入内存屏障来限制处理器的重排序。各种处理器内存模型的强弱也不尽相同,JMM屏蔽了不同处理器内存模型的差异,为程序员在不同处理器平台上提供一致的内存模型。
  34. 单线程程序不会出现内存可见性问题。正确同步的多线程程序的执行将具有顺序一致性,即程序的执行结果与在顺序一致性模型中的执行结果相同。未同步/未正确同步的多线程程序,JMM提供了最小的安全性保障:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,null,false)。
  35. 一个java程序的执行不仅仅是main()方法的运行,而是main线程和多个其他线程的同时运行。java线程优先级的范围是1~10,默认是5。设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高的优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。
  36. java程序的正确性不能依赖java线程的优先级高低,其实说白了,有时候java线程的优先级仅仅是java语言的“一厢情愿”,在不同的JVM以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定。
  37. Linux、Windows平台下,java线程靠内核线程实现,为一比一线程模型,当然,操作系统不会直接开放内核线程的使用权限,会通过一个轻量级进程接口供上层语言使用。
  38. java线程的生命周期:NEW,RUNNABLE(包括RUNNING,READY),BLOCKED,WAITING,TIME_WAITING,TERMINATED。线程在自身的生命周期中,并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换。
  39. Daemon线程:一种支持性线程,主要被用作程序中后台调度以及支持性工作,当JVM中年不存在非Daemon线程时,JVM会退出,可通过Thread.setDaemon(true)将线程设置为Daemon线程。
  40. 构造线程:线程对象在构造的时候需要提供线程所需要的属性,如线程所属的线程组、线程优先级、是否是Daemon线程等信息。一般而言,一个新构造的线程对象是由其parent线程(main线程)来进行空间的分配的,而child线程继承了parent线程是否为Daemon、优先级、和加载资源的contextClassLoader以及可继承的ThreadLocal,同时还会分配一个唯一的ID来标识这个child线程。
  41. 线程start()方法的含义:当前线程(即parent线程)同步告知JVM,只要线程规划器空闲,应立即启动调用start()方法的线程。
  42. 线程中断:中断可以理解为线程的一个标识位属性,它表示一个线程(不一定是运行中的线程,即被创建后未调用其start()方法)是否被其他线程进行了中断操作。其他线程调用该线程的interrupt()方法对其进行中断操作。线程可以通过isInterrupted()方法进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。如果线程已处于终结状态,即使该线程被中断过,在调用该线程的isInterrupted()方法时依旧会返回false。
  43. 过期的suspend(),resume()和stop()方法:suspend()方法在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。可以使用等待/通知机制代替线程的暂停和恢复操作。
  44. 死锁产生的条件:资源的互斥性,不可抢占,占有且等待,环形等待。除了资源的互斥性,可以从其他三个条件入手破坏死锁的产生。
  45. java支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程拥有的是这个变量的拷贝,所以在程序执行过程中,每个线程看到的变量不一定是最新的。
  46. java线程间的通信:volatile和synchronized关键字。volatile修饰变量能够保证所有线程对变量访问的可见性。synchronized可以修饰对变量访问的整个方法或者代码块以达到同一时间只允许一个线程进行访问的操作,从而保证对变量访问的可见性和排他性。
  47. 等待/通知机制:是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()方法或者notifyAll()方法,线程A收到通知后从对象O的wait()方法,进而执行后续操作。
  48. 等待/通知的相关方法是任意java对象都具备的,因为这些方法被定义在所有对象的超类Object类上。
  49. 管道输入/输出流:区别与普通的文件输入/输出流或者网络输入/输出流,它的主要作用是用于线程之间的数据传输,传输的媒介为内存。包括PipedOutputStream、PipedInputStream、PipedReader和PipedWriter。对于Piped类型的流,必须先调用connect()方法进行绑定(即output.connect(input)),否则对于该流的访问会报错。
  50. ThreadLocal与ThreadLocal.ThreadLocalMap:ThreadLocal.ThreadLocalMap,即线程变量,是一个以ThreadLocal对象为键,任意对象为值的存储结构。
  51. Java中的Lock锁:Lock接口出现之前,java程序靠synchronized关键字实现锁功能,但是synchronized关键字固化了锁的获取与释放。Java SE 5之后,并发包新增了Lock接口以及相关实现类用来实现锁功能,虽然它缺少了(synchronized块或者方法所提供的)隐式获取锁释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。
  52. 不要将获取锁的过程写在try块中,因为如果在获取锁时(自定义锁的实现)发生了异常,异常抛出时,也会导致锁无故释放。
  53. Lock接口的基本实现都是通过聚合了一个同步器的子类来完成线程访问控制的。
  54. 队列同步器AbstractQueuedSynchronizer(AQS):是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量(volatile修饰的)表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,通过同步器提供的三个方法getState()、setState()和compareAndSetState()来对同步状态进行更改。
  55. AQS同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。这一原则被称为“聚合优于继承原则”。
  56. AQS同步器可重写的方法:tryAcquire()-独占式获取同步状态,tryRelease()-独占式释放同步状态,tryAcquireShared()-共享式获取同步状态,tryReleaseShared()-共享式释放同步状态,isHeldExclusively()-当前同步器是否在独占模式下被线程占用。
  57. AQS同步器提供的模板方法:acquire()-独占式获取同步状态;acquireInterruptibly()-独占式获取同步状态,但是该方法响应中断;tyrAcquireNanos()-响应中断的基础上增加超时限制;acquireShared()-共享式获取同步状态;acquireSharedInterruptibly()-中断式共享获取同步状态;tryAcquireSharedNanos()-中断的基础是增加超时限制;release()-独占式的释放同步状态;releaseShared()-共享式的释放同步状态;getQueuedThreads()-获取等待在同步队列上的线程集合。
  58. 在同步措施的保障下,虽然只有一个线程可获得锁,但其余获取失败的线程同样需要在同步措施的保障下才可被正确地添加到同步队列中(区别于等待队列节点的添加)。
  59. enq()方法:同步器通过“死循环”的方式来保证同步队列节点(Node,封装了线程)的正确添加,在“死循环”中通过CAS将节点设置为尾节点之后,当前线程才能从该方法返回,否则当前线程不断地尝试设置。enq()方法将并发添加节点的请求通过CAS变得“串行化”了。
  60. 同步队列特性(先进先出)的维护:头节点是成功获取到同步状态的节点,当头节点的线程释放同步状态后,会唤醒同步队列中的所有节点,但只有前驱节点是头节点的节点才会获取到同步状态,例如可重入锁中的公平锁,而非公平锁则会破坏队列的特性。
  61. 区别于Lock锁的获得(即获得同步状态),无论是方法级同步还是代码块级同步,synchronized锁加锁的本质是,将锁对象的对象头中的MarkWrod字段复制到当前线程的桟上,并将锁对象头中的MarkWord替换为指向线程桟上MarkWord所在位置的指针。
  62. 可重入锁ReentrantLock:可重入锁,表示该锁能够支持一个线程对资源的重复加锁。synchronized关键字隐式的支持重进入。ReentrantLock虽然没能像synchronized关键字一样支持隐式的重进入,但是在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获得锁而不被阻塞。其实两者实现锁的重进入的本质差不多,都是证明同一线程对象在获取,只不过证明方式不一样。
  63. ReentrantLock锁的三个内部类:Sync-继承了AQS同步器,FairSync-公平锁,NonfairSync-非公平锁(默认)。
  64. NonfairSync较FairSync不公平的体现:获取锁时(即调用lock()方法),非公平锁不管同步状态字段(volatile修饰int变量,初始值为0)是否为预期值0,允许线程直接使用CAS更新同步状态字段预期值为1,更新失败后才同公平锁一样让线程去尝试获取锁;在尝试获取锁的过程中,非公平锁不管有无竞争,直接让获取锁的线程使用CAS将状态变量更新为预期值,而公平锁只允许队首的线程使用CAS去更新状态变量。
  65. 公平锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。
  66. 排他锁同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程均被阻塞。
  67. 读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大的提示。Java并发包提供读写锁的实现是ReentrantReadWriteLock,支持非公平锁(默认)和公平锁、支持重进入、支持锁降级(遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁)。
  68. LockSupport定义了一组公共的静态方法,这些方法提供了最基本的线程阻塞(park系列方法)和唤醒(uppark方法)功能,LockSupport也成为构建同步组件的基础工具。
  69. 等待/通知模式的两种实现:synchronized + wait(),notify()方法;Lock + Condition。虽然这两种行为都可以实现等待/同志模式,但是这两者在使用方式以及功能特性上还是有差别的。
  70. Condition对象是由Lock对象创建的,也就是说Condition对象依赖Lock对象。 当调用await()方法后,当前线程会释放锁并在此等待,当其他线程调用Condition对象的signal()方法,得到通知后当前线程才从await()方法返回,并且在返回前已经获取了锁。
  71. 一般而言,Lock锁的实现者会选择聚合同步器AQS类,而由Lock锁创建的Condition对象(通过newCondition()方法)正是同步器AQS的内部类ConditionObject类的实例,每个Condition对象都包含着一个队列(等待队列),该队列是Condition对象实现等待/通知功能的关键。
  72. 等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程的引用,该线程就是在Condition对象上等待的线程。等待队列中的节点复用了同步器AQS中节点的定义,也就是说,同步队列和等待队列中节点类型都是同步器AQS的静态内部类AbstractQueuedSynchronizer.Node。
  73. 在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的Lock(确切说是同步器)拥有一个同步队列和多个等待队列。
  74. 区别于同步队列节点的添加(enq()方法中循环使用CAS追加节点直至成功),等待队列节点的添加没有是CAS来保证,因为调用Condition对象await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。
  75. 从队列的角度看,获得锁的线程调用await()方法,相当于同步队列的首节点移动到Condition的等待队列中(尾插法)。同步队列的首节点并不会直接加入等待队列,而是通过addConditionWaiter()方法把当前线程构造成一个新的节点后再将其加入等待队列中。
  76. 调用Condition对象的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒(使用LockSupport.unpark()方法)节点前,会将节点移动到同步队列中(使用同步器的enq()方法)。同await()方法调用一样,调用signal()方法的线程一定是获得了锁的线程。被唤醒后的线程,将从await()方法中的while循环中退出,退出条件是节点已在同步队列中(isOnSyncQueue()方法返回true),进而调用同步器的acquireQueued()方法加入到获取同步状态的竞争中。
  77. Condition对象的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal()方法,效果就是将等待队列中的所有节点全部移动到同步队列中,并唤醒每个节点的线程。
  78. HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,进而导致Entry的next节点永远不会为空,就会产生死循环获取Entry。
  79. HashTable容器使用synchronized来保证线程的安全
  80. 并发编程中使用HashMap可能导致死循环,而使用线程安全的HashTable效率又非常低下,基于以上两个原因,ConcurrentHashMap诞生了。
  81. ConcurrentHashMap的锁分段技术:假如容器里有多把锁,每一把锁用于锁容器里的一部分数据,那么当线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而达到提高并发访问效率的目标。
  82. ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁,在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数据。Segment的结构和HashMap类似,是一种数组和链表结构,而HashEntry是一种链表结构的元素。一个Segment包含一个HashEntry数组,而一个ConcurrentHashMap包含一个Segment数组。一个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segmennt锁。
  83. 如果要实现一个线程安全的队列有两种方式:一种是使用阻塞算法,另一种是使用非阻塞算法。使用阻塞算法的队列可以用一个锁或者两个锁等方式来实现。非阻塞的方式则可以使用循环CAS的方式来实现。
  84. 有界队列是一种特殊的队列,当队列为空时,队列的获取操作将会阻塞获取线程,直到队列中有新增元素,当队列已满时,队列的插入操作将会阻塞插入线程,直到队列出现“空位”。
  85. ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,采用FIFO规则,尾插法添加新节点。
  86. 阻塞队列是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变非空。
  87. java里常见的阻塞队列:ArrayBlockingQueue-一个由数组结构组成的有界阻塞队列;LinkedBlockingQueue-一个由链表结构组成的有界阻塞队列;PriorityBlockingQueue-一个支持优先级排序的无界阻塞队列;Synchronous-一个不存储元素的阻塞队列;LinkedTransferQueue-一个由链表结构组成的无界阻塞队列;LinkedBlockingDeque-一个由链表结构组成的双向阻塞队列。
  88. 阻塞队列常用语生产者和消费者的场景,使用通知模式实现。所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。
  89. Fork/Join框架:是java7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
  90. 工作窃取算法:是指某个线程从其他队列里窃取任务来执行。
  91. Fork/Join框架的实现原理:ForkJoinPool由ForkJoinTask数组和ForkJoinWorkerThread数组组成,ForkJoinTask数组负责将存放程序提交给ForkJoinPool的任务,而ForkJoinWorkerThread数组负责执行这些任务。
  92. Java中提供了13个原子操作类,属于4种类型的原子更新方式,分别是:原子更新基本类型,原子更新数组,原子更新引用和原子更新属性。
  93. Java中的并发工具类:CountDownLatch、CyclicBarrier和Semaphore工具类提供了一种并发流程控制的手段,Exchanger工具类则提供了在线程间交换数据的一种手段。
  94. 合理使用线程池的好处:降低资源消耗,提高响应速度,提高线程的可管理性。
  95. 向线程池提交一个任务后,线程池的处理流程:线程池判断核心线程池里的线程是否都在执行任务,如果都在执行任务,则进入下一个流程;线程池判断工作队列是否已满,如果没有满,则将新的任务存储在这个工作队列里,如果已满,则进入下个流程;线程池判断线程池的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务,如果已满里,则交给饱和策略来处理这个任务。
  96. 线程池创建线程时,会将线程封装成工作线程Worker,Worker在执行完任务后,还会循环获取工作队列里的任务来执行。
  97. 创建线程池(ThreadPoolExecutor)所需的参数:corePoolSize-核心线程数,runnableTaskQueue-任务队列,maximumPoolSize-最大线程数,ThreadFactory-创建线程的工厂,RejectedExecutionHandler-饱和策略,keepAliveTime-线程活动保持时间,TimeUnit-线程活动保持时间的单位。
  98. corePoolSize(线程池基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小就不再创建。可调用线程池的prestartAllCoreThreads()方法提前创建并启动所有基本线程。
  99. runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可选的队列有:ArrayBlockingQueue-基于数组的有界阻塞队列;LinkedBlockingQueue-基于链表、吞吐量通常高于ArrayBlockingQueue,静态工厂方法Executors.newFixdThreadPool()创建的线程池使用了该队列;SynchronousQueue-一个不存储元素的阻塞队列,一个线程的插入操作必须等到另一个线程的移除操作,否则一直阻塞着,吞吐量通常高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool()使用了该队列;priorityBlockingQueue-一个具有优先级的无限阻塞队列。
  100. maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果任务队列满了,但是已创建的线程数小于最大线程数,则线程池会继续创建新的线程执行任务。如果使用了无界队列,该参数就没什么效果。
  101. ThreadFactory:用于设置创建线程的工厂。
  102. RejectedExecutionHandler(饱和策略):当队列和线程池都满了的话,提供一种策略来处理新提交的任务。默认使用AbortPolicy(直接抛出异常)表示无法处理,其他策略有CallerRunsPolicy(使用调用者所在线程来运行任务)、DiscardOldestPolicy(丢弃队列里最近的一个任务,并执行当前任务)、DiscardPolicy(不处理,丢弃)。当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略,如记录日志或持久化存储不能处理的任务。
  103. keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。当任务多且线程执行时间短的话,可以调大该参数。
  104. TimeUnit(线程活动保持时间的单位):天-DAYS、小时-HOURS、分钟-MINUTES、毫秒-MILLISECONDS、微秒-MICROSECONDS、纳秒-NANOSECONDS。
  105. 可以使用两个方法向线程池提交任务,分别为execute()和submit()方法。execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程执行成功;submit()方法用于提交需要返回值的任务,线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值。但是get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,任务可能没有执行完。
  106. 关闭线程池:可以通过调用线程池的shutdown()或shutdownNow()方法来关闭线程池,实现原理是逐个调用线程的interrupt()方法来中断线程,所以无法响应中断的任务可能永远无法终止。
  107. shutdownNow()方法首先将线程池的状态设置成STOP,然后尝试停止所有正在执行或暂停任务的线程,并返回等待执行任务的列表。而shutdown()方法只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。以上两个方法随便一个调用后,线程池的isShutdown()方法就返回true,但是只有当所有的任务关闭后,才表示线程池关闭成功,即线程池的isTerminaed()方法返回true。
  108. 线程池的合理配置需要分析任务的特性,例如任务的性质(CPU密集型或者IO密集型甚至混合型)、任务的优先级、任务的执行时间、任务的依赖性(是否依赖其他系统资源,如数据库连接)。
  109. 性质不同的任务可以使用不同规模的线程池分开处理。CPU密集型任务应配置尽可能小的线程(如N+1,N为CPU物理核数)。由于IO密集型任务并不是一直在执行任务,则应配置尽可能多的线程(如2N)。混合型任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量;如果两个任务的执行时间相差很大,则没必要进行任务拆分。可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
  110. 优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。
  111. 执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行。
  112. 依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,所以线程数应设置得越大才能更好的利用CPU。
  113. 建议使用有界队列,有界队列能增加系统的稳定性和预警能力。
  114. 线程池的监控:可以通过线程池提供的参数进行监控。在监控时可以使用的属性有taskCount(线程池需要执行的任务数量)、completedTaskCount(线程池在运行过程中已完成的任务数)、largestPoolSize(线程池里曾经创建过的最大线程数,当该数等于最大线程数时,证明线程池满过)、getPoolSize(线程池的线程数量)、getActiveCount(活动的线程数)。
  115. 通过扩展线程池进行监控:可以通过继承线程池来自定义线程池,重写线程池的beforeExecute、afterExecute和terminated方法,也可以在任务执行前、后和线程池关闭前添加代码块来进行监控。例如监控任务的平均执行时间、最大执行时间和最小执行时间等。
  116. Java的线程即是工作单元也是执行机制,从JDK5开始,把工作单元和执行机制分离开来。工作单元包括Runnable和Callable,而执行机制由Executor框架提供。
  117. Executor框架主要由3大部分组成:任务(Runnable或Callable接口)、任务的执行(包括任务执行机制的核心接口Executor,以及继承自Executor的ExecutorService接口)、异步计算的结果(包括Future和实现Future接口的FutureTask类)。
  118. Executor是一个接口,它是Executor框架的基础,它将任务的提交与任务的执行分离开来。Executor框架有两个关键类实现了ExecutorService接口,分别是ThreadPoolExecutor和ScheduledThreadPoolExecutor。
  119. Runnable接口和Callable接口的实现类,都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行,它们之间的区别是Runnable不会返回结果,而Callable可以返回结果。可以使用Executors工具类把一个Runnable对象封装为一个Callable对象(Executors.callable(Runnable task )方法或Executors.callable(Runnable task,T result)),事实上,在以submit()方法提交任务后,在FutureTask对象的构造方法里会将Runnable对象封装为Callable对象,但只有前面的第二种封装方法才会返回结果,即FutureTask.get()方法的结果为result对象,而第一种封装方式get方法会返回null。封装行为是在FutureTask对象的构造方法里,而调用传参行为确实在submit()方法的重载上体现的。
  120. Future接口和实现Future接口的FutureTask类用来表示异步计算的结果。 从API上看,submit()方法 只是返回了一个Future对象,在未来的JDK实现中,并不一定是FutrueTask对象。
  121. FutureTask除了实现Future接口外,还实现了Runnable接口,因此FutureTask可以交给Executor执行,也可以由调用线程直接执行(FutureTask.run())。根据FutureTask.run()方法被执行的时机,FutureTask将处于3种状态,即未启动(run()方法未执行前)、已启动(run()方法被执行的过程中)、已完成(run()方法执行完正常结束,或被调用FutureTask.cancle()方法取消,或执行时抛出异常而异常结束)。
  122. 当FutureTask处于未启动或已启动状态时,执行FutureTask.get()方法将导致调用线程阻塞;当处于已完成状态时,调用get()方法将导致调用线程立即返回结果或抛出异常。
  123. 但FutureTask处于未启动状态时,执行FutureTask.cancle()方法将导致此任务永远不会被执行;当处于已启动时,执行FutureTask.cancle(true)方法将以中断执行此任务线程的方式来试图停止任务,执行FutureTask.cancle(false)方法将不会对正在执行此任务的线程产生影响;当处于已完成状态时,执行cancle()方法将会返回false。
  124. 通过Executor框架的工具类Executors,可以创建出3种类型的ThreadPoolExecutor,即FixedThreadPool、SingleThreadExecutor、CachedThreadPool。
  125. FixedThreadPool被称为可重用固定线程数的线程池,即核心线程数等于最大线程数,该线程池使用无界队列LinkedBlockingQueue作为工作队列,队列容量为Integer.MAX_VALUE。
  126. SingleThreadExecutor是使用单个worker线程的Executor,即核心线程数和最大线程数被设置为1,SingleThreadExecutor使用无界队列LinkedBlockingQueue作为工作队列,队列容量为Integer.MAX_VALUE。
  127. CachedThreadPool是一个会根据需要创建新线程的线程池,即核心线程数为0,最大线程数为Integer.MAX_VALUE,是无界的,同时CachedThreadPool使用没有容量的Synchronous作为线程池的工作队列,这意味着如果主线程提交任务的速度高于线程池中线程处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU和内存资源。
  128. ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,主要用来在给定的延迟之后运行任务,或者定期执行任务。其功能与Timer类似且更强大、灵活。Timer对应的是单个后台线程,而ScheduledThreadPoolExecutor可以在构造函数中指定多个对应的后台线程数。
  129. ScheduledThreadPoolExecutor为了实现周期性的执行任务(ScheduledFutureTask),对ThreadPoolExecutor做了以下修改:使用DelayQueue作为工作队列;获取任务的方式不同;执行周期任务后,增加了额外的处理。
  130. ScheduledFutureTask主要包含3个成员变量:long型成员变量time,表示这个任务将要被执行的具体时间;long型成员变量sequenceNumber,表示这个任务被添加到ScheduledThreadPoolExecutor中的序号;long型成员变量period,表示任务执行的间隔周期。DelayQueue封装了一个PriorityQueue,这个PriorityQueue会对队列中的 ScheduledFutureTask进行排序。排序时,time小的排在前面;如果time相同,就比较sequenceNumber,小的排在前面。也就是说,两个任务的执行时间相同,那么先提交的任务将被先执行。从DelayQueue中获取任务使用DelayQueue.take()方法,向队列添加任务使用DelayQueue.add()方法,这两个方法都会涉及到锁的获取与释放。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值