内容导航
多线程可以使程序在同一时间内执行多个操作,采用Java中的多线程机制可以使计算机资源得到更充分的利用,多线程技术在网络编程中有广泛的应用。本章将介绍线程的生命周期,实现多线程的方法,线程的调度,线程同步等技术。
多线程概述
在计算机领域里,这种同时执行多个操作的行为模式被称为并发。在操作系统中同时运行着多个独立的任务,每个任务对应一个进程,每个进程可产生多个线程。通过这种并发运行的方式,计算机的性能被挖掘到了极限。
进程与线程
进程是程序的一次动态执行过程,它是从代码加载,执行中到执行完毕的一个完整过程,也是进程本身从产生,发展到最终消亡的过程。操作系统同时管理一个计算机系统中的多个进程,让计算机系统中的多个进程轮流使用中央处理器(Central Processing Unit)资源,或者共享操作系统的其他资源。由于CPU执行速度非常块,所有程序好像是在”同时“运行一样。
在操作系统中可以有多个进程,这些进程包括系统进程(由操作系统内部建立的进程)和用户进程(由用户程序建立的进程)。可以从Windows任务管理器中查看已启动的进程,进程是系统运行程序的最小单元。各进程之间是独立的,每个进程的内部数据和状态也是完全独立的。
线程是进程中执行运算的最小单位,是在进程基础上的进一步划分,一个线程可以完成一个独立的顺序控制流程。
与进程不同,同一进程内的多个线程共享同一块内存空间(包括代码空间,数据空间)和一块系统资源,所以系统在产生一个线程或各线程之间切换工作时,其负担要比在进程间切换下得多。
应用程序有单进程单线程的,有多进程但每个进程只有一个线程的,有单进程包含多线程的,还有多进程且每个进程有多个线程等四种情况。
如果在同一进程中同时有多个线程,用于执行不同的工作,则称之为”多线程“。这些线程可以同时存在,同时执行。
多线程的运行机制
以往开发的程序大多是单线程的,即一个程序只有从开始到结束这一条执行路径。而多线程是指一个进程同时存在几条执行路径且并发执行的工作方式。
并发运行与并行运行不同。并行运行通常表示同一个时刻有多条指令代码在处理器上同时运行,这种情况往往需要多个处理器支持。而并发运行表示在一个处理器中,操作系统为了提高程序的运行效率将 CPU 的执行时间分成多个时间片,分配给同一进程的不同线程。当执行完一个时间片后,当前运行的线程就可能交付出 CPU 权限,让其他线程执行下一个时间片,当然 CPU 也有可能将相邻的时间片分配给同一线程,即多个线程分享 CPU 时间,交替运行。之所以从表面上看是多个线程同时运行的,是因为不同线程之间切换的时间非常短,也许仅仅是几毫秒,对普通人来说是难以感知的,即所谓的“宏观并行,微观串行”。
小结:由于进程和线程含义不同,所以运行多个进程和多个线程是不同的。运行多个进程指在同一段时间内,可以同时运行一个以上的程序,即多个任务同时执行。例如,一边浏览网页,一边听音乐。而运行多个线程指在同一个程序内同时运行一个以上的子程序,如使用网盘同时下载多个文件。
多线程的优势
充分利用CPU的资源。运行单线程程序时,若程序发生阻塞,则 CPU 可能会处于空闲状态这将造成计算机资源浪费。而使用多线程可以在某个线程处于休眠或阻塞状态时运行其他线程,这样将大大提高资源利用率。
简化编程模型。可以考虑将一个既长又复杂的进程分为多个线程,成为几个独立执行的模块,如使用时、分、秒来描述当前时间。如果是单线程程序,则需要多重判断。如果使用多线程,时、分秒各使用一个线程控制,每个线程仅需实现简单的流程,简化了程序逻辑,更方便编码和维护。
良好的用户体验。由于多个线程可以交替运行,减少或避免了程序阳寨或意外情况造成的响应过慢现象,减少了用户等待时间,提升了用户体验。
多线程技术在实际开发中是非常有价值的。例如,Word 文本编辑工具需提供一边编辑一边保存的功能,且同时进行规范化格式和标记错别字检查,一个浏览器必须能同时下载多个图片,一个 Web 服务器必须能同时响应多个用户请求:JVM 本身就在后台提供了一个超级线程进行垃圾收....总之,多线程在实际编程中的应用是非常广泛的。
多线程编程
Java语言提供了java.lang.Thread类支持多线程编程,下面介绍Thread类的用法。
Thread类介绍
Thread类提供了大量的方法类控制和操作线程:
Thread类的常用方法
方法 | 描述 | 类型 |
Thread() | 创建Thread对象 | 构造方法 |
Thread(Runnable target) | 创建Thread对象,target为run()方法被调用的对象 | 构造方法 |
Thread(Runnable target,String name) | 创建Thread对象,target为run()方法被调用的对象,name为新线程的名称 | 构造方法 |
void run() | 执行任务操作的方法 | 实例方法 |
void start() | 使该线程开始运行,JVM将调用该线程的run()方法 | 实例方法 |
void sleep(long millis) | 在指定的毫秒数内让当前正在运行的线程休眠(暂停运行) | 静态方法 |
Thread currentThread() | 返回当前线程对象的引用 | 静态方法 |
Thread类的静态方法currentThread0返回当前线程对象的引用。在Java程序启动时个线程立即随之启动,这个线程通常被称为程序的主线程。publicstatic void main0方法是主线程的入口,每个进程至少有一个主线程。它的重要性如下.
主线程是产生其他子线程的线程
主线程通常必须最后完成运行,因为它执行各种关闭动作。
尽管主线程是自动创建的,但是可以由一个 Thrcad 对象控制。因此,可以使用Thread 的方法获取主线程信息。
继承Thread类创建线程类
继承 Thread类是实现线程的一种方式。在使用此方法自定义线程类时,必须在格式上满足如下要求
此类必须继承Thread 类。
将线程执行的代码写在run0方法中。
线程从它的 run0方法开始执行,即run0方法是线程执行的起点,就像 main0方法是应用程序的起点一样。因为run0方法定义在 Thread 类中,所以在自定义线程类中必须重写run0方法,为线程提供实现具体任务的代码。使用Thread 类创建并启动线程的代码结构如下。
//继承Thread类的方式创建自定义线程类
public class MyThread extends Thread{
//省略成员变量和成员方法代码
//重写Thread类中的run()方法
public void run(){
//线程执行任务的代码
}
}
//启动线程的测试类
class ThreadTest{
public static void main(String[] args){
//使用start()方法启动线程
MyThread myThread=new MyThread();
myThread.start();
}
}
注意:
已启动的线程对象不能重复调用start()方法,否则会抛出IllegalThreadStateException异常。
sleep()方法用来控制线程的休眠时间。如果这个线程被其他线程中断,则会产生InterruptedException异常。所以sleep()方法必须进行异常处理。
可以想象得到:两个线程对象调用start()方法启动后,每个线程都会独立完成各自的线程操作,相互之间没有影响,并行运行。这是由于CPU在一个时间片内只能运行一个线程,所以多线程交替运行。而每次分配的时间片长度可能不完全一致,因此每次运行的时长有所不同,这是由CPU资源调度决定的。
既然线程对象执行start()方法启动线程会调用run()方法,那么它和直接执行run()方法有什么区别呢?线程对象执行start()方法表示启动线程,等待操作系统分配资源执行run()方法中的代码,在多个线程同时处于启动状态时,它们是交替运行的。而直接调用run()方法和之前调用实列方法无区别,程序按照顺序运行,属于单线程运行模式。
实现Runnable接口创建线程类
在使用继承Thread类的方式创建线程的过程中,子类无法再继承其他父类。这是因为Java语言不支持多重继承。在这种情况下,可以通过实现Runnable接口的方式创建线程。这种方式更具有灵活性,用户线程还可以通过继承,再具有其他类的特性,这是开发中经常使用的方式。
Runnable接口位于java.lang包中,其中只提供一个抽象方法run()的声明,Thread类也实现了Runnable接口。使用Runnable接口时离不开Thread类,这是因为它要用到Thread类中的start()方法。在Runnable接口中只有run()方法,其他操作都要借助于Thread类。使用Runnable接口创建线程的一般格式如下。
//实现Runnable接口方式创建线程类
class MyThread implements Runnable{
public void run(){
//这里写线程的内容
}
}
//测试类
public class RunnableTest{
public static void main(){
//通过Thread类创建线程对象
MyThread myThread=new MyThread();
Thread thread=new Thread(myThread);
thread.start();
}
}
线程的状态转换
Java语言使用Thread类及其子类的对象表示线程,新建的线程通常会有五种状态中转换,即新建(New),就绪(Runnable),运行(Running),阻塞(Blocked)和死亡(Dead)。这五种状态组成了线程的生命周期。
![](https://img-blog.csdnimg.cn/img_convert/b53cb90d2715439509448d927025d4c7.png)
新建状态。一个 Thrcad 类或其子类的对象被声明并创建,但其在还未启动的这段时间里处于一种特殊的新建状态中。此时,线程类对象已经被分配了内存空间和资源,并且已被初始化,但是该线程尚未被调度。
就绪状态。就绪状态也被称为可运行状态,处于新建状态的线程被启动后,也就是调用 start0)方法后,新建线程将进入线程队列,排队等待 CPU 时间片。此时它已具备运行的条件,一旦轮到它使用CPU 资源,它就可以脱离创建它的主线程独立开始它的生命周期。这就好比一批作物种子被运输到农场,处于等待果农撒种的状态。
运行状态。当就绪状态的进程被调度并获得处理器资源后便进入运行状态,该状态表示线程正在运行,该线程已经拥有了 CPU 的占用权。这就好比作物种子被农夫种植到土地里,在阳光的照耀和雨水的浇灌下生根、发芽、苗壮成长。
当调用start0方法后,线程获取资源运行run0方法中的代码,线程进入运行状态。每个Thread类及其子类都拥有一个线程操作方法run0,当线程类对象被调度运行时,它将自动调用此 run0方法,从该Tun0方法的第一条语句开始执行,一直到执行完毕。处于运行状态的线程在以下四种情况下会让出CPU的控制权。
线程运行完毕
有比当前线程优先级更高的线程抢占了CPU。
线程休眠。
线程因等待某个资源而处于阻塞状态。
阻塞状态。一个正在运行的线程在某些特殊情况下需要让出 CPU 并暂时中止运行。这时,线程处于不可运行的状态被称为阻塞状态。这就好比作物在生长过程中由于干旱少雨或遭遇虫害停滞生长。
一个线程当被阻塞时不能进入就绪状态的排队队列,只有当阻塞的原因被取消时,线程才可以转为就绪状态。当一个线程执行此方法时,它就会放弃CPU使用权,转为阻塞状态
处于阻塞状态的线程通常需要在某些时刻被唤醒,至于用什么事件唤醒该线程,取决于其阻塞的原处于休眠状态的线程必须被阻寨一段固定的时间,这段时间结束后即转为就绪状态因。
死亡状态。一个线程的 run0方法运行完毕,表明该线程已死亡。处于死亡状态的线程不具有继续运行的能力。导致线程死亡的原因有两个:一是正常运行的线程完成了它的全部工作,即运行完run0)方法的最后一条语句并退出:二是当进程停止运行时,该进程中的所有线程将被强行中止。线程处于死亡状态并且没有该线程的引用时,JVM会从内存中删除该线程类对象。
线程调度相关方法
常用的线程操作方法
方法 | 描述 |
int getPriority() | 返回线程的优先级 |
void setPrority(int newPriority) | 更改线程的优先级 |
boolean isAlive() | 测试线程是否处于活动状态 |
void join() | 进程中的其他线程必须等待该线程终止后才能运行 |
void interrupt() | 中断线程 |
void yield() | 暂停当前正在执行的线程类对象并运行其他线程 |
线程的优先级
每个线程运行时都具有一定的优先级,优先级高的线程获得较多的运行机会,而优先级低的线程获得较少的运行机会。每个线程的默认优先级都与创建它的线程的优先级相同,在默认情况下,主线程main 具有普通优先级,由主线程创建的子线程也具有普通优先级。
Thread类提供了setPriority(int newPriority)方法、getPriority0方法用于设置和返回指定线程的优先级其中setPriority(intnewPriority)方法的参数nwPriority可以是一个整数范围是110也可以使用Thread类的如下三个静态常量设置线程的优先级。
MAX PRIORITY:其值是10,表示优先级最高
MIN PRIORITY:其值是1,表示优先级最低。
NORM PRIORITY:其值是5,表示普通优先级
提示:尽管为线程设定了不同的优先级,但实际上,程序还是不能精确控制这些线程的运行先后顺序。在不同的计算机或同一计算机不同时刻中运行本程序,都会得到不同的运行序列。
线程的强制运行
在线程操作中,可以使用 join0方法让一个线程强制运行。在线程强制运行期间,其他线程无法运行,必须等待此线程完成之后才可以继续运行。它有三个重载方法,定义如下。
public final void join()
public final void join(long mills)
public final void join(long mills,int nanos)
线程的礼让
当一个线程在运行中执行了Thread类的yield()静态方法后,如果此时还有相同或更高优先级的其他线程处于就绪状态,系统将会选择其他相同或更高优先级的线程运行,如果不存在这样的线程,则 该线程继续运行。yield()方法定义如下。
public static void yield()
注意:使用yield()方法实现线程礼让只是提供一种可能,不能保证一定会实现礼让,因为礼让的线程处于就绪状态时,还有可能被线程调度程序再次选中。
sleep()方法和yield()方法都是Thread类的静态方法,都会使当前处于运行状态的线程放弃CPU使用权,将运行机会让给其他线程,两者的区别如下。
sleep()方法会给其他线程运行机会,不考虑其他线程的优先级,因此较低优先级线程可能会获得运行机会。
yield()方法只会将运行机会让给相同优先级或更高优先级的线程。
调用sleep()方法需要处理InterruptedException异常,而调用yield()方法无此要求。
线程同步
当多个线程共享数据时,由于CPU负责线程的调度,所以程序无法精确地控制多线程的交替次序。如果没有特殊控制,则多线程对共享数据的修改和访问将导致数据的不一致。
为什么需要线程同步
前面学习的线程都是独立且异步运行的,也就是说每个线程都包含了运行时所需要的数据或方法不必关心其他线程的状态和行为。但是经常会有一些同时运行的线程需要操作共同数据,此时就要考虑其他线程的状态和行为,否则,不能保证程序运行结果的正确性。
实现线程的同步
当两个或多个线程需要访问同一资源时,需要以某种顺序来确保该资源某一时刻只能被一个线程使用,这被称为线程同步。线程同步相当于为线程中需要一次性完成不允许中断的操作加上一把锁,从而解决冲突。
加锁的过程使用线程同步实现,有同步代码和同步方法两种方式,这两种方式都使用到synchronized关键字。
同步代码块
代码块即使用“{}”括起来的一段代码,使用synchronized关键字修饰的代码块被称为同步代码块。
语法:
synchronized(obj){
//需要同步的代码
}
如果一个代码块带有sychronized(obj)标记,那么当线程执行到此代码块时,必须先获得obj变量所引用的对象的锁,其可以针对任何代码块,并且可以任意指定上锁的对象,因此灵活性更高。
同步方法
如果一个方法的所有代码都属于需同步的代码,那么这个方法定义处可以直接使用synchronized关键字修饰,即同步方法。其语法如下。
访问修饰符 synchronized 返回类型 方法名(参数列表){//省略方法体……}
或
synchronized 访问修饰符 返回类型 方法名(参数列表){//省略方法体……}
线程同步的特征
所谓线程之间保持同步,是指不同的线程在执行以同一个对象作为锁标记的同步代码块或同步方法时,因为要获得这个对象的锁而相互牵制,线程同步具有以下特征。
(1)当多个并发线程访问同一对象的同步代码块或同步方法时,同一时刻只能有一个线程运行,其他线程必须等待当前线程运行完毕后才能运行。
(2)如果多个线程访问的不是同一共享资源,则无需同步。
(3)当一个线程访问 Object 对象的同步代码块或同步方法时,其他线程仍可以访问该 Object 对象的非同步代码块及非同步方法。
综上所述,synchronized 关键字就是为当前的代码块声明一把锁,获得这把锁的线程可以执行代码块里的指令,其他的线程只能等待获取锁,然后才能执行相同的操作。
以上学习了使用同步方法和同步代码块实现线程同步,这两者从实现结果上看没有区别,只是同步方法便于阅读理解,而同步代码块可以更精确地限制访问区域,这样会更高效。
线程安全的类型
若程序所在的进程中有多个线程,而当这些线程同时运行时,每次的运行结果和单线程时的运行结果是一样的,而且其他变量的值也和预期相同,那么当前程序就是线程安全的。
一个类在被多线程访问时,不管运行时对这些线程有怎样的时序安排,它必须是以固定的、一致的顺序执行,这样的类型被称为线程安全的类型
1.对比Hashtable 和HashMap
1)是否线程安全
Hashtable 是线程安全的,其方法是同步的,可查看 Hashtable 类型源码中操作数据的方法为同步方法
而HashMap 中的方法在默认情况下是非同步的。在多线程并发的环境下,可以直接使用 Hashtable.如果使用HashMap,就要自行增加同步处理
2)效率比较
由于Hashtable 是线程安全的,其方法是同步的,而 HashMap 是非线程安全的,重速度,轻安全所以当只需单线程时,使用HashMap 的执行速度要高过Hashtable。
2.对比 StringBuffer 和 StringBuilder
StringBuffer 和 StringBuilder 都可用来存储字符串变量,是可变的对象。它们的区别是 StringBufer是线程安全的,而 StringBuilder 是非线程安全的。因此,在单线程环境下 StringBuilder 执行效率更高。
本章总结
多线程允许程序员编写出可最大程度利用CPU的高效程序
在Java 程序启动时,一个线程立刻运行,该线程通常被称为程序的主线程。主线程是产生其他子线程的线程。
通常,主线程必须最后完成运行,因为它执行各种关闭动作。
可通过两种方式创建线程。
声明一个继承了 Thread 类的子类,在此子类中,重写Thread 类的run(0方法。
声明一个实现 Runnable 接口的类,然后实现run0方法
每一个线程均会处于新建、就绪、运行、阻塞、死亡五种状态之一。
在Java 实现的多线程应用程序中,可以通过调用Thread 类中的方法实现对线程类对象的操作。
调整线程的优先级:在同等情况下,优先级高的线程会获得较多的运行机会,优先级低的线程则相反。Java 线程优先级用1-10的整数表示。
线程休眠:sleep(long millis)方法使线程转到阻塞状态
线程的强制运行:join0方法可以让某一线程强制运行。
线程礼让:yield0方法,暂停当前正在执行的线程类对象,把执行机会让给相同或更高优先级的线程。
当多个线程类对象操作同一共享资源时,要使用synchronized 关键字进行资源的同步处理,可以使用同步代码块或同步方法实现线程同步。
机遇永远是准备好的人得到的。