您当前位置: 南顺网络>> 官方资讯>> 建站知识

Java基础之多线程没那么复杂!

多线程的引入

1.什么是多线程

线程是程序执行的一条路径,一个进程中可以包含多条线程;多线程并发执行可以提高程序的效率
</br>


2.进程和线程之间的关系

操作系统可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程。
<br>


3.多线程的应用场景

红蜘蛛同时共享屏幕给多个电脑
迅雷开启多条线程一起下载
QQ开启多人聊天
服务器同时处理多个客户的请求
<br>


多线程并行和并发的区别
  • 并行性和并发性是两个概念,并行性指在同一时刻,有多条指令在多个处理器上同时执行<br>

  • 并发性指的是同一时刻只有一条指令被执行,但多个进程指令被快速切换执行是的在宏观上具有多个进程被同时执行的效果
    <br>


    Java程序运行原理和JVM的启动是多线程的吗?

  • java程序运行原理

  • Java命令启动jvm,启动jvm等于启动一个应用程序,也就是启动了一个进程,该进程会自动启动一个“主线程”,然后主线程去调用某个类的main方法

  • Jvm启动是多线程的的吗

  • JVM启动至少启动了垃圾回收线程和主线程,所以是多线程
    <br>


    多线程的实现方式

    1. 继承Thread类创建线程类

    (1) 定义Thread的子类,并重写该类的run方法,该ru的执行体就代表了线程需要完成的任务,因此run()方法被称为线程执行体
    (2) 创建Thread子类的实例,即创建了线程对象<br>
    (3) 调用线程对象的start()方法启动该线程

package Demo;public class Demo1__Thread {

    public static void main(String[] args) {
        ChThread t=new ChThread();
        t.start();
        for(int i=0;i<1000;i++) {
            System.out.println("我是主方法!");
        }

    }}
 class ChThread extends Thread {

    @Override
    public void run() {

        super.run();
        for(int i=0;i<1000;i++) {
            System.out.println("我是run方法");
        }

    }

 }

上述代码的执行验证了多线程,如果上述程序的执行过程是多线程的话,会发现屏幕中的 ”我是主方法”和“我是run方法” 的字样是交替出现的,这说明了程序的的执行过程为并行执行Thread类的Start()方法启动run()方法的线程,和主方法中的执行同时进行。
<br>


2.实现Runnable接口创建线程类

(1) 定义一个实现了Runnable接口的实现类<br>
(2) 创建Runnable实现类的实例<br>
(3) 将创建的实例作为Thread类的target类创建Thread对象,该对象才是真正的线程对象 <br>
(4) 用创建的Thread对象启动线程

package xianchenhg;public class Thread_Running implements Runnable {

    private int i;
    public void run() {
        for(;i<100;i++) {
            System.out.println(Thread.currentThread().getName()+"  "+i);
        }
    }
    public static void main(String[] args) {
        for(int i=0;i<100;i++) {
            System.out.println(Thread.currentThread().getName()+"  "+i);
            if(i==20) {
                new Thread(new Thread_Running(),"线程1").start();
                new Thread(new Thread_Running(),"线程2").start();
            }
        }
    }}

查看API文档,会发现Runnable接口只定义了run()方法这一个抽象类,所以实现Runnable接口的实现类只有run()方法,仅作为线程执行体,所以,Runnable对象仅仅作为Thread对象的target,而实际的线程对象依然是Thread实例,Thread实例负责执行target的run()方法。

3.实现Callable接口和Future接口创建多线程

(1)Callable接口更像是Runnable接口的增强版,相比较Runable接口,Call()方法新增捕获和抛出异常的功能;Call()方法可以返回值<br>
(2)Future接口提供了一个实现类FutureTask实现类,FutureTaks类用来保存Call()方法的返回值,并作为Thread类的target。<br>
(3)调用FutureTask的get()方法来获取返回值

package Demo;import java.util.concurrent.Callable;import java.util.concurrent.ExecutionException;import java.util.concurrent.FutureTask;/**
 * (1)创建Callable接口的实现类,并重写call()方法,该call()方法作为线程的执行体,且有返回值
 * (2)创建了Callable接口的实现类的实例,并用FutureTask()方法包装对象,该FutureTask()对象实现了
 *    将对象的返回值包装的功能
 * (3)使用FutureTask对象将Thread对象的target,创建并启动线程
 * (4)调用FutureTask对象的get()方法获得子线程执行结束后的返回值
 * */public class Callable_Future implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {   //重写Callable接口中的call()方法

        int i=0;
        for(;i<100;i++) {
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
        return i;
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //创建Callable的对象
        Callable_Future ca=new Callable_Future();
        FutureTask<Integer> ft=new FutureTask<Integer>(ca);
        for(int i=0;i<100;i++) {
            //返回值主线程的名称和执行代号
            System.out.println(Thread.currentThread().getName()+" "+i);  
            if(i==20) {
            new Thread(ft,"Callable线程").start();

            //该方法将导致主线程被阻塞,直到call()方法结束并返回为止
             //System.out.println("子线程的返回值"+ft.get());  
            }
        }
        try{
            System.out.println("子线程的返回值"+ft.get());
        }catch (Exception e) {
            e.printStackTrace();
        }

    }}

上面的程序中,FutufeTask方法的get()方法将获得Call()方法的返回值,但是该方法将导致主线程受阻直到Call()方法结束并返回为止。

4.三种实现多线程的方式对比

实现方式重写方法启动线程的方式优点缺点
继承Thread类run()Thread对象.start()编程简单,直接用this即可获取当前对象继承Thread的子类不能继承其他的类
实现Runnable接口run()Thread对象.start()1.实现类可以继承其他类<br> 2.多个对象共享一个target,形成了清晰的模型编程复杂,必须使用Thread.currentThread()返回当前对象
实现Callable接口Call()Thread对象.start()1.同Runnable接口<br>2.Call()方法有返回值并能抛出异常同Runnable接口

通过上面的对比发现,一般在项目中,我们使用Runnable接或者Callable接口来实现多线程。

5.线程的生命周期

  1. 线程并不是创建过之后就开始执行的,也不是一直处于执行状态,一个线程的生命周期一个又五个:新建、就绪、运行、阻塞、死亡。<br>2.线程的这五种状态的原理参照《计算机组成原理2》多线程部分

6.控制线程

1.join线程

1.Thread类中的成员方法,让调用join()方法的线程处于等待状态,直到子线程执行结束<br>2.join()方法通常由使用线程的程序调用,用于将一个线程拆分成若干小的线程执行,执行结束最后由主线程进行进一步的操作

代码演示:

public class joinThread extends Thread {

    //创建一个有参构造函数,用来为线程命名
    public joinThread(String str) {
        super(str);
    } 

    //重写run方法
    public void run() {
        for(int i=1;i<100;i++) {
            System.out.println(getName()+" "+i);
        }
    }
    public static void main(String[] args)throws Exception {

        //启动子线程
        new joinThread("子线程").start();
        for(int i=0;i<100;i++) {
            if(i==20) {
                joinThread jt=new joinThread("新线程的子线程");
                jt.start(); //启动子线程
                jt.join();  //让主线程处于等待状态
            }
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }}

mian线程调用join之后,出去阻塞状态,两个子线程并发执行。直到两个子程序执行结束之后,main线程才开始执行。

join()线程有如下三种重载形式

1.join() :<br>
2.join(long millis) :被join的线程的时间最长为millis毫秒,如果超过了这个millis则线程不被执行<br>
3.join(long millis,int nanos) :被join的线程的时间最长为millis毫秒+nanos微秒,如果超过了这个时间则线程不被执行 <br>

第三种重载行驶一般用不到,因为无论是计算机系统还是计算机硬件,还没发精确到微秒

2.后台线程

后台线程运行于后台,任务是为其他线程提供服务,也被称为“守护线程”或“精灵线程”。JVM的垃圾回收机制就是一个典型的后台线程。<br> 通过调用Thread类的setDaemon(true)方法将线程设置为后台线程

代码演示:

public class DaemonThread extends Thread{

    @Override
    public void run() {
        for(int i=0;i<1000;i++) {
            System.out.println(getName()+" "+i);
        }
    }
    public static void main(String[] args) {
        DaemonThread dt=new DaemonThread();
        dt.setDaemon(true);
        dt.start();
        for(int i=0;i<100;i++) {
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }}

运行结果:

image

1.Thread-0的后台进程本该当i=999的时候才停止进行,但是程序中只进行到114次,这是因为所有的前台进程结束之后,后天进程的存在也就是去了意义,所以后台进程也跟着死亡。<br>
2.前台线程死亡之后,JVM会通知后台线程死亡,但是从后台线程接收指令到做出反应需要一定的时间,这就是为什么上述程序中的后台进程在main线程死亡之后后台进程还进行到114的原因。

3.线程睡眠

如果需要线程停顿一段时间进入阻塞状态,可以调用Thread类的静态方法sleep(),sleep()有两种重载形式<br>

  • sleep(long millis)
    让当前正在执行的线程暂停mili毫秒,进入阻塞状态。该方法受到系统计时器和线程调度器精度的影响。

  • sleep(long millis,int nanos)
    让当前正在执行的线程暂停milis毫秒+nanos微秒,进入阻塞状态。该方法受到系统计时器和线程调度器精度的影响(不常用)

代码演示:

public class SleepThread{
    public static void main(String[] args) throws Exception {
        for(int i=0;i<100;i++) {
            System.out.println(Thread.currentThread().getName()+" "+i);
            Thread.sleep(1000);
        }
    }}

运行结果:
观察程序想运行过程会发现,每一个进程之间相隔1秒。

4.线程让步

调用Thrad类的静态方法 yield()方法可以实现线程让步,和sleep()方法类似,yield()方法也是让当前正在运行的线程暂停,但是不会使线程阻塞,而是让线程进入就绪状态,让线程调度器重新安排一次线程调度,完全有可能出现的状况是,刚刚调用yield()方法进入就绪状态的线程就被线程调度器重新调度出来重新执行。

代码演示:

public class YieldThread extends Thread{

     public YieldThread(String str) {
        super(str);
    }
    @Override
    public void run() {
        // TODO Auto-generated method stub
        for(int i=0;i<60;i++) {
            System.out.println(getName()+" "+i);
            if(i==20){
                Thread.yield();                  //线程让步
            }
        }
    }

    public static void main(String[] args) {
        YieldThread yt1=new YieldThread("高级");
        yt1.setPriority(MAX_PRIORITY);           //将次线程设置成最高优先级
        yt1.start();                             //启动线程
        YieldThread yt2=new YieldThread("低级");
        yt2.setPriority(MIN_PRIORITY);
        yt2.start();
    }}

1.线程调用yield()方法后将执行的机会让给优先级相同的线程<br>2.高优先级的线程调用yield()方法暂停之后,系统中没有与之相同优先级和更高的优先级的线程,则线程调度器会将该线程重新调度出来,重新执行。

  • sleep()方法和yield()方法比较

方法执行机会线程状态是否抛出异常可移植性
sleep()线程暂停之后,会给其他线程执行的机会,不用理会线程的优先级阻塞状态InterruptedExextion异常
yield()线程暂停之后,只会给优先级相同或更高优先级的线程执行的机会就绪状态

5.改变线程优先级

1.一般线程具有优先级,更高优先级的线程比优先级低的线程能获得更多的执行机会<br>2.每个线程默认的优先级和创建他们的父线程的优先级相同。<br> 3.Thread类提供了setPriorit(int newPriority)、getPriority() 方法来设置和获取线程的优先级<br>4.也可以使用Thread类的3个静态常量来设置线程的优先级:<br>

MAX_PRIORITY : 值是10<br> MIN_PRIORIT : 值是1<br> NORM_PRIORITY : 值是5


编辑:--ns868