java多线程总结

Posted 小艾路西里

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java多线程总结相关的知识,希望对你有一定的参考价值。

1. 多线程简介

在了解线程前,要先明白进程

进程是计算机正在运行的一个独立的应用程序

线程是组成进程的基本单位,可以完成特定的功能,一个进程是由一个或多个线程组成的

多线程是指在一个进程中,多个线程同时执行,但这里说的同时执行并不是真正意义上的同时执行(限于单个cpu的情况,多核就是真正的同时执行)

系统会为每个线程分配 CPU 资源,在某个具体的时间段内 CPU 资源会被一个线程占用

在不同的时间段内由不同的线程来占用 CPU 资源,所以多个线程还是在交替执行任务,不过因为 CPU 运行速度太快,我们感觉是在同时执行

可以总结为:线程启动后,线程便进入了就绪状态,但是cpu只能执行一个线程,所以多个线程启动需要等待占用的线程释放资源

2. java中多线程的使用

java程序一运行,就会开启两个线程,一是main方法的线程,二是垃圾回收线程(回收new出来的对象等 类似c++中的delete)

(1) java中开启多线程的方法

① 继承 Thread 类,重写run方法,使用start调用

② 继承 Runnable 接口,重写run方法,将该实现类对象注入到Thread构造器中生成Thread对象,再使用Thread对象的start方法

③ 继承 Callable 接口,重写call()方法,并包装成FutureTask对象,再作为参数传入Thread构造器,调用Thread对象的start方法

重写run()方法,相当于是将任务(业务代码)嵌入该线程(方法)中

调用run方法只是普通调用类的方法,而start()方法才是开启线程

只有通过start方法才能开启线程,进而去抢占 CPU 资源,当某个线程抢占到 CPU 资源后,会自动调用 run 方法

① 继承 Thread 类,重写run方法,使用start调用

注:Thread类继承了Runnable接口

public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("MyThread is running");
    }
}
public class TestThread {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start(); //开启myThread线程
        System.out.println("=============");
    }
}

② 继承 Runnable 接口,重写run方法,将该实现类对象注入到Thread构造器中生成Thread对象,再使用Thread对象的start方法

可以看作Runnable是线程要完成的任务,而 Thread才是线程

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("MyRunnable is running");
    }
}
public class TestThread {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable(); //创建MyRunnable的实例化对象
        Thread thread = new Thread(myRunnable); // 将实例化对象作为构造函数参数生成Thread对象
        thread.start(); //开启线程
    }
}

通过lambda表达式实现解耦的写法(因为Runnable是函数式接口,所以能用匿名函数的方式)

public class MyRunnable2 {
    void run1(){
        System.out.println("第1种run方法");
    }
    void run2(){
        System.out.println("第2种run方法");
    }
}
public class TestThread {
    public static void main(String[] args)  {
        
        new Thread(()->{
            new MyRunnable2().run1();   //调用第一种任务 run1
        },"线程1").start();

        new Thread(()->{
            new MyRunnable2().run2(); //调用第二种任务 run2
        },"线程2").start();
	}
}

③ 继承 Callable 接口,重写call()方法,并包装成FutureTask对象,再作为参数传入Thread构造器,调用Thread对象的start方法

callable接口不同在于call()方法有返回值

public class MyCallable implements Callable<String>{
    @Override
    public String call() throws Exception {
        return "MyCallable is running";
    }
}
public class TestThread {
    public static void main(String[] args)  {
        MyCallable myCallable = new MyCallable(); //创建MyCallable实例化对象
        FutureTask<String> futureTask = new FutureTask(myCallable); //将MyCallable实例化对象注入FutureTask构造器生成futureTask对象
        Thread thread = new Thread(futureTask); // 用futureTask生成Thread对象
        thread.start(); //开启线程
        try {
            String value =  futureTask.get(); // 获得线程执行的返回值
            System.out.println(value);  // 输出为MyCallable is running
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

④ 通过线程池创建

(2) 不同方式开启线程的区别

① 继承Thread类,重写run()方法即可

优点:编写简单
缺点:不能继承其他类,功能单一

备注:Thread类继承了Runnable接口

②实现Runnable接口,重写run()方法,并将该实现类作为参数传入Thread构造器

优点:可以继承其他类,避免了单继承的局限性;适合多个相同任务的线程共享一个资源(就是相同的任务不需要重写,直接放入不同的Thread中),实现了解耦操作,代码和线程独立

缺点:实现相对复杂

③ 实现Callable接口,重写call()方法,并包装成FutureTask对象,再作为参数传入Thread构造器

优点:可以获取返回值,其他和继承Runnable一样

缺点:实现复杂

(3) 线程的5种状态

创建状态

就绪状态

运行状态

阻塞状态

终止状态

① 创建状态 (实例化一个新的线程对象,还未启动)

比如 Thread thread = new Thread() 就是创建状态

注意线程必须是Thread类或者是子类,不能是Runnable接口或Callable

② 就绪状态 (线程对象调用start方法,进入线程池等待抢占 CPU资源)

thread.start()时线程便进入了就绪状态

之前已经说过,线程需要抢占cpu资源才能运行,在等待cpu资源这段时期便会进入线程池等待,这段等待时间便是就绪状态

③ 执行状态:线程对象获取了 CPU 资源,在一定的时间内执行任务

thread.start()之后,如果获取了cpu资源,则进入了执行状态

④ 阻塞状态:正在运行的线程暂停执行任务,释放所占用的 CPU 资源时的状态

在解除阻塞状态之后也不能直接回到运行状态,而是重新回到就绪状态,等待获取 CPU 资源

⑤ 终止状态 :线程运行完毕 或因为异常导致该线程终止运行

线程正常运行结束,或遇到异常提前结束

以一个简单例子解释多线程的争夺流程:

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 1; i <10 ; i++) {
            System.out.println(i+"myThread is running");
        }
    }
}
public class TestThread {
    public static void main(String[] args)  {

        MyThread myThread = new MyThread(); //创建新的线程
        myThread.start(); //线程进入就绪状态

        for (int i = 0; i < 10; i++) {
            System.out.println(i+"mainThread is running");
        }
}

在这里插入图片描述
当main方法执行时,便开启了main线程(主线程),代码一行一行执行,当到了MyThread myThread = new MyThread(); 这一行时,一个线程进入了创建状态,myThread.start();时,这个线程进入了就绪状态,在和main线程抢夺cpu资源,谁抢到cpu资源就执行谁的任务,所以开始了交替执行

3. 线程调度

(1) 线程休眠

线程休眠是指让线程暂停执行,从就绪或运行状态进入阻塞状态,将 CPU 资源让给其他线程的调度方式

通过调用Thread类的静态本地方法sleep使线程休眠

所谓native本地方法,就是利用c++/c实现的函数功能

因为是静态函数,所以类和类的对象都可以调用该方法
在这里插入图片描述
① 在线程的任务块中(run方法中)使其休眠

public class MyThread extends Thread{
    @Override
    public void run() {
        // TODO Auto-generated method stub
        try {
               sleep(1000); // 使线程执行任务时休眠1秒
           } catch (InterruptedException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();
        }
         System.out.println(i+"---------MyThread");
        }
    }
}

② 在线程启动前使线程休眠

MyThread2 thread = new MyThread2();
try {
    thread.sleep(1000); //使线程休眠1秒
} catch (InterruptedException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
}
thread.start(); //启动线程

③ 使main方法的线程休眠

直接在main方法中调用Thread.sleep方法

public class Test2 {
    public static void main(String[] args) {
        try {
              	Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
        	}
        }
    }
}

注意:sleep方法是Thread类的方法,所以继承Thread的类可以在run()方法里直接调用sleep,如果是实现Runnable接口则不能直接调用sleep方法,但是可以通过Thread.sleep()这种方式调用,在main中调用sleep也是同理

sleep()方法和wait()方法的区别
在这里插入图片描述

(2) 线程合并

线程合并是指将指定的某个线程加入到当前线程中,合并为一个线程,由两个线程交替执行变成一个线程中的两个子线程顺序执行(插入的先执行)

通俗点讲就是在一个线程执行的时候,插入另一个线程,让当前线程被堵塞,另一个线程先运行

实现方法:join函数

join函数有3个重载,常用的是以下两个

public final void join() throws InterruptedException {
        join(0);
    }
public final synchronized void join(long millis)

区别:

无参join函数是会让插入的线程执行结束,再回到当前线程执行任务

有参join函数会传入一个参数时间,该时间是允许插入线程所占用的时间,无论插入的线程在该时间内有没有执行结束,时间一到就回到当前线程执行

① join()方法的使用

先实现Runnable接口用作线程测试

public class JoinRunnable implements Runnable {
    @Override
    public void run() {
        // TODO Auto-generated method stub
        for(int i=0;i<100;i++) {
            System.out.println(i+"---JoinRunnable");
        }
    }
}

在mian方法中进行测试

代码说明:这时有2个线程,主线程、join线程,当i==5时,join线程合并到主线程中

public class JoinTest {
    public static void main(String[] args) {
        JoinRunnable joinRunnable = new JoinRunnable();
        Thread thread = new Thread(joinRunnable); // 创建线程
        thread.start(); // 开启线程
        for(int i=0;i<10;i++) {
            if(i == 5) {
                try {
                    thread.join(); //让该线程合并到main线程中
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            System.out.println(i+"---main");
        }
    }
}

运行结果:
在这里插入图片描述
9~94—JoinRunnable
在这里插入图片描述
可以看出,当i=5时,joinRunnable插入到主线程中,之后就一直在占用cpu资源,同时main方法主线程进入就绪状态,直到该线程执行完毕,再执行main方法的线程

② join(long millis) 的使用

给插入的线程给予了0.5秒的执行时间,并在JoinRunnable运行时的每次循环休眠0.1秒,好观察结果,其余和上面的代码流程相同

public class JoinRunnable implements Runnable {
    @Override
    public void run() {
        // TODO Auto-generated method stub
        for(int i=0;i<100;i++) {
            System.out.println(i+"---JoinRunnable");
            try {
                Thread.sleep(100); //在运行时的每次循环休眠0.1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class JoinTest {
    public static void main(String[] args) {
        JoinRunnable joinRunnable = new JoinRunnable();
        Thread thread = new Thread(joinRunnable); // 创建线程
        thread.start(); // 开启线程
        for(int i=0;i<10;i++) {
            if(i == 5) {
                try {
                    thread.join(500); //让该线程合并到main线程中,并给予其1秒的执行时间
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            System.out.println(i+"---main");
        }
    }
}

在这里插入图片描述
可以看出来JoinRunnable线程只插入执行了0.5秒,便将cpu资源让给了主线程,且主线程执行的时间内,JoinRunable线程是阻塞状态

(3) 线程礼让

线程礼让就是让当前正在执行的线程,停止占用cpu的行为,从执行状态进入就绪状态,CPU从就绪状态线程队列中只会选择与该线程优先级相同或者更高优先级的线程去执行

在yield方法源码上的注释是这么写的

/*Yield is a heuristic attempt to improve relative progression
between threads that would otherwise over-utilise a CPU. Its use
should be combined with detailed profiling and benchmarking to
ensure that it actually has the desired effect.*/

意思是

执行yield()方法后,该线程可能会放弃执行,让其他的线程执行,其实是给调度器一个暗示(hint),表示自己愿意让出CPU,但是调度器可能会忽略,因此还是当前线程继续执行

//暗示调度器让当前线程出让正在使用的处理器。调度器可自由地忽略这种暗示。也就是说让或者不让是看心情哒  

Yield方法的使用

public class YieldThread1 extends Thread{
    YieldThread1() {
        this.setName("线程1");
    }
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
           if (i==5)
                Thread.yield(); //使线程礼让
            System.out.println(Thread.currentThread().getName()+"---"+i);
        }
    }
}

public class YieldThread2 extends Thread{
    YieldThread2(){
        this.setName("线程2");
    }
    @Override
    public void run() { 
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"---"+i);
        }
    }
}

(4) 线程中断

线程中断是指让线程结束执行

线程会有多种结束执行的情况:

① 线程任务执行完毕,自然结束

② 线程执行过程中遇到错误抛出异常并停止

③ 手动停止线程

手动控制线程结束在java中有2种实现方式

① public void stop() (已过时)
② public void interrupt()

另外,有一个public boolean isInterrupted()方法,用于标记线程是否被执行了中断操作,也被称为中断标记位

如果一个线程被执行了中断操作(interrupt()方法),那么标记位会被记录为true(默认是false)

如果一个线程调用Object.wait() 或 Thread.sleep()、Thread.join()方法,那么标记位会被清空,也就是回到默认值false,且如果在这之后使用interrupt()方法使其中断,会抛出interrupt异常

interrupt() 方法的使用

interrupt()其实并没有终止线程,只是将线程的中断标记位设置为了true

如果想让线程停止执行任务,可以在重写run方法中用循环判断中断标记位,如下示例

public class MyThread extends Thread{
    @Override
    public void run() {
        int i=0;
        while(!isInterrupted()){
            i++;
            System.out.println(i+"---myThread is running");
        }
        System.out.println("线程任务结束");
    }
}
public class TestThread {
    public static void main(String[] args)  {
        MyThread myThread = new MyThread();
        myThread.start();

        System.out.println(myThread.getState());//打印线程的状态
        myThread.interrupt(); //中断线程(将中断标记位设为true)
        System.out.println(myThread.isInterrupted()); //打印标记位
        System.out.println(myThread.getState()); //打印中断后的状态
    }
}

在这里插入图片描述
可以看出,执行了 isInterrupted()操作,线程仍是在执行状态,而线程真正结束是在run方法执行结束才终止

4. 线程同步

(1) 线程不同步造成的问题

Java 中允许多线程并行访问,同一时间段内多个线程同时完成各自的操作

但是当多个线程同时操作同一个共享数据

以上是关于java多线程总结的主要内容,如果未能解决你的问题,请参考以下文章

经验总结:Java高级工程师面试题-字节跳动,成功跳槽阿里!

学习java第19天个人总结

号称史上最全Java多线程与并发面试题总结—基础篇

java多线程总结

Java多线程-线程池的使用与线程总结(狂神说含代码)

第十周java学习总结

(c)2006-2024 SYSTEM All Rights Reserved IT常识