(端午节给自己放了两天假,另外swing章节先跳过了)
多线程
一. 线程和多线程
1. 线程的概念
-
进程
- 程序的一次动态执行,对应了从代码加载,执行至执行完毕的一个完整过程
- 一个进程既包括所要执行的指令,又包括执行指令需要的任何系统资源(CPU,内存空间等)
- 不同进程所占用的系统资源相对独立
-
线程
- 线程是进程执行过程中产生的多条执行线索,比进程单位更小的执行单位
- 没有出入口,因此自身不能自动运行,必须栖身于一个进程之中
- 同一进程的所有线程共享该进程的资源(例如qq是一个进程,qq里面的功能是线程,它们之间的资源是共享的)
- 线程间的切换速度比进程快得多
-
多线程
-
宏观角度来说就是使多个作业同时执行
-
多线程可以使系统资源特别是CPU的利用率得到提高,整个程序的执行效率得到提高
-
Java把线程或执行环境当作一个封装对象,包含CPU以及自己的程序代码和数据,由虚拟机提供控制
-
Thread允许创建这样的线程,并可控制所创建的线程
-
2. 线程的结构
- 线程的组成
- 虚拟CPU,封装在Thread类中,控制整个线程的运行
- 执行的代码,传递给Thread类,由Thread控制按序执行
- 处理的数据,传递给Thread,在代码执行过程中需要处理的数据
- 当一个线程被构造时,由构造方法参数,执行代码,操作数据来初始化
- 线程与进程的区别
- 多线程编程简单,效率高,使用多线程可以在线程间直接共享数据和资源
- 多线程适合开发有多种交互接口的程序
- 其机制可以减轻编写交互频繁,涉及面多的程序问题(如侦听网络端口的程序)
- 提供了Thread类来实现多线程
3. 线程的状态
-
线程的4种状态图解
- **Thread类本身只是线程的虚拟CPU,**线程所要完成的功能是通过run()来完成的
- run称为线程体,实现线程体的特定对象是在初始化线程时传递给线程的
- 在一个线程被建立并初始化之后,Java运行时系统自动调用run( )方法
-
新建
- 线程刚创建还未启动,此时处于新建状态,但已有了相应的内存空间以及其他资源
-
可运行状态
- 这种情况下线程可能正在运行,也可以没运行,只要CPU一空闲,马上运行
- 可运行但没在运行的线程都排在一个队列中,称为就绪队列
- 调用线程的 start()方法可使线程处于可运行状态
-
死亡
- run( )方法执行完毕,结束运行
- 当线程遇到异常退出
-
阻塞
- 阻塞时线程不能进入就绪队列 , 必须等引起阻塞的原因消除,才可重新进入队列
- sleep 和 wait 是两个常用的引起阻塞的办法
二. 创建线程
1. 继承Thread类创建线程
-
Thread类中一个典型的构造方法
-
Thread(ThreadGroup group, Runnable target, String name)
- name作为新线程的名字,且是group中的一员
- target必须实现Runnable接口, 它是另外一个线程对象, 当本线程启动时, 将调用target的run( )方法
- 当target为null时, 启动本线程的run( )方法
-
-
Thread类本身实现了Runnable接口
- 任何实现Runnable接口的对象都可以作为一个线程的目标对象
- 构造方法中的各参数都可以缺省
- 在Thread类派生出一个子类, 在类中一定要实现run( )方法
-
定义一个线程类,继承Thread类并重写run( ), Java只支持单重继承, 用这种方法定义的类不能再继承其他类
- Thread的构造方法中有一个Runnable实例的参数
-
使用Thread类的子类创建线程
-
public class Test extends Thread { @Override public void run() { // 线程体 for (int i = 0; i < 5; i++) { System.out.println("我是钢铁侠"); // 输出6次信息 try { sleep(500); // 线程等待一会鹅 } catch (InterruptedException e) { e.printStackTrace(); } } } } class Test2 extends Thread { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("我是美国队长"); try { sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } } } } class ThreadTest { public static void main(String[] args) { Test test = new Test(); // 创建线程 Test2 test2 = new Test2(); test.start(); // 启动线程 test2.start(); } } 控制台: 我是钢铁侠 我是美国队长 我是美国队长 我是钢铁侠 我是美国队长 我是美国队长 我是钢铁侠 我是美国队长 我是钢铁侠 我是钢铁侠
- 定义了Test 和 Test2两个类, 都是Thread类的子类, 都是线程类
- 覆盖了父类中的run( )方法
- 启动线程时, 它们的线程体是不一样的
- 执行顺序按系统来决定
-
2. 实现Runnable接口创建线程
-
任何实现线程功能的类都必须实现该接口
-
使用Runnable接口实现多线程时, 也必须实现run( )方法,必须使用start( )启动线程
-
Runnable接口创建线程
-
public class Test implements Runnable { private int i; @Override public void run() { for (; i < 20; i++) { System.out.println(Thread.currentThread().getName()+"\t"+i); if (i == 20) { System.out.println(Thread.currentThread().getName()+"\t"+"over"); } } } } class MyThreadTest{ public static void main(String[] args) { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName()+"\t"+i); if (i == 5) { Test t1 = new Test(); Thread thread1 = new Thread(t1, "线程1"); Thread thread2 = new Thread(t1, "线程2"); thread1.start(); thread2.start(); } } } } 控制台: main 0 main 1 main 2 main 3 main 4 main 5 main 6 main 7 main 8 main 9 线程1 0 线程1 1 线程1 2 线程1 3 线程1 4 线程1 5 线程1 6 线程1 7 线程1 8 线程1 9 线程1 10 线程1 11 线程1 12 线程1 13 线程1 14 线程1 15 线程1 16 线程1 17 线程1 18 线程1 19 线程2 0 线程2 over
- Test是实现了Runnable接口的类,所以可以使用它创建线程t1
- 然后又将t1作为构造方法中的target创建了两个线程thread1 和 thread2
- 这两个线程使用的是同一个t1 , 它们共享t1, 即都执行t1的run( )方法
- 先输出主线程main的, 然后两个新线程共享的信息随机输出
-
3. 创建线程的两种方法的适用条件
-
适用于采用实现Runnable接口方法的情况
- 如果一个类已经继承了Thread, 就不能再继承其他类, 就被迫采用实现Runnable的方法
- 原本的线程是采用实现Runnable接口的方法**,为了保持风格一致继续使用这种方法**
-
适用于采用继承Thread方法的情况
-
当一个run( )方法置于Thread的子类中时, this实际上引用的是控制当前运行系统的Thread实例
-
Thread.currentThread().getState();
-
可简写为
-
getState();
-
-
代码比较简洁, 所以很多情况下愿意使用继承Thread的方法
-
三. 线程的基本控制
1. 线程的启动
-
要使线程真正的运行, 必须通过 start( )方法来启动, 此时线程中的虚拟CPU已经就绪
-
API 提供了以下有关线程的操作方法
- start( ); 启动线程对象, 让线程从新建状态转为就绪状态
- run( ); 用来定义线程对象被调度之后所执行的操作, 用户必须重写run( )方法
- yield( ); 强制终止线程的执行
- isAlive( ); 测试当前线程是否在活动
- sleep( ); 使线程休眠一段时间
- void wait( ); 使线程处于等待状态
2. 线程调度
-
Java中线程调度是抢占式
- 抢占式调度是指可能有多个线程准备运行, 只有一个在真正的运行
- 一个线程获得执行权, 这个线程将持续运行下去
-
直到运行结束或者某种原因阻塞, 或者有另外一个高优先级线程就绪
-
线程调度的优先级策略
- 优先级高的先执行
- 每个线程创建时会自动分配一个优先级, 默认时,继承其父类的优先级
- 任务紧急的线程, 其优先级较高
- 同优先级的线程 按照 先进先出 的调度原则
-
线程优先级的静态量
- MAX_PRIORITY 最高优先级, 10
- MIN_PRIORITY 最低优先级, 1
- NORM_PRIORITY 默认优先级 5
-
优先级的几个常用方法
-
void setPriority(int newPriority) // 重置线程优先级
-
int getPriority() // 获取当前线程的优先级
-
void yield() // 停止当前正在执行的线程,即让当前线程放弃执行权
-
-
线程被阻塞的原因可能:
- 执行了sleep调用, 暂停一段时间
- 也可能需要等待一个较慢的外部设备, 例如磁盘或者用户操作的键盘
-
所有被阻塞的线程按次序排列, 组成一个阻塞队列
-
所有就绪但没运行的线程根据其优先级进入一个就绪队列
-
当CPU空闲时, 就绪队列中第一个具有最高优先级的线程将运行
-
当一个线程被抢占而停止运行时, 它的运行状态会被改变并放到就绪队列的队尾
-
一个被阻塞的线程就绪后也会放到就绪队列的队尾
-
调用sleep方法
-
由于Java线程调度不是时间片式, 在程序设计时要合理安排不同线程之间的运行顺序
-
保证其他线程留有执行的机会
-
@Override public void run() { while (true) { // 执行若干操作 // 给其他线程运行的机会 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } }
- sleep中的参数指定了必须休眠的时间
- 最小时间之后只保证它回到就绪状态, 能否获到CPU运行, 要视线程调度而定
-
-
yield方法
- yield方法可以给其他同优先级线程一个运行的机会
- 如果在就绪队列中有其他同优先级的线程, yield把调用者放入就绪队列队尾, 并允许其他线程运行
- 如果没有同优先级, 则yield不做任何工作
- sleep 允许低优先级线程运行, yield 只给同优先级线程允许运行
3. 结束线程
-
线程的死亡
- 当一个线程从run( )方法的结尾处返回时, 自动消亡并不能再被运行, 可理解为自然死亡
- 遇到异常使得线程结束, 将其理解为强迫死亡, 可以使用interrupt( )方法中断线程的执行
-
可以利用Thread类中的静态方法currentThread( )来引用正在运行的线程
-
可以使用 isAlive( )来获取一个线程是否还在活动状态的信息
-
活动状态不意味着这个线程正在执行, 只说明这个线程已经被启动
4. 挂起线程
-
暂停一个线程也称为挂起, 挂起之后必须重新唤醒线程进入就绪状态
-
挂起线程的方法有以下几种
-
sleep
- 暂时停止一个线程的执行, 不是休眠期满就立刻被唤醒, 此时有可能其他线程正在执行
- 重新调度只在以下几种情况发生
- 被唤醒的线程具有更高的优先级
- 正在执行的线程因为其他原因被阻塞
- 程序处于支持时间片的系统中
-
wait notify notifyAll
- wait方法导致当前线程等待, 直到其他线程调用此对象的notify 或者 notifyAll方法, 才可唤醒线程
-
join
-
将引起当前正在执行的线程等待, 直到join方法所调用的线程结束
-
比如在线程B中调用了线程A的join方法, 直到线程A执行完毕, 才会继续执行线程B(强行插队)
-
join方法可以在调用时使用一个以毫秒计的时间值
-
void join(long millis)
- 此时join方法将挂起线程millis毫秒, 或者直到线程的结束
-
-
四. 线程的互斥
1. 互斥问题的提出
-
同时运行的线程需要共享数据, 此时每个线程就必须要考虑与它一起共享数据线程的状态和行为
-
否则就无法保证共享数据的一致性,因此就不能保证正确性
-
设计一个代表栈的类
-
public class Stack { int idx = 0; char data[] = new char[6]; public void push(char c) { data[idx] = c; idx ++; } public char pop() { idx --; return data[idx]; } }
- 栈具有后进先出模式,使用 下标值idx 表示栈中下一个放置元素的位置
- 设想有两个独立的 线程A 和 B 都具有对这个类同一对象的引用,线程A 负责入栈,线程B 负责出栈
- 要求 线程A 放入栈中的数据都要由 线程B 读出,看起来可以将数据成功移出移入
- 但因为入栈方法push()和出栈方法 pop()中含有多条语句,执行过程中存在问题
- 假设此时栈中已有字符 1 和 2 ,当前线程 A 要入栈一个字符 3 ,调用push(3)
- 执行了语句 data[idx] = c; 后被其他线程抢占了,此时还没执行 idx++ 语句
- 如果此时线程被唤醒,可以继续修正 idx 的值,否则,入栈操作执行了一半,恰巧线程B此时正占有CPU,调用 pop(),执行出栈操作,则它返回的字符是2,因为它先执行 idx- - 语句,idx的值变为 1 ,返回的是 data[1] 处的字符,即字符串 2 ,字符 3 被漏掉了
-
以上例子说明的就是 多线程访问共享数据时通常会引发的问题
- 原因是:对共享资源访问的不完整性
- 有一种机制保证对共享数据的完整性
- 对共享数据操作的同步
- 共享数据称为条件变量
-
可以选择禁止线程在完成关键代码部分时被切换
- 对于线程A就是 入栈操作 以及 下标值增加操作
- 对于线程B就是 下标值递减 和 出栈操作(它们要么一起完成,要么都不执行)
-
2. 对象的锁定标志
-
对象互斥锁阻止多个线程同时访问同一个条件变量
-
有两种方法可以实现 对象互斥锁
- 用关键字 volatile 来声明一个共享数据(变量)
- 用关键字 synchronization 来声明操作共享数据的一个方法或一段代码
-
设想一个场景
- 有一间实验室,实验人员都可以来用,但任何时候实验室只允许一组实验人员在里面做实验
- 否则就会引发混乱,为了进行控制,在门口设置了一把锁
- 实验室没人的时候锁是开放的,有人员进入之后第一件事就是将门锁上,然后开始工作
- 之后如果有人再想进入,会因为门已被锁只能等待,直到里面的实验人员完成工作后将锁打开才能进入
- 这种机制保证了在一组人员工作的过程中不会被另一组人员打断
- 保证了数据操作的完整性,在同一时刻只能有一个任务访问的代码区称为临界区
-
修改栈实例
-
public class Stack { int idx = 0; char data[] = new char[6]; public void push(char c) { synchronized (this) { // 增加同步标志 data[idx] = c; idx ++; } } public char pop() { synchronized (this) { // 增加同步标志 idx --; return data[idx]; } } }
- 当线程执行到被同步的语句时,将传递的对象参数设为锁定标志,禁止其他线程对该对象的访问
- 对pop()和 push()操作的部分增加了一个对 synchronization(this)的调用,在第一个线程拥有锁定标记时,如果另一个线程企图执行 synchronization(this)中的语句时
- 将从对象 this 中索取锁定标记,因为这个标记不可得,该线程不能继续执行
- 实际上将这个线程加入了一个等待队列,当标志被返回给对象时,等待标志的第一个线程得到该标志继续运行
- 当持有锁定标志的线程运行完 synchronization()调用包含的程序块之后
- 不管这个程序块是否发生了异常,或者循环中断跳出了该程序块,这个标志都将自动返还
- 如果同一个线程两次调用了同一个对象,在退出最外层后这个标志也会被正确释放
- 在退出内层时不会执行释放
-
-
synchronization关键字
-
参数必须是this,因此可以使用下面第二种简洁的写法:
-
public char pop() { synchronized (this) { } } public synchronized char pop() { }
-
把synchronization作为方法的修饰字,将整个方法都视为同步块
-
但是可能使持有锁定标记的时间比实际需要的时间要长,从而降低效率
-
-
五. 线程的同步
1. 同步问题的提出
- 生产者和消费者问题
- 有两个人各自代表一个线程,一个人在刷盘子,另一个人在烘干,它们之间有一个共享对象—盘架
- 显然盘架上有刷好的盘子时,烘干的人才能开始工作,如果刷盘子的太快,占满了盘架,就不能再继续了,得等盘架有空位置才行
- 以上例子说明一个问题
- 生产者生产一个产品后放入共享对象中,不去管共享对象是否有产品
- 消费者从共享对象中取产品,不检测是否已经取过
- 若共享对象中只能存放一个数据,可能会出现以下问题
- 生产者比消费者快时,消费者会漏掉一些数据
- 反之,消费者会取相同重复的数据
2. 解决方法
-
每个对象实例都有两个线程队列和它相连
- 第一个用来排列等待锁定标志的线程
- 第二个用来实现 wait()和 notify()交互机制
-
Java中可以使用Object类中的 wait()和 notify() / notifyAll()方法来协调线程间的运行速度关系
- wait()方法导致当前线程等待,让当前线程释放其所持有的 对象互斥锁 ,进入 wait队列(等待队列)
- notify() / notifyAll()方法的作用是唤醒一个或所有 正在等待队列中等待的线程,并将 它 移入同一个对象互斥锁 的队列
- notify()和 notifyAll()方法只能在被声明为 synchronization的方法 或 代码段中调用
-
再来看刷盘子的例子
-
线程 A 代表刷盘子,线程 B 代表烘干,都有对盘架 drainingBoard 的访问权,假设 线程B 想要进行烘干操作,此时盘架是空的,则应该如下表示
-
synchronized (drainingBoard) { if (drainingBoard.isEmpty()) { drainingBoard.wait(); } }
- **当 线程B 执行了 wait()操作后,不可再执行,并加入到对象 drainingBoard 的等待队列中,**在有线程将它从这个队列中释放之前,不能再次运行
-
-
烘干线程想要重新运行,则需要由 洗刷线程A 来通知它已经有工作可以做了,运行 drainingBoard 的 notify()方法可以做到
-
synchronized (drainingBoard) { drainingBoard.addItem(plate); if (!drainingBoard.isEmpty()) { drainingBoard.notify(); } }
-
-
-
线程执行被同步的语句时必须要拥有对象的锁定标志
-
不会出现wait()方法时阻塞
- 因为在执行 wait 调用时,首先将锁定标志返回给对象
- 即使一个线程由于执行 wait 调用被阻塞,也不会影响到其他等待锁定标志的线程的运行
-
为了避免打断程序的运行
-
当一个线程被 notify()之后,并不会立即变为可执行状态,仅仅是从等待队列中移入锁定标志队列中
-
在获得锁定标志之前,不能继续运行
执行,并加入到对象 drainingBoard 的等待队列中,**在有线程将它从这个队列中释放之前,不能再次运行 -
烘干线程想要重新运行,则需要由 洗刷线程A 来通知它已经有工作可以做了,运行 drainingBoard 的 notify()方法可以做到
-
synchronized (drainingBoard) { drainingBoard.addItem(plate); if (!drainingBoard.isEmpty()) { drainingBoard.notify(); } }
-
-
-
线程执行被同步的语句时必须要拥有对象的锁定标志
-
不会出现wait()方法时阻塞
- 因为在执行 wait 调用时,首先将锁定标志返回给对象
- 即使一个线程由于执行 wait 调用被阻塞,也不会影响到其他等待锁定标志的线程的运行
-
为了避免打断程序的运行
- 当一个线程被 notify()之后,并不会立即变为可执行状态,仅仅是从等待队列中移入锁定标志队列中
- 在获得锁定标志之前,不能继续运行
-
wait()既可以被 notify终止,也可以通过调用线程的 interrupt()方法终止