L11-多线程
程序 - 进程 - 线程
基本概念
程序(program):是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
进程(process):是程序的一次执行过程,或是正在运行的一个程序。动态过程:有它自身的产生、存在和消亡的过程。
- 如:运行中的QQ,运行中的MP3播放器
- 程序是静态的,进程是动态的
线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径。
- 若一个程序可同一时间执行多个线程,就是支持多线程的
进程与多线程
每个Java程序都有一个隐含的主线程: main 方法
何时需要多线程
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
- 需要一些后台运行的程序时。
使用多线程的优点
背景:只使用单个线程完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?
多线程程序的优点:
- 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
- 提高计算机系统CPU的利用率。
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
线程的分类
Java中的线程分为两类:一种是守护线程,一种是用户线程。
- 它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。
- 守护线程是用来服务用户线程的,通过在
start()
方法前调用thread.setDaemon(true)
可以把一个用户线程变成一个守护线程。 - Java垃圾回收就是一个典型的守护线程。
- 若JVM中都是守护线程,当前JVM将退出。
多线程的创建和使用
Thread类
概述
Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来实现。
Thread类的特性
- 每个线程都是通过某个特定Thread对象的
run()
方法来完成操作的,经常把run()
方法的主体称为线程体 - 通过该Thread对象的
start()
方法来调用这个线程
- 每个线程都是通过某个特定Thread对象的
Thread类构造方法
Thread()
:创建新的Thread对象Thread(String threadname)
:创建线程并指定线程实例名Thread(Runnable target)
:指定创建线程的目标对象,它实现了Runnable接口中的run方法Thread(Runnable target, String name)
:创建新的Thread对象
常用方法
void start()
:启动线程,并执行对象的run()
方法run()
:线程在被调度时执行的操作String getName()
:返回线程的名称void setName(String name)
:设置该线程名称static currentThread()
:返回当前线程static void yield()
:线程让步- 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
- 若队列中没有同优先级的线程,忽略此方法
join()
:当某个程序执行流中调用其他线程的join()
方法时,调用线程将被阻塞,直到join()
方法加入的 join 线程执行完为止- 低优先级的线程也可以获得执行
static void sleep(long millis)
:(指定时间:毫秒)- 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。
- 抛出InterruptedException异常
stop()
:强制线程生命期结束boolean isAlive()
:返回boolean,判断线程是否还活着
多线程示例
1 | public class TestThread { |
线程的调度
调度策略
- 时间片
- 抢占式:高优先级的线程抢占CPU
Java的调度方法
- 同优先级线程组成先进先出队列(先到先服务),使用时间片策略
- 对高优先级,使用优先调度的抢占式策略
线程的优先级
线程的优先级控制
- MAX_PRIORITY(10);
- MIN _PRIORITY(1);
- NORM_PRIORITY(5);
涉及的方法:
getPriority()
:返回线程优先值setPriority(int newPriority)
:改变线程的优先级- 线程创建时继承父线程的优先级
练习
创建两个子线程,让其中一个输出1-100之间的偶数,另一个输出1-100之间的奇数。
1 | public class TestThread2 { |
线程创建的两种方式
继承Thread类
- 定义子类继承Thread类。
- 子类中重写Thread类中的run方法。
- 创建Thread子类对象,即创建了线程对象。
- 调用线程对象start方法:启动线程,调用run方法。
实现Runnable接口
- 定义子类,实现Runnable接口。
- 子类中重写Runnable接口中的run方法。
- 通过Thread类含参构造器创建线程对象。
- 将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法中。
- 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
联系和区别
- 区别:
- 继承Thread: 线程代码存放Thread子类run方法中。
- 实现Runnable:线程代码存在接口的子类的run方法。
- 实现方法的好处:
- 避免了单继承的局限性
- 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
车站售票
继承的方式
1 | //模拟火车站售票,开启三个窗口售票,总票数为100张 |
实现的方式
1 | //模拟火车站售票,开启三个窗口售票,总票数为100张 |
注意:两种方式去实现,存在线程安全问题(会出现重票、错票)
线程的生命周期
概述
JDK中用Thread.State枚举表示了线程的几种状态。
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
- 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪:处于新建状态的线程被
start()
后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件 - 运行:当就绪的线程被调度并获得处理器资源时,便进入运行状态, run()方法定义了线程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止
线程状态转换
线程的同步
问题的提出
- 多个线程执行的不确定性引起执行结果的不稳定。
- 多个线程对账本的共享,会造成操作的不完整性,会破坏数据。
售票问题分析
- 理想状态
- 极端状态
1 | int ticket = 100; |
- 问题的原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
- 解决办法:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
Synchronized
Java对于多线程的安全问题提供了专业的解决方式:同步机制
在实现的方式中,使用同步Synchronized,可以用this作为锁,但是在继承的方式中,不能,只能在外部设置Object对象。
同步代码块
1 | synchronized (对象/同步监视器){ |
1 | class TicketSale implements Runnable{ |
同步方法
- synchronized 还可以放在方法声明中,表示整个方法为同步方法。
1 | public synchronized void show (String name){ |
1 | class TicketSale2 implements Runnable{ |
互斥锁
在Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。
- 每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
- 关键字 synchronized 来与对象的互斥锁联系。当某个对象用 synchronized 修饰时,表明该对象在任一时刻只能由一个线程访问。
- 同步的局限性:导致程序的执行效率要降低
- 同步方法(非静态的)的锁为this。
- 同步方法(静态的)的锁为当前类本身。
单例设计模式之懒汉式
1 | //关于懒汉式的单例模式的安全问题:使用同步机制 |
释放锁的操作
- 当前线程的同步方法、同步代码块执行结束
- 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
- 当前线程在同步代码块、同步方法中执行了线程对象的**
wait()
**方法,当前线程暂停,并释放锁。
不会释放锁的操作
- 线程执行同步代码块或同步方法时,程序调用
Thread.sleep()
、Thread.yield()
方法暂停当前线程的执行 - 线程执行同步代码块时,其他线程调用了该线程的
suspend()
方法将该线程挂起,该线程不会释放锁(同步监视器)。- 应尽量避免使用
suspend()
和resume()
来控制线程
- 应尽量避免使用
练习
银行有一个账户。有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。问题:该程序是否有安全问题,如果有,如何解决?
【提示】
- 明确哪些代码是多线程运行代码,须写入
run()
方法 - 明确什么是共享数据
- 明确多线程运行代码中哪些语句是操作共享数据的
1 | //银行账户 |
拓展问题:可否实现两个储户交替存钱的操作。需要使用线程通信!
线程死锁
死锁
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
解决方法
- 专门的算法、原则
- 尽量减少同步资源的定义
线程通信
wait()
与 notify()
和 notifyAll()
wait()
:令当前线程挂起并放弃CPU、同步资源,使别的线程可访问并修改共享资源,而当前线程排队等候再次对资源的访问notify()
:唤醒正在排队等待同步资源的线程中优先级最高者结束等待notifyAll()
:唤醒正在排队等待资源的所有线程结束等待
Java.lang.Object提供的这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常。
wait() 方法
- 在当前线程中调用方法: 对象名.wait()
- 使当前线程进入等待(某对象)状态 ,直到另一线程对该对象发出 notify (或notifyAll) 为止
- 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
- 调用此方法后,当前线程将释放对象监控权 ,然后进入等待
- 在当前线程被notify后,要重新获得监控权,然后从断点处继续代码的执行
notify()和notifyAll()
- 在当前线程中调用方法: 对象名.notify()
- 功能:唤醒等待该对象监控权的一个线程。
- 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
练习
使用两个线程打印 1-100,线程1, 线程2交替打印。
1 | /** |
生产者/消费者问题
- 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
- 这里可能出现两个问题:
- 生产者比消费者快时,消费者会漏掉一些数据没有取到。
- 消费者比生产者快时,消费者会取相同的数据。
1 | public class TestProduceConsume { |
面试题
- java中有几种方法可以实现一个线程?用什么关键字修饰同步方法? stop()和suspend()方法为何不推荐使用?
答:有两种实现方法,分别是继承Thread类与实现Runnable接口
用synchronized关键字修饰同步方法
反对使用stop(),是因为它不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。结果很难检查出真正的问题所在。suspend()方法容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被”挂起”的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用suspend(),而应在自己的Thread类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()重新启动线程。
- sleep() 和 wait() 有什么区别?
答:sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。
- 同步和异步有何异同,在什么情况下分别使用他们?举例说明。
答:如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个
线程写过了,那么这些数据就是共享数据,必须进行同步存取。
当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。
- 启动一个线程是用run()还是start()?
答:启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。这并不意味着线程就会立即运行。run()方法可以产生必须退出的标志来停止一个线程。
- 当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法?
答:不能,一个对象的一个synchronized方法只能由一个线程访问。
- 请说出你所知道的线程同步的方法。
答:wait():使一个线程处于等待状态,并且释放所持有的对象的lock。
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。
notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
notityAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。
- 多线程有几种实现方法,都是什么?同步有几种实现方法,都是什么?
答:多线程有两种实现方法,分别是继承Thread类与实现Runnable接口
同步的实现方面有两种,分别是synchronized,wait与notify
- 线程的基本概念、线程的基本状态以及状态之间的关系
答:线程指在程序执行过程中,能够执行程序代码的一个执行单位,每个程序至少都有一个线程,也就是程序本身。
Java中的线程有四种状态分别是:运行、就绪、挂起、结束
- 简述synchronized和java.util.concurrent.locks.Lock的异同 ?
答:主要相同点:Lock能完成synchronized所实现的所有功能
主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求
程序员手工释放,并且必须在finally从句中释放。
- 判断题
- C 和 Java 都是多线程语言。(错,C是单线程)
- 如果线程死亡,它便不能运行。(对)
- 在 Java 中,高优先级的可运行线程会抢占低优先级线程。(对)
- 程序开发者必须创建一个线程去管理内存的分配。(错,Java 提供了一个系统线程来管理内存的分配。)
- 一个线程在调用它的 start 方法,之前,该线程将一直处于出生期。(对)
- 当调用一个正在进行线程的 stop()方法时,该线程便会进入休眠状态。(错,应该是sleep()方法)
- 如果线程的 run 方法执行结束或抛出一个不能捕获的例外,线程便进入等待状态。(错,线程会死亡)
- 一个线程可以调用 yield 方法使其他线程有机会运行。(对,yield 方法总是让高优先级的就绪线程先运行)
- 选择题
- Java 语言中提供了一个▁D▁线程,自动回收动态分配的内存。
A 异步
B 消费者
C 守护
D 垃圾收集 - 当▁A▁方法终止时,能使线程进入死亡状态。
A run
B setPrority
C yield
D sleep - 用▁B▁方法可以改变线程的优先级。
A run
B setPrority
C yield
D sleep - 线程通过▁C▁方法可以使具有相同优先级线程获得处理器。
A run
B setPrority
C yield
D sleep - 线程通过▁D▁方法可以休眠一段时间,然后恢复运行。
A run
B setPrority
C yield
D sleep - ▁B▁方法使对象等待队列的第一个线程进入就绪状态。
A run
B notify
C yield
D sleep - 方法 resume( )负责重新开始▁D▁线程的执行。
A 被 stop( )方法停止
B 被 sleep( )方法停止
C 被 wait( )方法停止
D 被 suspend( )方法停止 - ▁BCD▁方法可以用来暂时停止当前线程的运行。
A stop( )
B sleep( )
C wait( )
D suspend()
- Java 语言中提供了一个▁D▁线程,自动回收动态分配的内存。
Java为什么要引入线程机制,线程、程序、进程之间的关系是怎样的。
答:线程可以彼此独立的执行,它是一种实现并发机制的有效手段,可以同时使用多个线程来完成不同的任务,并且一般用户在使用多线程时并不考虑底层处理的细节。
程序是一段静态的代码,是软件执行的蓝本。进程是程序的一次动态执行过程,即是处于运行过程中的程序。
线程是比进程更小的程序执行单位,一个进程可以启动多个线程同时运行,不同线程之间可以共享相同的内存区域和数据。多线程程序是运行时间后嗣可能出现在一个进程之内的、有一个以上线程同时运行的情况的程序。
Runnable接口包括哪些抽象方法?Thread类有哪些主要域和方法?
答:Runnable接口中仅有
run()
抽象方法。
Thread类主要域有:MAX_PRIORITY
,MIN_PRIORITY
,NORM_PRIORITY
。
主要方法有start()
,run()
,sleep()
,currentThread()
,setPriority()
,getPriority()
,join()
等。
- 创建线程有哪两种方式?试写出每种的具体的流程。比较两种创建方式的不同,哪个更优。
- 继承Thread类
- 定义类继承Thread类
- 覆盖Thread类中的run方法。
- 创建Thread子类对象,即创建了线程对象。
- 调用线程对象start方法:启动线程,调用run方法。
- 实现Runnable接口
- 定义类,实现Runnable接口。
- 覆盖Runnable接口中的run方法。
- 通过Thread类建立线程对象。
- 将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法中。
- 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
- 【区别】
- 继承Thread: 线程代码存放Thread子类run方法中
- 实现Runnable:线程代码存在接口的子类的run方法
- 【实现方法的好处】
- 避免了单继承的局限性
- 多个线程可以共享同一个接口子类的对象,非常适合多个相同线程来处理同一份资源。
- 继承Thread类