JavaEE:多线程初阶(一)

一.认识线程

1.概念

(1)线程是什么?

一个线程就是⼀个"执行流".每个线程之间都可以按照顺序执行自己的代码.多个线程之间"同时"执行

着多份代码.

(2)为什么要有线程

1.单核 CPU 的发展遇到了瓶颈。要想提高算力,就需要多核 CPU. 而并发编程能更充分利用多核 CPU 资源.

2.有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做一些其他的工作,也需要用到并发编程.

其次,虽然多进程也能实现并发编程,但是线程比进程更轻量.

1.创建线程比创建进程更快.

2.销毁线程比销毁进程更快.

3.调度线程比调度进程更快.

(3)进程和线程的关系

进程是包含线程的。每个进程至少有一个线程存在,即主线程。

进程和进程之间不共享内存空间。同一个进程的线程之间共享同一个内存空间.

2.第一个多线程程序

感受多线程代码与普通代码的区别:

1.每一个线程都是单独的执行流

2.多个线程都是并发执行

这里我们创建两个线程,然后启动两个线程,多次执行,可以发现有时候先打印"Thread1 is running",有时候会先打印"Thread2 is running"

这就说明了CPU在线程调度这一块是随机调度的

java 复制代码
    Thread thread1 = new Thread(() -> {
        System.out.println("Thread1 is running!");
    });
    Thread thread2 = new Thread(() -> {
        System.out.println("Thread2 is running!");
    });
    thread1.start();
    thread2.start();

同时,我们还可以使用jconsole来观察线程

在这个路径下找到jconsole,然后打开

这个是jdk内置的功能,用来帮助我们查看线程

3.创建线程

start() 方法: 负责请求操作系统分配资源、创建新的执行栈,并最终调用 run()。这是底层的启动逻辑,开发者不应更改。

run() 方法: 这是一个空方法(或者说是一个占位符)。它定义了线程在获取 CPU 后要执行的业务逻辑

1.创建一个类继承Thread类重写run方法,通过start启动线程

java 复制代码
class myThread extends Thread{
    @Override
    public void run(){
        System.out.println("Thread1 is running!");
    }
}
public class Demo2 {
//通过继承Thread类来创建线程
    public static void main(String[] args) {
        myThread thread1 = new myThread();
        thread1.start();
    }
}

还可以根据匿名内部类的方法来实现

java 复制代码
        Thread thread2 = new Thread(){
            @Override
            public void run(){
                System.out.println("Thread2 is running!");
            }
        };
        thread2.start();

2.创建一个类实现Runnable接口,重写run方法,搭配Thread实例,通过start启动线程

java 复制代码
public class Demo3 {
    static class mythread implements Runnable{
    @Override
    public void run(){
        System.out.println("Thread1 is running!");
    }
}
    //通过实现Runnable接口来创建线程
public static void main(String[] args) {
    mythread thread1 = new mythread();
    Thread thread2 = new Thread(thread1);
    thread2.start();
}
}

或者通过匿名内部类的方式来创建

java 复制代码
    Thread t1 = new Thread(new Runnable(){
        @Override
        public void run(){
            System.out.println("Thread2 is running!");
        }
    });
    t1.start();

3.lambda表达式创建线程

java 复制代码
    public static void main(String[] args) {
        //通过lambda表达式来创建线程
        Thread t1 = new Thread(() -> {
            System.out.println("Thread1 is running!");
        });
        t1.start();
    }

4.多线程的优势----增加运行速度

测试代码:

java 复制代码
public class ThreadAdvantage {
    // 多线程并不一定就能提高速度,可以观察,count 不同,实际的运行效果也是不同的
    private static final long count = 10_000_0000;

    public static void main(String[] args) throws InterruptedException {
        // 使用并发方式
        concurrency();
        // 使用串行方式
        serial();
    }

    private static void concurrency() throws InterruptedException {
        long begin = System.nanoTime();

        // 利用一个线程计算 a 的值
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = 0;
                for (long i = 0; i < count; i++) {
                    a--;
                }
            }
        });
        thread.start();

        // 主线程内计算 b 的值
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }

        // 等待 thread 线程运行结束
        thread.join();

        // 统计耗时
        long end = System.nanoTime();
        double ms = (end - begin) * 1.0 / 1000 / 1000;
        System.out.printf("并发: %f 毫秒%n", ms);
    }

    private static void serial() {
        // 全部在主线程内计算 a、b 的值
        long begin = System.nanoTime();
        int a = 0;
        for (long i = 0; i < count; i++) {
            a--;
        }

        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }

        long end = System.nanoTime();
        double ms = (end - begin) * 1.0 / 1000 / 1000;
        System.out.printf("串行: %f 毫秒%n", ms);
    }
}

从这里我们可以看出多线程在提高效率的效果

二.Thread类及常见方法

1.Thread()常见的构造方法

我们可以通过代码来分别看一下通过这些方法创建的线程的区别

java 复制代码
    public static void main(String[] args) {
        //方法1创建线程
       Thread thread1 = new Thread();
       //方法2创建线程
       Thread thread2 = new Thread(new Runnable() {
           @Override
           public void run() {
               System.out.println("方法2创建线程");
           }
       });
       //方法3创建线程
       Thread thread3 = new Thread("线程3");
       //方法4创建线程
       Thread thread4 = new Thread(new Runnable() {
           @Override
           public void run() {
               System.out.println("方法4创建线程");
           }
       }, "线程4");
           // 启动线程
    thread1.start();
    thread2.start();
    thread3.start();
    thread4.start();
    }

来运行这四个线程,观察结果,可以看到只输出了两句话

也就是说,使用方法1和方法3创建线程,

对于方法1来说,我们只创建了线程对象,没有重写run方法,也就是说这个线程里面没有任何内容执行

对于方法3来说,我们只在创建线程对象的基础上给这个线程对象命名了,但是仍然没有重写run 方法,也就是说也不会去执行任何操作.

对于方法2和方法4,方法2是只重写了run方法,方法4是重写了run方法,也给这个线程对象进行命名

2.Thread的几个常见属性

1.ID 是线程的唯一标识,不同线程不会重复

2.名称是各种调试工具用

3.状态表示线程当前所处的一个情况,下面我们会进一步说明

4.优先级高的线程理论上来说更容易被调度到

5.关于后台线程,需要记住一点:JVM 会在一个进程的所有非后台线程结束后,才会结束运行。

6.是否存活,即简单的理解,为 run 方法是否运行结束了

我们来创建一个新的线程,用上述方法得到线程的各种属性

java 复制代码
        Thread t1 = new Thread(() ->{
        for(int i = 0 ; i < 10 ;i++){
            System.out.println("线程1正在执行"+i);
        }
         System.out.println("当前线程名称"+Thread.currentThread().getName()+"当前线程ID"+Thread.currentThread().getId());
        System.out.println("当前线程名称"+Thread.currentThread().getName());
        System.out.println("当前线程名称"+Thread.currentThread().getName()+"当前线程状态"+Thread.currentThread().getState());
        System.out.println("当前线程名称"+Thread.currentThread().getName()+"当前线程优先级"+Thread.currentThread().getPriority());
        System.out.println("当前线程名称"+Thread.currentThread().getName()+"当前线程是否为后台进程"+Thread.currentThread().isDaemon());
        System.out.println("当前线程名称"+Thread.currentThread().getName()+"当前线程是否存活"+Thread.currentThread().isAlive());
        System.out.println("当前线程名称"+Thread.currentThread().getName()+"当前线程是否中断"+Thread.currentThread().isInterrupted());
        }, "线程1");
        t1.start();

我们可以通过调用这些方法在需要观察状态的时候使用

3.启动一个线程

之前我们可以知道线程执行的是run方法里面的代码内容,

但是只有使用.start()方法之后,一个线程才算是被创建出来.

1.调用start()方法是非常快的,几乎没有阻塞

所以观察下列代码,在start完之后打印main线程正在执行

重复执行这个代码,大部分时候都是先打印"main线程正在执行",然后再打印"线程1正在执行"

虽然操作系统对于线程的调度是随机的.但是线程的创建是有开销的.

也就是说在start之后,操作系统对于t1线程和main线程是"并行关系"

所以说大部分时候,都是会先执行到打印"main线程正在执行"这句话

2.同一个线程只能start一次

我们可以试着对同一个线程start两次,看看会有什么情况,可以看到给我们报错了

显示IllegalThreadStateException非法线程状态异常

这是因为当一个线程执行完start之后就是处于就绪或者堵塞状态了

start里面对线程状态进行了判断,处于就绪或者堵塞状态的线程不能再次start.

4.中断一个线程

正常情况下,我们需要等一个线程执行完run方法里面的内容才能结束线程

但是有时候我们需要提前结束线程,尤其是在sleep的时候

这时候我们就有两种方法来提前结束线程了

1.通过变量

就如同下面的代码,通过设置一个flag变量,改变flag来控制线程是否结束

java 复制代码
public class Demo9 {
    public static boolean flag = true;
public static void main(String[] args) {
    Thread t1 = new Thread(() ->{
        while(flag){
            System.out.println("线程1正在执行");
        }
    });
    t1.start();
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    flag = false;
}
}

2 .使用isInterruptted()方法

在Thread对象中,包含了一个内置boolean变量,也就是相当标志位,

如果为false,就说明没有人去尝试结束这个线程,如果为true就说明有人尝试结束这个线程

当我们直接尝试去调用这个方法的时候会显示异常

这是因为针对lambda表达式的定义,是在new Thread()之前的,也就是说,这个时候还不存在t1这个变量

这里我们就需要使用Thread.currentThread()这个方法了,哪个线程调用这个方法,就返回哪个线程的引用

然后需要结束的时候在调用interrupt()这个方法,就是把标志位从false改为true.

java 复制代码
public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        while(!Thread.currentThread().isInterrupted()){
            System.out.println("线程1正在执行");
        }
    });
    t1.start();
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    t1.interrupt();
    System.out.println("线程1是否被中断: " + t1.isInterrupted());
    }

这个就是上述代码执行的效果.

注意:在程序终止这里有一个奇怪的设定,如果说线程正在sleep的时候,调用interrupt()方法会把线程提前唤醒.同时在唤醒之后,会把这个标志位重置为false,这个时候就需要手动决定是否结束线程了

还是刚刚那个代码,这次在线程中增加了一个2秒的休眠,然后再main线程中让线程启动,然后调用interrupt()方法

这个时候,线程并不会停止,而是会持续打印,

这是因为在调用interrupt方法的时候,t1线程在休眠中

这时候interrupt方法将线程1唤醒,

然后线程里面的标志位又重新回到false,

但是之后的代码又没有人把这个标志位改为true,所以线程就不会中断

java 复制代码
public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        while(!Thread.currentThread().isInterrupted()){
            System.out.println("线程1正在执行");
        }
    });
    t1.start();
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    t1.interrupt();
    System.out.println("线程1是否被中断: " + t1.isInterrupted());
    }

我们看try,catch这一块,其实当sleep被唤醒的时候,就会触发InterruptedException,但是下面我们只进行了打印这个异常的操作,并没有其他操作,所以说在捕获异常之后,还是会继续执行

我们将循环里面的打印操作删除,这样方便让我们看异常提示,这里就捕获到异常并打印了,但是没有进行其他操作,所以不会中断线程

如果我们在里面加入break,也可以起到中断线程的效果,同时还可以再break前面加一些善后逻辑

5.等待一个线程

由于操作系统是随机调度线程的,所以说当两个线程同时启动的时候,我们并不能确定哪一个线程先启动.

但是有时候我们需要将线程启动的顺序确定下来

这时候就可以使用.join()方法了

对于这个代码在上面就已经被说过了,大部分时候都是会先打印main线程正在执行

但是如果我们就是要先打印"线程1正在执行"呢?

这时候可以在main线程里面调用t1.join()

这时候main线程就会等待t1线程结束之后再继续t1.join这句代码后面的逻辑

同时在使用join的时候是需要抛出一个异常的

这时候我们无论执行多少次都是会先打印"线程1 正在执行"

同时我们还可以通过增加参数来限制等待的时长,如果不加参数的话,就是死等.

6.获取当前线程引用

使用这个方法就可以获取调用这个方法的线程的引用,在上面已经演示过很多次了

7.休眠当前线程

通过sleep()方法进行休眠操作,第二种方法可以更高精度的填写休眠时间.也已经使用过很多次了在上面

三.线程的状态

下面是线程的各种状态

1.NEW

Thread对象创建了,但是还没开始start

java 复制代码
public static void main(String[] args) {
    Thread t1 = new Thread(() ->{
        for(int i = 0 ; i < 10 ;i++){
            System.out.println("线程1正在执行"+i);
        }
    });
    System.out.println("线程1当前状态:"+t1.getState());
}

2.RUNNABLE

就绪状态,随时可以去cpu上运行,代码中不触发阻塞状态的时候都是RUNNABLE状态

java 复制代码
public static void main(String[] args) {
    Thread t1 = new Thread(() ->{
        for(int i = 0 ; i < 3 ;i++){
            System.out.println("线程1正在执行"+i);
        }
        System.out.println("线程1当前状态:"+Thread.currentThread().getState());
    });
    t1.start();
}

3.TERMINATED

线程执行完了,但是Thread对象还在

剩下的这三个都是属于阻塞状态,只不过是不同情况下的阻塞状态

4.BLOCKED

这个是由于加锁时产生的阻塞状态,后面会重点讨论,先演示一下

对于这个代码,我们给线程1和线程2都加了同一把锁,

这时候线程1没解锁的时候,线程2是拿不到锁的,此时线程2就是在等待线程1解锁,这个状态就是BLOCKED

java 复制代码
public class Demo13 {
    static Object object = new Object();
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() ->{
        synchronized (object) {
            while(true){

            }
        }
    });
    Thread t2 = new Thread(() ->{
        synchronized (object) {
            while(true){

            }
        }
    });
    t1.start();
    t2.start();
    System.out.println("线程1当前状态:"+t1.getState());
    System.out.println("线程2当前状态:"+t2.getState());
}
}

5.WAITING

这个是无时间的等待,通常出现在无时间版本的join中

通过在t2线程中加入t1.join(),等待t1线程执行结束再执行t2

此时t1线程的状态是RUNNABLE,t2的状态就是WATING

java 复制代码
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() ->{
        while(true){

        }
    });
    Thread t2 = new Thread(() ->{
        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    t1.start();
    t2.start();
}

6.TIMED_WATING

这个是有时间的等待,通常是出现在有时间版本的join中

将上述代码的join增加等待时间,t2线程的状态就会变成TIME_WATING

java 复制代码
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() ->{
        while(true){

        }
    });
    Thread t2 = new Thread(() ->{
        try {
            t1.join(1000000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        while(true){
            
        }
    });
    t1.start();
    t2.start();
}

多线程初阶(一)到这里就结束了,后面会为大家带来更多关于线程问题的知识讲解.

相关推荐
C_心欲无痕18 小时前
ts - 模板字面量类型与 `keyof` 的魔法组合:`keyof T & `on${string}`使用
linux·运维·开发语言·前端·ubuntu·typescript
最贪吃的虎18 小时前
Redis其实并不是线程安全的
java·开发语言·数据库·redis·后端·缓存·lua
一勺菠萝丶18 小时前
Java 后端想学 Vue,又想写浏览器插件?
java·前端·vue.js
乾元18 小时前
无线定位与链路质量预测——从“知道你在哪”,到“提前知道你会不会掉线”的网络服务化实践
运维·开发语言·人工智能·网络协议·重构·信息与通信
xie_pin_an18 小时前
C++ 类和对象全解析:从基础语法到高级特性
java·jvm·c++
AC赳赳老秦18 小时前
Unity游戏开发实战指南:核心逻辑与场景构建详解
开发语言·spring boot·爬虫·搜索引擎·全文检索·lucene·deepseek
Tao____18 小时前
企业级物联网平台
java·网络·物联网·mqtt·网络协议
山峰哥18 小时前
数据库工程与SQL调优实战:从原理到案例的深度解析
java·数据库·sql·oracle·性能优化·编辑器
kaico201819 小时前
远程调用组件openfeign
java·spring cloud