JUC编程
一、并发编程中的挑战
1、上下文切换
在多线程编程中,操作系统是利用时间片让多条线程抢占cup,来执行的,当每条线程的时间片执行完毕后,就要切换到其他就绪等待的线程,当下次该线程得到时间片并抢占cpu时,cup要接着上次执行的点,继续执行该线程,这就需要系统要保存每条线程执行的上下文,并在执行时完成上下文的切换,但这是需要耗费系统资源,并需要时间的;而大量的上下文不仅消耗系统资源,而且会拖慢系统执行的效率;所以并不是线程数越多越好;
开发者需要判断:
- 执行的任务需不需要使用多线程
- 合理的创建线程
2、死锁
多线程并发,当线程需要对系统的共享资源进行操作时,为了避免数据的丢失、数据的紊乱等,需要同步处理,即每次只能允许一个线程访问共享资源;在使用同步时,需要使用锁机制,而不合理的使用锁,不但不会保证系统更加健壮,反而会造成死锁问题
3、资源的限制
加快代码执行的速度,将串行的代码,使用多个线程并发执行,而系统资源是有限的,不能无限制创建线程,来并发执行代码,如cup的核数、网络带宽、内存等,当超过系统所能承载的线程数,其他的线程还是时间片抢占cup,所以实际上还是串行的,并且多了上下文的系统消耗,系统性能更慢;
硬件资源的限制
考虑集群
软件资源的限制
池化技术,进行资源共享,如:线程池、数据库连接池等
二、线程创建
三种方法:
- 继承Thread类
- 实现runnable接口
- 实现Callable接口的Call
callable的实现是由返回值的,可用Future对象的get方法接收
三、JAVA的内存模型
如图JAVA的内存模型(JMM),在并发编程中,在多个线程访问共享资源或者变量时,是将共享变量(通过四组操作)拷贝到各自的本地内存中进行修改,修改完成后再刷新回主内存,而这个过程早些时候的java版本中,其他线程是不可见的,也就是说,其他线程是不知道该共享变量已经发生变化;
四、volatile
volatile是轻量级的synchronized
作用:
-
用volatile修饰的共享变量,保证了所有线程的可见性,保证所有线程看到的共享变量值是一致的
在加锁前,线程需要读取最新的值到本地内存中
在解锁前,将结果刷回主内存
加锁、解锁是同一把锁
-
不保证原子性
即执行要么成功,要么失败,不可再中途打断
-
禁止指令重排
什么是指令重排?
在java虚拟机中,java的源码被编译字节码,解释器、编译器又将其转换为机器码,在执行过程中会对指令执行顺优化,进行重排
有一下几种重排序:
- 编译器优化的重排序
- 指令级并行的重排序(不存在数据依赖情况下)
- 内存系统的重排序
如何保证原子性(不使用锁)
使用原子类
这些类都和操作系统底层直接挂钩,再内存中修改值
五、锁机制
4.1、synchronized
是java提供的关键字
在jdk1.6之前,synchronized是jvm级别的重量级锁,在1.6之后做了优化,不那么重量了
使用synchronized实现同步,有以下几种方式
-
普通同步方法 锁的是当前的实例对象
-
静态同步方法 锁的是当前类的class对象
-
同步代码块 锁的是括号里的对象
代码块同步使用的是monitorenter、monitorexit;在同步代码块的首尾会自动插入这两种指令实现同步,而且指令一一对应
在使用synchronized进行同步时,要注意锁的对象要一致,即使用同一把锁
注意:八锁现象
4.2、lock
Lock锁是JUC包下的接口
lock具有和synchronized相同的同步功能,具备了多种synchronized所不具备的功能,只是需要显示的加锁、释放锁
lock锁的实现依靠队列同步器
通过判断、设置当前的同步状态来实现同步
使用CAS(compareAndState)设置当前状态,并保证原子性
同步状态(就是锁):
- 独占式状态
- 独占式释放状态
- 共享式状态
- 共享式释放状态
- 是否被当前线程独占
独占:同一时刻,只能有一个线程获得到锁,而其他获取锁的线程只能处于同步队列中等待,只有获得锁的线程,释放了锁,后继的线程才能获得锁
在使用lock锁时,同步队列器中维护了一个同步队列,线程在tryAcquire中通过CAS获取同步状态,tryRelease将同步状态设置为0,即恢复到独占使释放状态,此时其他线程可尝试去获得独占锁,获取同步状态失败,会被加入到同步队列中等待
什么是CAS?
CAS : 比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就 一直循环!
缺点: 1、 循环会耗时
2、一次性只能保证一个共享变量的原子性
3、ABA问题
什么是ABA问题
在多线程并发中,线程在本地内存中将共享变量,进行了修改,在刷回主内存时又改了了回去,其他线程是不知道的
解决ABA问题的办法:使用原子引用
4.3、原子引用
思想:与乐观锁原理相同
理解为:为共享变量设置了版本号,在修改后对应的版本号+1
使用:AtomicStampedReference对象
4.4、同步队列
1.什么是同步队列?
一个先入先出的双向队列
2.基于同步队列的lock原理
获取同步状态失败后,同步器将当前线程阻塞,把其信息以及线程状态等构成一个节点加入到同步队列,释放时又将其唤醒,在此尝试获取同步状态
既然同步队列是节点构成(其实就是双向链表),便具有前驱节点、后继节点
且没有获得同步状态的线程会使用基于CAS的方法,保证线程安全,从队尾加入队列,同步器中有两个地址应用(一个队列头、一个队列尾)
每次只有头节点会获得同步状态,头节点释放后,会唤醒后继节点,后继节点会判断自己的前驱是不是头节点,如果是后继节点尝试获得同步状态后,将自己设置为头节点
4.5、lock锁与synchronized的区别
- synchronized是关键字,lock是juc下的类
- synchronized是隐式锁,会自动释放锁,lock是显式锁,必须手动加锁、解锁否则陷入死锁
- synchronized无法判断锁的状态,lock可判断是否获得了锁
- synchronized获得锁,阻塞后,其他线程只能等,lock锁不一定,比如异常后,自动释放锁
- synchronized可重入锁,非公平锁,不可中断,lock可重入锁,默认非公平锁,也可自己设置
- synchronized适合少量代码,lock可适合大段代码
4.6、读写锁
读锁是共享锁,写锁是独占锁
- 允许多个线程同时读,只能由一个线程同时写
- 加了读锁,写操作就会阻塞
- 加了写锁,读操作就会阻塞,避免脏读
4.7、可重入锁
获得锁的线程可继续加锁,如同步方法的嵌套等
不可重入锁,线程只能获取一次锁,第二次加锁线程便会阻塞
在lock锁进行重入时,是将同步状态+1,在释放锁时,将同步状态-1,在同步状态为0时,恢复为释放状态
4.8、公平/非公平锁
公平锁:按顺序执行,每次等待时间最长的线程执行
非公平锁:不按顺序执行,可插队
非公平锁的效率比公平锁的效率高
刚刚执行完的线程有机会重新抢占cup继续执行,相比省略了线程的上下文切换
4.9、偏向锁
锁的级别:
无锁状态------>偏向锁------>轻量级锁------->重量级锁
会随着竞争情况逐渐升级,锁会升级,但不会降级(为了提高获得锁与释放锁的效率)
级别越高,获得锁的代价就越高
偏向锁,级别最低的锁,使线程获得锁的代价最低
研究:大多数情况下,不存在多线程竞争锁,而是总是一个线程获得锁,因此使用高级别的锁,代价越高
在对象头、栈帧里存放着锁偏向的线程id,在进出同步块时,只需要判断一下对象头中有无指向当前线程的偏向锁,如果有,获取锁成功,否则失败;如果失败判断当前是否是偏向锁,不是,使用cas竞争,是,使用cas将偏向锁指向当前线程
竞争出现时,才会释放偏向锁;
首先暂停拥有偏向锁的线程,判断持有偏向锁的线程是否在获得,不活动,将对象头设置为无锁状态;活动,拥有偏向锁的栈会被执行,要么恢复到无锁状态或者标记为不合适作为偏向锁,最后唤醒其他线程;
六、并发容器和框架
5.1、并发容器
juc报下提供了大量的并发容器
在java的util包下提供的集合容器大多是线性不安全的,在并发编程中,使用线性不安全的容器会发生并发修改异常,或者数据丢失
在并发中使用容器由以下几种选择:
- 使用collecions工具类将普通容器转换成线性安全的
- 使用java提供的并发容器
- 使用vector,hashtable等容器,但效率较低
5.2、队列
中有7个阻塞队列
4中处理方式:
- 抛出异常:队列满\空时,再加入\删除时会抛出异常
- 返回特殊值:队列满\空时,再加入\删除时会返回true或false
- 一直阻塞:队列满\空时,再加入\删除时一直阻塞
- 超时退出:队列满\空时,再加入\删除时会阻塞一段时间,如果超过设定时间,就会退出
方法\处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入方法 | add | offer | put | offer |
移除方法 | remove | poll | take | poll |
检查方法 | element | peek | - | - |
5.3、Fork\join框架
说白了,所谓的Fork\join框架就是一种分而治之的思想
- Fork:将任务分解为多个小任务
- join:将分解的多个小任务的结果进行聚合
再如今的大数据场景中有许多类似的框架:
如MapReduce、Flink、spark等
七、线程池
三大方法、7大参数、4种拒绝策略
6.1、线程池的执行流程
6.2、三大方法
-
ThreadPoolExecutor
-
CachedThreadPool
大小无界限的线程池
-
FixedThreadPool
创建固定线程数的线程池
-
SingleThreadPoolExecutor
创建单个线程数的线程池
-
-
ScheduledThreadPoolExecutor适应周期任务
-
ScheduledThreadPoolExecutor
创建若干
-
SingleThreadScheduledExecutor
创建一个
-
6.3、七大参数
根据源码分析和阿里巴巴java参考书册,在使用线程池时最好手动创建线程池,负责可能会导致OOM