多线程编程
进程
一般可以在同一时间内执行多个程序的操作系统都 有进程的概念。一个进程就是一个执行中的程序, 而每一个进程都有自己独立的一块内存空间、一组 系统资源。在进程的概念中,每一个进程的内部数 据和状态都是完全独立的。
在Windows操作系统中一个进程就是一个exe或者dll 程序,它们相互独立,互相也可以通信,在 Android操作系统中进程间的通信应用也是很多 的。
线程
线程与进程相似,是一段完成某个特定功能的代 码,是程序中单个顺序控制的流程,但与进程不同 的是,同类的多个线程是共享一块内存空间和一组 系统资源。所以系统在各个线程之间切换时,开销 要比进程小的多,正因如此,线程被称为轻量级进程。一个进程中可以包含多个线程。
主线程
Java程序至少会有一个线程,这就是主线程,程序 启动后是由JVM创建主线程,程序结束时由JVM停 止主线程。主线程它负责管理子线程,即子线程的 启动、挂起、停止等等操作。下图所示是进程、 主线程和子线程的关系,其中主线程负责管理子线 程,即子线程的启动、挂起、停止等操作
[图] 进程、主线程和子线程关系
获取主线程示例代码如下:
// 获取主线程
ThreadmainThread=Thread.currentThread();
System.out.println("主线程名:"+mainThread.getName());
创建子线程
Java中创建一个子线程涉及到:java.lang.Thread类 和java.lang.Runnable接口。Thread是线程类,创建一个Thread对象就会产生一个新的线程。而线程执行的程序代码是在实现Runnable接口对象的run()方法中编写的,实现Runnable接口对象是线程执行对 象。线程执行对象实现Runnable接口的run()方法,run() 方法是线程执行的入口,该线程要执行程序代码都 在此编写的,run()方法称为线程体。
提示 主线程中执行入口是main(String[] args) 方法,这里可以控制程序的流程,管理其他的 子线程等。子线程执行入口是线程执行对象 (实现Runnable接口对象)的run()方法,在这个方法可以编写子线程相关处理代码。
实现Runnable接口
创建线程Thread对象时,可以将线程执行对象传递 给它,这需要是使用Thread类如下两个构造方法:
Thread(Runnable target, String name):target是线程执行对象,实现Runnable接口。name为线程 指定一个名字。
Thread(Runnable target):target是线程执行对 象,实现Runnable接口。线程名字是由JVM分配的。
下面看一个具体示例,实现Runnable接口的线程执 行对象Runner代码如下:
publicclassRunnerimplementsRunnable{
@Override
publicvoidrun() {
for(inti=0;i<10;i++) {
//打印次数和线程名称
System.out.printf("第%d次执行 - %s\n",i,Thread.currentThread().getName());
longsleepTime=(long) (1000*Math.random());
try{
//线程休眠
Thread.sleep(sleepTime);
}catch(InterruptedExceptione) {
e.printStackTrace();
}
}
//线程执行结束
System.out.println("执行完成! "+Thread.currentThread().getName());
}
}
代码Thread.sleep(sleepTime)是休 眠当前线程, sleep是静态方法它有两个版本:
static void sleep(long millis):在指定的毫秒数内 让当前正在执行的线程休眠。
static void sleep(long millis, int nanos) 在指定的毫 秒数加指定的纳秒数内让当前正在执行的线程 休眠。
测试程序代码如下:
publicclassRunnerTest{
publicstaticvoidmain(String[]args) {
//创建线程t1,参数时一个线程执行对象Runner
Threadt1=newThread(newRunner());
//开始线程t1
t1.start();
//创建线程t2,参数时一个线程执行对象Runner
Threadt2=newThread(newRunner(),"这里指定线程名称");
//开始线程t2
t2.start();
}
}
提示 仔细分析一下运行结果,会发现两个线 程是交错运行的,感觉就像是两个线程在同时 运行。但是实际上一台PC通常就只有一颗 CPU,在某个时刻只能是一个线程在运行,而 Java语言在设计时就充分考虑到线程的并发调 度执行。对于程序员来说,在编程时要注意给 每个线程执行的时间和机会,主要是通过让线 程休眠的办法(调用sleep()方法)来让当前线 程暂停执行,然后由其他线程来争夺执行的机
会。如果上面的程序中没有用到sleep()方法, 则就是第一个线程先执行完毕,然后第二个线 程再执行完毕。所以用活sleep()方法是多线程 编程的关键。
继承Thread线程类
Thread类也实现了Runnable接口,所以 Thread类也可以作为线程执行对象,这需要继承 Thread类,覆盖run()方法。
采用继承Thread类重新实现Runner示例,自定义 线程类MyThread代码如下:
publicclassMyThreadextendsThread{
publicMyThread() {
super();
}
publicMyThread(Stringname) {
super(name);
}
//程序执行代码
@Override
publicvoidrun() {
for(inti=0;i<10;i++) {
//打印次数和线程名称
System.out.printf("第%d次执行 - %s\n",i,Thread.currentThread().getName());
longsleepTime=(long) (1000*Math.random());
try{
//线程休眠
Thread.sleep(sleepTime);
}catch(InterruptedExceptione) {
e.printStackTrace();
}
}
//线程执行结束
System.out.println("执行完成! "+getName());
}
}
通 过super调用父类Thread构造方法,这两个Thread类 构造方法:
Thread(String name):name为线程指定一个名 字。代码第④行调用都就是此构造方法。
Thread():线程名字是JVM分配的。代码第②行 调用都就是此构造方法。
测试程序代码如下:
//通过继承Thread线程类来创建线程
//创建线程t1,参数时一个线程执行对象Runner
Threadt3=newMyThread();
//开始线程t1
t3.start();
//创建线程t2,参数时一个线程执行对象Runner
Threadt4=newMyThread("这里指定线程名称");
//开始线程t4
t4.start();
提示 由于Java只支持单重继承,继承Thread 类的方式不能再继承其他父类。当开发一些图 形界面的应用时,需要一个类既是一个窗口 (继承JFrame)又是一个线程体,那么只能采 用实现Runnable接口方式。
使用匿名内部类和Lambda表达式实现线程体
如果线程体使用的地方不是很多,可以不用单独定 义一个类。可以使用匿名内部类或Lambda表达式直 接实现Runnable接口。Runnable中只有一个方法是 函数式接口,可以使用Lambda表达式。
代码如下:
//使用匿名内部类和Lambda表达式实现线 程体
//创建线程t5,参数是实现Runnable接口的匿名内部类
Threadt5=newThread(newRunnable() {
@Override
publicvoidrun() {
for(inti=0;i<10;i++) {
//打印次数和线程名称
System.out.printf("第%d次执行 - %s\n",i,Thread.currentThread().getName());
longsleepTime=(long) (1000*Math.random());
try{
//线程休眠
Thread.sleep(sleepTime);
}catch(InterruptedExceptione) {
e.printStackTrace();
}
}
//线程执行结束
System.out.println("执行完成! "+Thread.currentThread().getName());
}
});
// 开始线程t5
t5.start();
//创建线程t6,参数是,参数是实现Runnable接口的Lambda表达式
Threadt6=newThread(()->{
for(inti=0;i<10;i++) {
//打印次数和线程名称
System.out.printf("第%d次执行 - %s\n",i,Thread.currentThread().getName());
longsleepTime=(long) (1000*Math.random());
try{
//线程休眠
Thread.sleep(sleepTime);
}catch(InterruptedExceptione) {
e.printStackTrace();
}
}
//线程执行结束
System.out.println("执行完成! "+Thread.currentThread().getName());
},"Lambda表达式");
// 开始线程t6
t6.start();
上述代码第①行采用匿名内部类实现Runnable接 口,覆盖run()方法。这里使用的是Thread(Runnable target)构造方法。代码第②行采用Lambda表达式实
现Runnable接口,覆盖run()方法。这里使用的是 Thread(Runnable target, String name)构造方法, Lambda表达式是它的第一个参数。匿名内部类和 Lambda表达式代码虽然很多,但是它只是一个参 数,实现了Runnable接口线程执行对象,
提示 匿名内部类和Lambda表达式不需要定 义一个线程类文件,使用起来很方便。特别是 Lambda表达式使代码变得非常简洁。但是客 观上匿名内部类和Lambda表达式会使代码可 读性变差,对于初学者不容易理解。
线程的状态
在线程的生命周期中,线程会有几种状态,如图 23-5所示,线程有5种状态。下面分别介绍一下。
新建状态
新建状态(New)是通过new等方式创建线程对 象,它仅仅是一个空的线程对象
就绪状态
当主线程调用新建线程的start()方法后,它就进 入就绪状态(Runnable)。此时的线程尚未真正 开始执行run()方法,它必须等待CPU的调度。
运行状态
CPU的调度就绪状态的线程,线程进入运行状 态(Running),处于运行状态的线程独占 CPU,执行run()方法。
阻塞状态因为某种原因运行状态的线程会进入不可运行 状态,即阻塞状态(Blocked),处于阻塞状态的线程JVM系统不能执行该线程,即使CPU空 闲,也不能执行该线程。如下几个原因会导致 线程进入阻塞状态:
当前线程调用sleep()方法,进入休眠状态。
被其他线程调用了join()方法,等待其他线程 结束。
发出I/O请求,等待I/O操作完成期间。
当前线程调用wait()方法。
处于阻塞状态可以重新回到就绪状态,如:休 眠结束、其他线程加入、I/O操作完成和调用 notify或notifyAll唤醒wait线程。
死亡状态
线程退出run()方法后,就会进入死亡状态 (Dead),线程进入死亡状态有可以是正常实 现完成run()方法进入,也可能是由于发生异常 而进入的。
线程管理
线程优先级
线程的调度程序根据线程决定每次线程应当何时运 行,Java提供了10种优先级,分别用1~10整数表 示,最高优先级是10用常量MAX_PRIORITY表 示;最低优先级是1用常量MIN_PRIORITY;默认 优先级是5用常量NORM_PRIORITY表示。
Thread类提供了setPriority(int newPriority)方法可以 设置线程优先级,通过getPriority()方法获得线程优 先级。
设置线程优先级示例代码如下:
//通过实现Runnable接口来创建线程
//创建线程t1,参数时一个线程执行对象Runner
Threadt1=newThread(newRunner());
//设置线程优先级10
t1.setPriority(Thread.MAX_PRIORITY);
//开始线程t1
t1.start();
//创建线程t2,参数时一个线程执行对象Runner
Threadt2=newThread(newRunner(),"这里指定线程名称");
//设置线程优先级1
t1.setPriority(Thread.MIN_PRIORITY);
//开始线程t2
t2.start();
提示 多次运行上面的示例会发现,t1线程经 常先运行,但是偶尔t2线程也会先运行。这些 现象说明了:影响线程获得CPU时间的因素, 除了受到的线程优先级外,还与操作系统有 关。
等待线程结束
在介绍现在状态时提到过join()方法,当前线程调用 t1线程的join()方法,则阻塞当前线程,等待t1线程 结束,如果t1线程结束或等待超时,则当前线程回到就绪状态。
Thread类提供了多个版本的join(),它们定义如下:
void join():等待该线程结束。
void join(long millis):等待该线程结束的时间最 长为millis毫秒。如果超时为0意味着要一直等下 去。
void join(long millis, int nanos):等待该线程结束 的时间最长为millis毫秒加nanos纳秒。
使用join()方法示例代码如下:
publicclassJoin{
staticintvalue=0;
publicstaticvoidmain(String[]args)throwsInterruptedException{
System.out.println("主线程开始");
//创建线程t,参数是一个线程执行对象Runner
Threadt1=newThread(newRunnable() {
@Override
publicvoidrun() {
System.out.println("子开始");
for(inti=0;i<3;i++) {
System.out.println("子执行");
value++;
}
System.out.println("子结束");
}
},"ThreadA");
//开始t线程
t1.start();
//主线程被阻塞,等待t1线程结束
t1.join();
System.out.println("value = "+value);
System.out.println("主线程结束");
}
}
结果:
提示 使用join()方法的场景是,一个线程依赖 于另外一个线程的运行结果,所以调用另一个 线程的join()方法等它运行完成。
线程让步
线程类Thread还提供一个静态方法yield(),调用 yield()方法能够使当前线程给其他线程让步。它类 似于sleep()方法,能够使运行状态的线程放弃CPU 使用权,暂停片刻,然后重新回到就绪状态。与 sleep()方法不同的是,sleep()方法是线程进行休眠,能够给其他线程运行的机会,无论线程优先级 高低都有机会运行。而yield()方法只给相同优先级 或更高优先级线程机会。
示例代码如下:
Thread.yield();
提示 yield()方法只能给相同优先级或更高优 先级的线程让步,yield()方法在实际开发中很
少使用,大多都使用sleep()方法,sleep()方法 可以控制时间,而yield()方法不能。
线程停止
线程体中的run()方法结束,线程进入死亡状态,线 程就停止了。但是有些业务比较复杂,例如想开发 一个下载程序,每隔一段执行一次下载任务,下载 任务一般会在由子线程执行的,休眠一段时间再执 行。这个下载子线程中会有一个死循环,但是为了 能够停止子线程,设置一个结束变量。
示例下面如下:
publicclassStopThread{
privatestaticStringcommand="";
publicstaticvoidmain(String[]args) {
//创建线程t1,参数是一个线程执行对象Runner
Threadt1=newThread(newRunnable() {
@Override
publicvoidrun() {
// 一直循环,直到满足条件在停止线程
while(!"exit".equalsIgnoreCase(command)) {
// 线程开始工作
System.out.println("下载中...");
try{
//线程休眠
Thread.sleep(1000);
}catch(InterruptedExceptione) {
e.printStackTrace();
}
}
//线程结束
System.out.println("执行完成");
}
});
//开始线程t1
t1.start();
//输入
//BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
try(BufferedReaderbr=newBufferedReader(newInputStreamReader(System.in))){
command=br.readLine();
}catch(IOExceptione) {
e.printStackTrace();
}
}
}
运行结果:
提示 控制线程的停止有人会想到使用Thread 提供的stop()方法,这个方法已经不推荐使用 了,这个方法有时会引发严重的系统故障,类似还是有suspend()和resume()挂起方法。Java现在推荐的做法就是采用本例的结束变量方式。
线程安全
多一个线程同时运行,有时线程之间需要共享数 据,一个线程需要其他线程的数据,否则就不能保 证程序运行结果的正确性。
例如有一个航空公司的机票销售,每一天机票数量 是有限的,很多售票点同时销售这些机票。下面是 一个模拟销售机票系统,示例代码如下
publicclassTicketDB{
//机票数量
privateintticketCount=5;
//获取当前机票数量
publicintgetTicketCount(){
returnticketCount;
}
//销售机票
publicvoidsellTicket(){
//等待用户付款
//线程休眠
try{
Thread.sleep(1000);
}catch(InterruptedExceptione) {
e.printStackTrace();
}
System.out.printf("第%d号票已经售出\n",ticketCount);
ticketCount--;
}
调用代码如下:
publicclassTicketTest{
publicstaticvoidmain(String[]args) {
Runnablerun=newTicket();
// 创建线程t1
Threadt1=newThread(run);
t1.start();
Threadt2=newThread(run);
t2.start();
}
staticclassTicketimplementsRunnable{
TicketDBdb=newTicketDB();
@Override
publicvoidrun() {
while(true){
intcurrentTC=db.getTicketCount();
//查询是否有票
if(currentTC>0) {
db.sellTicket();
}else{
//无票退出
break;
}
}
}
}
}
一次运行结果如下:
虽然可以能每次运行的结果都不一样,但是从结果 看还是能发现一些问题:同一张票重复销售、出现 第0号票和5张票卖了6次。这些问题的根本原因是 多个线程间共享的数据导致数据的不一致性。
提示 多个线程间共享的数据称为共享资源或 临界资源,由于是CPU负责线程的调度,程序
员无法精确控制多线程的交替顺序。这种情况 下,多线程对临界资源的访问有时会导致数据 的不一致性。
多线程同步
为了防止多线程对临界资源的访问有时会导致数据 的不一致性,Java提供了“互斥”机制,可以为这些 资源对象加上一把“互斥锁”,在任一时刻只能由一 个线程访问,即使该线程出现阻塞,该对象的被锁 定状态也不会解除,其他线程仍不能访问该对象, 这就多线程同步。线程同步保证线程安全的重要手 段,但是线程同步客观上会导致性能下降。可以通过两种方式实现线程同步,两种方式都涉及 到使用synchronized关键字,一种是synchronized方 法,使用synchronized关键字修饰方法,对方法进 行同步;另一种是synchronized语句,使用 synchronized关键字放在对象前面限制一段代码的 执行。
synchronized方法
synchronized关键字修饰方法实现线程同步,方 法所在的对象被锁定,修改售票系统示 例, TicketDB.java文件代码如下:
publicclassTicketDB{
//机票数量
privateintticketCount=5;
//获取当前机票数量
publicsynchronizedintgetTicketCount(){
returnticketCount;
}
//销售机票
publicsynchronizedvoidsellTicket(){
//等待用户付款
//线程休眠
try{
Thread.sleep(1000);
}catch(InterruptedExceptione) {
e.printStackTrace();
}
System.out.printf("第%d号票已经售出\n",ticketCount);
ticketCount--;
}
}
上述代码第①行和第②行的方法前都使用了 synchronized关键字,表明这两个方法是同步 的,被锁定的,每一个时刻只能由一个线程访 问。并不是每一个方法都有必要加锁的,要仔 细研究加上的必要性,上述代码第①行加锁可
以防止出现第0号票情况和5张票卖出6次的情况;代码第②行加锁是防止出现销售两种一样 的票
synchronized语句
synchronized语句方式主要用于第三方类,不方 便修改它的代码情况。同样是售票系统示例,可以不用修改TicketDB.java类,只修改调用代码TicketTest.java实现同步。
代码如下:
publicclassTicketTest{
publicstaticvoidmain(String[]args) {
// Runnable run = new Ticket();
// new Thread(run).start();
// new Thread(run).start();
// 创建线程t1
Runnablerun=newTicket();
Threadt1=newThread(run);
t1.start();
Threadt2=newThread(run);
t2.start();
}
staticclassTicketimplementsRunnable{
TicketDBdb=newTicketDB();
@Override
publicvoidrun() {
while(true){
//同步代码语句
synchronized(db) {
intcurrentTC=db.getTicketCount();
//查询是否有票
if(currentTC>0) {
db.sellTicket();
}else{
//无票退出
break;
}
}
}
}
}
}
将需要同步的代码用大括号括起来。 synchronized后有小括号,将需要同步的对象括 起来。
线程间通信
上面的示例只是简单地为特定对象或方法加 锁,但有时情况会更加复杂,如果两个线程之间有 依赖关系,线程之间必须进行通信,互相协调才能 完成工作。例如有一个经典的堆栈问题,一个线程生成了一些 数据,将数据压栈;另一个线程消费了这些数据, 将数据出栈。这两个线程互相依赖,当堆栈为空 时,消费线程无法取出数据时,应该通知生成线程 添加数据;当堆栈已满时,生产线程无法添加数据 时,应该通知消费线程取出数据。为了实现线程间通信,需要使用Object类中声明的5 个方法:
void wait():使当前线程释放对象锁,然后当前 线程处于对象等待队列中阻塞状态,如下图所 示,等待其他线程唤醒。
void wait(long timeout):同wait()方法,等待 timeout毫秒时间。
void wait(long timeout, int nanos):同wait()方法,等待timeout毫秒加nanos纳秒时间。
void notify():当前线程唤醒此对象等待队列中 的一个线程,如下图所示该线程将进入就绪状 态。
void notifyAll():当前线程唤醒此对象等待队列 中的所有线程,如下图所示这些线程将进入就 绪状态