13.1 线程的概念模型
1.线程、进程与多任务
1)程序(program)是对数据描述与操作的代码的集合,是应用程序执行的脚本。
2)进程(process)是程序的一次执行过程,是系统运行程序的基本单位。程序的静态的,进程是动态的。系统运行一个程序即是一个进程从创建、运行到消亡的过程。
3)多任务(multi task)在一个系统中可以同时运行多个程序,既有多个独立运行的任务,每个任务对应一个进程。
2.线程
1)线程(thread):比进程更小的运行单位,是程序中单个顺序的流控制。一个进程中可以包含多个线程。
2)简单来讲,线程是一个独立的执行流,是进程内部的一个独立执行单元,相当于一个子程序。
3)一个进程中的所有线程都在该进程的虚拟地址空间中,使用该进程的全局变量和系统资源。
4)操作系统给每个线程分配不同的CPU时间片,在某一时刻,CPU只执行一个时间片内的线程,多个时间片中的相应线程在CPU内轮流执行。
13.2 线程的创建和启动
1.创建多线程
1)每个Java程序启动后,虚拟机将自动创建一个主线程。
2)可以通过以下两种方式自定义线程类:
---创建java.lang.Thread类的子类,重写该类的run方法
---创建java.lang.Runnable接口的实现类,实现接口中的run方法
2.继承Thread类
Thread:代表一个线程类
构造方法 | 含义 |
Thread() | 创建一个新的线程对象 |
Thread(Runnable target) | 基于Runnable接口实现类的实例创建一个线程对象 |
Thread(Runnable t, String name) | 基于给定的Runnable接口实现类的实例和指定名字创建一个线程对象 |
Thread(String name) | 基于给定的名称创建一个线程对象 |
3.Thread类
Thread类中的重要方法:
---run方法:包括线程运行时执行的代码,通常在子类中重写它。
---start方法:启动一个新的线程,然后虚拟机调用新线程的run方法
4.Thread 类代码示例
5.创建多线程
问题:要定义的线程类已经显式继承了一个其他的类怎么办?
6.Runnable 接口
1)Runnable 接口中只有一个未实现的run方法,实现该接口的类必须重写该方法。
2)Runnable 接口与 Thread 类之间的区别:
---Runnable 接口必须实现run方法,而Thread类中的run方法是一个空方法,可以不重写。
---Runnable 接口的实现类并不是真正的线程类,只是线程运行的目标类。要想以线程的方式执行run方法,必须依靠Thread 类。
---Runnable 接口适合于资源的共享。
7.问题
8.线程的生命周期
1)线程的生命周期:
---指线程从创建到启动,直至运行结束
---可以通过调用Thread类的相关方法影响线程的运行状态
2)线程的运行状态
---新建(New)
---可执行(Runnable)
---运行(Running)
---阻塞(Blocking)
---死亡(Dead)
3)线程的生命周期
4)线程的生命周期
(1)新建状态(New)
---当创建了一个Thread对象时,该对象就处于“新建状态”
---没有启动,因此无法运行
(2)可执行状态(Runnable)
---其他线程调用了处于新建状态线程的start方法,该线程对象将转换到“可执行状态”
---线程拥有获得CPU控制权的机会,处于等待调度阶段。
(3)运行状态(Running)
---处于“可执行状态”的线程对象一旦获得了CPU控制权,就会转换到“执行状态”
---在“执行状态”下,线程状态占用CPU时间片段,执行run方法中的代码
---处在“执行状态”下的线程可以调用yield方法,该方法用于主动出让CPU控制权。线程对象出让控制权后回到“可执行状态”,重新等待调度。
(4)阻塞状态(Blocking)
((1))线程在“执行状态”下由于受某种条件的影响会被迫出让CPU控制权,进入“阻塞状态”。
((2))进入阻塞状态的三种情况:
- 调用sleep方法
- 调用join方法
- 执行I/O操作
--1 调用sleep方法
public void sleep(long millis)
---Thread类的sleep方法用于让当前线程暂时休眠一段时间
---参数millis的单位是毫秒
sleep方法实例:
--调用join方法(合并某个线程)
---处在“执行状态”的线程如果调用了其他线程的join方法,将被挂起进入“阻塞状态”
---目标线程执行完毕后才会解除阻塞,回到“可执行状态”
--调用I/O操作
---线程在执行过程中如果因为访问外部资源(等待用户键盘输入、访问网络)时发生了阻塞,也会导致当前线程进入“阻塞状态”。
((3))解除阻塞
---睡眠状态超时
---调用join后等待其他线程执行完毕
---I/O操作执行完毕
---调用阻塞线程的interrupt 方法(线程睡眠时,调用该线程的interrupt 方法会抛出InterruptedException)
(5)死亡状态(Dead):处于“执行状态”的线程一旦从run方法返回(无论是正常退出还是抛出异常),就会进入“死亡状态”。
已经“死亡”的线程不能重新运行,否则会抛出IllegalThreadStateException
可以使用Thread类的isAlive方法判断线程是否活着
9.线程调度
1)线程调度
---按照特定机制为线程分配CPU时间片段的行为
---Java程序运行时,由Java虚拟机负责线程的调度
2)线程调度的实现方式
---分时调度模型:让所有流程轮流获得CPU的控制权,并且为每个线程平均分配CPU时间段
---抢占式调度模型:选择优先级相对较高的线程执行,如果所有线程的优先级相同,则随机选择一个线程执行。
Java虚拟机采用此种调度模型。
10.线程的优先级
1)Thread类提供了获取和设置线程优先级的方法
---getPriority:获取当前线程的优先级
---setPriority:设置当前线程的优先级
2)Java语言为线程类设置了10个优先级,分别使用1~10内的整数表示,整数值越大代表优先级越高。每个线程都有一个默认的优先级,主线程的默认优先级是5。
3)Thread类定义的三个常量分别代表了几个常用的优先级:
---MAX_PRIORITY::代表了最高优先级10
---MIN_PRIORITY::代表了最低优先级1
---NORM_PRIORITY::代表了正常优先级5
4)setPriority不一定起作用,在不同的操作系统、不同的JVM上,效果也可能不同。操作系统也不能保证设置了优先级的线程就一定会先运行或得到更多的CPU时间。
5)在实际使用中,不建议使用该方法
11.进程同步
1)问题:通过多线程解决小朋友分苹果的问题:一共有5个苹果,2个小朋友同时拿苹果,每次拿一个,拿完为止
2)测试方法
3)线程同步
为了得到两个线程并发操作的结果,下面稍微修改一下run方法
12.线程安全
多线程应用程序同时访问共享对象时,由于线程间相互抢占CPU的控制权,造成一个线程夹在另一个线程的执行过程中运行,所以可能导致错误的执行结果。
13.使用synchronized关键字
1)为了防止共享对象在并发访问时出现错误,Java中提供了“synchronized”关键字。
2)synchronized关键字
确保共享对象在同一时刻只能被一个线程访问,这种处理机制称为“线程同步”或“线程互斥”。Java中的“线程同步”基于“对象锁”的概念。
3)使用synchronized关键字
---修饰方法:被“synchronized”关键字修饰的方法称为“同步方法”
//定义同步方法
public synchronized void methd(){
//方法实现
}
---当一个线程访问对象的同步方法时,被访问对象就处于“锁定”状态,访问该方法的其它线程只能等待,对象中的其它同步方法也不能访问,但非同步方法则可以访问。
4)重新修改小朋友分苹果的例子,将getApple方法改为同步方法
14.线程通信
1)当一个线程正在使用同步方法时,其它线程就不能使用这个同步方法,而有时涉及一些特殊情况:
---当一个人在一个售票窗口排队买电影票时,如果她给售票员的不是零钱,而售票员有没有零钱找她,那么她必须等待,并允许后面的人买票,以便售票员获取零钱找她,如果第2个人也没有零钱,那么她俩必须同时等待。
2)当一个线程使用的同步方法中用到某个变量,而此变量又需要其它线程修改后才能符合本线程的需要,那么可以在同步方法中使用wait()方法
3)wait()方法:
---中断方法的执行,使本线程等待,暂时让出CPU的使用权,并允许其它线程使用这个同步方法。
notify()方法:
---唤醒由于使用这个同步方法而处于等待线程的某一个结束等待。
notifyall()方法:
---唤醒所有由于使用这个同步方法而处于等待的线程结束等待
4)线程通信示例
模拟3个人,张飞、关羽和刘备,来买电影票,售票员只有一张5元的钱,电影票5元钱一张:张飞拿20元一张的人民币排在关羽和刘备的前面,关羽和刘备各拿了一张5元的人民币买票。因此张飞必须等待(还是关羽和刘备先买了票)