3 进程/线程(并发)
3.1 线程基础
线程存在于操作系统中,不仅仅是Java
-
进程:系统调用的最小单位
分配 cpu/内存/磁盘io
-
线程:cpu调用的最小单位
内核数与线程数: 一比一 (逻辑处理器,2倍)
RR调度: cpu时间片轮转 机制
-
并行(各自执行):同时运行的任务数
-
并发(交替执行):单位时间内,处理的任务数
os限制: linux 分配1000个线程, windows 分配2000个 (线程池来控制线程数量)。
句柄: 系统分配了一段连续的内存空间, 指向这个内存空间的叫做句柄。
jdk 线程是协作式, 不是强制式
Thread.currentThread() : 获取当前线程
创建线程:一种:Thread; 二种:Runnable
-
stop() (过时): 不建议使用,方式野蛮,直接停止线程操作。如写文件时,中途停止,文件未写完整。
-
interrupt()(推荐): 标识线程中断,标识位, 实际不一定是停止线程。
-
isInterrupted(): 判断是否停止 while(isInterrupted()){…} , 不建议使用 设置boolean 标识 while(cancel){…}
-
interrupted(): 判断是否停止 (会修改标识为 true)
sleep捕获异常,需要interupt(), 外部不会让线程中断。
static class T extends Thread{
@Override
public void run() {
System.out.println("run...2-1 "+ isInterrupted());
//没有打断
while (!isInterrupted()){
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
//需要打断,否则while会一直执行下去
interrupt();
System.out.println("run...sleep "+ isInterrupted());
}
System.out.println("run...1 "+ isInterrupted());
}
System.out.println("run...2-2 "+ isInterrupted());
}
}
-
run() : 主线程执行
-
start() : 启动一个子线程执行
-
yield(): 从cpu中让出执行权,转为就绪状态。 不会让出锁
-
join(): 获取执行权, 但是有顺序的执行(串行) (如何让2个线程有顺序的执行?使用join)
-
setPriority(): 线程优先级
-
setDaemon(): 设置为守护线程。 当前线程完毕后,守护线程停止
当前线程外, 设置了setDaemon的线程 都是守护线程。
线程状态交互:
线程间的共享
多个线程对同一个资源访问 ===> 加锁 (方法加锁,对象加锁)
静态方法上内加锁===>
注: 相同锁, 多线程串行; 锁不同,多线程并行
死锁:
- 多个操作者(线程)
- 争夺多个资源
volatile: 最轻量的线程同步机制,可见性。(旧值变成新值时,马上被其他线程看到) ,不能保证线程安全。 一写多读的场景下使用。
- 可见性:对一个volatile变量的读,能看到其他线程对这个变量最后的写入。(强制刷新值同步回主内存,强制其他读线程去读。没有写的情况下,是各自工作线程从主内存中拷贝一个副本并进行各自操作)
- 只能保证可见性,不能保持原子性(复合操作(自增)。 要么一起成功,要么都不成功)
synchronized: 独占锁, 保证 可见性+原子性
ThreadLocal: 每个线程都有遍历的副本,线程隔离。 拥有一个线程独有的 TheadLocalMap (内含Entry[])
threadLocal使用后, 不remove,会发生内存泄漏 。
没有使用好,也会线程不安全。
可见性与原子性
-
**可见性:**指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
由于线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,那么对于共享变量V,它们首先是在自己的工作内存,之后再同步到主内存。可是并不会及时的刷到主存中,而是会有一定时间差。很明显,这个时候线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性了 。
要解决共享对象可见性这个问题,我们可以使用volatile关键字或者是加锁。
-
**原子性:**即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
-
“时间片”:CPU资源的分配都是以线程为单位的,并且是分时调用,操作系统允许某个进程执行一小段时间,例如 50 毫秒,过了 50 毫秒操作系统就会重新选择一个进程来执行(我们称为“任务切换”),这个 50 毫秒称为**“时间片”**。
-
线程切换问题原理:线程切换为什么会带来bug呢?因为操作系统做任务切换,可以发生在任何一条CPU 指令执行完!,CPU 指令,而不是高级语言里的一条语句。
count++; //一条java语句,对应多条 cpu指令
-
volatile:把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步
-
可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
-
原子性:对任意单个volatile变量的读/写具有原子性。能保证执行完及时把变量刷到主内存中
-
但对于count++这种非原子性、多指令的情况。由于线程切换,线程A刚把count=0加载到工作内存,线程B也拿到0(执行++ =>1),这样就会导致线程A和B执行完的结果都是1,都写到主内存中,主内存的值还是1不是2
-
**原理:**该修饰的变量进行写时,会使用cpu提供的Lock前缀指令,可以理解为CPU指令级的一种锁
- 将当前处理器缓存行的数据写回系统内存
- 这个写回操作会使其他cpu缓存了该内存地址的数据无效。重新来读取。
引用分类
- 强引用
- 软引用:softReference gc时, 内存不足时, 才会回收。
- 弱引用:weakReference gc时,不管内存是否充足,都会回收。
- 虚引用:
线程之间的协作:
- wait() : object的方法, 写在syn 同步块或者方法中。 //释放锁
- notify()/ notifyAll() : object的方法, 写在syn 同步块或者方法中
//等待
syn(对象){
while(条件不满足){
对象.wait() //释放锁
}
//业务
}
//通知
syn(对象){
//业务, 改变条件
对象.notify()/notifyAll()
}
object o = new object()
o = null; (代表栈指向空,但是对应的堆对象还存在,等待gc回收)
fork/join: 分而治之 , 大任务拆分(fork)成若干小任务, 再将小任务的直接结果汇总(join)
3.2 CAS (compare and swap)(无锁算法)
原子操作: 不可分,要么全部做,要么一个都不做
-
目的:实现原子操作(synchronized也能实现原子操作)
synchronized缺点:
- 被阻塞的线程优先级很高很重要怎么办?
- 获得锁的线程一直不释放锁怎么办?
- 如果有大量的线程来竞争资源,那CPU将会花费大量的时间和资源来处理这些竞争。会出现如死锁等情况。
- 锁机制是一种比较粗糙,粒度比较大的机制,相对于像计数器这样的需求有点儿过于笨重。
-
释义1:比较再交换,java.util.concurrent.*,其下面的类直接或者间接使用CAS算法实现,区别synchronouse同步锁的一种乐观锁。
-
释义2:CPU指令级的操作,只有一步原子操作,非常快。避免了请求操作系统来裁定锁的问题,直接在CPU执行。
-
原理: 利用现代处理器都支持的CAS的指令, 循环这个指令,直到成功为止。 (自旋:类似死循环,长时间不成功会带来cpu高开销)。
-
get变量值(旧值)—>计算后得到新值—> compare内存中变量值和旧值---->如果相等-----旧值swap为新值
---->如果不相等,从头再来一次
(如果一个线程一直不相等,则最后一次也会相等)
-
**思想:**获取当前变量最新值A(预期值),然后进行CAS操作。此时如果内存中变量的值V(内存值V)等于预期值A,说明没有被其他线程修改过,我就把变量值改成B(更新值);如果不是A,便不再修改。(被其他线程修改过,则重新循环)
eg: count++ 多个线程来操作 A:0–>1
CAS接收三个参数(1. count的内存地址; 2. 期望的值(旧值,count 0); 3. 新值(1))
当执行时,比较和交换是不能被外部线程打断: 原子性
缺点:
- ABA问题: 中间已经改过了。
- 开销问题:不停重试。
- 只能保证一个共享变量的原子操作。 (如果内部有个变量修改,就需要syn)
JDK中相关原子操作类(java.util.concurrent.atomic,提供一种高效的CAS操作):
- 更新基本类型类: AtomicBoolean, AtomicInteger, AtomicLong
- 更新数组类: AtomicIntegerArray, AtmoIntegerArray, AtomicReferenceArray
- 更新引用类型:AtomicReference, AtomicMarkableReference, AtomicStampedReference
syn: (一个线程操作,其他线程都得等待)
ConcurrentHashMap (线程安全,解决并发问题)
- jdk 1.8以前: Segment+ReentrantLock
- jdk 1.8以后: CAS+Synchronized
1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(
O(logn)
),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。
解决多线程并发问题:
- 加synchronized 。锁机制
- threadLocal。副本,4个方法,get/set/remove/initialValue
- concurrent下的原子操作类。 concuuurentHashMap,AtomicBoolean…
- volatile+CAS操作:替换synchronized
阻塞队列
-
队列满了:再添加,会阻塞
-
队列空了:去取,会阻塞
BlockingQueue: 阻塞方法(put()/ take()),有阻塞方法也有非阻塞方法。
- 线程take()取值时,如果取不到值,会阻塞在那里。
- ArrayBlockingQueue : 由数组构成的有界阻塞
- LinkedBlockingQueue:由链表构成的有界阻塞
- PriorityBlockingQueue:支持优先级排序构成的无界阻塞
- DelayQueue:支持优先级排序构成的无界阻塞 (有效时间, 缓存时间控制)
- …
生产者/消费者:
- 中间建立一个容器(阻塞队列),生产者和消费者各自执行各自(解决生产者和消费者的强耦合问题)
- 阻塞队列就相当于一个缓冲区,平衡生产者和消费者的处理能力。
方法 | 抛出异常 | 返回值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入方法 | add | offer | put | offer(e,time,unit) |
移除方法 | remove | poll | take | poll(time,unit) |
获取方法 | element | peek | - | - |
抛出异常:
- 当队列满时,如果再往队列里插入元素,会抛出IllegalStateException(“Queuefull”)异常。
- 当队列空时,从队列里获取元素会抛出NoSuchElementException异常。
3.3 线程池原理
作用:
- 降低资源消耗。重复利用已创建的线程 降低线程创建和销毁造成的消耗。
- 提高响应速度。只有执行时间(已创建线程的情况下)。
- 提高线程的可管理性。统一分配、调优和监控。
一个线程所需要的资源时间
- 创建时间
- 任务执行时间 (线程池就只包含了该时间,不需要重复创建/销毁)
- 销毁时间
Exceutor–ExecutorService–ThreadPoolExecutor
ThreadPoolExecutor(
int corePoolSize, //核心线程池数量
int maximumPoolSize, //线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize (立即执行)
long keepAliveTime, //核心线程 保活时间
TimeUnit unit, //保活时间单位
BlockingQueue<Runnable> workQueue, //阻塞队列, 超过核心数量后,加入到该队列中
ThreadFactory threadFactory, //设置,线程名、守护线程。Executors静态工厂里默认的threadFactory,线程的命名规则是“pool-数字-thread-数字”。
RejectedExecutionHandler handler // 超过最大线程池数量,拒绝
)
//eg: 核心线程数 3, 最大线程数 6, 阻塞队列 10
//1、2、3(核心线程数),4--13(阻塞队列),14、15、16 (最大线程数 6-3=3)的执行顺序,1、2、3、14、15、16、4--13
线程池工作机制
- 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)
- 如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
- 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务。
- 如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。
怎么让线程一直进行?
当前线程执行run方法中,可以使用BlockingQueue阻塞队列进行 take()阻塞
任务特性
-
cpu密集型:cpu纯计算, 从内存中取出来计算。 线程数:不要超过cpu核心数+1。 速度快
Runtime.getRuntime().availableProcessors()
-
io密集型:网络通信/ 读写磁盘。 线程数:机器的cpu核心数*2 速度慢
-
混合型:
摘自《Jeff Dean在Google全体工程大会的报告》
操作 | 响应时间 |
---|---|
打开一个站点 | 几秒 |
数据库查询一条记录(有索引) | 十几毫秒 |
1.6G的CPU执行一条指令 | 0.6纳秒 |
从机械磁盘顺序读取1M数据 | 2-10毫秒 |
从SSD磁盘顺序读取1M数据 | 0.3毫秒 |
从内存连续读取1M数据 | 250微秒 |
CPU读取一次内存 | 100纳秒 |
1G网卡,网络传输2Kb数据 | 20微秒 |
1秒=1000毫秒 1毫秒=1000微秒 1微秒=1000纳秒
AQS:AbstractQueuedSynchronizer 抽象队列同步器 (state锁状态值)
-
作用:用来构建锁或者其他同步组件的基础框架。使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
-
**使用:**继承AQS,管理同步状态值state
-
CLH队列锁(Craig, Landin, and Hagersten (CLH) locks):基于链表的自旋锁。
-
内部一个state状态值。包含模版方法模式。 同步工具类的内部类来继承AQS
设计模式:模版方法模式
doSomthing(){
method1();
method2();
…
}
abstract public void method1();
compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。
非公平锁:不排队 拿锁。 抢占锁
ReentrantLock: 可重入锁,线程每进入一次会进入累加,释放一次进行累减。 基于AQS实现
cpu---->高速缓存---->内存
基于cpu读取速度快, 内存读取相对慢的情况,java内存模型引入了主内存和工作内存。
java内存模型: JMM
java线程从主内存中拷贝一个副本到各自的工作内存中进行相应操作。
3.4 synchronized的实现原理
**描述:**基于进入和退出Monitor对象来实现方法同步和代码块同步,都可以通过成对的MonitorEnter和MonitorExit指令来实现。常量池中多了ACC_SYNCHRONIZED标示符。
- JVM根据该标示符来实现方法的同步的:当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置。
- 如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。
- 在方法执行期间,其他任何线程都无法获得同一个monitor对象。
javap -v xxxx.class : 反编译class文件
monitorenter/monitorexit 指令实现 syn同步代码块
锁的状态 比较 (会随着竞争情况逐渐升级)
- 无锁状态
- 偏向锁 : 加锁/解锁不需要额外消耗,和非同步方法执行接近。适用于一个线程访问同步块(减少不必要的CAS操作)
- 轻量级锁:由偏向锁升级来,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。得不到锁会使用自旋消耗cpu,不会阻塞。 追求响应时间
- 重量级锁:线程阻塞,响应时间慢。不实用自旋,不消耗cpu。 追求吞吐量
synchronized是加锁在某一个具体的对象上, 如对象/ class/ 变量
DCL:双重检查锁定(单例模式)
//最终懒汉式 单例模式 ===类加载机制,保证线程安全
public class SingleObject {
private SingleObject(){}
static class Instance{
private static SingleObject singleObject = new SingleObject();
}
public static SingleObject getInstance(){
return Instance.singleObject;
}
}
指令重排序
-
sleep/yield :依然拥有锁 (sleep会抛出异常,所有会中断)
-
wait : 让出锁的执行权
-
线程顺序执行(T1/T2/T3):T3方法中调用t2.join,再调用t1.join。 依次从t1–>t2–>t3
3.5 并发编程
工具包:java.util.concurrent.*
│ AbstractExecutorService.java
│ ArrayBlockingQueue.java //<-----
│ BlockingDeque.java
│ BlockingQueue.java
│ ConcurrentHashMap.java //<-----
│ ConcurrentLinkedDeque.java
│ ConcurrentMap.java
│ DelayQueue.java
│ Executor.java
│ ExecutorService.java //<-----
│ ForkJoinPool.java
│ ForkJoinWorkerThread.java
│ Future.java
│ LinkedBlockingDeque.java
│ LinkedBlockingQueue.java
│ RejectedExecutionException.java
│ RejectedExecutionHandler.java
│ ScheduledExecutorService.java
│ ...
│
├─atomic
│ AtomicBoolean.java //<-----
│ AtomicInteger.java //<-----
│ AtomicIntegerArray.java //<-----
│ AtomicLong.java
│ AtomicLongArray.java
│ AtomicReference.java
│ AtomicReferenceArray.java
│ AtomicStampedReference.java
│
└─locks
AbstractOwnableSynchronizer.java
AbstractQueuedLongSynchronizer.java
AbstractQueuedSynchronizer.java //<-----
Lock.java
ReadWriteLock.java
ReentrantLock.java