多线程学习路线
1. 并行和并发
并行: 多个CPU实例或者是多台机器同时执行一段处理逻辑, 是真正的同时
并发: 一个CPU或一台机器, 通过CPU调度算法, 让用户看上去是同时执行, 实际上从CPU操作层明并不是真正的同时。并发往往需要公共的资源,对公共资源的处理和线程之间的协调是并发的难点。
并发编程:同时执行多个任务。
2. 进程和线程
- 进程:操作系统分配资源的基本单位,比如应用和后台服务,windows是一个支持多进程的操作系统。
- 线程:操作系统执行调度的基本单位,线程是共享进程的内存空间。每个线程可以同时并发的,但是只有一个占用内存。
- 纤程:也叫协程,用户态的线程,线程中的线程,切换和调度不需要经过操作系统
进程和线程的区别?
(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线
程。
(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。
(3)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法
实现同步。
(4)处理机分给线程,即真正在处理机上运行的是线程。
(5)线程是指进程内的一个执行单元,也是进程内的可调度实体
联系:
(1)线程是进程的最小执行和分配单元,不能独立运动,必须依赖于进程,这也就
可以说众多的线程组成了进程
(2)同一个进程中的线程是共享内存资源的,比如全局变量,每一个线程都可以改
变其共同进程中的全局变量的数据
区别:
(1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。
(2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可以并
发执行。
(3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以
访问隶属于进程的资源。
3. 使用多线程的好处
main{
任务一;
任务二;
任务三;
}
*任务一挂了,任务二不执行main{
任务一启动——执行;
任务二启动——执行;
任务三启动——执行;
}
如果是多线程,异步调用,共享内存,一个挂了,其余的线程还可继续执行;
京东商城,进入之后是一个大的框架,并不是所有的图片或者模块都能加载出来,是一点一点加载的,可能上面没加载完,下面的先加载出来了(异步)
优势:
(1)使用线程可以把占据时间长的程序中的任务放到后台去处理
(2)用户界面更加吸引人,这样比如用户点击了一个按钮去触发某件事件的处理,可
以弹出一个进度条来显示处理的进度
(3) 程序的运行效率可能会提高
(4)在一些等待的任务实现上如用户输入,文件读取和网络收发数据等,线程就比较
有用了
(5)可以分别设置各个任务的优先级以优化性能
(6)当前没有进行处理的任务时可以将处理器时间让给其它任务
劣势:
(1)如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换。
(2)更多的线程需要更多的内存空间
(3)线程中止需要考虑对程序运行的影响.
(4)通常块模型数据是在多个线程间共享的,需要防止线程死锁情况的发生
(5)线程的死锁。即较长时间的等待或资源竞争以及死锁等多线程症状
4. 线程的状态
- 新建(new):线程对象被创建时,他只会短暂的处于这种状态,此时他已经分配了必须的系统资源,并执行了初始化。
例如Thread thread = new Thread()。
- 就绪(Runnable):称为“可执行状态”。线程对象被创建后,其他线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
- 运行(Running):线程获取CPU权限进行执行。
注意:线程只能从就绪状态进入运行状态。
- 阻塞(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行,直到线程进入就绪状态,才会转到运行状态。阻塞的情况分为三种:
1)等待阻塞:通过调用线程的wait()方法,让线程等待某工作的完成。
2)同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态
3)其他阻塞:通过调用现成的sleep()或发出了I/O请求时,线程会进入到阻塞状态。
注1:当sleep()状态超时,join()等待线程终止或者超时。或是I/O处理完毕时,线程重新转入就绪状态。
注2:写多线程的时候要调用Thread.sleep(ms)方法,可以大大降低多线程CPU调用的负担(如果发现多线程在执行过程中运行速度很慢,可以找一个合理的位置,sleep一下)
- 死亡(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期
5. 实现多线程的三种方式
继承Thread类
Task task = new Task(); //Task为继承了Thread的类
task.start(); //在主线程中,使线程处于就绪状态,等待CPU调用
实现Runnable接口
Task task = new Task(); //Task为实现了Runnable接口的类
Thread thread = new Thread(task); //在主线程中启用一个新线程,参数是实现了Runnable接口的类
thread.start(); //在主线程中,使线程处于就绪状态,等待CPU调用
实现Callable接口
该接口要实现call()方法,并且线程执行会有返回值。上面两种都是重写run方法,没有返回值
6. 线程的优先级(可作为一种阻塞方式使用)
setPriority(int p):设置线程的优先级。优先级越高的线程理论上被分配时间
片的机会越多
优先级的取值范围:
常量:MAX_PRIORITY 最高优先级,值为10
常量:MIN_PRIORITY 最低优先级,值为1
常量:NORM_PRIORITY 默认优先级,值为5
线程优先级不能保证一定按照优先级的方式执行。线程并发运行时是靠线程调度机
制分配时间片做到的。分配时间片是不可控的。
既然使用多线程,就不应纠结线程执行的先后顺序,因为是不确定的
6.1 进程调度策略
默认调度策略:
- 实时调度策略:
FIFO:First In First Out,按优先级分高低
RR:Round Robin,优先级相同轮询 - 普通调度策略:
CFS策略:Completely Fair Scheduler,按优先级分配时间片的比例,记录每个进程的执行时间,如果哪个进程执行时间不到他应分配的比例,将优先执行
7. 线程的特性
安全性:
由于线程之间可以共享内存,则某个对象(变量)是可以被多个线程共享或同时访问的。当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替进行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
例如:甲乙两个人,甲向箱子里面装苹果,乙负责从箱子里面数苹果,甲乙同时进行,当乙数到5的时候,甲又放入一个,此时,箱子里面苹果总实际是6个。显然代码不是线程安全的。
可见性:
当多个线程访问同一个变量x时,线程1修改了变量x的值,线程1线程2线程n能够立即读取到线程1修改后的值。
有序性:
即程序执行时按照代码书写的先后顺序执行。在java内存模型中,允许编译器和处理器对指令进行重新排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
原子性:
两种或几种操作是不可分割的,要么都成功,要么都失败
若要乙数清,则在乙数的时候甲不能放或者在甲放完后乙再数。
这时,不能并行,只能串行才能数清楚。
8. 锁的概念和使用
临界区: 访问或操作共享数据的代码段(Synchronize锁住的部分
竞态条件: 两个线程同时拥有临界区的执行权
同步: 避免竞态条件
锁: 完成同步的手段
竞态条件会使运行结果不可靠,程序的运行结果取决于方法的调用顺序,将方法以串行的方式来访问,我们称这种方式为同步锁。
Java实现同步锁的方式有:
>同步方法:synchronized method() //在方法前加synchronized
>同步代码块synchronized(Lock): //在要执行同步的地方加入锁,比上面范围小
>等待与唤醒 wait 和notify:
>使用特殊域变量(volatile)实现线程同步:
>使用重入锁实现线程同步ReentrantLock:
>使用局部变量实现线程同步ThreadLocal:
synchronized
synchronized是Java的内置锁机制,是在JVM上使用的。
可以同步代码块,也可以同步方法。
注:同步是一种高开销的操作,因此应尽量减少同步的内容。通常没有必要同步整个方法。
synchronized机制是将所有线程排在一个队列上,同步在整个方法上,在并发访问的时候,只能排队。
ReentrantLock
可重入锁,是一种显示锁,在JavaSE5.0中新增java.util.concurrent包来支持同步。
ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
ReentrantLock():创建一个ReentrantLock实例,与synchronized显著区别,synchronized{//代码}即可,ReentrantLock需要new一个对象。
lock:获得锁
// 中间部分代码被锁住
unlock:释放锁
可重入:甲获得锁后释放锁或锁失效,乙可继续获得这个锁。
volatile变量
volatile具有java内存模型中的可见性、有序性,不具备原子性。
我们了解到synchronized是阻塞式同步,称为重量级锁。
而volatile是非阻塞式同步,称为轻量级锁。
注:被volatile修饰的变量能够保证每个线程都能获取该变量的最新值,从而避免出现数据脏读1的现象。
synchronized与ReentranLock的区别:
synchronized是关键字,在方法或要加锁的代码块前使用,只能用在一个队列当中。需要wait和notify搭配使用。对于锁的粒度控制比较粗,同时对于实现一些锁的状态的转移比较困难。在JDK1.5之后synchronized引入了偏向锁,轻量级锁和重量级锁,从而大大的提高了synchronized的性能。
ReentrantLock是对象,使用前需要new,并用一个监视器(Condition condition = lock.newCondition())监听最近的Lock,在加锁的地方通过lock()方法加锁,在finally中释放锁(unlock(),如不释放,可能造成死锁),通过condition.await()和condition.signal()实现加锁和唤醒。可以用在多个队列中。
9. wait和notify PCM-生产消费者模型
— 生产者向存储中放,消费者从存储中拿,两个线程同时进行
— 当存储队列满了的时候,生产者要挂起,并通知消费者拿,当队列空的时候,消费者挂起,并通知生产者放。
— 内部通知是通过wait和notify实现的;
— 放和拿是需要同步锁的
Store队列存储类
/**
* 队列存储类
*/
import java.util.LinkedList;
public class Store {
// 队列存储(因为要使用队列,这里用LinkedList)
LinkedList<Integer> list = new LinkedList<>();
private final int max = 10; // 设置队列最大值
// 生产者向队列中放东西(放入队尾)
public void push(int n) {
try {
// 锁住队列
synchronized (list) {
// 如果队列满了,等待消费者去队列中拿走
if (list.size() >= max) {
System.out.println(">>>容器满了");
// 线程挂起
list.wait();
}
// 如果队列不满,可以放入
else {
System