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被中断时会抛出异常并且恢复中断标记位

相关推荐
Chan162 小时前
Java 集合面试核心:ArrayList/LinkedList 底层数据结构,HashMap扩容机制详解
java·数据结构·spring boot·面试·intellij-idea
Boop_wu2 小时前
[Java EE] 多线程 -- 初阶(2)
java·开发语言·jvm
q***98522 小时前
Spring Boot(快速上手)
java·spring boot·后端
IT_Beijing_BIT2 小时前
Rust入门
开发语言·后端·rust
凌凌02 小时前
macOS安装SDKMAN
java
百***92022 小时前
Spring Boot 多数据源解决方案:dynamic-datasource-spring-boot-starter 的奥秘(上)
java·spring boot·后端
青山的青衫2 小时前
【Java基础07】链表
java·开发语言·链表
麦麦鸡腿堡2 小时前
Java事件处理机制
java·开发语言·python
AA陈超2 小时前
ASC学习笔记0017:返回此能力系统组件的所有属性列表
c++·笔记·学习·ue5·虚幻引擎