Java并发编程——Concurrent Programming

进程和线程
进程
程序由指令和数据组成,指令要运行,数据要读写,必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令,管理内存,管理IO的。当一个程序被运行,从磁盘加载这个程序的代码至内存,就开启了一个进程。进程可以视为程序的一个实例,大部分进程可以同时运行多个实例进程,如记事本、浏览器等,也有进程只能启动一个实例进程,如音乐软件等
线程
一个进程之内可以分为一到多个线程;一个线程就是一个指令流,将指令流中的一条指令从一定的顺序交给CPU执行,Java中,线程作为最小调度单位,进程作为资源分配的最小单位。在windows中进程是不活动的,只是作为线程的容器
二者对比:
1、进程基本上相互独立,线程存在于进程内,是进程的一个子集
2、进程拥有共享的资源,如内存空间等,供内部的线程共享
3、进程间通信较为复杂:同一台计算机的进程通信称为IPC;不同计算机之间的进程通信需要通过网络,并遵守共同的协议,如HTTP
4、线程通信相对简单,因为它们共享进程内的内存,多个线程可以访问同一个共享变量
5、线程更轻量,线程上下文切换成本一般比进程上下文切换低
并行与并发
单核CPU下,线程实际还是串行执行的。操作系统中有一个组件叫做任务调度器,将CPU的时间片(最小约为15ms)分给不同的线程使用,CPU在线程间的切换非常快,给人感觉是同时运行的。即微观串行,宏观并行。将线程轮流使用CPU的做法称为并发(concurrent)
多核CPU下,每个核都可以调度运行线程,这时线程可以是并行的(parallel)
并发:同一时间应对多件事情的能力     并行:同一时间动手做多种事情的能力
应用之异步调用
从方法调用的角度,需要等待结果返回,才能继续运行同步;不需要是异步
多线程可以让方法执行变为异步的,如读取磁盘文件时,假设读取操作花费了5s,若没有线程调度机制,这5s调用者什么都做不了
结构
1、在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,可以避免阻塞主线程
2、tomcat的异步servlet,目的是让用户线程处理耗时较长的操作,避免阻塞tomcat的工作线程
3、ui程序中,开线程进行其他操作,避免阻塞ui进程
应用之提高效率
计算1花费10ms,计算 2花费11ms,计算3花费9ms,汇总需要1ms,如果串行执行,总花费31ms。如果是四核,各核心分别使用线程1执行计算1,...,3个线程是并行的,花费时间只取决于最长线程运行的时间,即11ms,再加汇总时间
结论
1、单核CPU下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用CPU,不至于一个线程总占用CPU
2、多核CPU可以并行跑多个线程,但能否提高程序运行效率还是分情况的
3、IO操作不占用CPU,一般拷贝文件使用的是阻塞IO,要一直等待IO结束,没能充分利用线程
Java线程
创建和运行线程
方法一:直接使用Thread
Java并发编程——Concurrent Programming_第1张图片
方法二:使用Runnable配合Thread
Java并发编程——Concurrent Programming_第2张图片
法一法二比较
1、方法一是把线程和任务合并在一起,方法二是线程和任务分开了
2、用Runnable更容易与线程池等高级API配合
3、用Runnable让任务脱离了Thread继承体系,更为灵活
线程运行原理
线程上下文切换(Thread Context Switch)
以下原因导致CPU不再执行当前线程,转而执行另一个线程代码
1、线程的CPU时间片用完   2、垃圾回收   3、有更高优先级的线程需要运行    4、线程自己调用了sleep、yield、wait、park、synchronized、lock等方法
当上下文切换发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java对应的概念是程序计数器,作用是记住下一条JVM指令的执行地址,是线程私有的;状态包括程序计数器,JVM栈中每个栈帧的信息,如局部变量,操作数栈,返回地址等;Context Switch频繁发送会影响性能
sleep和yield
sleep
1、调用sleep会让当前线程从Running进入Timed Waiting状态
2、其他线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException
3、睡眠结束后的线程未必会立刻得到执行
4、建议用TimeUnit的Sleep代替Thread的sleep获得更好的可读性
yield
1、调用yield会让当前线程从Running进入Runnable状态,然后调度执行其他同优先级的线程。如果这时没有同优先级的线程,不能保证让当前线程暂停的效果
2、具体的实现依赖于操作系统的任务调度器
线程优先级
1、线程优先级会提示调度器优先调度该线程,仅仅是一个提示,调度器可以忽略
2、如果CPU比较慢,优先级高的线程会获得更多时间片,CPU闲时,优先级几乎没有作用
案例:防止CPU占用100%
sleep实现
在没有利用CPU来计算时,不要让while(true)空转浪费CPU,可以使用yield或sleep来让出CPU的使用权给其他程序
while(true){
try{
       Thread.sleep(50);  //防止CPU占用资源
  } catch(InterruptedException e){
     e.printStackTrace();
  }
}
join方法
等待线程运行结束
join(long n) 等待线程运行结束最多等待n ms
interrupt() 方法
interrupt 打断线程有两种情况,如下:
1、如果一个线程在在运行中被打断,打断标记会被置为 true 。
2、如果是打断因sleep wait join 方法而被阻塞的线程,会将打断标记置为 false 。
isInterrupted() 与 interrupted() 比较,如下:
首先,isInterrupted 是实例方法,interrupted 是静态方法,它们的用处都是查看当前打断的状态,但是 isInterrupted 方法查看线程的时候,不会将打断标记清空,也就是置为 false,interrupted 查看线程打断状态后,会将打断标志置为 false,也就是清空打断标记,简单来说,interrupt() 方法类似于 setter 设置中断值,isInterrupted() 类似于 getter 获取中断值,interrupted() 类似于 getter + setter 先获取中断值,然后清除标志。
用代码测试如下
Java并发编程——Concurrent Programming_第3张图片
Java并发编程——Concurrent Programming_第4张图片
线程状态
从操作系统层划分,线程有 5 种状态
Java并发编程——Concurrent Programming_第5张图片
1、初始状态,仅仅是在语言层面上创建了线程对象,即Thead thread = new Thead();,还未与操作系统线程关联
2、可运行状态,也称就绪状态,指该线程已经被创建,与操作系统相关联,等待cpu给它分配时间片就可运行
3、运行状态,指线程获取了CPU时间片,正在运行
4、 阻塞状态
如果调用了阻塞API,如BIO读写文件,那么线程实际上不会用到CPU,不会分配CPU时间片,会导致上下文切换,进入【阻塞状态】
等待BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
与【可运行状态】的区别是,只要操作系统一直不唤醒线程,调度器就一直不会考虑调度它们,CPU就一直不会分配时间片
5、终止状态,表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

线程的 6 种状态

Java并发编程——Concurrent Programming_第6张图片
共享模型之管程
一个程序运行多个线程本身没有问题;多个线程读共享资源也没有问题;在多个线程对共享资源读写操作时发生指令交错,就会出现问题;
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
Java并发编程——Concurrent Programming_第7张图片
竞态条件
多个线程在临界区执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
synchronized解决方案
应用之互斥  为避免临界区的竞态条件发生,有多种手段可以达到目的
1、阻塞式解决方案:synchronized、Lock   2、非阻塞式解决方案:原子变量
synchronized俗称对象锁,采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程再想获取这个对象锁时会被阻塞住。能保证拥有锁的线程可以安全执行临界区的代码,不必担心线程上下文切换
注:虽然Java中互斥和同步都可以采用synchronized完成,但有区别
1、互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
2、同步是由于线程执行的先后顺序不同,需要一个线程等待其他线程运行到某个列
synchronized实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换打断
变量的线程安全分析
成员变量和静态变量是否线程安全?
答:如果没有共享,则线程安全;如果被共享,根据它们的状态是否能够改变,分为两种情况
1、如果只有读操作,则线程安全   2、如果有读写操作,这段代码是临界区,需要考虑线程安全
局部变量是线程安全的,但局部变量引用的对象则未必,分两种情况
1、如果该对象没有逃离方法的作用访问,是线程安全的   2、如果该对象逃离方法作用的范围,需要考虑线程安全
不可变类线程安全性
String、Integer等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
局部变量被初始化为基本数据类型是安全的,代码如下,因为每个线程都会有一份 test() 放在线程私有的栈中,多个线程就有多个,是不被多个线程共享的,所有就没有线程安全问题。
Java并发编程——Concurrent Programming_第8张图片

不安全原因分析

Java并发编程——Concurrent Programming_第9张图片
如图所示,因为 list 是实例变量,则多个线程都会使用到这个共享的实例变量,就会出现线程安全问题,为什么会有安全问题呢,首先要理解 list 添加元素的几步操作,第一步会获取添加元素的下标 index,第二步对指定的 index 位置添加元素,第三步将 index 往后移。
当 t0 线程从 list 拿到 index = 0 后,t0 线程的时间片用完,出现上下文切换,t1 获取时间片开始执行,从 list 也拿到 index =
0,然后将元素添加到 index 位置,然后将 index 值加 1,然后 t0 线程获取时间片,对 index = 0 位置添加元素,此时 index = 0 已经存在元素,就会出现报错。

解决方法

可以将 list 修改成局部变量,然后将 list 作为引用传入方法中,因为局部变量是每个线程私有的,不会出现共享问题,那么就不会有上述问题了。修改的代码如下
Java并发编程——Concurrent Programming_第10张图片

常见线程安全类

String、Integer、StringBuffer、Random、Vector (List的线程安全实现类)、Hashtable (Hash的线程安全实现类)、java.util.concurrent 包下的类

线程安全类方法的组合

但注意它们多个方法的组合不是原子的,看如下代码
Java并发编程——Concurrent Programming_第11张图片
Java并发编程——Concurrent Programming_第12张图片
如上图所示,当使用方法组合时,出现了线程安全问题,当线程 1 执行完 get(“key”) ,这是一个原子操作没出问题,但是在 get(“key”) == null 比较时,如果线程的时间片用完了,线程 2 获取时间片执行了 get(“key”) == null 操作,然后进行 put(“key”, “v2”) 操作,结束后,线程 1 被分配 cpu 时间片继续执行,执行 put 操作就会出现线程安全问题。

不可变类的线程安全

String和Integer类都是不可变的类,因为其类内部状态是不可改变的,因此它们的方法都是线程安全的,有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,其实调用这些方法返回的已经是一个新创建的对象了
Java并发编程——Concurrent Programming_第13张图片
Monitor(锁)被称作监视器或管程
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,对象头的Mark Word被设置指向Monitor对象的指针,刚开始Monitor中Owner为null,当Thre ad—2执行synchronized(obj)会将Monitor的所有者Owner置为 Thre ad—2,Monitor中只能有一个Owner,在 Thre ad—2上锁的过程中。如果 Thre ad—3、 Thre ad—4、 Thre ad—5也来执行synchronized(obj),就会进入EntryList BLOCKED
Thre ad—2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁
注:1、synchronized必须是进入同一个对象的monitor才有上述的效果
2、不加synchronized的对象不会关联监视器,不遵从以上规则
Java并发编程——Concurrent Programming_第14张图片
轻量级锁
使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的,可以使用轻量级锁来优化
轻量级锁对使用者是透明的,即语法仍是synchronized 
假设有两个方法同步块,利用同一个对象加锁
Java并发编程——Concurrent Programming_第15张图片
1、每次指向到 synchronized 代码块时,都会创建锁记录(Lock Record)对象,每个线程都会包括一个锁记录的结构,锁记录内部可以储存对象的 Mark Word 和对象引用 reference
Java并发编程——Concurrent Programming_第16张图片
2、让锁记录中的 Object reference 指向对象,并且尝试用 cas(compare and sweep) 替换 Object 对象的 Mark Word ,将 Mark Word 的值存入锁记录中。 
Java并发编程——Concurrent Programming_第17张图片
3、如果 cas 替换成功,那么对象的对象头储存的就是锁记录的地址和状态 00 表示轻量级锁,如下所示
Java并发编程——Concurrent Programming_第18张图片
4、如果cas失败,有两种情况
如果是其它线程已经持有了该 Object 的轻量级锁,那么表示有竞争,首先会进行自旋锁,自旋一定次数后,如果还是失败就进入锁膨胀阶段。
如果是自己的线程已经执行了 synchronized 进行加锁,那么再添加一条 Lock Record 作为重入的计数。
Java并发编程——Concurrent Programming_第19张图片
5、当线程退出 synchronized 代码块的时候,如果获取的是取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
Java并发编程——Concurrent Programming_第20张图片
6、当线程退出 synchronized 代码块的时候,如果获取的锁记录取值不为 null,那么使用 cas 将 Mark Word 的值恢复给对象
成功则解锁成功
失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
锁膨胀
如果在尝试加轻量级锁的过程中,CAS操作无法成功,一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变成重量级锁
1、当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
Java并发编程——Concurrent Programming_第21张图片
2、这时 Thread-1 加轻量级锁失败,进入锁膨胀流程,
即为对象申请Monitor锁,让Object指向重量级锁地址
然后自己进入Monitor 的EntryList 变成BLOCKED状态
Java并发编程——Concurrent Programming_第22张图片
3、当 Thread-0 退出 synchronized 同步块时,使用 cas 将 Mark Word 的值恢复给对象头,对象的对象头指向 Monitor,那么会进入重量级锁的解锁过程,即按照 Monitor 的地址找到 Monitor 对象,将 Owner 设置为 null ,唤醒 EntryList 中的 Thread-1 线程
自选优化
重量级锁竞争的时候,可以使用自旋来进行优化,如果当前线程自选成功(即此时持锁线程已经退出同步块,释放了锁),当前线程可以避免阻塞
自旋重试成功的情况
Java并发编程——Concurrent Programming_第23张图片
自旋重试失败的情况,自旋了一定次数还是没有等到持锁的线程释放锁
Java并发编程——Concurrent Programming_第24张图片
偏向锁
轻量级锁在没有竞争时,每次重入仍需执行CAS操作
Java6引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重现CAS。以后只要不发生竞争,这个对象就归线程所有
一个对象创建时:
1、如果开启了偏向锁,那么对象创建后,mark word值为0x05即为最后3位101,这时它的thread epoch  age都为0
2、偏向锁默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,加VM参数-XX:BiasedLockingStartupDelay = 0来禁用延迟
3、如果没有开启偏向锁,对象创建后,markword值为0x01即最后3位为001,这时它的hashcode、age都为0,第一次用到hashcode时才会赋值
添加参数-xx:-UseBiasedLocking禁用偏向锁
撤销—调用对象hashCode
调用了对象的hashCode,但偏向锁的对象Mark Word中存储的是线程id,如果调用hashCode会导致偏向锁被撤销
轻量级锁会在锁记录hashCode;重量级锁会在Monitor中记录hashCode
在调用hashCode后使用偏向锁,记得去掉-XX: -UseBiasedLocking
撤销—其他线程使用对象
当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
批量重偏向
如果对象被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID
当撤销偏向锁阈值超过20次后,JVM会在给这些对象加锁时重新偏向至加锁线程
原理之wait/notify
Owner线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态
BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片;BLOCKED线程会在Owner线程释放锁时唤醒
WAITING线程会在Owner线程调用notify或notify All时唤醒,但唤醒后并不意味着立刻获得锁,仍需进入EntryList重新竞争
Java并发编程——Concurrent Programming_第25张图片
sleep(long n)和wait(long n)的区别
1、sleep是Thread方法,而wait是Object方法
2、sleep不需要强制和synchronized配合使用,但wait需要和synchronized一起使用
3、sleep在睡眠的同时,不会释放对象锁,wait在等待的时候会释放对象锁
4、线程状态都是TIMED_WAITING
什么时候适合使用wait
当线程 不满足某些条件 ,需要暂停运行时,可以使用 wait 。这样会 将对象的锁释放 ,让其他线程能够继续运行。如果此时使用 sleep, 会导致所有线程都进入阻塞 ,导致所有线程都没法运行,直到当前线程 sleep 结束后,运行完毕,才能得到执行。
使用wait/notify需要注意什么
当有多个线程在运行时,对象调用了 wait 方法,此时这些线程都会进入 WaitSet 中等待。如果这时使用了 notify 方法,可能会 造成虚假唤醒 (唤醒的不是满足条件的等待线程),这时就需要使用 notifyAll 方法
Java并发编程——Concurrent Programming_第26张图片
异步模式之保护性暂停
用在一个线程等待另一个线程的执行结果
要点:1、有一个结果需要从一个线程传递到另一个线程,让它们关联同一个GuardedObject
2、如果有结果不断从一个线程到另一个线程,可以使用队列消息   3、JDK中,join的现、Future的实现,采用此模式
4、因为要等待另一方的结果,因此归类到同步模式
多任务版 GuardedObject 图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右侧的 t1,t3,t5 就好比邮递员如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类,这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理。和生产者消费者模式的区别就是:这个生产者和消费者之间是一一对应的关系,但是生产者消费者模式并不是。rpc 框架的调用中就使用到了这种模式。
Java并发编程——Concurrent Programming_第27张图片
异步模式之生产者/消费者
要点:1、与前面的保护性暂停中的GuardObject不同,不需要产生结果和消费结果一一对应   2、消费队列可以用来平衡生产和消费的线程资源   3、生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据   4、消费队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据   5、JDK各种阻塞队列,采用这种模式
“异步”的意思就是生产者产生消息之后消息没有被立刻消费,而“同步模式”中,消息在产生之后被立刻消费了。
Java并发编程——Concurrent Programming_第28张图片
Park&Unpark
LookSupport.park(); //暂停当前线程     LookSupport.unpark(); //恢复某个线程的运行
与Object的Wait&Notify相比
1、wait.notify和notifyAll必须配合Object Monitor一起使用,unpark不必
2、park&unpark是以线程为单位来阻塞和唤醒线程,而notify只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,不那么精确
3、park&unpark可以先unpark,而wait¬ify不能先notify
先调用park再调用upark的过程
1、先调用 park
当前线程调用 Unsafe.park() 方法
检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁(mutex对象有个等待队列 _cond)
线程进入 _cond 条件变量阻塞
设置 _counter = 0
Java并发编程——Concurrent Programming_第29张图片
2、调用 upark
调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
唤醒 _cond 条件变量中的 Thread_0
Thread_0 恢复运行
设置 _counter 为 0
Java并发编程——Concurrent Programming_第30张图片
先调用upark再调用park的过程
1、调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
2、当前线程调用 Unsafe.park() 方法
3、检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
4、 设置 _counter 为 0
Java并发编程——Concurrent Programming_第31张图片
线程状态转换
Java并发编程——Concurrent Programming_第32张图片
情况一:NEW –> RUNNABLE
当调用了 t.start() 方法时,由 NEW –> RUNNABLE
情况二: RUNNABLE <–> WAITING
当调用了t 线程用 synchronized(obj) 获取了对象锁后,调用 obj.wait() 方法时,t 线程从 RUNNABLE –> WAITING
调用 obj.notify() , obj.notifyAll() , t.interrupt() 时,会在 WaitSet 等待队列中出现锁竞争,非公平竞争
竞争锁成功,t 线程从 WAITING –> RUNNABLE
竞争锁失败,t 线程从 WAITING –> BLOCKED
情况三:RUNNABLE <–> WAITING
当前线程调用 t.join() 方法时,当前线程从 RUNNABLE –> WAITING
注意是当前线程在 t 线程对象的监视器上等待
t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING –> RUNNABLE
情况四: RUNNABLE <–> WAITING
当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE –> WAITING
调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING –> RUNNABLE
情况五: RUNNABLE <–> TIMED_WAITING
t 线程用 synchronized(obj) 获取了对象锁后
调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE –> TIMED_WAITING
t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
竞争锁成功,t 线程从 TIMED_WAITING –> RUNNABLE
竞争锁失败,t 线程从 TIMED_WAITING –> BLOCKED
情况六:RUNNABLE <–> TIMED_WAITING
当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE –> TIMED_WAITING
注意是当前线程在 t 线程对象的监视器上等待
当前线程等待时间超过了 n 毫秒,或 t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING –> RUNNABLE
情况七:RUNNABLE <–> TIMED_WAITING
当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE –> TIMED_WAITING
当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING –> RUNNABLE
情况八:RUNNABLE <–> TIMED_WAITING
当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线 程从 RUNNABLE –> TIMED_WAITING
调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING–> RUNNABLE
情况九:RUNNABLE <–> BLOCKED
t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE –> BLOCKED
持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争 成功,从 BLOCKED –> RUNNABLE ,其它失败的线程仍然 BLOCKED
情况十: RUNNABLE <–> TERMINATED
当前线程所有代码运行完毕,进入 TERMINATED
活跃性:代码执行不完
无锁:一个线程需要同时获取多把锁,容易发生死锁
如:t1 线程获得 A 对象锁,接下来想获取 B 对象的锁 t2 线程获得 B 对象锁,接下来想获取 A 对象的锁
Java并发编程——Concurrent Programming_第33张图片
定位死锁:检测死锁可以使用jconsole工具,或使用jps定位进程id,再用jstack定位死锁
避免死锁的方法
在线程使用锁对象时, 顺序加锁 即可避免死锁
Java并发编程——Concurrent Programming_第34张图片
活锁:出现在两个线程相互改变对方的结束条件,最后谁也无法结束
死锁与活锁的区别
死锁是因为线程互相持有对象想要的锁,并且都不释放,最后到时 线程阻塞,停止运行的现 象。
活锁是因为线程间修改了对方的结束条件,而导致代码 一直在运行,却一直运行不完 的现象
ReentrantLock
Java并发编程——Concurrent Programming_第35张图片
特点:1、可中断   2、可以设置超时时间   3、可以设置为公平锁   4、支持多个条件变量   5、与synchronized一样,都支持可重入
可重入:指同一个线程如果首次获得了这把锁,因为它是这把锁的拥有者,因此有权利再次获取这把锁;如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
可打断: 如果某个线程处于阻塞状态,可以调用其 interrupt 方法让其停止阻塞,获得锁失败, 简而言之就是:处于阻塞状态的线程,被打断了就不用阻塞了,直接停止运行
Java并发编程——Concurrent Programming_第36张图片
锁超时
使用 lock.tryLock 方法会返回获取锁是否成功。如果成功则返回 true ,反之则返回 false 。
并且 tryLock 方法可以指定等待时间,参数为:tryLock(long timeout, TimeUnit unit), 其中 timeout 为最长等待时间,TimeUnit 为时间单位
简而言之就是:获取锁失败了、获取超时了或者被打断了,不再阻塞,直接停止运行
条件变量
ReentrantLock的条件变量相较于synchronized支持多个条件变量
使用流程:1、await前需要获得锁   2、await执行后,会释放锁,进入conditionObject等待    3、await的线程被唤醒(或打断、超市)取重新竞争lock锁   4、竞争lock锁成功后,从await后继续执行
共享模型之内存
Java内存模型
Java Memory Model即JMM,定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等
JMM体现在以下几个方面
原子性:保证指令不会受到线程上下文切换的影响
可见性:保证指令不会受到CPU缓存的影响
有序性:保证指令不会受到CPU指令并行优化的影响
volatile(易变关键字)
可以用来修饰成员变量和静态成员变量,可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存 
synchronized既可以保证原子性,也同时保证代码块内变量的可见性,缺点是synchronized属于重量级操作,性能相对低
volatile的底层原理是内存屏障,对volatile变量的写指令后会加入写屏障,对volatile变量的读指令前会加入读屏障
写屏障保证在该屏障之前的,对共享变量的改动,都同步到主存当中
读屏障保证在该屏障之后的,对共享变量的读取,加载的是主存中最新数据
注:写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去;有序性的保证也只是保证了本线程内相关代码不被重排序
happens-before
规定了对共享变量的写操作对其他线程的读操作可见,是可见性与有序性一套规则总结,抛开以下happens-before规则,JMM并不能保证一个线程对共享变量的写,对于其他线程对该共享变量的读可见
无锁
CAS的底层原理是lock cmpolxchg指令(X86架构),在单核CPU和多核CPU下都能保证比较—交换的原子性
在多核状态下,某个核执行到带lock的指令时,CPU会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的
CAS必须借助volatile才能读取到共享变量的最新值来实现比较并交换的效果
CAS特点
1、CAS和volatile可以实现无锁并发,适用于线程数少、多核CPU的场景下
2、CAS是基于乐观锁的思想,不怕别的线程修改共享变量
3、CAS体现的是无锁并发、无阻塞并发,因为没有使用synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一,但如果竞争激烈,重试必然频繁发生,效率会受影响
原子数组
AtomicIntegerArray   AtomicLongArray   AtomicReferenceArray
方法内会启动10个线程,并发让数组所有元素总共自增10000次
参数1:提供数组,可以是线程不安全数组或线程安全数组   参数2:获取数组长度的方法   
参数3:自增方法,回传array length  index                    参数4:打印数组的方法
源码之LongAdder
LongAdde有几个关键域
transient volatile Cell[] cells ;  //累加单元数组,懒惰初始化   transient volatile long base; //基础值,如果没有竞争,则用cas累加这个域    transient volatile int cellsBusy; //在cells创建或扩容时,置为1,表示加锁
Unsafe
提供了非常底层的、内存操作、线程的方法,Unsafe对象不能直接调用,只能通过反射获得
不可变
设置final变量的原理
final变量的赋值会通过putfield指令来完成,同样在这条指令之后也会写入写屏障,保证在其他线程读到它的值时不会出现为0的情况
无状态
在web阶段学习时,设计Servlet时为了保证其线程安全,不要为Servlet设置线程安全,没有任何成员变量的类是安全的;成员变量保存的数据称为状态信息;没有成员变量称之为无状态
ThreadPoolExecutor构造方法
corePoolSize核心线程数据(最多保留的线程数)  keepAliveTime 生存时间—针对救急线程   unit 时间单位—针对救济线程
workQueue 阻塞队列   threadFactory 线程工厂—可以为线程创建时起个好名字
handler 拒绝策略
线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务,当线程数达到corePoolSize并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue队列排队,直到有空闲的线程。如果队列选择了有界队列,那么任务超过了队列大小时,会创建maximumPoolSize—corePoolSize数目的线程来救济。如果线程到达maximumPoolSize仍有新任务这时会执行拒绝策略
当高峰过去后,超过corePoolSize的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由keepAliveTime核unit来控制
newFixedThreadPool(固定大小)
特点  1、核心线程数=最大线程数(没由救急线程被创建),因此也无需超时时间   2、阻塞队列是无界的,可以放任意数量的任务
适用于任务量已知,相对耗时的任务
newCachedThreadPool
特点 1、核心线程数是0,最大线程是Interger、MAX-VALUE,救急线程的空闲生存时间是60s,即全部都是救急线程且救急线程可以无限创建    2、队列采用了SynchronizedQueue,没有容量,没有线程来取放不进去
适用于任务数比较密集,任务执行时间短的情况
newSingleThreadExecutor
使用场景:多个任务排队执行。线程数固定为1,任务数多于1时,会放入无界队列排队。任务执行完毕,唯一的线程也不会被释放
特点  1、自己创建一个单线程串行执行任务,如果任务执行失败而终止,那没有任何补救措施,线程池还会新建一个线程,保证池的正常工作    
2 、Executor.newSingleThreadExecutor()线程个数始终为1,不能修改    
3、 Executor.newFixedThreadPool(1)初始时为1,以后还可以修改,对外暴露的是ThreadPoolExecutor对象,可以强转后调用setCorePoolSize等方法进行修改
关闭线程池
shutdown
线程池状态为SHUTDOWN,不会接受新任务,但已提交任务会执行完。此方法不会阻塞调用线程的执行
shutdownNow
线程池状态变为STOP,不会接收新任务,会将队列中的任务返回,并用interrupt的方式中断正在执行的任务
任务调度线程池
在任务调度线程池功能加入之前,可以使用java.util.Timer来实现定时功能,Timer的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务
Fork/Join
JDK1.7加入的新的线程池实现,体现的是一种分治思想,适用于能够进行任务拆分的密集型运算
任务拆分是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。与递归相关的计算,如归并排序,费波纳实数列,都可以用分治思想进行求解
Fork/Join在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运算效率
Fork/Join默认会创建与CPU核心数大小相同的线程池
JUC
AQS原理
概述:全称是AbstractQueuedSynchronized,是阻塞式锁和相关的同步器工具的框架
特点  1、用state属性表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
getState获取state状态   setState设置state状态   compareAndSetState乐观锁机制设置state状态
2、独占模式是只有一个线程能够访问资源,共享模式允许多个线程访问类源
3、提供了基于FIFO的等待队列,类似于Monitor的EntryList
4、条件变量来实现等待、唤醒机制,支持多个条件变量,类似于Monitor的WaitSet,子类主要实现这样一些方法(默认抛出UnsupportedOperationException)
读写锁
ReentrantReadwriteLock:当读操作远远高于写操作时,使用读写锁让读—读可以并发,提高性能。类似于数据库中的select...from...lock in share mode,提供一个数据容器类内部分别使用读锁保护数据的read()方法,写锁保护数据的write()方法
读锁不支持条件变量
重入时升级不支持:即持有读锁的情况下去获取写锁会导致获取写锁永久等待
重入时降级支持:即持有写锁的情况下去获取读锁
StampedLock
该类自JDK8加入,为了进一步优化读性能,特点是在使用读锁、写锁时都必须配合戳使用
加解读锁
long stamp = lock.readLock();
lock.unlockRead(stamp);
加解写锁
long stamp = lock.writeLock();
lock.unlockWrite(stamp);
乐观读,StampedLock支持tryOptimisticRead()方法,读取完毕后需要做一次戳校验,如果检验通过,表示这期间没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全
long stamp = lock.tryOptimisticRead();  //验戳
if (!lock.validate(stamp)){ //锁升级
}
stampedLock:不支持条件变量,不支持可重入
Semaphore
信号量,用来限制能同时访问共享资源的线程上限
Semaphore应用
使用Semaphore限流,在访问高峰期时,让请求线程阻塞,高峰期过去再释放许可,只适合限制单机线程数量,并且仅是限制线程数,而不是限制资源数
Semaphore实现简单连接池,对比享元模式下的实现(用wait notify),性能和可读性更好
CountdownLatch
用来进行线程同步协作,等待所有线程完成倒计时。其中构造参数用来初始化等待计数值,await()用来等待计数归零,countDown()用来让计数减一
CyclicBarrier
循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置计数个数,每个线程执行到某个需要“同步”的时刻调用await()方法进行等待,当等待的线程满足计数个数时,继续执行
Blocking大部分实现基于锁,并提供用来阻塞的方法
CopyOnWrite之类容器修改开销相对较重
Concurrent类型的容器:内部很多操作使用cas优化,一般可以提供较高吞吐量。遍历时若一致性,例如,当利用迭代器遍历时,如果容器发生修饰,迭代器可以继续进行遍历,这时内容是旧的。求大小弱一致性,size操作未必是100%准确。遍历时如果发生了修饰,对于非安全容器来说,使用fail-fast让遍历立即失败,抛出ConcurrentModificationException,不再继续遍历

你可能感兴趣的:(并发编程,java)