浅谈Java多线程

多线程模型是Java程序最基本的并发模型,本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。

一、进程和线程

在计算机中,一个任务就是一个进程,比如打开Word就是打开一个进程。在进程内部还需要同时执行多个子任务。例如,Word可以让我们一边打字,一边进行拼写检查,我们把子任务称为线程。一个进程可以包含一个或多个线程,但至少会有一个线程。因为同一个应用程序,既可以有多个进程,也可以有多个线程。

进程

进程是程序的一次执行过程,是系统运行程序的基本单位。进程是操作系统资源分配和处理器调度的基本单位,拥有独立的代码、内部数据和运行状态,因此,频繁的进程状态的切换必然消耗大量的系统资源。系统运行一个程序即是一个进程从创建、运行到消亡的过程。如每一个正在Windows操作系统上执行的程序,都可以视为一个进程。每个进程之间是独立的。

线程

线程也称为轻量级进程,线程是cpu调度的最小单位,线程是进程的一个执行单元,负责当前进程中程序的执行。同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多。线程不能独立存在,必须存在于进程中。

区别

线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。

  • 根本区别: 进程是操作系统资源分配的基本单位(最小单位),而线程是cpu调度和执行的基本单位(最小单位),进程和线程是包含关系,通常一个进程都有若干个线程,至少包含一个线程。

  • 资源开销: 每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

  • 包含关系: 进程和线程是包含关系,线程是进程的一部分,一个进程至少包含一个线程。

  • 内存分配: 同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的

  • 影响关系: 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

  • 执行过程: 每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。


二、并行和并发

并发是指一个处理器同时处理多个任务。并行是指多个处理器同时处理多个不同的任务。
并发是逻辑上的同时发生,而并行是物理上的同时发生。

并行

指在同一时刻,有多条指令在多个处理器上同时执行。就好像两个人各拿一把铁锨在挖坑,一小时后,每人一个大坑。所以无论从微观还是从宏观来看,二者都是一起执行的。
请添加图片描述

  • 并行:你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行

并发

指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。就像两个人用同一把铁锨,轮流挖坑,一小时后,两个人各挖一个小一点的坑,要想挖两个大一点得坑,一定会用两个小时。
请添加图片描述

  • 并发:你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。

三、多线程的实现

Java语言内置了多线程支持,当Java程序启动的时候,实际上是启动了一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动其他线程。此外,JVM还有负责垃圾回收的其他工作线程等。

其多线程实现方式主要有三种:继承Thread类、实现Runnable接口、使用ExecutorService、Callable、Future实现有返回结果的多线程。其中前两种方式线程执行完后都没有返回值,只有最后一种是带返回值的。

继承Thread类

Java语言的基本类库中已定义了Thread这个基本类,内置了一组方法,使程序利用该类提供的方法去产生一个新的线程、执行一个线程、终止一个线程的工作,或是查看线程的执行状态,这种方式实现多线程很简单。

继承Thread类实现多线程的步骤:

  1. 定义一个类,继承Thread类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此,run()方法的方法体被称为线程执行体;
  2. 创建Thread子类的对象,即创建了子线程;
  3. 用线程对象的start方法来启动该线程;

代码如下:

class MyThread extends Thread {
    private String name;
    static int count = 10;

    MyThread(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        // 循环售票
        while (count > 0) {
            count--;
            //System.out.println(Thread.currentThread().getName() + "售出一张票,剩余" + count);
            System.out.println(name + " > 售出一张票,剩余:" + count);
        }
    }
}

class Main {
    public static void main(String[] args) {
        // 创建两个线程来售票
        Thread th1 = new MyThread("1号窗口");
        Thread th2 = new MyThread("2号窗口");
        // 启动线程
        th1.start();
        th2.start();
    }
}

注意到start()方法会在内部自动调用实例的run()方法。

实现Runable接口

通过实现Runable接口实现多线程是受欢迎。因为Java只能单继承,继承了Thread类就不能再继承其他类了。

通过Runnable接口实现多线程的步骤:

  1. 定义一个Runnable接口的实现类,并重写该接口中的run方法,该run方法的方法体同样是该线程的线程执行体;
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象;
  3. 调用线程对象的start方法来启动该线程;

代码如下:

class MyThread2 implements Runnable {
    static int count = 10;
    private String name;

    public MyThread2(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        while (count > 0) {
            count--;
            System.out.println(name + " > 售出一张票,剩余:" + count);
        }
    }
}

class Main {
    public static void main(String[] args) {
    // 为了启动MyThread,需要首先实例化一个Thread,并传入自己的MyThread实例:
        Thread th3 = new Thread(new MyThread2("3号窗口"));
        Thread th4 = new Thread(new MyThread2("4号窗口"));

        th3.start();
        th4.start();
    }
}

通过callable实现

使用ExecutorService、Callable、Future实现有返回结果的多线程
ExecutorService、Callable、Future这个对象实际上都是属于Executor框架中的功能类。

Thread与Runnable的比较

继承Thread类的方式

  • 编写简单,如果要访问当前线程,除了可以通过Thread.currentThread()方式之外,还可以使用 super关键字
  • 弊端:因为Java是单继承,所以线程类继承了Thread类,则不能再继承其他类。

实现Runnable接口的方式

  • 线程类只是实现了Runnable接口,还可以继承其他类。可以多个线程共享同一个target对象,所以非常适合多个线程来处理同一份资源的情况
  • 弊端:编程稍微复杂,不直观,如果要访问当前线程,必须使用Thread.currentThread()

start()与run()方法的区别

必须调用Thread实例的start()方法才能启动新线程,查看Thread类的源代码,会看到start()方法内部调用了一个private native void start0()方法,native修饰符表示这个方法是由JVM虚拟机内部的C代码实现的,不是由Java代码实现的。

start()方法会新建一个线程,并且让这个线程执行run()方法。

Thread thread = new Thread();
thread.start();

调用run()也能正常执行。但是,却不能新建一个线程,而是在当前线程中调用run()方法,只是作为一个普通的方法调用。

Thread thread = new Thread();
thread.run();

效果如下:

所以不要用run()来开启新线程,它只会在当前线程中,串行执行run()方法中的代码,相当于调用了一个普通的Java方法,当前线程并没有任何改变,也不会启动新线程。


四、线程的状态与生命周期

在Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。

线程的状态

  • New:新创建的线程,尚未执行;
  • Runnable:运行中的线程,正在执行run()方法的Java代码;
  • Blocked:运行中的线程,因为某些操作被阻塞而挂起;
  • Waiting:运行中的线程,因为某些操作在等待中;
  • Timed Waiting:运行中的线程,因为执行- sleep()方法正在计时等待;
  • Terminated:线程已终止,因为run()方法执行完毕。

当线程启动后,通过线程的控制与调度可使线程在Runnable、Blocked、Waiting、Timed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。

一个线程还可以等待另一个线程直到其运行结束。当某一线程调用join()方法时,则其他线程会等到该线程结束后才开始执行。也就是说语句t.join()将使t线程“加塞”到当前线程之前获得CPU,当前线程则进入阻塞状态,直到线程t结束为止,当前线程恢复为就绪状态,等待线程调度。

例如,main线程在启动t线程后,可以通过t.join()等待t线程结束后再继续运行:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("hello");
        });
        System.out.println("start");
        t.start();
        t.join();
        System.out.println("end");
    }
}

当main线程对线程对象t调用join()方法时,主线程将等待变量t表示的线程运行结束,然后才继续往下执行自身线程。所以,上述代码打印顺序是main线程先打印start,t线程再打印hello,main线程最后再打印end。

线程的生命周期

线程终止的原因

  • 线程正常终止:run()方法执行到return语句返回,即线程完成了它的全部工作,即执行完了run()方法的最后一条语句并退出;
  • 线程意外终止:run()方法因为未捕获的异常导致线程终止;
  • 对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)

五、线程的优先级与调度

优先级

在多线程系统中,每个线程都被赋予一个执行优先级。优先级决定了线程被CPU执行的优先顺序。优先级高的线程可以在一段时间内获得比优先级低的线程更多的执行时间。

Java语言中线程的优先级从低到高以整数1~10表示,共分为10级。Thread类有三个关于线程优先级的静态常量:MIN_PRIORITY表示最小优先级,通常为1;MAX_PRIORITY表示最高优先级,通常为10;NORM_PRIORITY表示普通优先级,默认值为5。可以通过调用线程对象的setPriority()方法来设置线程的优先级。

对应一个新建的线程,系统会遵循如下的原则为其指定优先级。

  • 新建线程将继承创建它的父线程的优先级。父线程是指执行创建新线程对象语句所在的线程,它可能是程序的主线程,也可能是某一个用户自定义的线程。

调度

调度就是指在各个线程之间分配CPU资源。多个线程的并发执行实际上是通过一个调度来进行的。线程调度有两种模型:分时模型和抢占模型。在分时模型中,CPU资源是按照时间片来分配的,获得CPU资源的线程只能在指定的时间片内执行,一旦时间片使用完毕,就必须把CPU让给另一个处于就绪状态的线程。在分时模型中,线程本身不会让出CPU;在抢占模型中,当前活动的线程一旦获得执行权,将一直执行下去,直到执行完或由于某种原因主动放弃执行权。如在一个低优先级线程的执行过程中,又有一个高优先级的线程准备就绪,那么低优先级的线程就把CPU资源让给高优先级的线程。为了使低优先级的线程有机会执行,高优先级的线程应该不时地主动进入“睡眠”状态,而暂时让出CPU。Java语言支持的就是抢占式调度模型。


感谢大家的耐心阅读,如有建议请私信或评论留言
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值