《java并发编程的艺术》学习小结
Posted guangdeshishe
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《java并发编程的艺术》学习小结相关的知识,希望对你有一定的参考价值。
java并发编程的艺术
第一章 并发编程的挑战
- 上下文切换:cpu通过时间片让不同线程轮流运行,从线程状态保存到下一次线程运行这个过程就是一次上下文切换
- 多线程并不一定比单线程快,因为多线程会有线程创建和上下文切换的开销
- 如何减少上下文切换:
- 无锁并发:锁竞争会导致其他线程挂起,加锁、解锁会导致线程频繁上下文切换
- 不同线程处理不同分段数据
- 使用CAS算法:比如Atomic包
- 减少线程数
- 协程:在单线程中实现多个任务同时运行
- 无锁并发:锁竞争会导致其他线程挂起,加锁、解锁会导致线程频繁上下文切换
- 避免死锁办法:
- 避免一个线程同时获取多个锁资源
- 尝试使用定时锁,lock.tryLock(timeout)
- 资源限制导致并发效率低,例如:服务器带宽只有2M/s,某个资源下载限速是1M/s,通过并发10个线程,也不能达到10M/s的下载速度
- 网络带宽、硬盘读写速度、cpu处理速度等资源限制
第二章 Java并发机制的底层实现原理
- Volatile:volatile修饰的变量在多线程下能保证它的可见性,也就是其他线程在访问该变量值时总是能获取到最新的值;通过内存屏障还可以防止指令重排
- 实现原理:可见性是在写操作时增加lock指令,它会使得cpu高速缓存区内的值修改后立即同步到内存中,同时会让其他高速缓存区内的数据失效,当其他线程访问该变量值时会从新从内存中读取到高速缓存区中
- 防止指令重排:cpu执行的指令时不一定是按照固定的顺序来执行的,会根据情况来调整执行顺序,比如在单例模式中对象创建时,一般要经过这三个步骤,分配内存获取对象内存地址、调用构造方法初始化,最后把地址赋值给变量;由于指令重排,有可能出现先给变量赋值,后调用构造方法初始化的情况,就是会导致对象虽然不为null。但是由于对象还没有初始化完成从而导致异常,volatile就可以避免这种情况发生
- 使用Volatile变量时队列优化:可以通过填充的字节数到64个字节,这样在缓存行是64个字节宽的CPU上执行,入队和出队的效率会更高,这是因为如果对头和队尾都不足64位字节情况下,会把它们读取到同一个缓存行中,当操作队头时,根据缓存一致性原则会锁定缓存行,这个时候队尾就不能操作了,会影响性能
- JUC包中jdk7新增的LinkedTransferQueue就是这种实现方式,仅适用于缓存行是64位宽的处理器,以及写操作频繁时使用,如果写不频繁,由于增加了字节数会导致一定的性能消耗
- Synchronized:JVM通过Monitor对象在编译时同步代码前后加入monitorenter和monitorexit指令来实现的
- 不管是对象锁还是类锁,多线程访问不同synchronized方法也是需要等待的
- jdk1.6开始对Synchronized进行个各种优化,加入了偏向锁和轻量级锁,锁就有个四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态
- 对象内存结构:对象头信息(MarkWord+对象所属类的指针+数组长度)+实例数据+对齐填充
- MarkWord:HashCode+分代年龄+是否偏向锁+锁状态
- 锁膨胀过程:
- 当一个线程进入同步方法或者同步代码块时,会先检查锁状态,如果值是01,表示无锁或者偏向锁状态;如果已经是偏向锁状态,并且线程id是自己的,则直接运行代码;否则执行下一步
- 通过cas操作尝试将偏向锁状态改为1,同时将MarkWord中的线程ID改成自己的;如果成功则获取锁成功,如果失败则表示有其他线程在竞争该锁,就会执行释放偏向锁
- 当达到安全点的时候,原来获取偏向锁的线程会暂停,检查它的状态,如果线程已经处于不活动状态或者退出了同步代码块,则会重新设置为无锁状态(线程id置空+是否偏向锁为0+锁标记位为01);如果线程还在执行同步代码块中的代码,则升级为轻量级锁;
- 在升级轻量级锁之前会将已经获取偏向锁的线程在栈帧中开辟一块锁记录空间,将MarkWord信息拷贝到锁记录中,修改锁状态为00,并将对象中MarkWord指向该锁记录地址,也就是把升级后的轻量级锁给原有获取了偏向锁的线程
- 接着当前线程也会开辟一块空间记录锁记录,将MarkWord拷贝一份到锁记录中,并通过CAS操作尝试将对象头中MarkWord中地址指向当前线程的锁地址,如果成功则表示获取到了轻量级锁,如果多次失败后;会升级为重量级锁,对象的MarkWord就会记录Monitor对象的地址并修改锁状态为10;其他线程会阻塞并等待唤醒
- 原来持有轻量级锁的线程释放锁时会将线程栈帧中的锁记录通过cas操作替换对象头中的MarkWord数据,如果成功则表示没有发生竞争,如果失败则表示存在竞争,此时锁已经升级成重量级锁了,然后会唤醒其他线程重新竞争锁
- 锁的种类:
- 偏向锁:锁状态[01];对象头Markword中记录获得偏向锁的线程ID;解锁时清空MarkWord中记录的线程id,并重置锁状态为[01];适合没有竞争,只有一个线程访问同步代码块的情况;
- 轻量级锁:锁状态[00];拷贝对象头中的Markword信息到栈帧锁记录中,cas操作将对象头中Markword替换为锁记录的地址;解锁时cas操作将Markword信息从锁记录拷贝回来;适合竞争的线程较少,并且获取锁等待时间较短的场景;
- 重量级锁:锁状态[10];将对象头中Markword信息修改为重量级锁的地址,也就是Monitor对象地址;只有线程获取到monitor锁时才能访问同步代码块,其他线程会阻塞进入休眠,等待解锁后被唤醒重新竞争锁资源;适合线程竞争较多,获取锁等待时间较长的场景
- GC:锁状态[11]
- 为什么锁的膨胀只能升级不能降级?
- 因为锁一旦升级了,说明有更多的线程竞争资源,cas操作是通过自旋来实现的,一旦其他线程不能很快获取到锁资源就会一直自旋等待,浪费了cpu资源,而如果升级到了重量级锁,未获得锁的线程会阻塞进入休眠状态,就不会浪费cpu资源了
- 偏向锁记录的线程ID会导致HashCode值丢失吗?
- 不会,因为计算HashCode的时候,如果已经是偏向锁状态,会膨胀为轻量级锁或者重量级锁,将MarkWord信息保存到栈帧锁记录中或者Monitor对象中,其中就有HashCode值
- 如果MarkWord已经记录有HashCode的情况下不会进入偏向锁
- 原子操作实现原理
- 处理器实现原子操作:
- 总线锁:当一个cpu处理内存数据时,会在总线上发送Lock信号,其他cpu会被阻塞,避免同时访问内存
- 缓存锁:当cpu修改缓存行中数据时会锁定缓存行中数据,回写到内存后,缓存一致性机制会使得其他cpu的缓存行中数据失效,需要重新从内存中读取最新数据到缓存行中
- 当操作的数据无法缓存到CPU三级缓存中或者操作的数据跨越多个缓存行数据时会调用总线锁定
- java实现原子操作:
- 锁机制:ReentrantLock
- 自旋CAS操作:循环执行cas操作直到成功为止;AtomicInteger\\AtomicBoolean(volatile+cas实现)等
- ABA问题:cas操作是通过比较是否与预期的值一致,如果相同就执行替换操作,但是如果一个变量的值一开始是A,然后变成B,最后有变回A,这个时候的A其实是发生变化了的,如果对修改操作敏感的场景下就会出现问题;解决办法就是增加一个字段记录值的版本,每次修改值则版本号+1;cas操作时同时检查值和版本号;JDK提供了AtomicStampedReference类解决ABA问题
- 自旋时间太长会占用CPU资源:可以根据自旋次数或者超时来取消操作
- 处理器实现原子操作:
第三章 Java内存模型
- 在每个线程中会有主内存的一份拷贝,叫做本地内存(或者工作内存),线程中操作的变量都是本地内存中的,当修改完之后会同步到主内存
- 主内存和工作内存之间的8大操作:
- Lock(锁定):作用于主内存变量,锁定变量为一个线程独占
- UnLock(解锁):作用于主内存变量,对变量解锁
- read(读取):作用于主内存变量,将变量值从主内存传输到工作内存
- load(载入):作用于工作内存变量,将read获取到的值赋值给工作内存中的变量
- use(使用):作用于工作内存,将变量值传给执行引擎执行
- assign(赋值):作用于工作内存,将执行引擎返回的值赋值给变量
- store(存储):作用于工作内存,将工作内存中的值传递到主内存
- write(写入):作用于主内存,将上一步store操作获取到的值赋值给主内存中的变量
- happens-before、as-if-serial:避免指令重排序导致结果错误
- 指令重排序:为了优化处理器的并行处理速度
- 顺序一致性:一个线程所有操作必须按照程序的顺序来执行;每个操作都必须是原子性且对其他线程可见的
- volatile:可以保证可见性和防止指令重排序,但是不能保证操作的原子性;保证可见性是当volatile变量被修改后会立刻同步都主内存,同时让其他工作空间中的值失效,其他工作空间总是能读取到内存中最新的值;通过在字节码中加入内存屏障防止指令重排实现
- 锁在释放时会把数据同步到主内存,保证可见性
- final:可以保证其他线程读取到时都是初始化之后的值,但是需要保证final变量所在的对象在构造方法初始化阶段不能逃逸出来;
- 双重检查锁定:可能会导致获取到的对象还没有初始化,因为实例初始化分为三个步骤:1、分配内存空间;2、对象初始化;3、变量指向内存地址;由于第二步和第三步可能发生指令重排,也就是变量不为null时,对象可能还没初始化;
- 解决办法:1、变量加上volatile,禁止指令重排序;2、使用静态内部类初始化实例;
第四章 java并发编程基础
- 线程状态:
- NEW:线程被创建,但是还没调用start方法
- RUNNING:运行状态,线程准备就绪和已经在运行统称为运行中
- BLOCKED:阻塞状态,属于被动等待,比如等待锁的释放
- WAITING:等待状态,主动调用Object.wait()、Thread.join()、LockSupport.park(thread)等方法,需要其他线程调用Object.notify()/notifyAll等才能唤醒
- wait必须要在synchronized内调用,调用后线程会进入等待状态,并且释放锁;sleep不会释放锁;nofity只能随机唤醒某个线程;park和unpark可以指定对某个线程休眠或者唤醒
- 调用notify后,线程会进入BLOCKED阻塞状态,重新获取锁后才会从等待的地方恢复
- Thread.join实现原理是调用等待线程的Objet.wait方法,当线程结束后会调用Object.notifyAll唤醒线程
- TIME_WAITING:超时等待状态,Thread.sleep(),等待一定时间后会自动恢复
- TERMINATED:终止状态,表示当前线程已经执行完毕结束了
- 终端命令:
- jps:查看所有java进程id
- jstack pid:查看某个进程所有线程状态
- Daemon线程:当jvm中不存在非Daemon线程时,虚拟机会退出,可以理解为不重要的线程,其中的finally代码不一定会执行
- 线程的终止:
- interrupt:只是一个标记位表示中止,不一定会终止线程
- suspend/resume/stop:已经过时了,因为suspend虽然可以暂停线程,但是暂停期间还持有锁,容易造成死锁问题,而stop虽然可以终止线程,但是会使得线程没有机会释放资源
- 标志位:在run方法中添加标记为判断
- javap xxx.class:查看字节码
- Synchronized:通过monitorenter/monitorexit指令实现;等待的线程会进入BLOCKED阻塞状态
- PipedOutputStream/PipedInputStream/PipedReader/PipedWriter:管道输入输出流,用于线程之间传输数据,传输媒介是内存
- ThreadLocal:该变量在每个线程都有一份,并且可以线程内独立变化不会影响到其他线程里的值,实现原理是在Thread类里面有个ThreadLocalMap用来保存当前线程所有的ThreadLocal变量值,这个Map把ThreadLocal变量作为key,把ThreadLocal变量的值作为Value;ThreadLocalMap内部结构其实是一个Entry数组,Entry继承自WeakReference弱引用,保存着ThreadLocal这个变量和对应的值,数组通过ThreadLocal的hashcode&数组长度-1作为数组的索引
- CountDownLatch:可以保证多个线程同时开始或者都结束后才继续;实现原理主要是通过它父类AQS实现,当调用await时会判断状态值是否为0,如果不是则会调用LockSupport.park方法阻塞当前线程,当其他线程调用countdown方法时,status状态值会减1,直到status==0时再调用unpark唤醒线程
- Collections.synchronizedList:获取线程安全的List;内部实现是将List封装到另一个List中,当调用List的一些方法时,会加上Synchronized同步锁
第五章 Java中的锁
- AQS(AbstractQueuedSynchronizer):队列同步器
- JUC包实现锁相关类的基础
- 通过CAS操作的状态stat
- 同步等待队列(FIFO);获取同步状态失败时会被构建成Node节点加入到队列中,然后每个节点都在不停的自旋,并判断如果前置节点是队列中的头节点则尝试获取同步状态(为了保证FIFO特点),如果成功则将当前节点设置为头结点,接着调用LockSupport.park阻塞当前线程等待被唤醒;队列由双向链表实现
- 当同步状态被释放后,队首线程会被唤醒重新获取同步状态;队首线程释放同步状态后又会唤醒它下一个节点线程
- 共享式获取同步状态与独占式主要区别在于,共享式可以允许多个线程同时获取锁,比如对文件的读操作,或者服务端处理请求时可以限制同时有几个线程可以访问Semaphore,而独占式只允许同一时间允许一个线程访问
- 超时等待:如果设置的超时时间小于1000纳秒,也就是1微妙,则会忽略超时,因为时间太短超时也不准确
- ReentrantLock:支持公平和非公平锁,公平的意思是先进队列的先获得锁,非公平是指所有线程进行竞争;默认是非公平锁
- 可重入锁特点:同一个线程如果已经获取到了锁,则可以多次调用Lock再次获取锁,每次获取锁state状态会+1;但是释放锁时也需要释放N次,其他线程才能重新获取到锁
- 公平性:公平和非公平在获取锁时主要区别在于,公平锁会先判断当前节点是否有前置节点并且是队列头节点,如果有的话则优先获取锁;非公平锁性能更高,因为它会优先把锁分配给上一个获取到锁的线程减少线程切换开销;而公平锁会导致线程切换频繁
- ReentrantReadWriteLock:同时支持可重入以及读写的锁,内部通过将int整形的状态划分成高16位表示读锁的状态,低16位表示写锁的状态
- 写锁的获取需要保证当前没有线程获取到读锁,因为如果有读锁的情况下获取写锁会导致读到的数据不是最新的
- 读锁的获取需要保证当前没有除了自己以外的其他线程获取到写锁,自身获取到写锁的情况下可以获取读锁
- 锁的降级:线程先获取到写锁,然后获取到读锁,接着释放了写锁,这个时候就降级到了读锁,主要用于修改数据后又立刻需要使用数据的场景
- 锁不支持升级:因为如果有多个线程同时获取到了读锁,其中一个升级为写锁修改了数据,那么其他线程读到的数据就不能保证是最新的
- LockSupport:内部实现是调用了Unsafe的park和park方法;park方法中Blocker对象是用于标记当前阻塞等待的对象
- Condition.await/signal/signalAll:依托于ReentrantLock.newCondition创建,内部维护一个等待队列
- 当调用await方法时,当前线程必须获取到锁,然后Node节点会从同步队列中移除并创建新的Node节点放到Condition队列的末尾,然后调用LockSupport.park将当前线程休眠并释放锁;接着会唤醒同步队列中下一个节点去获取锁
- 当调用signal方法时,也必须要先获取到锁,然后将Condition中等待定队列中的头结点转移到同步队列中并尝试获取锁,如果获取锁成功,则从调用await的地方返回
- signal则相当于对Condition中每个节点都执行一遍signal方法;
第六章 Java并发容器和框架
- HashTable是通过Synchronized保证线程安全,这会导致写方法被锁住后,也无法执行读取的方法,效率很低
- HashMap在多线程环境下容易导致死循环,因为在put时可能会触发扩容操作,扩容是采用头插入,可能会导致链表出现死循环
- CocurrentHashMap:采用了分段锁技术,把数据分成一段一段的,每一段数据由一把锁控制,当访问某一段数据时,不会影响其他段数据的读写
- Segment分段锁继承自ReentrantLock
- get方法不需要加锁,因为get方法涉及到的变量都使用了volatile修饰,保证了线程之间的可见性,每次都能获取到最新的值
- 相比HashMap;它扩容时会先判断是否超过容量,超过时才扩容,而HashMap是插入后才扩容,可能会导致扩容后没有新元素插入从而浪费空间
- 扩容时仅仅对当前Segment扩容,不会对整个ConcurrentHashMap扩容
- ConcurrentLinkedQueue:使用cas保证线程安全
- 阻塞队列:为空或者满时会阻塞队列
- add/remove/element:抛出异常
- offer/poll/peek:返回结果;或者超时返回结果
- put/take:一直阻塞
- ArrayBlockingQueue:数组结构组成的有界阻塞队列
- LinkedBlockingQueue:链表结构组成的无界阻塞队列
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列
- DelayQueue:延迟无界阻塞队列,只有延迟的时间到了才可以取出元素;可用于定时任务;或者缓存有效期检测
- SynchronousQueue:不存储元素的阻塞队列,当队列加入一个元素时必须要等待消费了才能返回
- LinkedTransferQueue:链表组成无界阻塞队列;相比普通LinkedBlockingQueue,它还支持transfer方法,用于实现类似SynchronousQueue效果,必须要等待消费了才能返回
- LinkedBlockingDequeue:链表实现的双向无界阻塞队列
- 阻塞实现原理:内部是通过两个Condition,一个用于控制队列满,一个用于控制队列空时的阻塞,通过调用condition的await和signal方法来阻塞和唤醒线程,而await和signal又是通过调用LockSupport的part/unpark方法将当前线程睡眠或者唤醒
- Fork/Join框架:将复杂的任务分割成一个个很小的任务,然后将每个任务的结果合并起来得到最终的结果
- 工作窃取算法:当多个线程同时执行很多任务时,有些线程会提前执行完,这时候它可以从其他线程的任务队列中窃取任务继续执行,提高执行效率,为了减少竞争,任务队列会采用双端队列,正常获取任务从队列头部获取,而窃取任务则从队列尾部获取任务
- 缺点就是当双端队列中只有一个任务时,窃取任务时还是有可能会有竞争;由于采用多线程、多个双端队列,会造成更多系统资源的消耗
- ForkJoinPool:包含一个ForkJoinTask数组和ForkJoinWorkerThread数组,前者保存要执行的任务,后者保存执行任务的线程
- ForkJoinTask:fork方法主要用于将当前任务保存到Task数组中,然后交给线程去执行;join方法用于阻塞线程并获取结果
- RecursiveTask:有返回结果的任务
- RecursiveAction:没有返回结果的任务
第七章 Java中的13个原子操作类
- AtomicBoolean/AtomicInteger/AtomicLong/:通过自旋调用Unsafe类里的cas操作实现,cas支持object/int/long操作,AtomicBoolean是通过将boolean转成int后使用cas操作实现的
- AtomicIntegerArray/AtomicLongArray/AtomicReferenceArray
- AtomicReference/AtomicReferenceFieldUpdater/AtomicMarkableReference
- AtomicIntegerFieldUpdater/AtomicLongFieldUpdater/AtomicStampedReference:需要先调用newUpdater方法设置操作的类和字段;要更新的字段必须volatile修饰
第八章 Java中的并发工具类
- CountDownLatch(countDown/await):等待多个线程都完成才能继续,内部实现是通过不断的循环判断计数器是否为0,为0 了才能继续,否在继续等待,countDown方法会将计数器减一并调用notify唤醒线程;不可以重复使用
- CyclicBarrier(await):同步屏障,只有每个线程都达到await调用地方后才能继续;可以调用reset方法复用;和CountDownLatch相比,CountDownLatch调用coutnDown方法后线程可以继续执行,而CyclicBarrier线程调用await则进入的等待,不能继续执行
- Semaphore(acquire/release):信号量,用于控制能有多少个线程同时访问某个资源;调用acquire方法时会判断允许访问的线程数是否已经满,满了则阻塞不允许访问
- Exchanger:线程之间数据交换;当调用exchange方法时,会阻塞当前线程等到另一个线程也调用exchange方法,通过该方法可以拿到另一个线程的值,应用场景:当两个数据需要对比结果时可以用
第九章 Java中的线程池
- 线程池关键参数:
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数,包括核心线程数在内
- keepAliveTime:线程执行完任务后等待多长时间关闭,核心线程需要设置allowCoreThreadTimeOut为true才有效
- unit(TimeUnit):线程等待关闭时间的单位
- workQueue(BlockingQueue):阻塞等待队列
- ArrayBlockingQueue:有界阻塞队列
- LinkedBlockingDequeue:链表实现无界等待队列
- SynchronousQueue:不存储元素的阻塞队列,当队列加入一个元素时必须要等待消费了才能返回
- PriorityBlockingQueue:支持优先级的无界等待队列
- threadFactory(ThreadFactory):线程创建工厂类
- handler(RejectedExecutionHandler):队列和线程池都满时的处理策略
- AbortPolicy:直接抛出异常
- CallerRunsPolicy:在调用者所在线程执行任务
- DiscardOldestPolicy:丢弃队列中最早的任务,并加入当前任务到队列中
- DiscardPolicy:丢弃当前任务
- 线程池核心原理:
- 当任务进入线程池时,首先判断核心线程数是否已满,如果未满,则创建新的核心线程池执行任务
- 如果核心线程数已满,则判断等待队列是否已满,如果等待队列未满,则进入等待队列中;当线程执行完其他任务会从等待队列中取出新的任务执行
- 如果等待队列满了,则判断总的线程数是否已满,如果未满,则开启新的普通线程执行任务
- 如果总的线程数也已经满了,则执行对应的拒绝策略
- 线程池的几种创建方法:
- Executors.newFixedThreadPool:核心线程数固定,最大线程数等于核心线程数,等待队列是LinkedBlockingQueue无界等待队列
- Executors.newSingleThreadPool:核心线程数为1的FixedThreadPool
- Executors.newCachedThreadPool:核心线程数为0;最大线程数为整数的最大值,使用同步等待队列,SynchronousQueue,也就是来一个任务就创建一个线程
- Executors.newScheduledThreadPool:核心线程数固定,最大线程数为整数的最大值,使用DelayQueue
- DelayQueue封装了PriorityQueue,根据delay时间和优先级对任务排序,时间更早的和优先级更高的优先执行
- 周期性执行实现是通过执行完一次后自动修改delay时间,然后重新放入队列中,相比Timer来说更灵活功能强大,Timer是单线程
- FixedThreadPool和SingleThreadPool使用了无界阻塞队列,如果任务量过大的话可能会导致队列爆满内存溢出
- CachedThreadPool和ScheduleThreadPool设置了线程最大数为整数的最大值,这可能会导致线程数过大从而导致内存溢出
- 建议根据业务需求自定义ThreadPoolExecutor参数
- 线程池任务提交:
- submit:这种方式提交的会返回一个Feature对象用于获取执行结果,需要搭配Callable使用,如果使用runnable依然无法获取到结果
- execute:无法获取执行结果
- Callable支持返回结果和抛出异常,而runnable则不行
- 线程池关闭:
- shutdown:取消还没有开始执行的任务,已经在执行的任务等待执行完后关闭线程池
- shutdownNow:取消所有未开始执行的任务,已经在执行的任务调用interrupt方法尝试中断线程
- isTerminated:判断线程池是否完全关闭,isShutdown为true只能说明调用过shutdown/shutdownNow方法
- 线程池的缺点和优化:
- 核心线程默认不会超时后关闭,需要主动调用allowCoreThreadTimeOut
- 任务提交后如果判断核心线程数量没满,则不管核心线程有没有空闲的,都会去创建新的核心线程;可以先判断有没有空闲的线程再考虑创建新线程
- 当等待队列数很大的时候,可能会导致任务很久才能执行到,可以自定义等待队列,判断当最大线程数没满的时候,优先创建新的线程执行
- 线程池类型:
- CPU密集型:计算量比较大,占用cpu时间比较长的,应该尽量少配置线程数,一般cpu核心数+1个就可以了
- IO密集型:由于IO操作不会一直在执行占用cpu,所以可以配置多点线程数,一般cpu核心数*2个
- 获取cpu核心数:Runtime.getRuntime().availableProcessors()
- 线程池监控,可以通过以下几个方法获取状态:
- taskCount:当前需要执行的任务数
- completedTaskCount:已完成的任务数
- largestPoolSize:创建过的最大线程数
- getPoolSize:当前线程池中线程的数量
- getActiveCount:当前活动的线程数
第十章 Executor框架
- FeatureTask内部是通过AQS实现,调用get方法时如果任务还没执行结束则会阻塞当前线程,当任务结束时会唤醒当前线程并返回结果
以上是关于《java并发编程的艺术》学习小结的主要内容,如果未能解决你的问题,请参考以下文章