第一章 走进并行世界
1、临界区
表示共享资源或者共享数据
2、同步与异步
如果系统中存在临界资源(资源数量少于竞争资源的线程数量的资源),例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就必须进行同步存取(数据库操作中的排他锁就是最好的例子)。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。事实上,所谓的同步就是指阻塞式操作,而异步就是非阻塞式操作。
3、死锁、饥饿、活锁
死锁:多个线程互现持有对方手中的资源,多个线程一起进去阻塞状态
饥饿:高优先级线程插队,低优先级线程无法获得资源,但未来有可能得到解决
活锁:让路问题,线程谦让,主动释放所持有地资源,导致资源不断地在两个线程之间跳动而又无法进行下去
【面试题】死锁的解决方案:
· 死锁出现的原因是因为各个线程均不肯释放手中的资源而直接进入阻塞状态,因此可以使用可重入锁ReentrantLock的中断响应和限时等待来解决
①中断响应:lock.lockInterruptibly(); 等待锁时可以被中断,中断响应逻辑中可以释放已持有的锁资源
②锁申请等待限时:lock.tryLock(时间),规定时间内等不到返回false,可以采取释放锁的操作
· 释放所持有的锁并不意味着操作失败,可以在外层设置循环,再重新尝试获取锁资源,直到成功。
4、并发级别
阻塞、无饥饿、无障碍、无锁、无等待
· 无饥饿:公平锁
·无障碍:自由进出,遇到冲突回滚操作(不循环)。例如一致性标志法检测冲突
·无锁:自由进出,遇到冲突循环重试,最终总有能走出临界区的,例如CAS锁
·无等待:访问只有,修改加锁并检测冲突
5、加速比
· 加速比 = time优化前 / time优化后
· 不仅取决于CPU的核数,还取决于串行比例
6、JMM:原子性、可见性、有序性
· 原子性:
基本、引用数据类型的赋值和引用是原子性操作。
但在32位的机子中,long和double的引用和赋值则是可分割的。
· 有序性:(happen-before原则)
指令重排问题,为了减少中断流水线的次数,在汇编指令层面上。
指令重排的仅保证串行语义的一致,但没有义务保证多线程之间的语义也一直,因此多线程时不能保证有序性。
哪些指令不能重排:happen-before规则
第二章 Java并行程序基础
2.1 进程
· 进程是系统资源分配和调度的基本单位
· 进程是程序的实体
· 线程是轻量级进程,是程序执行的最小单位
· 使用多线程而不是多进程的原因?
线程间切换和调度的成本远小于进程
· 线程状态6种:new、runable、waiting、timed waiting、blocked、terminated
2.2 线程基本操作
· 不要用run方法来开启线程,他只会在当前线程中串行地执行run方法中的代码
· 创建线程三种方式
2.2.2 终止线程
· Thread.stop():太过暴力,会直接强制终止线程,并释放其持有的所有资源,包括锁,引发一致性问题
可以通过增加stopme标志和对应标志函数来实现安全的终止
2.2.3 线程中断(interrupt)
· 需要自己在run方法中增加中断处理逻辑
2.2.4 等待和通知(wait和notify)
· Object类中的方法
· 二者在使用前均需要获得目标对象的一个监视器,因此他必须被包含在synchronized语句中
· notify执行后仅唤醒在目标对象的等待队列中的线程,使其进入runable状态,但自己并不会立马释放该对象的监视器
· wait()和sleep()的区别:sleep()不会释放任何资源
2.2.5 挂起和继续执行(suspend和resume)
· suspend不会释放任何锁资源,直到调用resume
· 因此不推荐,已被弃用。若resume在suspend前执行(在不同线程中调用时会出现这种情况),则线程会一直处于挂起状态
· 可以利用wait()和notify()方法,在应用层面实现suspend()和resume()的功能(会释放锁资源)
2.2.6 等待线程结束(join)和谦让(yield)
· 线程1.join() :需先调用start方法,再用join(), 暂停当前线程,无限期等调用线程结束,也有含参方法,可设置等待时间。
其本质是让调用线程wait在线程对象实例上。
因此在应用程序中,尽量不要使用线程对象作为锁对象,避免出现错误
· yield() :让出当前cpu,进入runable状态,等待cpu重新调度,当然也可能还是他自己抢到cpu执行权
2.3 volatile与Java内存模型(JMM)
· 在虚拟机的Server模型下,由于系统优化的结果,临界区数据的修改并不一定发生能被其他线程发现
· volatile关键字:高速虚拟机,该变量是不稳定的,会在不同的线程中被修改,每次使用记得实时地去临界区看看最新的数据
· volatile保证修改可见性
2.4 分门别类管理:线程组
ThreadGroup:方便统一管理和进行一些统计
2.5 驻守后台:守护线程(Daemon)
· 是系统的守护者,负责为用户(工作)线程提供服务,例如垃圾回收线程、JIT线程就可以理解为守护线程。但一个java应用内只有守护线程时,虚拟机就会自然退出。
2.6 线程优先级
· 1-10,数值越大优先级越高
2.7 线程安全的概念与synchronized
· 作用是实现线程间的同步
· 同步代码块、同步方法、静态同步方法的锁对象
· synchronized可以保证多线程的有序性和可靠性
· synchronized既保证了可见性又保证了访问共享变量的原子性
· synchronized原理:
获得锁
清空线程变量副本
拷贝共享变量到线程变量副本
执行代码
将修改后的变量副本拷贝到共享数据区
释放锁
2.8 隐蔽的错误
· 并发下的ArrayList
数组越界问题:保存容器大小的变量被多线程不正常的访问
数据丢失问题:多个线程对容器同一个位置进行赋值
· 并发下的HashMap
jdk 1.7:多个线程操作容器,执行扩容操作的数据迁移函数transfer函数时,有可能造成数据丢失和链表循环的情况
jdk 1.8:解决了之前的问题。将头插改为尾插,并将数据迁移操作合并到了resize函数中,然而在多个线程进行put操作时存在数据覆盖的问题
· 加锁的注意事项
不要把不可变类型对象当作锁对象,例如String、Integer,因为当进行拼接或计算时,这类引用会重新指向新的对象,并发操作时就可能获取到不同的对象
面试题:
合适的线程数量是多少?CPU核心数和线程数的关系?
https://blog.csdn.net/qq_29860591/article/details/113618636
①CPU密集型任务:
首先,我们来看 CPU 密集型任务,比如加密、解密、压缩、计算等一系列需要大量耗费 CPU 资源的任务。对于这样的任务最佳的线程数为 CPU 核心数的 1~2 倍,如果设置过多的线程数,实际上并不会起到很好的效果。此时假设我们设置的线程数量是 CPU 核心数的 2 倍以上,因为计算任务非常重,会占用大量的 CPU 资源,所以这时 CPU 的每个核心工作基本都是满负荷的,而我们又设置了过多的线程,每个线程都想去利用 CPU 资源来执行自己的任务,这就会造成不必要的上下文切换,此时线程数的增多并没有让性能提升,反而由于线程数量过多会导致性能下降。针对这种情况,我们最好还要同时考虑在同一台机器上还有哪些其他会占用过多 CPU 资源的程序在运行,然后对资源使用做整体的平衡。
②耗时IO型任务:
第二种任务是耗时 IO 型,比如数据库、文件的读写,网络通信等任务,这种任务的特点是并不会特别消耗 CPU 资源,但是 IO 操作很耗时,总体会占用比较多的时间。对于这种任务最大线程数一般会大于 CPU 核心数很多倍,因为IO 读写速度相比于 CPU 的速度而言是比较慢的,如果我们设置过少的线程数,就可能导致 CPU 资源的浪费。而如果我们设置更多的线程数,那么当一部分线程正在等待 IO 的时候,它们此时并不需要 CPU 来计算,那么另外的线程便可以利用 CPU 去执行其他的任务,互不影响,这样的话在任务队列中等待的任务就会减少,可以更好地利用资源。
作者:疯狂麦克斯鸭
链接:https://www.jianshu.com/p/26d4d667f9fd
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。