多线程(超详细) (ε≡٩(๑>₃<)۶ 一心向学)

发布于:2025-03-15 ⋅ 阅读:(22) ⋅ 点赞:(0)

多线程目录

一、认识线程

1、概念:

1) 线程是什么

2) 线程为什么存在

3) 进程与线程的区别

二、创建线程

1、方法1:继承Thread类

2、方法2:实现 Runnable 接口

3、方法3:匿名内部类创建 Thread 子类对象

4、方法4:匿名内部类创建 Runnable 子类对象

5、方法5:lambda 表达式创建 Runnable 子类对象

三、Thread类 及其 常见的方法

1、Thread 的常见构造方法

2、Thread 的常见的几个属性

3、启动一个线程

4、中断一个线程

5、等待一个线程

6、获得当前线程的引用

7、休眠当前线程

四、线程的状态

1、线程的所有状态

2、线程状态和状态转义的意义​编辑

3、观察线程的状态和转移

1)观察 NEW、RUNNABLE、TERMINATED

2)观察 WAITING、BLOCKED、TIMED_WAITING

五、多线程带来的风险-线程安全(重中之重)

1、观察一下线程不安全

2、线程安全的概念

3、线程不安全的原因

1)线程调度是随机的

2)修改共享数据

3)原子性

什么是原子性

4)可见性 

5)指令重排序

4、解决之前的线程不安全的问题


如果对于下文的进程是什么有疑问的,可以跳转到我的上一篇博客中:

初识操作系统 感谢各位大佬的支持💓💓💓

一、认识线程

1、概念:

1) 线程是什么

线程是操作系统中最小的执行单元,它是进程中的一个独立执行路径。线程可以共享进程的资源,但每个线程都有自己的程序计数器、栈和寄存器。线程的使用可以提高程序的并发性和执行效率。main() 这个线程一般称之为——主线程(Main Thread)

2) 线程为什么存在

首先「并发编程」称为「刚需」

  因为对于单核CPU的发展遇到了瓶颈,算力不够,就需要多核CPU,而并发编程就可以充分利用多核CPU资源。

  有时候我们会出现等待"I/O"的时候,那么在等待的过程中,我们可以在等待I/O过程中,可以去干一些别的事情,这时候同样需要使用并发编程。

虽然多进程也可以实现「并发编程」,但是线程比进程更加的轻量。因为对于线程来说:创建、销毁、调度线程比进程更快。

3) 进程与线程的区别

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

• 进程和进程之间不共享内存空间的,但是在同一个进程中的线程和线程之间是共享同一个内存空间的。

• 进程是系统分配资源的最小单位,线程是系统调度的最小单位

• 一个进程挂了一般是不会影响到其他的进程,但是当一个进程中的某个线程挂了,可能会把同进程的其余线程一起带走,进而导致整个进程崩溃。


二、创建线程

1、方法1:继承Thread类

继承 Thread 来创建一个线程类,使用 start 启动线程

class MyThread extends Thread {
    @Override
    public void run() {
        while(true) {
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class Test {
    public static void main(String[] args) {
        // 创建 Thread 类子类,在子类中 重写 run 方法
        Thread thread = new MyThread();
        thread.start();

        while(true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

运行此代码的话,就会发现 “hello thread” 和 “hello main” 是随机执行的,多线程的调度顺序是随机的,这种也叫做 —— “抢占式执行”。

后面的构造方法就不进行循环观察了。

2、方法2:实现 Runnable 接口

实现 Runnable 接口,创建 Thread 实例,调用构造方法,使用实现Runnable 接口的类作为参数

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程运行的代码");
    }
}

public class Test2 {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();

        System.out.println("main线程运行的代码");
    }
}

3、方法3:匿名内部类创建 Thread 子类对象

public class Test3 {
    public static void main(String[] args) {
        Thread thread = new Thread(){
            // 使用匿名类创建 Thread 子类对象
            @Override
            public void run() {
                System.out.println("使用匿名内部类创建 Thread 子类对象");
            }
        };
        thread.start();

        System.out.println("这是main线程");
    }
}

4、方法4:匿名内部类创建 Runnable 子类对象

public class Test4 {
    public static void main(String[] args) {
        // 使用匿名内部类创建 Runnable 子类对象
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("使用匿名内部类创建 Runnable 子类对象");
            }
        });
        thread.start();

        System.out.println("这是main线程");
    }
}

5、方法5:lambda 表达式创建 Runnable 子类对象

public class Test5 {
    public static void main(String[] args) {
        // 使用lambda 配合 匿名内部类创建 Runnable 子类对象
        Thread thread1 = new Thread(() -> {
            System.out.println("使用匿名内部类创建 Runnable 子类对象");
        });
        Thread thread2 = new Thread(() -> System.out.println("使用lambda 配合 匿名内部类创建 Runnable 子类对象"));
        thread1.start();
        thread2.start();
        
        System.out.println("这是main线程");
    }
}

注:如果 不造 lambda 表达式的那么可以去这里进行参考一下 Java-数据结构-Lambda表达式 (✪ω✪) 感谢支持💓💓💓


三、Thread类 及其 常见的方法

1、Thread 的常见构造方法

由上面的创建线程的介绍,我们可以得出一个大概的构造方法,接下来看看完整的构造方法:

方法 说明
Thread() 创建线程对象
Thread(Runnable target) 使用 Runnable 对象创建线程对象
Thread(String name) 创建线程对象,并且命名
Thread(Runnable target,String name) 使用 Runnable 对象创建线程对象,并命名
「了解」Thread(ThreadGroup group,Runnable target) 线程可以被用来分组管理,分好的组即为线程

简略的看一下如何创建:

Thread thread1 = new Thread();
Thread thread2 = new Thread(new MyRunnable());
Thread thread3 = new Thread("线程的名称");
Thread thread4 = new Thread(new MyRunnable(),"线程的名称");

2、Thread 的常见的几个属性

属性 获得方法
ID getId()
名称 getName()
状态 getState()
优先级 getPriority()
是否是后台线程 isDaemon()
是否存活 isAlive()
是否被中断 isInterrupted()

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

  名称 是在各种调试工具中用到的

  状态 表示线程当前所处的一个情况

  优先级 高的线程理论上更容易被调用

  后台线程 JVM会在一个进程的所有非后台线程结束后,才会结束运行

  是否存活 简单来说,就是 run 方法是否运行结束了

  至于线程中断问题呢,我们在后面进行了解

先来简单的使用一下这些方法:

public class Test5 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
           for(int i = 0;i < 10;i++) {
               try {
                   System.out.println(Thread.currentThread().getName() + ": 活着");
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
            System.out.println(Thread.currentThread().getName() + ": 即将死去");
        });
        thread.start();
        System.out.println(Thread.currentThread().getName() + ": ID: " + thread.getId());
        System.out.println(Thread.currentThread().getName() + ": 名称: " + thread.getName());
        System.out.println(Thread.currentThread().getName() + ": 状态: " + thread.getState());
        System.out.println(Thread.currentThread().getName() + ": 优先级: " + thread.getPriority());
        System.out.println(Thread.currentThread().getName() + ": 后台线程: " + thread.isDaemon());
        System.out.println(Thread.currentThread().getName() + ": 活着: " + thread.isAlive());
        System.out.println(Thread.currentThread().getName() + ": 被中断: " + thread.isInterrupted());
    }
}

3、启动一个线程

在前面我们知道了,如何使用重写 run 方法创建一个线程对象,但是线程对象创建出来并不会直接进行运行,这时候我们需要使用另一个方法进行执行线程——start()

调用 start() 方法,才真的在操作系统的底层创建出一个线程。


4、中断一个线程

方法 说明
public void intreeupt() 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位
public static boolean intreeupted() 判断当前线程的中断标志位是否设置,调用后清除标志位
public boolean isIntreeupted() 判断对象关联的线程的标志位是否设置,调用后不清除标志位

 进行一下简单的测试一下:

class MyThread extends Thread {
    @Override
    public void run() {
        // 下面的两个方法都可以进行
        while(!Thread.interrupted()) {
//      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 class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new MyThread();
        thread.start();
        Thread.sleep(10 * 1000);
        System.out.println(Thread.currentThread().getName() + ": 即将中断thread线程");
        thread.interrupt();
    }
}

上述代码运行结果: 


thread 收到通知的方式有两种:

1、如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通知,清除中断标志

  •  当出现 InterruptedException 的时候,要不要结束线程取决于 catch 中代码的写法。可以选择         忽略这个异常,也可以跳出循环结束线程。

2、否则,只是内部的一个中断标志被设置,thread 可以通过

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


5、等待一个线程

方法 说明
public void join() 等待线程结束
public void join(long millis) 等待线程结束,最多等待 millis 毫秒
public void join(long millis,int nanos) 等待线程结束,最多等待 millis 毫秒,但是可以更高精度
public class Test6 {
    public static void main(String[] args) throws InterruptedException {
        Runnable target = () -> {
            for(int i = 0; i < 5;i++) {
                System.out.println(Thread.currentThread().getName() + ": 正在执行");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ": 结束");
        };

        Thread thread1 = new Thread(target,"线程1");
        Thread thread2 = new Thread(target,"线程2");
        System.out.println("先让线程1致性");
        thread1.start();
        thread1.join();
        System.out.println("线程1结束,再让线程2执行");
        thread2.start();
        thread2.join();
        System.out.println("线程2结束");
    }
}

大家可以去运行一下试一试,并且试一下当把 join() 的方法的代码注释掉会发生什么情况呢?


6、获得当前线程的引用

方法 说明
public static Thread currentThread() 返回当前线程对象的引用
public class Test4 {
    public static void main(String[] args) {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName());
    }
}

7、休眠当前线程

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

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


四、线程的状态

1、线程的所有状态

NEW: 安排了工作,还未开始行动。
RUNNABLE: 可工作的。又可以分成正在工作中和即将开始工作。
BLOCKED: 表示排队等着其他事情。
WAITING: 表示排队等着其他事情。
TIMED_WAITING: 表示排队等着其他事情。
TERMINATED: 工作完成了。


2、线程状态和状态转义的意义

 来进行一下简单的介绍:


3、观察线程的状态和转移

1)观察 NEW、RUNNABLE、TERMINATED

public class Test4 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
            }
        });
        System.out.println(t.getName() + ": " + t.getState());
        t.start();
        while (t.isAlive()) {
            System.out.println(t.getName() + ": " + t.getState());
        }
        System.out.println(t.getName() + ": " + t.getState());
    }
}

这个就是上述代码所执行到状态


2)观察 WAITING、BLOCKED、TIMED_WAITING

public class Test4 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    while (true) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }, "t1");
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    System.out.println("hehe");
                }
            }
        }, "t2");
        t2.start();
    }
}

上述的代码我们是用 jconsole 来进行观察 BLOCKED、TIMED_WAITING 这两个状态

 把上述的代码中的 t1 的sleep 更改成 wait,进行观察 WAITING 状态

public class Test4 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    while (true) {
                        try {
                            object.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }, "t1");
        t1.start();
    }
}

可以得出:

• BLOCKED 表示等待获取锁,WAITING 和 TIMED_WAITING 表示等待其他线程发来通知。

• TIMED_WAITING 线程在等待唤醒,但设置了时限。WAITING 线程在无限等待唤醒。

在上面的代码中 出现了一些关键词如:synchronized 、wait 这些关键字会在后面进行详细的介绍


五、多线程带来的风险-线程安全(重中之重)

1、观察一下线程不安全

public class Test4 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
           for(int i = 0;i < 50000;i++) {
               count++;
           }
        });
        Thread thread2 = new Thread(() -> {
            for(int i = 0;i < 50000;i++) {
                count++;
            }
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        // 预期为100000
        System.out.println("count = " + count);
    }
}

运行之后会发现,这个程序的 count 不会出现 100000,并且每次运行的结果是不同的,这就是线程不安全的。


2、线程安全的概念

对于这个概念是非常之复杂的,但是我们可以这样去理解:

如果多线程环境下代码的结果是符合我们预期的,即在单线程环境应该的结果,则说这个结果是正确的。


3、线程不安全的原因

1)线程调度是随机的

这是线程安全的罪魁祸首,随机调度使一个程序在多线程的环境下,执行顺序存在很多的变数,程序员必须保证 在任意执行顺序下 ,代码都能正常运行。

2)修改共享数据

也就是多线程修改同一个数据。上面的线程不安全的代码中,涉及到多个线程针对 count 变量进行修改。此时这个 count 是一个多个线程都能访问到的 “共享数据”。

3)原子性

什么是原子性

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入这个房间,打断 A 在房间里的隐私。这个就是不具备原子性的。
那我们应该如何解决这个问题呢?可以给房间加⼀把锁,A 进去就把门锁上,其他人就进不来了。这样就保证了这段代码的原子性了。
有时也把这个现象叫做 同步互斥,表示操作是互相排斥的。

不保证原子性会给线程带来什么问题

如果一个线程正在对一个变量进行操作,这个时候如果有其余的线程进来了,打断了这个操作,结果可能就是错误的。

这一点也和线程的 抢占式 调度密切相关,如果线程不是 “抢占” 的 ,就算没有原子性,问题也不大

4)可见性 

可见性是指,一个线程对共享变量值的修改,能够及时被其他线程所看到。

Java内存模型:

 • 线程之间的共享变量存在 主内存

 • 每个线程都有自己的 “工作内存”

 • 当线程读取一个共享变量的时候,会先把变量从主内存拷贝到工作内存,再从工作内存读取数据

 • 当线程要修改一个共享变量的时候,也会先修改工作内存中的副本,再同步回主内存

由于每个线程有自己的工作内存,这些工作内存中的内容相当于同⼀个共享变量的 "副本"。此时修改线程1的工作内存中的值,线程2的工作内存不一定会及时变化。


5)指令重排序

一段代码是这样的:
1. 去前台取下U盘
2. 去教室写10分钟作业
3. 去前台取下快递
如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2 的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序。


总结:

线程安全问题 产生原因

1、「根本」操作系统对于线程的调度是随机的,抢占式执行

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

3、修改操作,不是原子的

4、内存可见性问题,引起的线程不安全问题

5、指令重排序,引起的线程不安全问题


4、解决之前的线程不安全的问题

public class Test4 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();

        Thread thread1 = new Thread(() -> {
           for(int i = 0;i < 50000;i++) {
               synchronized (object) {
                   count++;
               }
           }
        });
        Thread thread2 = new Thread(() -> {
            for(int i = 0;i < 50000;i++) {
                synchronized (object) {
                    count++;
                }
            }
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        // 预期为100000
        System.out.println("count = " + count);
    }
}

这里面同样遇到了 synchronized 关键词,这里可能不会知道是什么意思,在下一篇的文章中,会进行详细的介绍,如何解决线程不安全的问题,敬请期待吧~~~ 


如果觉得这篇文章不错的话,期待你的一键三连哦 ~ ~ ~ 让我们一起加油,顶峰相见!! !

                           💓💓💓💓💓💓💓💓💓💓💓💓💓💓💓