Java核心基础(3)——多线程设计与并发
线程基础
什么是线程?
线程是程序的一个载体,是调度CPU的最小资源单位。
必须要有线程去调度CPU执行指令。
线程与进程
进程
运行程序后,程序指令和数据会被放入内存。
程序员开始运行后,放到内存中的内容就是进程,是资源分配的基本单位。
启动一个应用程序,就会开启一个进程。
线程
线程是程序执行的基本单位,是一条进程的执行路径。
多线程
在同一个进程中开启了多条执行路径,每条执行路径不会相互影响。
程序是如何运行的?
程序运行需要指令和数据。
- CPU读指令,指令存在CPU的PC(程序计数器)中
- CPU再读数据,将数据存到CPU的寄存器中
- 计算结果回写,完成后再读下一条指令与所需数据,依次反复
谁调度线程
由线程调度器与操作系统算法进行线程的调度。
线程切换的概念
线程的执行并不一定是顺序的,多个线程可由线程调度器切换。
切换方式:如A线程正在执行,此时需要切换至另一线程B先执行,程序计数器与寄存器会保存A线程到缓存,B线程完成后,CPU再从缓存恢复线程A继续执行。
线程是不是越多越好?
不是,线程越多,线程切换越复杂,程序运行效率越低。
线程的类型
线程可分为用户线程与守护线程。
用户线程
一般由用户程序实现,应用创建管理,速度快,不依赖内核,如果用户线程阻塞则进程阻塞,JVM必须确保用户线程执行完毕才会退出。
守护线程
又叫内核线程,由操作系统管理,守护线程阻塞不会导致进程阻塞,任何一个守护线程守护所有用户线程,JVM不用确保守护线程执行完毕就可以先退出,守护线程执行完再最后退出。常见守护线程有GC、内存监控线程等。
Java默认情况下创建用户线程
thread.setDaemon(true) 可设置线程为守护线程;
thread.setDaemon(false) 可设置线程为用户线程;
线程池
核心思想
可复用线程,管理线程,统一创建和销毁。
可缓存,可定长度,可定时,单例模式,内部有多个worker,通过复用worker来处理线程。
何时使用线程池
- 单个任务处理时间短
- 需要处理的任务数大
线程池的5种状态

ThreadPoolExecutor参数
通常使用ThreadPoolExecutor类创建线程池。
corePoolSize:核心线程数maximumPoolSize:最大线程数keepAliveTime:针对非核心线程的最长闲置时间TimeUnit:时间单位BlockingQueue<Runnable>:阻塞队列,此队列只允许一个线程操作出队与入队ThreadFactory:线程工厂RejectedExecutionPolicy:拒绝策略
线程池原理与使用步骤
- 假设线程池
corePoolSize为2,maximumPoolSize为3,执行t1、t2任务线程,这两个线程会交给线程池中的两个核心线程worker处理 - 假设阻塞队列长度为
5,当再想执行t3~t7任务,若t1和t2没执行完,则t3~t7任务放入阻塞队列 - 此时再想入
t8至阻塞队列,超出阻塞队列最大长度5,并且此时t1和t2没有执行完,线程会新建一个临时线程处理t8,本例中最大临时线程的数量为maximumPoolSize - corePoolSize = 1
若t8处理完,闲置keepAliveTime后,临时线程自动销毁;
若t8未处理完,且有t9进入,此时无多余临时线程,则执行线程池的拒绝策略:- AbortPolicy(默认):丢弃任务
t9并抛出异常 - DiscardPolicy:丢弃任务
t9但不抛异常 - DiscardOlderstPolicy:丢弃队列最前任务
t3,并重新提交任务t9至队列 - CallerRunsPolicy:由提交任务
t9的线程(可能是主线程)处理,回退给提交者
- AbortPolicy(默认):丢弃任务
- 当
t1或t2处理结束后,空出的核心线程worker处理阻塞队列头任务
线程状态
Thread.state
- NEW:尚未启动的线程
- RUNNABLE:在JVM中执行的线程
- BLOCKED:被阻塞等待监视器锁定的线程
- WAITTING:正在等待另一个线程执行特定动作的线程
- TIMED_WATTING:正在等待另一个线程执行动作到达指令等待时间的线程
- TERMINATED:已退出的线程
多线程的创建方法
- 继承
Thread类,重写run()方法 - 实现
Runnable接口,重写run()方法,无返回值 - 实现
Callable接口,重写run()方法,结合FutureTask.get()获得返回值 - 线程池创建线程
- 使用Spring异步注解
@Asyn创建
如何停止线程
- 建议线程正常停止,利用次数,不建议死循环
- 建议使用标志位,设置一个标志位,
while(flag),当需要停止时,设置flag为false - 不要使用
stop()或destroy()
线程生命周期

sleep() 线程休眠
sleep(t)指定当前线程阻塞的毫秒数tsleep()需抛出异常Interrupted Exceptiont时间到达后,进入就绪状态,等待CPU调度sleep不会释放锁! 区别于wait()会释放锁
运行 ————> 阻塞 ————> 就绪 ————>运行
sleep t时间后 CPU调度
yield() 线程礼让
- 让当前正在执行的线程暂停,但不阻塞
- 将线程从运行状态转为就绪状态
- 让CPU重新调度,礼让不一定成功,看CPU状态。
如线程B在就绪状态,线程A在运行,A使用yield()礼让B线程后进入就绪状态,此时A、B都在就绪状态等待调度,下一次CPU调度不一定就让B执行,也有可能还是让A执行,所以礼让不一定成功,可以提升B线程优先级,提高CPU调度概率。 - 礼让结束后,继续执行后续代码,不是从头开始
运行 ————> 就绪 ————>运行
yield CPU调度
join() 线程合并
join()合并线程,让线程先执行- 类似插队
- 调用
t.join()的线程A让t先运行,A进入等待池,其它线程不受影响
线程优先级
Java提供一个线程调度器,监控就绪状态的所有线程,根据优先级决定哪个线程先执行。
但是,线程优先级高未必一定先执行,还是看CPU状态,只能说优先级高的线程先执行的概率大一些。
同样的,优先级低只意味着先获得调度的概率低,并不是不会被调度。
可能出现优先级低反而先被调度的情况,导致性能倒置。
优先级用数字 1~10 表示,默认是 5,使用t.getPriority().setPriority(num)修改优先级。
优先级的设立应在start()之前。
JMM(Java内存模型)
什么是JMM
JMM是抽象概念,由JSR133规范,描述的是程序间变量的访问规则,与CPU缓存模型相似,是标准化的,用于屏蔽各种硬件与操作系统的内存访问差异。
JMM规范了多线程程序允许表现出的行为

JMM使不同版本的JDK、操作系统、硬件运行出来的代码具有一致性。
主内存中有:堆、方法区中元素或任何被线程共享的数据(如数据库中读取的数据);
工作内存中有:栈、CPU三级缓存、CPU寄存器中数据。
JMM中三大特性
- 可见性
- 原子性
- 有序性
什么是可见性问题
假设线程1、2同时使用主内存共享变量 x = 1
① 
② 若此时线程1 x += 1,线程2能否嗅探到?
默认是不能的,因为线程工作内存间不可互相访问。
这就是JMM的可见性问题。
JMM 8大数据原子操作
① lock:作用于主内存,把一个变量标记为一条线程独占状态。
② unlock:作用于主内存变量,把一个处于锁状态的变量释放,释放后的变量可被其它线程锁定。
③ read:把一个变量值从主内存传输到线程的工作内存中,以便随后的load使用。
④ load:把read操作从主内存中得到的变量值放入内存的变量副本中。
⑤ use:把工作内存中的一个变量值传递给执行引擎。
⑥ assign:把一个从执行引擎接收到的值赋给工作内存中的变量。
⑦ store:把工作内存中的一个变量的值传送到主内存中,以便随后的write操作。
⑧ write:把store操作从工作内存中的一个变量的值传送到主内存变量中。
详细共享变量实现

当线程B 进行⑥后,为何线程A不会回到主内存查看共享变量是否变化并修改变量副本?
答:因为线程A运行时,工作内存中已有initFlag副本,线程会先在工作内存中找此变量。
- 若工作内存中有,则直接使用;
- 若无,才会回到主内存查找read
如何让线程间可见,线程B改变变量值之后,如何让线程A立刻嗅探到?
使用volatile修饰变量,实现可见性并禁止指令重排序。
由于JMM对线程的规范(线程工作内存间不可互相访问)导致共享变量的修改不能被各线程及时嗅探,而添加volatile修饰变量,则可解决这一问题。
volatile可见性实现原理
底层实现:通过汇编lock前缀指令触发底层缓存锁定机制(缓存一致性协议或总线锁,优先使用缓存一致性协议,总线锁效率较低)
例如触发其中一种缓存一致性协议MESI,lock指令会触发锁定变量缓存行区域并写回主内存,这个操作称为“缓存锁定”。
MESI
MESI为一种缓存一致性协议,主要有四个状态:
- M:已修改,代表变量已修改
- E:独占,被一个CPU核独占
- S:共享,代表变量在多个CPU核上都有副本
- I:代表变量失效,对应副本应销毁
lock前缀触发MESI过程
总线锁
当缓存一致性协议不可用时再使用,非volatile优先,效率低,几乎不用。
CPU从主存读取数据到缓存区中,总线会加锁锁定该缓存对应的主存区域,来自其它CPU或总线代理的控制请求将被阻塞,无法读写内存直到锁定被释放。
volatile不能保证原子性
volatile可保证可见性,有序性,但不保证原子性,原子性需用sycronize满足。
如A线程、B线程都想num++。
A线程num++,还没来及发出“写”消息同步回主内存,B线程拿到num,也num++,此时A线程发出“写”消息,置B内部工作内存无效,导致Bnum++无效,数据丢失,重新回主内存读num,少加了一次num,违背原子性原则。
锁与同步
线程通信的方法
wait():无参情况下,表示线程一直等待,直到其它线程通知,与sleep()不同,wait()会释放锁!wait(long timeout):指定等待的毫秒数timeoutnotify():唤醒一个处于等待状态的线程notifyAll():唤醒同一个对象上所有调用wait()方法的线程,优先级高的优先调度
上述均是Object类的方法,不是Thread的方法,只能在同步方法或同步代码块中使用。
为什么这些方法放在Object类中?
答:因为syncronized可对任意对象加锁。

浙公网安备 33010602011771号