多线程(一)
平时我们写的程序其实都是单线程的,它们都是从main方法开始一条顺序向下执行的。多线程其实就是使程序可以分为好几个路径去执行代码,每个路径之间互不干扰。
Java语言提供了非常优秀的多线程支持,程序可以非常简单的方式来启动多线程。
进程与线程
学习多线程之前我们要了解程序、线程和进程的关系
进程
几乎所有的操作系统都支持进程的概念,每一个运行中的任务,都对应一个进程,进程是处于运行过程中的程序,并且具有一定独立功能,进程是操作系统进行资源分配和调度的一个独立单位。进程中包含着多个多运行中的线程,线程才是任务执行者。
进程的特点:
- 独立性:
- 进程是操作系统进行资源分配和调度的一个独立单位, 每一个进程都拥有自己私有的地址空间,在没有经过进程本身允许情况下,一个 用户进程不可以直接访问其他进程的地址空间,哪怕在同一台计算机上运行,进程之间的通讯也需要网络、独立于进程的文件来进行交换数据。
- 动态性:
- 程序只是一个静态的指令的集合,而进程是一个正在系统中运行的活动的指令集合,所以进程就是一个处于运行状态的程序,在进程中加入的时间的概念,进程具有自己的生命周期和各个不同的状态,这个概念在程序中都是不具备的。
- 并发性:
- 多个进程可以在单个处理器上并发执行,多个进程之间不会相互影响,现在的操作系统都支持多进程的并发,但是在具体的实现细节上可能因为硬件和操作系统的不同而采用不同的策略,目前大多数采用效率更高的抢占式多任务策略。
- 对于一个CPU而言,它在某一个时间点上只能执行一个程序,也就是只能运行一个进程,但是多个任务并发,CPU在处理这些任务时,就是不断的在这些进程之间轮换执行。
- 我们可以一边写代码、一边听音乐、还可以查资料。为什么我们察觉不到CPU轮换执行这些程序中的进程呢,这是因为CPU的执行效率非常快,每个线程之间的切换速度也非常快,快到我们自身感觉不到,所以我们才会看到同时运行多个程序。当然,我们运行的程序过多时,我们也就能感觉到程序的卡顿。
线程
了解完了进程,我们对比着来学习线程
多线程则扩展了多进程的概念,使得一个进程可以并发处理多个任务,线程也可以理解为一个轻量级的进程。和进程在操作系统中的地位一样,线程在进程中也是一个独立的、并发的执行流。当进程被初始化时,主线程就被创建了,对于Java程序来说,main线程就是主线程,但是我们可以在该进程中创建多个顺序执行的其他路径。这些路径就子线程。
- 进程中的每一个线程可以完成一定的任务,并且是独立的,线程可以拥有自己独立的堆栈、自己的程序计数器、和自己的局部变量,但不在拥有系统资源,它与父进程的其他线程共享该进程拥有的全部内存资源。
- 由于线程间的通讯是在同一个地址空间上进行的,所以不需要额外的通信机制,这就使得通信更加简便而且信息传递的速度也更快,因此可以通过简单编程实现多线程相互协同来完成进程所要完成的任务。
- 多线程是存在安全问题的,当多个独立的线程去完成同一个任务时,因为每一个线程对共享系统资源的操作都会给其他调用这个资源的线程产生影响,因此,线程同步也是一个非常重要的问题
- 线程的执行也是抢占式的,也就是说,当前运行的程序,在任何时候都可能被挂起,以便另一个线程可以运行,我们说CPU在不同的进程之间轮换,进程又在不同的线程之间轮换,因此线程是CPU执行和调度的最小单元。
- 总之,一个程序的运行至少有一个进程,一个进程中至少也要有一个线程或者多个线程,当操作系统创建一个进程时,必须为该进程分配独立的内存空间,并分配大量的相关资源。但是创建一个线程就比较简单,而且多个线程共享同一个进程的虚拟空间,所以使用多线程来实现并发比多进程实现并发的性能要高得多。
并行性:同一时刻,多个任务在多个处理器上同时执行。(一个有多个核心的CPU也是可以实现)
并发性:同一时刻,多个任务在一个处理器都需要执行被执行,但是同时只能有一个程序被执行,处理器在多个任务之间快速的交替执行。
线程的生命周期
一个完整的线程生命周期会经过以下五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)
CPU在多个线程之间不断切换,那么线程也就在运行、阻塞、就绪之间不断切换。
新建:
当一个Thread类或者其子类对象被声明并创建时,这个线程就属于新建状态了,此时他个其他的Java对象一样,由JVM为其分配了内存,并初始化了实例变量的值,此时线程对象并没有任何线程的动态特征。
就绪:
一个处于新建状态的线程,当它调用了start()方法后,就到达了就绪状态。此时JVM就会为他创建方法,调用栈和程序计数器,此时的线程并没有开始运行,但是它已经具备了运行的条件,随时等待JVM里的线程调度器的调度。
运行:
一个处于就绪状态的线程,当被调度时,获得了CPU的使用权,就进入了运行状态。开始执行run()方法中的代码。
- 但是一个单核的CPU同时只能有一个线程执行任务,此时还会有其他线程处于就绪状态等待CPU的调用
- 对于抢占式策略的系统而言,系统不会让这个线程任务全部执行完再去运行下一个线程。系统会给每一个线程都有执行的机会,所以每个线程运行时,一次只有一小段的时间得到运行,时间到了,CPU会剥夺这个线程所占用的资源,让它重新回到就绪状态,等待下一次的运行,然后再让其他线程运行一小段时间去完成它的任务,
- 就是因为每个线程的每次得到运行的时间很短,而且CPU在线程之间切换速度很快,我们才会宏观的感觉到,线程是在同时运行,其实这只是我们的错觉。
阻塞:
一个处于运行状态的线程,当它遇到了以下的情况就会发生阻塞:
- 线程调用了sleep方法,主动放弃了占用CPU的资源,sleep方法执行完之前,该线程一直被阻塞
- 线程调用了一个阻塞式的IO方法(比如使用Scanner类中的方法等待键盘输入),在该方法返回之前,该线程一直被阻塞
- 线程试图获得一个同步监视器,但是同步监视器被其他线程持有,在没有得到同步监视器的过程中,该线程一直被阻塞
- 线程执行过程中,同步监视器调用了wait方法,让它等待一个通知(notify),等待过程中,该线程一直被阻塞
- 线程执行过程中,遇到了其他线程的加塞(其他线程的join方法),其他线程任务执行结束前,该线程一直被阻塞
- 线程调用了suspend方法,被挂起(已过时,容易发生死锁)
此时该线程被阻塞,暂时放弃了CPU的使用权,停止执行,其他线程被调度继续执行任务。该线程只有遇到以下情况时,阻塞才会解除:
- 线程的sleep方法时间到了。
- 线程调用的阻塞式IO方法返回了。
- 线程得到了同步监视器
- 线程等到了通知(notify)
- 线程中加塞的其他线程任务执行结束了。
- 被挂起的方法又被调用了resume方法,恢复了(已过时)
当阻塞解除时,该方法重新进入就绪状态,等待下一次的调度
死亡:
一个运行中的线程遇到以下情况,就处于了死亡状态:
- run方法结束,任务全部执行完成,线程正常结束(正常死亡)
- 线程执行过程中发生了一个未捕获的异常(Exception),或者错误(Error),线程停止(非正常死亡)
- 直接调用stop方法结束线程(已过时,不建议使用。容易发生死锁)
我们可以使用线程中的isAlive方法,判断该线程是否死亡(当线程处于就绪、阻塞、运行三中状态时,该方法返回true,当线程处于新建和死亡状态时,该方法返回false)
Thread类
java使用Thread类表示线程,所有的线程对象都是Tread类对象或者它的子类对象。
- 每一个线程都会自己的代码完成一定的任务,这段代码就是线程执行体,Java中使用run方法来封装这些代码。
构造方法:
- Thread() 创建一个新的线程对象
- Thread(String Threadname)创建线程,并为线程设置名称
- Thread(Runnable target)创建线程使用指定的目标对象,该对象实现了Runnable接口中的run方法
- Thread(Runnable target,String Threadname)指定创建线程的目标对象,并为线程设置名称
常用方法:
返回值 | 方法名 | 说明 |
---|---|---|
void | start() | 使线程开始执行,JVM调用此线程中的run方法 |
static Thread | currentThread() | 静态方法,返回当前线程对象 |
boolean | isAlive() | 测试这个线程是否活着 |
void | setName(String name) | 设置该线程的名字 |
String | getName() | 返回该线程的名字 |
void | setPriority(int newPriority) | 修改线程的优先级(newPriority只能在1-10之中) |
int | getPriority() | 返回当前线程的优先级值 |
线程中的优先级:
每一个线程都有一定的优先级,优先级较高的线程先获得执行的概率就比较高,每一个线程默认的优先级都与创建它的父类线程具有相同的优先级,使用方法可以设置和获取线程的优先级,优先级最小为1最大为10,设置时推荐使用Thread类中的三个优先级常量:
- MAX_PRIORITY(10)最高优先级
- MIN_PRIORITY(1)最低优先级
- NORM_PRIORITY(5)普通优先级,(默认情况下main方法就是普通优先级)
优先级只是提升了线程被优先执行的概率,优先级高的线程不一定就先被执行,低优先级的线程不一定就在优先级高的线程后面执行。优先级这个概念在Linux系统中是没有的,所以实际开发中我们也要考虑到系统的支持。
线程的创建
这里我们使用两种方法创建线程:
1、创建一个线程类,继承Thread类
定义继承Thread子类,重写父类的run方法。run方法是线程的执行体,查看Thread的源码发现,run方法没有传入target时,run方法中没有其他代码,所以什么也不会做。所以我们重写run方法,让这个线程执行我们的任务。
class MyThread01 extends Thread{
@Override
public void run() {
//打印0~9
for (int i=0;i<10;i++){
//打印出该线程的名字
System.out.println(Thread.currentThread().getName()+"---->"+i);
}
}
}
创建MyThread01的对象
调用start方法启动线程,让它得到运行,从而执行run方法中的代码(一个线程对象只能调用一次start方法!)
public class Test1 {
public static void main(String[] args) {
MyThread01 myThread01 = new MyThread01();
//修改线程的名字
myThread01.setName("MyThread-1");
myThread01.start();
//此时我们让main方法也执行打印0~9的任务
for (int i=0;i<10;i++){
//main方法是主线程,线程名就叫main
System.out.println(Thread.currentThread().getName()+"---->"+i);
}
}
}
运行程序后就可以发现线程的运行方式,子线程中的代码和主线程中的代码交替轮流被执行。
2、创建Runnable接口的实现类
创建一个Runnable接口的实现类,并实现run()方法
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
}
创建一个MyRunnable类的对象
再创建一个Thread类的对象,把MyRunnable类的对象作为参数target。
此时的Thread对象传入了target,那么start启动Thread时,就会调用target中的run方法
public class Test2 {
public static void main(String[] args) {
//创建Runnable实现类的对象
MyRunnable myRunnable = new MyRunnable();
//创建Thread线程类,把实现类作为target参数
Thread thread1 = new Thread(myRunnable);
thread1.setName("线程一");
thread1.start();
//可以使用Runnable实现类创建两个线程对象
Thread thread2 = new Thread(myRunnable);
thread2.setName("线程二");
thread2.start();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
}
实际开发中,建议选择实现Runnable接口这个方法,因为Java中只能单继承,但是一个类可以实现多个接口;而且这种方式只用创建一个实现类对象,更适合处理多个线程操作一个共享数据的情况
控制线程
Thread类中有一些方法可以对线程进行一定的控制。
返回值 | 方法名 | 说明 |
---|---|---|
static void | sleep(long millis) | 在指定毫秒数内让该线程睡眠(暂停执行) |
static void | sleep(long millis,int nanos) | 在指定毫秒数加纳秒数内让该线程睡眠 |
static void | yield() | 主动释放当前线程的运行权 |
void | join() | 等待这个线程死亡 |
void | join(long millis) | 等待这个线程死亡,超过指定时间不再等待 |
void | setDaemon(boolean on) | 将此线程设置为守护线程(true),或者用户线程(false) |
boolean | getDaemon() | 测试这个线程是否为守护线程 |
线程睡眠
当我们想让我们正在执行任务的线程暂停一段时间,就可以使用Thread类中的静态方法sleep(),它会让当前线程进入阻塞状态
class MyThread01 extends Thread{
@Override
public void run() {
//打印0~9,每循环一次线程就会休眠一秒
for (int i=0;i<10;i++){
try {
//让当前线程休眠1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"---->"+i);
}
}
}
线程让步
让当前线程暂停,但是不会进入阻塞状态,而是直接转入就绪状态,重新等待执行
- 线程让步只是让出了当前的执行机会,重新回到就绪状态,让步之后,完全有可能调度器下次还是调度该线程,让步只是让其他线程有一次被调用的机会。
public class Test1 {
public static void main(String[] args) {
MyThread01 myThread01 = new MyThread01();
myThread01.setName("线程一");
myThread01.setPriority(Thread.MIN_PRIORITY);
MyThread02 myThread02 = new MyThread02();
myThread02.setName("线程二");
myThread02.setPriority(Thread.MAX_PRIORITY);
myThread01.start();
myThread02.start();
}
}
class MyThread01 extends Thread{
@Override
public void run() {
//打印0~9
for (int i=0;i<10;i++){
//线程让步
Thread.yield();
System.out.println(Thread.currentThread().getName()+"---->"+i);
}
}
}
class MyThread02 extends Thread{
@Override
public void run() {
for (int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"-》"+i);
}
}
}
对于线程的执行先后快慢,是看处理器的情况,这不是我们可以控制的,所以我们让线程发生了让步,但是让步后处理器会再次调用哪一个线程也不是我们可以控制的,不要对运行结果产生疑惑。
线程加塞
当在一个线程的线程体中,调用了另一个线程的join方法,当前线程被阻塞,直到调动join方法的线程执行完(死亡),阻塞才会结束,当前线程才会继续执行。当然我们可以为它添加一个等待时间,时间到了阻塞也会结束。
public class Test2 {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable5(),"子线程");
thread.start();
for (int i = 0; i < 10; i++) {
if (i==5){
try {
//i等于5时,线程加塞,等待子线程执行结束后继续执行
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"==="+i);
}
}
}
class MyRunnable5 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
}
守护线程
有一种线程是在后台一直运行的,它为其他线程提供服务,这种线程被称为守护线程,JVM中的垃圾回收器就是一个典型的守护线程。
- 守护线程会跟随其他线程一直执行,在其他所有非守护线程都死亡时自动死亡
public class Test3 {
public static void main(String[] args) throws InterruptedException {
MyRunnable6 myRunnable6 = new MyRunnable6();
Thread thread = new Thread(myRunnable6);
//将该线程设置为守护线程
thread.setDaemon(true);
thread.start();
//让主线程每0.5秒打印一次
for (int i = 0; i < 10; i++) {
Thread.sleep(500);
System.out.println(Thread.currentThread().getName()+i);
}
}
}
class MyRunnable6 implements Runnable{
@Override
public void run() {
//死循环,但是在其他线程任务都结束时,该线程也会自动结束
for (;;) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("守护线程执行中···");
}
}
}