【JavaSE】---多线程

2021/10/7 17:10:53

本文主要是介绍【JavaSE】---多线程,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

【JavaSE】---多线程

前言:并发执行与并行执行

  • 并行执行通常表示同一时刻有多个代码在处理器上执行,这往往需要多个处理器,比如CPU等硬件的支持。
  • 并发执行通常表示,在单个处理器上,同一时刻只能执行一个代码,但在一个时间段内,这些代码交替执行,也就是所谓的“微观串行,宏观并行”。

一、线程的基本概念

1、线程的概念

  • 以往开发的程序大多数都是单线程,从头到尾只有一个执行路径。
  • 生活中的多线程:
    • 边喝咖啡,边听音乐。
    • 一个网络服务器要同时处理几个客户机的请求。

多线程是指在同一个进程中同时存在几个执行体,按几条不同的执行路径同时工作的情况。

2、程序、进程、多任务与线程

  • 程序
    • 是含有指令和数据的文件,被存储在磁盘或者其他的数据存储设备中,也就是说程序是静态的代码。
  • 进程
    • 进程是程序的一次执行过程,是系统运行程序的基本单位,因此程序是动态的。
    • 进程之间相互独立,除非利用某些通信管道来通信。
  • 多任务
    • 是指在一个系统中可以同时运行多个进程,即有多个独立运行的任务,每一个任务对应一个进程。
  • 线程
    • 将资源分配和处理器调度的基本单位分离。
    • 进程只是资源分配的单位,线程是处理器调度的基本单位。
    • 一个进程包含一个以上的线程,一个进程中的线程只能使用该进程的资源和环境。

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

  • 每个Java程序都有一个默认的主线程,对于程序来说就是main()方法执行的线程。
  • 要想实现多线程,必须在主线程中创建新的线程对象。
  • 使用Thread类及其子类的对象来表示线程。
  • 新创建的线程在它的一个完整的生命周期内要经理无重状态。

image-20211007094101816

  • 新建状态:当一个Thread类或者其子类的对象被声明并且创建,但是还没有执行,处于一种新建状态。此时,线程对象已经被分配了内存空间和其他资源,并且已经初始化,但是该线程尚未被调度,此时的线程可以被调度,变成就绪状态。

  • 就绪状态:就绪状态就是可运行的状态。新建状态的线程被启动之后,将进入线程队列排队等待CPU资源,此时已经具备了运行的条件。一旦轮到它来享用CPU资源的时候,就可以脱离创建它的主线程独立开始自己的生命周期了。另外原来处于阻塞状态的线程被解除阻塞之后也会进入就绪状态。

  • 执行状态:线程正在执行,已经拥有了对CPU的控制权。每一个Thread类及其子类的对象都有一个重要的run()方法。当线程对象被调度执行的时候,它将自动调用本对象的 run()方法,从该方法的第一条语句开始执行,一直到执行完毕,除非该线程主动让出CPU的控制权或者CPU的控制权被优先级更高的线程抢占。处于执行状态的线程在下列情况下将让出CPU的控制权。

    • 线程执行完毕
    • 有比当前线程优先级更高的线程处于就绪状态
    • 线程主动睡眠一段时间
    • 线程在等待某一组元
  • 阻塞状态:一个正在执行的线程在某些特殊的情况下,将会让出CPU并暂停中止自己的执行。

    • 调用sleep()或者yield()方法
    • 为等待一个条件变量,线程调用wait()方法
    • 该线程与另一线程join()在一起
    • 当线程阻塞时候,不能进入排队的队列,只有当引起阻塞的原因被消除的时候,线程才可以转入就绪状态。
  • 消亡状态:处于消亡状态的线程不具有继续执行的能力。

    • 一个正常运行的线程完成了它的全部工作,执行完了run()方法的最后一条语句并退出。
    • 当进程因故停止运行的时候,该进程中的所有线程将被强制终止。
    • 当线程处于消亡状态,并且没有该线程对象的引用时,垃圾回收期会从内存中删除该线程对象。

4、线程的优先级与调度

①优先级

  • 优先级决定了线程被CPU执行的优先顺序。如果线程的优先级完全相等,就按照先来先执行的原则进行调度。

  • Java中,线程的优先级从低到高以整数1~10表示,共分为10级。

  • Thread类有三个关于线程优先级的静态常量:

    • MIN_PRIORITY表示最小的优先级,通常为1.
    • MAX_PRIORITY表示最高优先级,通常为10.
    • NORM_PRIORITY表示普通优先级,默认值为5。
  • 对于新创建的线程,系统会遵循下面的原则为其指定优先级:

    • 新线程将继承创建它的父线程的优先级。
    • 一般情况下,主线程具有普通优先级。
      • 如果想改变线程的优先级,可以通过调用线程对象的setPriority()方法来进行设置。

②调度

  • 指各个线程之间分配CPU资源。
  • 两种模型:分时模型和抢占模型
  • 分时模型:
    • CPU资源按照时间片来分配,获得CPU资源的线程只能在指定的时间片内执行,一旦时间片使用完毕,就必须把CPU让给另一个处于就绪状态的线程。
    • 线程本身不会让出CPU
  • 抢占模型:
    • 通过优先级来分配,如果低优先级正在执行,有一个高优先级的线程准备就绪,那么低优先级的线程就把CPU资源让给高优先级的线程。
    • 为了使得低优先级的线程有机会执行,高优先级的线程应该不时的主动进入“睡眠”状态,从而暂时让出CPU。
    • JAVA支持的就是抢占式调度模型。

二、Java的Thread线程类与Runnable

​ Java实现多线程的方法有两种:继承java.lang包下面的Thread类,另一种就是用户在定义自己的类中实现Runnable接口。但是不管采用什么方法,都要用到Java语言类库中的Thread类以及相关的方法。

1、利用Thread类的子类来创建线程

①Thread类的构造方法

构造方法 功能说明
public Thread() 创建一个线程对象,此线程对象的名称是“Thread-n"的形式,n为整数,使用这个构造方法,必须创建Thread类的一个子类并覆盖其run()方法。
public Thread(String name) 创建一个线程对象,参数name指定了线程的名称。
public Thread(Runnable target) 创建一个线程对象,此线程对象的名称是”Thread-n"的形式,其中n是一个整数。参数target的run()方法将被线程对象调用,作为其执行代码。
public Thread(Runnable target,String name) 功能同上,name指定了新创建线程的名称。

②Thread类的常用方法

常用方法 功能说明
public static Thread currentThread() 返回当前正在执行的线程对象
public final String getName() 返回线程的名称
public void start() 使该线程由新建状态变为就绪状态,如果该线程已经是就绪状态,则产生IllegalStateException异常
public void run() 线程应该执行的任务
public final boolean isAlive() 如果线程处于就绪、阻塞或者运行状态,返回true,否则返回false
public void interrupt() 当线程处于就绪或者执行状态时候,给该线程设置中断标志。一个正在执行的线程让睡眠线程调用这个方法,则可导致睡眠线程发生InterruptedException异常而唤醒自己,从而进入就绪状态。
public static boolean isInterrupted() 判断该线程是否被中断,若是返回true,否则返回false
public final void join() 暂停当前线程的执行,等待调用该方法的线程结束后再继续执行本线程
public final int getPriority() 返回线程的优先级
public final void setPriority(int newPriority) 设置线程的优先级。如果当前线程不能修改这个线程,则产生SecurityException异常。
public static void sleep(long milis) 指定当前线程的睡眠时间。参数millis是线程睡眠的毫秒数,如果这个线程已经被别的线程中断,则产生InterruptedException异常
public static void yield() 暂停当前线程的执行,但该线程仍处于就绪状态,不转为阻塞状态。该方法只给同优先级线程以执行的机会

要在一个Thread的子类里激活线程,必须做好下面两件事情:

  • 此类必须继承自Thread类
  • 线程要执行的代码必须写在run()方法内。

③测试案例

package thr01;

public class MyThread extends Thread{
    private String who;
    public MyThread(String str){
        who = str;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            try {
                sleep((int)(1000 * Math.random()));
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            System.out.println(who + "正在运行!");
        }
    }
}

class App11_1{
    public static void main(String[] args) {
        MyThread you = new MyThread("你");
        MyThread she = new MyThread("她");
        you.start();
        she.start();
        System.out.println("主线程main()运行结束");
    }
}

需要注意的点就是上面sleep()函数,Math.random()返回的时0~1的浮点数。

2、用Runable接口来创建线程

需求:

  • 如果一个类本身已经继承了某个父类,由于Java语言不允许类的多重继承,所以就无法再继续继承Thread类。
  • 这种情况下就可以使用Runnable接口。
  • 用户可以声明一个类实现Runnable接口,除了重写run()方法,这个接口没有任何对任何线程的支持,还必须创建Thread类的实例,这一点通过Thread(Runnable target)类的构造方法来实现。
  • 具体方法就是自己定义一个实现了Runnable接口的类,然后将这个类所创建的对象作为参数传递给线程的构造方法。

测试案例

package runable01;

public class MyThread implements Runnable{
    private String who;
    public MyThread(String str){
        who = str;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep((int)(1000 * Math.random()));
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            System.out.println(who + "正在运行!");
        }
    }
}

class App11_2{
    public static void main(String[] args) {
        MyThread you = new MyThread("你");
        MyThread she = new MyThread("她");
        Thread t1 = new Thread(you);
        Thread t2 = new Thread(she);
        t1.start();
        t2.start();
    }
}

3、join()方法的使用

​ 从前面的例子中,我们看到,程序中被同时激活的多个线程将会同时执行,但是有时候需要有序地执行,这个时候可以使用Thread类中的join()方法。当某一个线程调用join()方法的时候,则其他线程会等到该线程结束后才开始执行。也就是说t.join()将使得t线程“加塞”到当前线程之前获得CPU,当前线程则进入阻塞状态,指导线程t结束为止,当前线程恢复为就绪状态,等待调度。

测试案例

package join01;
class MyThread extends Thread{
    private String who;
    public MyThread(String str){
        who = str;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            try {
                sleep((int)(1000 * Math.random()));
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            System.out.println(who + "正在运行!");
        }
    }
}
public class App11_3 {
    public static void main(String[] args) {
        thr01.MyThread you = new thr01.MyThread("你");
        thr01.MyThread she = new thr01.MyThread("她");
        you.start();
        try {
            you.join();
        }
        catch (InterruptedException e){}
        she.start();
        try {
            she.join();
        }
        catch (InterruptedException e){}
        System.out.println("主方法main()运行结束");
    }
}

激活线程you之后,继续往下执行,但是下面有you.join()语句,所以它会使得程序的流程先停留在此处,直到you线程结束之后,才会执行到第14行的she线程。同理,由于she线程也调用了join()方法,所以要等到she线程结束之后,才会输入主方法结束。

4、线程之间的数据共享

  • 获取当前进程的名字:

    • 当可运行对象包含线程对象的时候,即线程对象是可运行对象的成员的时候,则在run()方法中可以通过调用Thread.currentThread()方法来获得正在运行的线程的引用。
    • 当可运行对象不包含线程对象的时候,在可运行对象run()方法中需要使用语句Thread.currentThread().getName()来返回当前正在运行线程的名字。
  • 同一进程的多个线程之间可以共享相同的内存单元,并可以利用这些共享单元来实现数据交换、实时通信和必要的同步操作。

  • 对于利用构造方法Thread(Runnable target)这种方式创建的线程,当轮到它来享用CPU资源的时候,可运行对象target就会自动调用接口中的run()方法,因此,对于同一个可运行对象的多个线程,可运行对象target就会自动调用接口中的run()方法,因此,对于同一个可运行对象的多个线程,可运行对象的成员变量自然就是这些线程共享的数据单元。

①测试案例-01

package ticket;

class ThreadSale extends Thread{
    private int tickets = 10;

    @Override
    public void run() {
        while (true){
            if (tickets > 0)
                System.out.println(this.getName() + " 售机票第"+tickets--+"号");
            else
                System.exit(0);
        }
    }
}
public class App11_4 {
    public static void main(String[] args) {
        ThreadSale t1 = new ThreadSale();
        ThreadSale t2 = new ThreadSale();
        ThreadSale t3 = new ThreadSale();
        t1.start();
        t2.start();
        t3.start();
    }
}

结果:

Thread-0 售机票第10号
Thread-0 售机票第9号
Thread-0 售机票第8号
Thread-0 售机票第7号
Thread-0 售机票第6号
Thread-0 售机票第5号
Thread-0 售机票第4号
Thread-0 售机票第3号
Thread-0 售机票第2号
Thread-2 售机票第10号
Thread-2 售机票第9号
Thread-2 售机票第8号
Thread-2 售机票第7号
Thread-1 售机票第10号
Thread-2 售机票第6号
Thread-0 售机票第1号
Thread-2 售机票第5号
Thread-2 售机票第4号
Thread-2 售机票第3号
Thread-2 售机票第2号
Thread-2 售机票第1号
Thread-1 售机票第9号
Thread-1 售机票第8号
Thread-1 售机票第7号
Thread-1 售机票第6号
Thread-1 售机票第5号
Thread-1 售机票第4号
Thread-1 售机票第3号
Thread-1 售机票第2号
Thread-1 售机票第1号

分析:

  • 每张机票都被卖了3次,即3个线程格子卖了10张机票,而不是去卖共同的10张机票。
  • 原因是:上面的程序分别创建了3个ThreadSale线程对象,而每个线程都拥有各自的方法和变量,并且每个线程对象均可以独立地从CPU哪里得到可以执行的时间片,其结果虽然方法是相同的。但是变量tickets却不是共享的,而是各有10张机票,每个线程都在独立地处理各自的资源,因而结果会是各卖出10张机票,与原来的意思不一样。

②测试案例-02

package ticket;

class ThreadSale2 implements Runnable{
    private int tickets = 10;

    @Override
    public void run() {
        while (true){
            if (tickets >0)
                System.out.println(Thread.currentThread().getName()+"售机票第"+tickets--+"号");
            else
                System.exit(0);
        }
    }
}

public class App11_5 {
    public static void main(String[] args) {
        ThreadSale2 t = new ThreadSale2();
        Thread t1 = new Thread(t, "第1售票窗口");
        Thread t2 = new Thread(t, "第2售票窗口");
        Thread t3 = new Thread(t, "第3售票窗口");
        t1.start();
        t2.start();
        t3.start();
    }
}

结果:

第1售票窗口售机票第10号
第1售票窗口售机票第7号
第1售票窗口售机票第6号
第1售票窗口售机票第5号
第1售票窗口售机票第4号
第1售票窗口售机票第3号
第1售票窗口售机票第2号
第1售票窗口售机票第1号
第3售票窗口售机票第8号
第2售票窗口售机票第9号

结论:

  • Runnable接口适合处理多线程访问同一资源的情况,并且可以避免由于Java语言的单继承性带来的局限。

三、多线程的同步控制

  • 上面介绍的线程,线程功能简单,每个线程都包含了运行时所需要的数据和方法。这样的线程在运行的时候,因为不需要外部的数据和方法,就不需要关心其他线程的状态或者行为,称这样的线程为独立的、不同步的或者是异步执行的。
  • 当应用问题的功能增强、关系复杂、存在多个线程之间共享数据的时候,如果线程仍然以异步方式访问共享数据,有时是不安全的。
  • 此时、当一个线程对共享的数据执行操作的时候,应该让它成为一个“原子操作”,在没有完成相关操作之前,不允许其他线程打断它,否则就会破坏数据的完整性,必然会得到错误的处理结果,这就是线程的同步。

1、用户从银行取款出错案例

package bank01;

class Mbank{
    private static int sum = 2000;
    public static void take(int k){
        int temp = sum;
        temp -= k;
        try{
            Thread.sleep((int)(1000 * Math.random()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        sum = temp;
        System.out.println("sum=" + sum);
    }
}
class Customer extends Thread{
    public void run(){
        for (int i = 1;i<=4;i++)
            Mbank.take(100);
    }
}
public class App11_6 {
    public static void main(String[] args) {
        Customer c1 = new Customer();
        Customer c2 = new Customer();
        c1.start();
        c2.start();
    }
}

结果:

sum=1900
sum=1900
sum=1800
sum=1800
sum=1700
sum=1700
sum=1600
sum=1600

错误原因:

  • 在线程执行过程中,在执行有关的若干个动作的时候,没有能够保证独占相关的资源,而是在对该资源进行处理时又被其他线程的操作打断或者干扰而引起的。
  • 要防止这样的情况发生,就必须保证线程在一个完整的操作所有动作的执行过程中,都占有相关资源而不被打断,这就是线程同步的概念。

2、互斥锁

image-20211007160723707

synchronized的用法:

//格式一:同步语句
Synchronized(对象){
    //临界代码段
}

//格式二:
public synchronized 返回类型 方法名(){
    //方法体
}
//或者
public 返回类型 方法名(){
    synchronized(this){
        //方法体
    }
}
package bank02;
class Mbank{
    private static int sum = 2000;
    public synchronized static void take(int k){
        int temp = sum;
        temp -= k;
        try{
            Thread.sleep((int)(1000 * Math.random()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        sum = temp;
        System.out.println("sum=" + sum);
    }
}
class Customer extends Thread{
    public void run(){
        for (int i = 1;i<=4;i++)
            Mbank.take(100);
    }
}
public class App11_7 {
    public static void main(String[] args) {
        Customer c1 = new Customer();
        Customer c2 = new Customer();
        c1.start();
        c2.start();
    }
}

结果:

sum=1900
sum=1800
sum=1700
sum=1600
sum=1500
sum=1400
sum=1300
sum=1200

3、synchronized的说明

image-20211007162123262

四、线程之间的通信

image-20211007163019764

image-20211007163032605

1、模拟存票、售票的过程

要求每存入一张票,就售出一张票,售出后,再存入,直到售完为止。

package ticket02;

public class App11_8 {
    public static void main(String[] args) {
        Tickets t = new Tickets(10);
        new Producer(t).start();
        new Consumer(t).start();
    }
}

class Tickets{
    protected int size;//总票数
    int number = 0;//票号
    boolean available = false;//表示当前是否有票可售
    public Tickets(int size){
        this.size = size;
    }
    public synchronized void put(){//同步方法,实现存票功能
        if (available){//如果还有存票待售出,则存票线程等待
            try {
                wait();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        System.out.println("存入第【"+(++number)+"】号票");
        available = true;
        notify();//存入票后唤醒售票线程开始售票
    }
    public synchronized void sell(){//同步方法,实现售票功能
        if (!available){//如果没有存票,则售票线程等待
            try {
                wait();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        System.out.println("售出第【"+(number)+"】号票");
        available = false;
        notify();//售票后唤醒存票线程开始存票
        if (number == size)
            number = size + 1;//在售完最后一张票后,设置一个结束标志
        //number > size表示售票结束
    }
}

class Producer extends Thread//存票线程类
{
    Tickets t = null;
    public Producer(Tickets t){
        this.t = t;
    }
    public void run(){
        while (t.number < t.size){
            t.put();
        }
    }
}

class Consumer extends Thread //售票线程类
{
    Tickets t = null;
    public Consumer(Tickets t){
        this.t = t;
    }
    public void run(){
        while (t.number <= t.size)
            t.sell();
    }
}

image-20211007164658079



这篇关于【JavaSE】---多线程的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程