zl程序教程

您现在的位置是:首页 >  后端

当前栏目

Java多线程基础知识

2023-06-13 09:13:13 时间

目录

一.进程和多线程的概述以及使用场景

二.并发和并行

三.线程的创建

1.Thread类实现多线程

 2.Runnable接口实现多线程

 3.Callable接口实现多线程

四.3种多线程实现方式的对比分析。

五.后台线程



一.进程和多线程的概述以及使用场景

进程:一个正在操作系统中运行的exe程序可以理解为一个进程,完全可以将运行在内存中的exe文件理解为进程-----进程就是受操作系统管理的基本运行单元。一个最简单的Java程序的运行也可以叫做一个进程。

所有的应用程序都是由CPU执行的,对于一个CPU而言,在某一个时间点只能运行一个程序,也就是说只能执行一个进程。操作系统会给每一个进程分配一段有限的cpu使用时间,cpu在这段时间中执行某个进程。由于CPU的执行速度非常快,能在极短的时间内在不同的进程之间进行切换,所以给人同时执行多个程序的感觉。

 线程:在一个进程中,还可以有多个执行单元同时运行,来同时完成一个或多个程序任务。这些执行单元可以看作是一条条线索,被称为线程。

多线程技术的使用场景:(1)阻塞。一旦系统出现了阻塞现象,则可以根据实际情况来使用多线程技术提高开发效率。(2)依赖。业务如果分为2个执行过程,分别是A和B。当A业务发生阻塞时,B业务的执行不依赖于A业务的执行结果,这时就可以通过多线程来提高运行效率。如果B业务不依赖于A业务的结果,则不必使用多线程技术,A业务出结果后再运行B业务,按顺来就行。

二.并发和并行

并发:指两个或多个事件在同一个时间段内发生,同一时刻只能发生一件事。

并行:指两个或多个事件在同一时刻发生。(同时发生)

特别注意:Java程序属于优先抢占式地调度,哪个线程的优先级高,哪个线程就有可能优先执行。优先级相同时,就随机选择执行。优先级大小只是指CPU被优先执行的概率的大小,并不一定优先级大就一定先被执行。

三.线程的创建

Java为多线程开发提供了非常优秀的支持,在java中,可以通过以下三个方式来实现多线程:

1.Thread类实现多线程

Thread类是java.lang包下的一个线程类,用来实现Java多线程。其实步骤非常简单。如下

(1)创建一个Thread线程类的子类,同时重写Thread类的run()方法。

(2)创建该子类的实例对象,并通过调用start()方法来启动线程。

start()方法使线程开始执行,Java虚拟机调用该线程的run()方法。结果是程序的main入口方法和创建的线程并发的运行。多次启动同一个线程是非法的,特别是当线程已经结束执行后,不能再重新启动。

package demo01;

public class Test {
    public static void main(String[] args) {
        //创建MyThread1线程实例对象
        MyThread1 thread1=new MyThread1("thread1");
        thread1.start();
        MyThread1 thread2=new MyThread1("thread2");
        thread2.start();
    }
}

//定义一个线程类继承自Thread类
class MyThread1 extends Thread{
    //构造方法
    public MyThread1(String name){
        super(name);
    }
    //重写run()方法


    @Override
    public void run() {
        int i=0;
        while (i++<5){
            //打印输出当时运行的线程的名字
            System.out.println(Thread.currentThread().getName());
        }
    }
}

 2.Runnable接口实现多线程

通过继承Thread类来实现多线程的方式有一些局限性,因为java只支持类的单继承。在这种情况下就可以考虑通过实现Runnable接口的方式来实现多线程。

步骤如下:

(1)创建一个Runnable接口的实现类,同时重写接口中的run()方法。

(2)创建Runnable接口的实现类对象。

(3)使用Thread有参构造方法来创建线程实例,并将Runnable接口的实现类对象作为参数传入。

(4)调用线程实例的start()方法来启动线程

package demo02;

public class test {
    public static void main(String[] args) {
        //(2)创建Runnable接口的实现类对象。
        myThread mythread1=new myThread();
        myThread mythread2=new myThread();
        //(3)使用Thread有参构造方法来创建线程实例,并将Runnable接口的实现类对象作为参数传入。
        Thread thread1=new Thread(mythread1,"thread1");
        Thread thread2=new Thread(mythread2,"thread2");
        //(4)调用线程实例的start()方法来启动线程
        thread1.start();
        thread2.start();
    }
    
}

//(1)定义一个Runnable接口的实现类
class myThread implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"的run()方法正在运行");
    }
}

 3.Callable接口实现多线程

通过Thread类和Runnable接口实现多线程时都要要重写run()方法。但是,该方法却没有返回值,因此无法从多个线程中获取返回值,但实际应用中肯定会用到返回值的。为了解决这个问题,从jdk5开始,java提供了一个Callable接口,来满足这种既能创建多线程,又能有返回值的需求。

通过这个方式实现多线程和Runnable的方式实现多线程差不多,都是通过Thread类的有参构造方法传入各自接口对象为参数来实现。只不过这里传入的不是Runnable对象了,而是Runnable类的子类FutureTask对象作为参数,而FutureTask对象中则封装带有返回值的Callable接口实现类。

实现步骤如下:

(1)创建一个Callable的实现类,同时重写Callable接口的call()方法。

(2)创建实现了Callable接口的实现类对象。

(3)通过FutureTask线程结果处理类的有参构造方法来封装Callable接口实现类对象

(4)使用参数为FutureTask类对象的Thread有参构造方法创建Thread实例。

(5)调用线程实例的start()方法启动线程。

package demo03;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

//(1)创建一个Callable的实现类,同时重写Callable接口的call()方法。
class MyThread implements Callable<Object>{

    @Override
    public Object call() throws Exception {
        int i=0;
        while (i++<5){
            //打印输出当时运行的线程的名字
            System.out.println(Thread.currentThread().getName());

        }
        return i;
    }
}




public class test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //(2)创建实现了Callable接口的实现类对象。
        MyThread mythread1=new MyThread();
        //(3)通过FutureTask线程结果处理类的有参构造方法来封装Callable接口实现类对象
        FutureTask<Object> ft1=new FutureTask<>(mythread1);
        //(4)使用参数为FutureTask类对象的Thread有参构造方法创建Thread实例。
        Thread thread1=new Thread(ft1,"thread1");
        //(5)调用线程实例的start()方法启动线程。
        thread1.start();

        //(2)创建实现了Callable接口的实现类对象。
        MyThread mythread2=new MyThread();
        //(3)通过FutureTask线程结果处理类的有参构造方法来封装Callable接口实现类对象
        FutureTask<Object> ft2=new FutureTask<>(mythread2);
        //(4)使用参数为FutureTask类对象的Thread有参构造方法创建Thread实例。
        Thread thread2=new Thread(ft2,"thread2");
        //(5)调用线程实例的start()方法启动线程。
        thread2.start();
        System.out.println("thread1返回的结果为:"+ft1.get());
        System.out.println("thread2返回的结果为:"+ft2.get());

    }
}

FutureTask实现了RunnableFuture接口,RunnableFuture接口继承自Runnable接口和Future接口。所以FutureTask的本质是Runnable接口和Future接口的实现类。

Future接口的方法:

boolean cancel(boolean running)用于取消任务,running参数为是否取消还在运行的任务

boolean isCancelled()用于判断任务是否被取消成功

boolean isDone()判断任务是否已经完成

V get()用于获取执行结果(返回值),这个方法会发生阻塞,一直等到任务执行完才会执行结果。

V get(long timeout,TimeUnit unit)如果指定时间内没有得到结果就返回null

四.3种多线程实现方式的对比分析。

实现Runnable接口比继承Thread类所更具有的优势:

 1.可以避免Java中的单继承的局限性

2.线程池只能放入实现Runnable或Callable类的线程,不能直接放入继承Thread类的线程

其它方面的对比,我通过下面一个多窗口售票的案例来对比分析更多优略:

4个窗口卖100张票,这100张票可以看作是共享资源。4个售票窗口可以看成是4个线程。

我们先用第1种方法(继承Thread类)来实现这个案例:

package demo04;

//定义一个继承自thread类的子类
class TicketWindow extends Thread{
    private int tickets=100;

    @Override
    public void run() {
        while (true){
            if (tickets>0){
                System.out.println(Thread.currentThread().getName()+"正在发售第"+tickets--+"张票");
            }
        }
    }
}

public class test01 {
    public static void main(String[] args) {
        //创建4个线程对象来作为窗口卖票
        new TicketWindow().start();
        new TicketWindow().start();
        new TicketWindow().start();
        new TicketWindow().start();
    }
}

 运行后会发现重复卖票了,也就是说票没有共享。同一张票被卖了四次。这很明显不是我们想要的答案吧。

这时候就要通过实现Runnable接口来实现多线程:

package demo04;

//定义一个实现了Runnable接口的实现类
class TicketWindow implements Runnable{
    private int tickets=100;
    @Override
    public void run() {
        while (true){
            if (tickets>0){
                System.out.println(Thread.currentThread().getName()+"正在发售第"+tickets--+"张票");
            }
        }
    }
}


public class test02 {
    public static void main(String[] args) {
        //创建TicketWindow实例对象
        TicketWindow ticketWindow=new TicketWindow();
        new Thread(ticketWindow,"窗口1").start();
        new Thread(ticketWindow,"窗口2").start();
        new Thread(ticketWindow,"窗口3").start();
        new Thread(ticketWindow,"窗口4").start();
    }
}

 这次就可以共享资源了,不会出现票被重复卖的情况。

五.后台线程

新创建的线程默认是前台线程,对于java程序来说,只要有前台线程在运行,这个进程就不会关闭。如果某个线程在启动之前(start()方法)调用了setDaemon(true)语句,这个线程就会变成一个后台线程。

java中,每次程序运行至少启动2个线程,1个是main线程,另一个是垃圾收集线程。因为每当执行一个java程序时,都会启动一个jvm,每一个jvm其实就是操作系统中启动了一个进程。