JavaEE:多线程基础,多线程的创建和用法

多线程基础

为什么要使用多线程

  • 充分利用CPU资源
    现代计算机几乎都是多核CPU,如果是单线程那便只能使用其中的一个核心,而其它核心处于空闲状态.一核有难,七核围观就是指一个8核的CPU只有一个核在使用,无法充分利用CPU的资源
  • 防止卡顿
    程序在运行中经常会遇到阻塞操作,比如网络请求,文件读写.这些操作会让当前的线程处于等待模式(一直等着外部响应,在获取响应数据前线程无法处理其它任务).如果是使用单线程,就会导致在等待期间程序卡死,无法对外界行为做出反应.但使用多线程时,可以让等待操作在后台执行,主程序仍然可以响应外部操作.你也不希望你刷短视频刷一半,突然断网程序就一直在等待网络,正在看的视频也被终止,必须等到网络重新连接上才能继续刷视频吧

进程与线程的概念与区别

进程和线程的概念
  • 进程是一个独立运行的程序实例.是操作系统资源分配的基本单位.
  • 线程是进程内的执行单元,是CPU调度的基本单位
    对于下面最简单的输出代码,就有一个线程
java 复制代码
public class Test {
    public static void main(String[] args) {
        System.out.println("hallo world");
    }
}

在运行这个代码时,会先创建一个java进程.然后在java进程中有一个线程会调用main方法

进程和线程的区别
- 进程 线程
资源分配 拥有独立的内存,文件 共享所属进程的资源,仅拥有独立的栈和程序计数器
独立性 进程之间完全独立,一个进程崩溃不会影响其它进程 线程依赖进程,同一进程内的线程共享资源,一个线程崩溃可能导致整个进程崩溃
开销 创建,销毁,切换的开销大(需要分配资源和释放资源) 开销小(仅需要维护少量独立资源,共享进程资源)
数量 系统中进程数量较少 一个进程可包含多个线程,数量通常远多于进程
总结
  1. 进程包含线程

一个进程中可以有一个线程,也可以有多个线程.但一个进程中不会有0个进程

  1. 进程是资源分配的基本单位;线程是调度执行的基本单位
  2. 每个进程都有自己独立的资源;一个进程的多个线程之间,共用同一份资源
  3. 进程和进程之间是隔离 的,因此一个进程崩溃不容易影响到别的进程.同一个进程的线程和线程之间是共享资源的.共享资源的好处是节省资源申请和销毁的开销,坏处是容易冲突,一个线程出问题容易影响到其它的线程

因此,线程也称为"轻量级进程"

创建线程的多种方式

Threand是一个在java.lang中的类.要使用Thread类得先继承thread类并且重写run()方法.还有一种是实现Runnable接口

继承Thread类并重写run()方法
  1. 创建一个类继承Thread类
java 复制代码
class MyThreand extends Thread{
        
}
  1. 重写Thread类的run()方法.重写的逻辑就是要执行的逻辑
java 复制代码
class MyThreand extends Thread{
    @Override
    public void run() {
        while (true) {
            System.out.println("hallo Thread");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
  1. 创建该子类的实例
java 复制代码
public class Test {
    public static void main(String[] args) {
        MyThreand myThreand = new MyThreand();
    }
}
  1. 调用实例的start()方法启动线程(不是直接调用run()方法)
java 复制代码
public class Test {
    public static void main(String[] args) {
        MyThreand myThreand = new MyThreand();
        myThreand.start();
    }
}

最后的代码如下

java 复制代码
class MyThreand extends Thread{
    @Override
    public void run() {
        while (true) {
            System.out.println("hallo Thread");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class Test {
    public static void main(String[] args) {
        MyThreand myThreand = new MyThreand();
        myThreand.start();
    }
}

运行之后会每0.5秒打印一次hallo Thread

实现Runnable接口
  1. 创建一个类实现Runnable接口
java 复制代码
class MyRunnable implements Runnable{
    @Override
    public void run() {
        
    }
}
  1. 实现Runnable的run()方法(定义线程逻辑)
java 复制代码
class MyRunnable implements Runnable{
    @Override
    public void run() {
        while(true){
            System.out.println("hallo Thread");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
  1. 创建Runnable实现类的实例,并将该实例作为参数传入Thread构造器,创建Thread实例
java 复制代码
public class Test1 {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
    }
}
  1. 调用实例的start()方法启动线程
java 复制代码
public class Test1 {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}

运行结果同继承Thread类,每0.5秒打印一次hallo Thread

使用Thread的注意点
  1. 线程启动必须调用start()方法(start()方法会自动调用run()方法),不是直接调用run()方法.调用start()方法会真正的创建一个线程,而调用run()方法则是只调用该方法,不会创建一个线程.相当于还是单线程.
  2. 一个线程只能启动一次.线程的生命周期是一次性的.调用start()后无法再对start()重新调用(即使run()方法执行完毕).
  3. 线程的执行顺序由操作系统调度决定.即使先调用thread1.steart()也有可能会是后调用的thread2.start()先执行
两种方式的优缺点
方式 优点 缺点
继承Thread 代码简单 受限于单继承,任务与线程耦合
实现Runnable 避免单继承限制,任务与线程解耦 需通过Thread.cuurentThread()获取线程
使用匿名内部类

使用匿名内部类的好处是可以简化代码(不用单独定义一个Thread子类),适合创建简单的线程

  1. 通过匿名内部类继承Thread

new Thread(){...}表示创建一个Thread的匿名类实例.这个匿名子类没有类名,只能能在创建时使用一次

java 复制代码
public class Test2 {
    public static void main(String[] args) {
        Thread thread = new Thread(){
            @Override
            public void run(){

            }
        };
    }
}
  1. 重写run()方法
java 复制代码
public class Test2 {
    public static void main(String[] args) {
        Thread thread = new Thread(){
            @Override
            public void run(){
                while(true){
                    System.out.println("hallo Thread");
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
    }
}
  1. 调用匿名内部类实例的start()方法

就是简单的加上一个thread.start();

java 复制代码
public class Test2 {
    public static void main(String[] args) {
        Thread thread = new Thread(){
            @Override
            public void run(){
                while(true){
                    System.out.println("hallo Thread");
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        thread.start();
    }
}
  • 使用匿名内部类的特点
  1. 代码更加简洁,可读性高
  2. 一次性
使用匿名内部类(基于Runnable)
  1. 创建Runnable的匿名实例
java 复制代码
public class Test3 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                
            }
        };
    }
}
  1. 重写run()方法
java 复制代码
public class Test3 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println("hallo thread");
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
    }
}
  1. 将runnable传入Thread并调用实例后的start()方法

runnable只是一个接口,不包含Thread的start()方法.所以要将runnable中重写的run()方法交给Thread执行

java 复制代码
public class Test3 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println("hallo thread");
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        Thread t = new Thread(runnable);
        t.start();
    }
}
  • 特点
    1. 代码简洁,不用定义类
    2. 避免单继承:java中一个类只能继承一个类,但可以实现多个接口
    3. 符合面向对象设计原则
使用匿名内部类(基于lambda表达式)

lambda表达式是一个语法糖(简化写法).使用有两个前提

  1. 匿名内部类
  2. 函数式接口:只包含一个抽象方法的接口,例如Runnable只有run()方法,Comparator只有compare()方法

Thread的构造器也可以使用

使用方法:

  1. 用lambda表达式替代匿名内部类

()->{...}是匿名内部类的语法

java 复制代码
public class Test4 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {

        });
    }
}

Thread thread = new Thread((这里面可以加入函数的参数列表) -> {函数体});

  1. 添加一次性代码的逻辑
java 复制代码
public class Test4 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true){
                System.out.println("hallo thread");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }
}
  1. 运行
java 复制代码
public class Test4 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true){
                System.out.println("hallo thread");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        thread.start();
    }
}

进一步简化代码,直接将lambda表达式作为参数传给Thread

java 复制代码
    public static void main(String[] args) {
        new Thread(() -> {
            while (true){
                System.out.println("hallo thread");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();
    }
  • 特点
  1. 更加简洁:省略new 接口名(),@Override,方法名等代码
  2. 只能用于函数式接口或Thread构造器
  3. 本质仍然是匿名内部类:lambda表达式编译后会自动生成匿名内部类

Thread方法的用法和示例

Thread的构造方法
方法 说明
Thread() 创建线程对象
Thread(Runnable target) 使用Runnbale对象创建线程对象
Thread(String name) 创建线程对象并命名
Thread(Runnable target,String name) 使用Runnable对象创建线程对象并命名
*Thread(ThreadGroup group,Runnable target) 线程可以被分组管理,分好的组即为线程组

线程组了解即可,在现在的开发中线程组已经被替代为更优的线程池

Thread() 创建线程对象

在前面的"使用Thread创建线程"已经详细解释过,在此不多加赘述

Thread(Runnable target) 使用Runnable创建对象

在前面的"使用Thread创建线程"已经详细解释过,在此不多加赘述

Thread(String name) 创建线程对象并命名
  1. 创建MyThread类并添加构造函数,用来接收并设置线程名
Java 复制代码
class MyThread extends Thread {
    public MyThread(String name){
        super(name);
    }
}
  1. 重写run()方法,即线程的主逻辑
java 复制代码
class MyThread extends Thread{
    public MyThread(String name){
        super(name);
    }
    @Override
    public void run() {
        while (true) {
            System.out.print(Thread.currentThread().getName() + ":");
            System.out.println("hallo Thread");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    };
}
  1. 创建线程实例,并传入线程名
java 复制代码
    public static void main(String[] args) {
        MyThread myThread = new MyThread("这是MyThread线程");
        myThread.start();
    }

最后的代码如下:

java 复制代码
class MyThread extends Thread{
    public MyThread(String name){
        super(name);
    }
    @Override
    public void run() {
        while (true) {
            System.out.print(Thread.currentThread().getName() + ":");
            System.out.println("hallo Thread");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    };

    public static void main(String[] args) {
        MyThread myThread = new MyThread("这是MyThread线程");
        myThread.start();
    }
}
  • 注意:必须添加构造函数MyThread(String name)并调用父类的构造函数来命名.如果不添加该构造函数会默认使用Thread()构造方法.无法添加重命名
Thread(Ruable target,String name) 使用Runnable创建对象并命名
Java 复制代码
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.print(Thread.currentThread().getName() + ":");
                    System.out.println("hallo Thread");
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        Thread t = new Thread(runnable, "这是Thread线程");
        t.start();
    }

Thread.currentThread().getName()方法是获取当前线程名

查看输出内容会发现获取到的线程名就是我们设置的线程名

Thread类的常见属性和对应属性的获取方法
属性 获取方式
ID getId()
名称 getName()
状态 getState()
优先级 getPriority()
是否后台线程 isDaemon()
是否存活 isAlive()
是否被中断 isInterrupted()

以下是示例的MyThread类的构造

java 复制代码
class MyThreand extends Thread{
    @Override
    public void run() {
        while (true) {
            System.out.println("hallo Thread");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
ID,getId()

是JVM为线程生成的一个Id编号.注意是JVM内,和系统内核中的PCB的PID不是同一个东西

  • 使用getId()方法获取Id
java 复制代码
public class Test {
    public static void main(String[] args) {
        MyThreand myThreand = new MyThreand();
        myThreand.start();
        System.out.println(myThreand.getId());
    }
}

输出内容为:

24

hallo Thread

hallo Thread

hallo Thread

...

名称,getName()
java 复制代码
public class Test {
    public static void main(String[] args) {
        MyThreand myThreand = new MyThreand();
        myThreand.start();
        System.out.println(myThreand.getName());
    }
}

输出内容为:

Thread-0

hallo Thread

hallo Thread

hallo Thread

...

状态,getState()

线程状态能反映线程在生命周期中的当前行为,java中一共定义了6种线程状态,getState()的返回值就是其中之一

状态 含义
NEW 线程已经创建,但未调用(start()方法)
RUNNABLE 线程已经启动,正在等待CPU调度(就绪状态)或正在执行run()方法(运行状态)
BLOCKED 线程因竞争同步锁而被阻塞(等待锁释放)
WAITING 线程因调用wait(),join()等方法,进入无限期等待状态(需要其它线程唤醒)
TIMED_WAITING 线程因调用sleep(),wait()等方法,进入限期等待状态(超时自动唤醒)
TERMINATED 线程执行完毕或因异常终止
java 复制代码
public class Test {
    public static void main(String[] args) {
        MyThreand myThreand = new MyThreand();
        myThreand.start();
        System.out.println(myThreand.getState());
    }
}

输出内容为:

RUNNABLE

hallo Thread

hallo Thread

hallo Thread

...

优先级,getPriority()

在java中线程优先级是一个整数,范围是1~10之间.数值越大代表进程的优先级越高.getPriority()是获取当前优先级.setPriority()是设置优先级

java 复制代码
public class Test5 {
    public static void main(String[] args) {
        Thread thread = new Thread(()-> {
            while (true){
                System.out.println("MyThread正在运行");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"MyThread");
        System.out.println("初始优先级是:" + thread.getPriority());
        thread.setPriority(10);
        System.out.println("设置后的优先级是:" + thread.getPriority());
        thread.start();
    }
}

输出内容是:

初始优先级是:5

设置后的优先级是:10

MyThread正在运行

MyThread正在运行

MyThread正在运行

  • 优先级的作用:
    优先级的主要作用是向操作系统的线程调度器提供一个"建议",表明该线程的重要性.通俗来说就是
    优先级高的线程获得CPU执行时间片的机会更多,更有可能被优先调度,反之亦然
  • 注意:
  1. 真正的决定权在操作系统:java的优先级只是给操作系统一个建议.不同的操作系统有不同的调度算法.有可能会忽略java的游戏机设置
  2. 不能保证执行顺序:优先级高的进程也不一定会比优先级低的进程更快执行
  3. 可能导致线程"饥饿
是否后台线程,isDaemon()

该方法用来判断线程是否为后台线程(守护线程).返回true就是后台线程,false就是前台线程(用户线程)

  • 后台线程与前台线程的区别
    类型|特点
    -|-
    后台线程(守护线程Daemon)|依赖前台进程存在.当所有前台线程结束后,后台线程会被JVM强行终止
    前台线程(用户线程User)|独立存在,当所有前台线程结束后,JVM就会退出,即程序终止

就好比一棵树.前台进程就是树干,后台进程就是树叶.一棵树可以没有树叶只有树干,也可以只有树干而没有树叶,但不会只有树叶而没有树干

java 复制代码
public class Test5 {
    public static void main(String[] args) {
        Thread thread = new Thread(()-> {
            while (true){
                System.out.println("MyThread正在运行");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"MyThread");
        System.out.println(thread.isDaemon());
        thread.start();
    }
}

输出内容是:

false

MyThread正在运行

MyThread正在运行

由此可见我们默认创建的线程默认是前台进程

  • 设置后台线程
    isDaemon()只用来查询状态,药创建后台进程,需要使用**Thread.setDaemon(boolean on)**方法来设置
java 复制代码
public class Test5 {
    public static void main(String[] args) {
        Thread thread = new Thread(()-> {
            while (true){
                System.out.println("MyThread正在运行");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"MyThread");
        System.out.println(thread.isDaemon());
        thread.setDaemon(true);
        thread.start();
        System.out.println(thread.isDaemon());
    }
}

输出内容:

false

true

MyThread正在运行

进程已结束,退出代码0

对于上段代码,第一行输出是MyThread默认为前台线程.第二行输出是使用setDaemon()方法设置为后台线程.注意到这次MyThread线程只进行了一次打印.原因是将该线程设置为后台线程后,当唯一的前台线程main执行完毕后会自动销毁.此时没有前台线程,作为后台线程的MyThread就被JVM强制关闭了
注意:setDeamon()方法必须在start()方法前使用,否则抛异常

是否存活,isAlive()
  • 什么是存活?
    线程的存活指的是:线程已经启动(start()调用,且线程还未终止)
    对于存活,包含以下状态
  1. RUNNABLE(就绪或正在运行)
  2. BLOCKED(竞争说阻塞)
  3. WAITING(无限期等待)
  4. TIMED_WAITING(限期等待)
    对于非存活,包含以下状态
  5. NEW(未启动线程)
  6. TERMINATED(线程已终止)
  • isAlive()用法示例
java 复制代码
public class Test5 {
    public static void main(String[] args) {
        Thread thread = new Thread(()-> {
            while (true){
                System.out.println("MyThread正在运行");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"MyThread");
        System.out.print("线程状态:" + thread.getState());
        System.out.println("是否存活:" + thread.isAlive());
        thread.start();
        System.out.print("线程状态:" + thread.getState());
        System.out.println("是否存活:" + thread.isAlive());
    }
}

输出内容:

线程状态:NEW是否存活:false

线程状态:RUNNABLE是否存活:true

MyThread正在运行

MyThread正在运行

MyThread正在运行

...

是否被中断,isInterrupted()
  • 什么是中断?
    java中的线程中断不是强制终止线程,是通过设置线程的"中断标志位"来通知线程"你要中断了",然后线程才开始判断是否中断
    isInterrupted()方法的作用就是查询线程的"中断标志位,以此来决定后续行为
  • 什么是中断标志位?
    中断标志位是线程内的一个布尔型变量,用于标记线程是否收到中断信号.中断标志位的初始状态是false,即未中断.我们可以使用interrupt()来设置中断标记为true
    注意 :当线程处于阻塞状态时(sleep(),wait()等等),会抛出异常,并且自动清除中断标志位.清楚中断标志位的原因就是让线程有足够的时间退出循坏,释放资源等等
java 复制代码
public class Test6 {
    public static void main(String[] args) {
        Thread thread = new Thread(()-> {
            while(!Thread.currentThread().isInterrupted()) {//判断线程是否收到中断信号
                System.out.println("线程运行中");
                try {
                    Thread.sleep(500);//sleep被中断会抛出InterruptedException
                } catch (InterruptedException e) {
                    System.out.println("线程休眠时被中断,正在退出线程");
                    System.out.println("sleep被中断后线程的中断标志位是" + Thread.currentThread().isInterrupted());
                    Thread.currentThread().interrupt();//重新标记中断信号
                }
            }
            System.out.println("线程是否被中断:" + Thread.currentThread().isInterrupted());
        });
        thread.start();
        System.out.println("main线程开始");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("main线程结束,结束前设置中断标记位");
        thread.interrupt();
    }
}

输出内容:

main线程开始

线程运行中

线程运行中

main线程结束,结束前设置中断标记位

线程休眠时被中断,正在退出线程

sleep被中断后线程的中断标志位是false

线程是否被中断:true

进程已结束,退出代码0

由代码和输出内容"sleep被中断后线程的中断标志位是false"可知sleep被中断时会抛出异常并且恢复中断标记位

相关推荐
zhangfeng11332 小时前
openclaw skills 小龙虾技能 通讯仿真 matlab skill Simulink Agentic Toolkit,通过kimi找到,mcp通讯
开发语言·matlab·openclaw·通讯仿真
Javatutouhouduan8 小时前
2026Java面试的正确打开方式!
java·高并发·java面试·java面试题·后端开发·java编程·java八股文
chao1898448 小时前
基于 SPEA2 的多目标优化算法 MATLAB 实现
开发语言·算法·matlab
JAVA面经实录9178 小时前
Java初级最终完整版学习路线图
java·spring·eclipse·maven
赏金术士8 小时前
Kotlin 习题集 · 高级篇
android·开发语言·kotlin
Cat_Rocky9 小时前
k8s-持久化存储,粗浅学习
java·学习·kubernetes
楼兰公子9 小时前
buildroot 在编译rust时裁剪平台类型数量的方法
开发语言·后端·rust
知识领航员10 小时前
蘑兔AI音乐深度实测:功能拆解、实测表现与适用场景
java·c语言·c++·人工智能·python·算法·github
AOwhisky10 小时前
虚拟化技术学习笔记
linux·运维·笔记·学习·虚拟化技术
吴声子夜歌10 小时前
Go——并发编程
开发语言·后端·golang