Java多线程编程:从入门到实战

一,认识线程

1.1 什么是线程

一个线程就是一个"执行流",每个线程之间都可以按照顺序执行自己的代码,多个线程之间"同时"执行着多份代码。同时,线程也是 CPU 真正执行任务的单位。

举个栗子:去奶茶店买奶茶(奶茶店中有自己独立的"程序"让我们拿到奶茶,所以也可把这个看为一个线程),奶茶店有不同岗位员工,每个员工都是独立的"执行流"。如果店里只有一个员工(单线程),那速度将会很慢效率极低;多个员工分工合作(多线程),那效率会得到大幅提高,这就是多线程的优势。

2.2 线程和进程的区别

进程包含线程的 ,每个进程至少有一个线程存在(主线程)

|------|-------------------------------------------|-------------------|
| | 线程(Thread) | 进程 |
| | 线程时cpu上调度执行的基本单位 | 进程是操作系统中资源分配的基本单位 |
| 资源 | 共享进程内存 | 独立内存,互不影响 |
| 开销 | 轻量,快速(只有第一个线程也就是和进程一起创建时,需要申请资源,后续创建则不需要) | 创建/销毁慢,消耗资源 |
| 奔溃影响 | 一个崩溃,可能把同进程内的线程都带崩溃 | 一个崩溃不影响其他 |

二,创建线程

2.1 继承Thread类,重写run方法

复制代码
class Thread1 extends Thread{
    public void run(){
        System.out.println("线程运行代码");
    }
}

//创建实例
    Thread1 t = new Thread(); 

2.2 实现Runnable接口,重写run方法

复制代码
clas Runnable1 implements Runnable{
    public void run(){
        System.out.println("线程运行代码");
    }
}
//创建实例
    Thread t = new Thread(new Runnable());

2.3 匿名内部类(本质就是方法1,2)

2.3.1 用匿名内部类创建Tread子类对象

复制代码
Thread t1 = new Thread(){
    public void run(){
        System.out.println("使用匿名类创建Tread子类对象");
    }
}

2.3.2 用匿名内部类创建Runnable子类对象

复制代码
Thread t2 = new Thread(new Runnable(){
    public void run(){
        System.out.println("使用匿名类创建Runnable子类对象");
    }
});

这两个线程对象(t1t2)的 start() 方法都只能调用一次,所以从 "启动线程" 的角度来说,它们是一次性的;但第二种写法里的 Runnable 任务逻辑,是可以被复用的。

2.4 用lambda表达式(常用)

复制代码
 //使用lambda 表达式创建Runnable 子类对象
Thread t3 = new Thread (() -> System.out.println("使用匿名类创建Thread 子类对象"));


Thread t4 = new Thread (() -> {
     System.out.println("使用匿名类创建Thread 子类对象");
 });

2.5 实现Callable

复制代码
// 用匿名内部类创建Callable对象
Callable<String> callableTask = new Callable<String>() {
    public String call() throws Exception {
        return "Callable任务执行完成,返回结果";
    }
};

// 包装成FutureTask,再交给Thread
FutureTask<String> futureTask = new FutureTask<>(callableTask);
Thread t5 = new Thread(futureTask);

2.6 线程池(ThreadFactory)

复制代码
// 用匿名内部类创建ThreadFactory
ThreadFactory factory = new ThreadFactory() {
    private int count = 0;
    public Thread newThread(Runnable r) {
        // 自定义线程名,方便调试
        return new Thread(r, "自定义线程-" + (++count));
    }
};

// 创建固定大小线程池
ExecutorService pool = Executors.newFixedThreadPool(3, factory);

// 提交任务(用匿名内部类Runnable)
pool.submit(new Runnable() {
    public void run() {
        System.out.println("线程池中的任务执行");
    }
});

// 关闭线程池
pool.shutdown();

三,Thread类

2.1 常见构造方法

|-------------------------------------------|-----------------------|
| 方法 | 说明 |
| Thread() | 创建线程对象 |
| Thread(Runnable target) | 使用Runnable对象创建线程对象 |
| Thread(String name) | 创建线程对象,并命名 |
| Thread(Runnable target,String name) | 使用Runnable对象创建线程对象并命名 |
| Thread(ThreadGroup gruop,Runnable target) | 线程可以被用来分组管理,分好的组为线 |

2.2 常见属性

|--------|------------------|-----------------------------|
| 属性 | 获取方法 | 说明 |
| ID | getID( ) | ID是线程的唯一标识,不同的线程不会重复 |
| 名称 | getName( ) | 名称在各种调试工具用到 |
| 状态 | getState( ) | 表示线程当前所处的一个情况 |
| 优先级 | getPriority( ) | 优先级高的线程理论上来说更容易被调度到 |
| 是否后台线程 | isDaemon( ) | JVM会在一个进程的所有非后台线程结束后,才会结束运行 |
| 是否存活 | isAlive( ) | 简单的理解,为run 方法是否运行结束了 |
| 是否被中断 | isInterrupted( ) | 检查线程是否收到中断信号 |

四,线程相关操作

4.1 启动线程 start( )

上面我们介绍了覆写run( )方法创建一个线程对象,但线程对象被创建出来并不意味着线程已经开始运行了,而调用start( )方法,才真的在操作系统的底层创建出一个线程,线程才真正的独立执行

举个栗子:我(主线程)奶茶店店长,我先写了一份完整的"工作流程"(new Thread()),我在纸上写下了做奶茶的步骤(run( ) 方法),这是我自己按照流程做一杯(调用run( ),并不会启动新的线程)。然后我画了个店员的名牌,写着 "店员 A"(创建了线程对象),此时店员A开始按步骤工作(调用start( )),这是店里就我和店员A同时干活,效率提升。

4.2 中断线程

举个栗子:我(主线程)安排店员A(子线程)去做奶茶,3秒之后,我告诉店员A"先不做奶茶了,过来点单",店员A立刻停下做奶茶,去点单。

常见的有以下两种方法:

a)通过共享的标记来进行沟通,使用自定义变量作为标志位(volatile)

b)调用interrupt(方法来通知)

代码实例:

复制代码
//采用  a)方法

public class demo {
    private static class MyRunnable implements Runnable{
        //volatile相当于一个主线程和子线程公用的开关
        public volatile boolean isQuit=false;
        @Override
        public void run() {
            while(!isQuit){
        //如果没有收到停止指令,循环一直满足,开始做奶茶
                System.out.println(Thread.currentThread().getName()+":我正在忙着做奶茶");
                try{
        //做一杯奶茶需要1s,也就是1s打印一次
                    Thread.sleep(1000);//这里把光标放在sleep下用alt+enter快捷键可以直接生成try,catch
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        //不进入while循环也就是收到停止指令时
            System.out.println(Thread.currentThread().getName()+":我现在来帮忙点单");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable target=new MyRunnable();
        Thread thread=new Thread(target,"店员A");
        System.out.println(Thread.currentThread().getName()+":A,快去做奶茶");
        //让A开始工作
        thread.start();
        //我去做3秒的事,a接着做奶茶每秒打印一次
        Thread.sleep(3000);
        //sleep结束,我叫a停止做奶茶
        System.out.println(Thread.currentThread().getName()+":A先别做奶茶了,快过来帮忙点单");
        target.isQuit=true;
    }
}

volatile就是保证:主线程改了isQuit,子线程可以立刻获取 。其核心就是修改了内存可见性问题(内存可见性将下面线程安全产生原因中进行详细讲解)

复制代码
//采用   b)方法

public class demo2 {
    private static class MyRunnable implements Runnable{
        @Override
        public void run() {
            //while(!Thread.isterrupted())
            while (!Thread.currentThread().isInterrupted()){
                System.out.println(Thread.currentThread().getName()+":我正忙着做奶茶");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //收到中断信号,处理异常
                    e.printStackTrace();
                    System.out.println(Thread.currentThread().getName()+":收到通知,停下手中的活");
                    //需着重注意这里的break,跳出循环,结束当前任务
                    break;
                }
            }
            //循环结束,执行中断后的收尾工作
            System.out.println(Thread.currentThread().getName()+":我现在过来点单");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable target=new MyRunnable();
        Thread thread=new Thread(target,"A");
        System.out.println(Thread.currentThread().getName()+":快去做奶茶");
        thread.start();
        Thread.sleep(5000);
        System.out.println(Thread.currentThread().getName()+":A先别做奶茶了过来点单");
        thread.interrupt();
    }
}
方法 类型 作用 是否清除中断标志
Thread.currentThread().isInterrupted() 实例方法 检查当前线程的中断状态 ❌ 不清除
Thread.interrupted() 静态方法 检查当前线程的中断状态 ✅ 会清除(调用后标志位变为false

使用Thread.interrupted( )方法来通知线程结束或Thread.currentThread( ).isInterrupted( )代替自定义标志位

在这里thread收到通知有两种方法

1.如果线程因为调用wait/join/sleep等方法而阻塞挂起,则以InterruptedException 异常的形式通知,清除中断标志,这个时候可以在catch中加入break;

2.只是内部的一个中断标志被设置,thread 可以通过Thread.currentThread().isInterrupted()判断指定线程的中断标志被设置,不清除中断标志这种方式通知收到的更及时,即使线程正在sleep 也可以马上收到。

4.3 等待线程 join( )

join可以要求多个线程间结束的线程顺序

t.join( )就是等t线程结束在完成别的,只要t不结束就一直等

|-----------------------------------------|--------------------|
| 方法 | 说明 |
| public void join() | 等待线程结束 |
| public void join(long millis) | 等待线程结束,最多等millis毫秒 |
| public void join(long millis,int nanos) | 等待线程结束,精度更高 |

复制代码
public class demo3 {
    public static void main(String[] args) throws InterruptedException {
        Runnable target=()->{
         for(int i=0;i<3;i++){
                try {
                    System.out.println(Thread.currentThread().getName()+ ":我还在工作!");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ":我结束了!");
        };
        Thread thread1=new Thread(target,"A");
        Thread thread2=new Thread(target,"B");
        System.out.println("先让A开始工作");
        thread1.start();
        thread1.join();
        System.out.println("A工作结束,B开始工作");
        thread2.start();
        thread2.join();
        System.out.println("B工作结束");
    }
}

这里的抛异常也不需要手动去写,alt+enter快捷键即可

4.4 获取当前线程引用

已经很熟悉了就不再赘述了

public static Thread currentThread( ); //返回当前线程对象的引用

4.5 休眠线程 sleep

因为线程的调度是不可控的,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的

|----------------------------------------------------------------------------|----------------|
| 方法 | 说明 |
| public static void sleep(long millis)throws InterruptedException | 休眠当前线程millis毫秒 |
| public static void sleep(long millis,int nanos)throws InterruptedException | 更高精度的休眠 |

这里的抛异常也不需要手动去写,alt+enter快捷键即可

五,线程状态

查看所有线程状态代码:

复制代码
public class demo4 {
    public static void main(String[] args) {
        for (Thread.State state:Thread.State.values()) {
            System.out.println(state);
        }
    }
}

代码运行结果:

NEW:安排了工作,还未开始行动

RUNNABLE:可工作的.又可以分成正在工作中和即将开始工作.(就绪:线程在cpu上执行;线程随时可以去cpu上执行)

BLOCKED:表示排队等着其他事情(由于锁导致的阻塞)

WAITING:表示排队等着其他事情(死等,无超时时间阻塞等待)

TIMED_WAITING:表示排队等着其他事情(线程阻塞,不参与cpu调度,不继续执行,阻塞时间是有上限的)

**TERMINATED:**工作完成了,内核中的线程已结束,但Thread对象还在

六,线程安全

6.1 什么是线程安全

一段代码,如果多线程环境下代码运行的结果是符合预期的(在单线程环境应该的结果),则说这个程序是线程安全的。

6.2 线程安全问题产生原因

1)操作系统对于线程的调度使抢占式随即执行的(根本原因)

2)多个线程同时修改同一个变量

3)修改操作不是原子的(如果修改操作只是对应一个cpu指令,就认为是原子的)

4)内存可见性问题(内存可见性:即一个线程对共享变量值的修改能够及时的被其他线程看见)

5)指令重排序问题

七,小结

到这两天感冒才稍微好一点,大家一定要注意身体哦!多线程是一个新的内容块,和之前的单线程比麻烦了很多。多敲吧,沉淀ing~~

相关推荐
Fanfanaas1 小时前
Linux 系统编程 进程篇 (六)
linux·服务器·c语言·开发语言
Seven971 小时前
Tomcat 线程池的设计与实现:StandardThreadExecutor
java
爱笑的Sunday1 小时前
Linux Java前后端项目 企业级0-1完整部署手册
java·linux·运维·服务器
小年糕是糕手1 小时前
【C/C++刷题集】顺序表、vector、链表、list核心精讲
c语言·开发语言·数据结构·c++·算法·leetcode·蓝桥杯
xyx-3v1 小时前
FreeRTOS队列通信
java·服务器·网络
会编程的土豆1 小时前
从 C/C++ 视角快速上手 Go 语言:核心差异与避坑指南
c语言·开发语言·c++·后端·golang
存在的五月雨1 小时前
uniapp 一些组件的使用
java·前端·uni-app
小白学大数据1 小时前
Python 3.7 高并发爬虫:接口请求与页面解析并发处理
开发语言·爬虫·python
我命由我123451 小时前
Kotlin 开发 - 双冒号操作符(引用顶层函数、引用成员函数、引用构造函数、引用属性、引用类)
android·java·开发语言·kotlin·android studio·android jetpack·android-studio