13 Java多线程

文章目录


前言

1 进程与线程

先介绍一些基本概念。

  • 多线程:多就是字面意思,我们只要了解什么是线程就可以了
    线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
    多线程可以简单理解为:应用软件中互相独立,可以同时运行的功能
    其实,最简单的理解就是理解为,一个线程 = 一个任务 就好理解了。
    有了多线程,我们就可以让程序同时做多件事情
  • 进程(操作系统正在运行的一个软件就称为一个进程,例如QQ):进程是程序的基本执行实体(可以这么理解,每一个运行的软件就是一个进程(当然,也有软件使用多进程))
    比较直观的进程理解就是,打开我们电脑上的任务管理器,在这里插入图片描述
    上图中每一个软件就是一个进程,一个进程下面有多个线程。
    也举一个例子:360软件
    在这里插入图片描述
    这4个功能是可以同时运行的,这就是4个线程。所以多线程,可以简单理解:应用软件中互相独立,可以同时运行的功能
  • 多线程的应用场景:只要你想让多个事情同时运行就需要用到多线程
    比如: 软件中的耗时操作、所有的聊天软件、所有的服务器
  • 线程和进程的关系:一个线程一定是属于某个进程,但一个进程可以拥有多个线程。

2 并发与并行

下面这个参考视频我感觉讲的很好,之前对这两个概念一种不是很了解,看了这个视频,感觉一下就通透了。
并发和并行的理解

(1)实际生活中的并发与并行

在讲编程中的并发和并行前,我们先来了解现实案例中的并发与并行。

有两个任务,劈柴和搬砖,一个人单独完成其中某项都是要10分钟完成。在这里插入图片描述

  • 并发:现在你只有你一个人去完成这两个任务,你采取了一种特殊的完成方法,频繁切换工作内容,不断的交替执行这两项任务。只要你切换的足够快,那么从宏观上来看的话,这两个任务是在同时被执行,但时间上任意一个时刻都只有其中一个任务在被推进。这就是我们现实生活中的并发。
  • 并行:现在假设你的朋友过来帮你一起执行这两个任务,那么你去搬砖,你的朋友去劈柴。这种情况下,两个任务真正的同时被推进,这就是所谓的并行。

(2)编程中的并发与并行

回到编程中的场景:

  • cpu中的一个核心就相当于上面案例中的一个人,一个cpu核心 = 一个人在这里插入图片描述

  • 系统中的一个线程thread就相当于上面案例中的一个任务,一个线程 = 一个任务 在这里插入图片描述
    线程就是cpu需要执行的任务。

a. 最早期的单核CPU情形下,我们如果有多个任务需要执行(多线程),很显然只能用并发的形式去执行这些线程(任务),不得不频繁切换工作任务(线程)。
假设现在系统中qq有两个任务(两个线程)要执行,但是由于我们是单核CPU,只有一个人能干活。那么这个单核cpu就不得不采用并发,频繁切换线程(工作任务)。单核cpu在这两个任务中切换的足够快到人感知不出来,所以从宏观上来看,qq中有两个程序在同时运行。
b. 现在都是多核CPU
不妨设我们是双核cpu
还是假设现在系统中qq有两个任务(两个线程)要执行,但是由于我们是双核CPU,有两个人能干活。那就简单了。一个核心去干这个线程,另一个核心去干另一个线程。此时两个线程(任务)同时推进,这就形成了并行。在这里插入图片描述
更复杂一点就是并行和并发同时存在
以2核4线程计算机举例,2核指有两个干活的人,4线程就是该cpu最多能干的活的个数
现在如果还是qq中有两个任务,那简单,一个核心干一个,直接并行就解决了
但是如果是4个任务呢?很显然两个核心,人不够啊,那么这么干,核心1在任务1和任务2之间频繁切换干,核心2在任务3和任务4间频繁切换干。这种情况显然是并行和并发同时存在的。
也行你会问,如果有5个任务呢?很抱歉2核4线程计算机不能再宏观上看似在同时执行4个任务,其中一个任务会等前面有一个任务结束(而且现在还有一种新技术就是一个核心可以同时干两个线程,就相当于一个核心=2个人这种,这里我们不管这么多,我们暂时就将一个核心=1个人就可以了)

还有更加复杂的,上述的所有案列都是在一个假设情况下的,所以线程(任务)相互独立,没有依赖关系。如果有依赖关系呢?那就更复杂了
c. 任务(线程)间存在依赖关系
在编程中的处理方案就是,存在依赖关系的线程之间有一把锁,只有拿到锁的线程才可以运行。
存在依赖关系的线程是不能够并行的,依赖就意味着存在执行的先后关系。
即使是两个线程(任务)属于不同cpu核心,但锁的机制保证,就算你两是不同人来干,但是也只有拿到锁的那个任务才可以干。

  • 最后回答一个问题:关于程序员在开发中是否要关心并发和并行者两个概念?
    答:大部分情况下并不需要关心其细节,因为操作系统的存在,关于cpu核心和线程的调度问题系统会自动帮我们调度。

一、Java中的三种多线程实现方式

1 多线程的第一种实现方式:继承Thread类的方式进行实现

将一个类声明为 Thread 的子类。该子类应重写类 Thread 的 run 方法(run方法里面写我们具体要执行的代码,要干的任务是什么就可以了)。然后再调用start方法启动这个线程就可以了(特别注意是调用start方法,不是run方法)。

package cn.hjblogs.business;

public class Test {
   
    public static void main(String[] args) {
   
        Mythread t1 = new Mythread();
        t1.start();
    }
}


class Mythread extends Thread{
   
    @Override
    public void run() {
   
        for (int i = 0; i < 100; i++) {
   
            // 重写run方法,run方法中的代码就是线程需要执行的代码,也就是这个线程要干的任务
            System.out.println(this.getName() + ": Hello World!");
        }
    }
}

在这里插入图片描述

我们可以用线程来验证一下,多线程是交替切换运行的

public class Test {
   
    public static void main(String[] args) {
   
        Mythread t1 = new Mythread();
        Mythread t2 = new Mythread();
        // 设置线程名称,可取可不取,里面有默认的线程名称
        t1.setName("线程1");
        t2.setName("线程2");


        t1.start();
        t2.start();
    }
}


class Mythread extends Thread{
   
    @Override
    public void run() {
   
        for (int i = 0; i < 100; i++) {
   
            // 重写run方法,run方法中的代码就是线程需要执行的代码,也就是这个线程要干的任务
            System.out.println(this.getName() + ": Hello World!");
        }
    }
}

在这里插入图片描述

2 多线程的第二种实现方式:实现Runnable接口(函数式接口)的方式进行实现

创建线程的另一种方法是声明一个实现 Runnable 接口的类。然后该类实现 run 方法。(run方法里面写我们具体要执行的代码,要干的任务是什么就可以了)。然后将这个实现类对象传给Thread就构建了一个线程,然后再调用start方法启动这个线程就可以了(特别注意是调用start方法,不是run方法)。

可以发现和继承那种写法基本一致,一个是继承重写run方法,这个是实现接口重新run方法;另外这种实现接口的方式多了一步,将实现类对象传给Thread对象。

public class Test {
   
    public static void main(String[] args) {
   
        Mythread mythread = new Mythread();
        // 多了一步,将mythread对象传入Thread类中,然后调用start方法
        Thread t1 = new Thread(mythread);
        t1.start();
    }
}


class Mythread implements Runnable{
   
    @Override
    public void run() {
   
        for (int i = 0; i < 10; i++) {
   
            System.out.println("hello world");
        }
    }
}

在这里插入图片描述
这么一看似乎这种调用方式没有第一种直接继承Thread类好用啊,但是接口接口,你如果想一想匿名内部类这种调用方式就有意义了
Runnable接口是一个函数式接口,那么使用匿名内部类,lambda表达式、方法引用就有意思了。

public class Test {
   
    public static void main(String[] args) {
   
        // 使用匿名内部类
        Thread t1 = new Thread(new Runnable() {
   
            @Override
            public void run() {
   
                for (int i = 0; i < 10; i++) {
   
                    System.out.println("hello");

                }
            }
        });
        t1.start();

//        // 使用lambda表达式
//        Thread t1 = new Thread(() -> {
   
//            for (int i = 0; i < 10; i++) {
   
//                System.out.println("hello");
//            }
//        });
//        t1.start();
    }
}
public class Test {
   
    public static void main(String[] args) {
   
        // 使用匿名内部类
        Thread t1 = new Thread(Test::run);
        t1.start();
    }
    
    public static void run() {
   
        for (int i = 0; i < 10; i++) {
   
            System.out.println("hello");
        }
    }
}

在这里插入图片描述
这才是这种方法的正确打开方式

3 多线程的第三种实现方式:实现Callable接口(优点:可以获得线程的返回值)

可以看到在前面两种方法中,run方法没有返回值,这意味着我们无法获得线程的返回值,如果我们想要获得返回值的话就需要采取这种方法。下面是步骤:

  • step1: 创建一个类MyCallable实现callable接口
  • step2: 重写call(是有返回值的,表示多线程运行的结果)
  • step3: 创建MyCallable的对象(表示多线程要执行的任务)
  • step4: 创建FutureTask的对象(作用管理多线程运行的结果)
  • step5: 创建Thread类的对象,并启动(表示线程)
public class Test {
   
    public static void main(String[] args) throws ExecutionException, InterruptedException {
   
        // 创建MyCallable对象(表示多线程要执行的任务)
        MyCallable mc = new MyCallable();
        // 创建FutureTask对象(作用管理多线程运行的结果),FutureTask是RunnableFuture接口的实现类
        FutureTask<Integer> ft = new FutureTask<>(mc);
        // 创建Thread对象,传入FutureTask对象
        Thread t1 = new Thread(ft);
        // 启动线程
        t1.start();

        // 获取线程执行结果
        Integer res = ft.get();
        System.out.println(res);  // 5050

    }

}

class MyCallable implements Callable<Integer> {
   
    // 这里的泛型参数是返回值类型
    @Override
    public Integer call() throws Exception {
   
        int sum = 0;
        for (int i = 0; i <= 100; i++) {
   
            sum += i;
        }
        return sum;
    }
}

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

优点 缺点
继承Thread类 编程比较简单,可以直接使用 ,编程比较简单,可以直接使用 可以扩展性较差,不能再继承其他的类
实现Runnable接口 扩展性强,实现该接口的同时还可以继承其他的类 编程相对复杂,不能直接使用 ,Thread类中的方法
实现Callable接口 扩展性强,实现该接口的同时还可以继承其他的类。最重要的是能返回多线程的结果 编程相对复杂,不能直接使用 ,Thread类中的方法

二、多线程的常用方法

方法名称 说明
string getName() 返回此线程的名称
void setName(string name) 设置线程的名字(构造方法也可以设置名字)
static Thread currentThread() 获取当前线程的对象
static void sleep(long time) 让线程休眠指定的时间,单位为毫秒
setPriority(int newPriority) 设置线程的优先级
final int getPriority() 获取线程的优先级
final void setDaemon( boolean on) 设置为守护线程
public static void yield() 出让线程/礼让线程
public static void join() 插入线程/插队线程

1 线程的name

细节:如果我们没有给线程设置名字,线程也是有默认的名字的。格式: Thread-X(X序号,从0开始的)

(1)string getName() :返回此线程的名称

(2) void setName(string name):设置线程的名字(构造方法也可以设置名字)

2 两个静态方法 :获取当前线程对象和线程休眠

(1)static Thread currentThread():获取当前线程的对象

public class Test {
   
    public static void main(String[] args) throws ExecutionException, InterruptedException {
   
        // 获取当前正在执行的线程对象
        Thread cur_t = Thread.currentThread();
        System.out.println(cur_t.getName());   // main

        // 细节
        // JVM虚拟机启动之后,会自动的启动多条线程其中有一条线程就叫做main线程
        // 他的作用就是去调用main方法,并执行里面的代码
        // 在以前,我们写的所有的代码,其实都是运行在main线程当中
    }
    
}

(2)static void sleep(long time) :让线程休眠指定的时间,单位为毫秒

细节:

  • 哪条线程执行到这个方法,那么哪条线程就公在这里停留对应的时间方法的参数:就表示唾I眠的时间,单位毫秒(1秒= 1800毫秒)
  • 当时间到了之后,线程会自动的醒来,继续执行下面的其他代码

我们先来休眠我们的main线程试试

public class Test {
   
    public static void main(String[] args) throws ExecutionException, InterruptedException {
   
        System.out.println("main线程开始执行了");
        Thread.sleep(5000); // 让main线程睡眠5秒
        System.out.println("main休眠5秒后又开始执行了");  // 等5秒后才会执行这个打印代码
    }

}

在来看我们自己的线程

public class Test {
   
    public static void main(String[] args) throws ExecutionException, InterruptedException {
   
        MyThread myThread = new MyThread();
        myThread.start();


    }

}

class MyThread extends Thread {
   
    @Override
    public void run() {
   
        for (int i = 0; i < 10; i++) {
   
            System.out.println("当前线程名字:" + Thread.currentThread().getName() + " i=" + i);
            try {
   
                Thread.sleep(2000);  // 线程休眠2秒在进行下一轮打印
            } catch (InterruptedException e) {
   
                throw new RuntimeException(e);
            }

        }
    }

}

3 线程的优先级(随机抢占cpu资源)

线程的优先级是什么,我们要知道在多线程的情况下,cpu的资源是有限的。所以Java采用的是枪占式调度(随机性),哪一个线程抢到了cpu资源这个时刻就归它用。既然是大家一起抢,那肯定可以设置有的人抢到的概率大,有的人抢到的概率小啊。

这里的线程优先级就是线程抢占cpu资源的级别,从底到高,一共 1-10级。
默认情况下我们不对线程的优先级进行设置,默认都是5(就算是main线程也是5)

(1)final int getPriority() :获取线程的优先级

public class Test {
   
    public static void main(String[] args) throws ExecutionException, InterruptedException {
   
        int priority = Thread.currentThread().getPriority();
        System.out.println("当前线程的优先级:" + priority);   // 当前线程的优先级:5
    }

}

(2)setPriority(int newPriority) :设置线程的优先级

public class Test {
   
    public static void main(String[] args) throws ExecutionException, InterruptedException {
   
        Thread t1 = new MyThread();
        Thread t2 = new MyThread();

        t1.setName("飞机");
        t2.setName("火车");

        t1.setPriority(10);   // 飞机这个线程优先级我们设置为10,最高
        t2.setPriority(1);    // 火车这个线程优先级我们设置为1,最低

        t1.start();
        t2.start();


    }

}

class MyThread extends Thread {
   
    @Override
    public void run() {
   
        for (int i = 0; i < 100; i++) {
   
            System.out.println(Thread.currentThread().getName() + " i=" + i);
        }
    }

}

多运行几次,可以看到火车这个线程最后完成的概率比较大。但这不是绝对的,也是一直概率情况。毕竟火车这条线程抢cpu资源的能力按上面设置是抢不过飞机这条线程的。

4 设置为守护线程

(1)设置为守护线程:final void setDaemon( boolean on) – 设置为守护线程

  • 守护线程:当其他的非守护线程执行完毕之后,守护线程会陆续结束。
public class Test {
   
    public static void main(String[] args) throws ExecutionException, InterruptedException {
   
        MyThread1 t1 = new MyThread1();
        MyThread2 t2 = new MyThread2();

        t1.setName("女神");
        t2.setName("备胎");

        // 将备胎线程设置为守护线程
        t2.setDaemon(true);

        t1.start();
        t2.start();



    }

}

class MyThread1 extends Thread {
   
    @Override
    public void run() {
   
        for (int i = 0; i <= 10; i++) {
   
            System.out.println(Thread.currentThread().getName() + " i=" + i);
        }
    }

}

class MyThread2 extends Thread {
   
    @Override
    public void run() {
   
        for (int i = 0; i <= 100; i++) {
   
            System.out.println(Thread.currentThread().getName() + " i=" + i);
        }
    }

}

在这里插入图片描述
从上面结果看出,非守护线程(女神)结束过后大概过了一小会守护线程(备胎)也结束了,并且守护线程才进行到25轮循环,都还没有运行完,这就是将一个线程设置成守护线程的效果。

(2)设置为守护线程的应用场景

这个有什么应用场景呢?
在qq聊天的场景中:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值