【JavaEE-多线程背景-线程等待-线程的六种状态-线程安全问题-详解】

🌈个人主页SKY-30

个人推荐基于java提供的ArrayList实现的扑克牌游戏 |C贪吃蛇详解

学好数据结构,刷题刻不容缓点击一起刷题

🌙心灵鸡汤总有人要赢,为什么不能是我呢

🌈🌈🌈Java的多线程

⚡⚡⚡多线程的背景

为了充分利用cpu的多核心特性,我们引进了一种新的编程形式,并发编程,当然进程也可以进行并发编程,但是进程太"重"了,我们每次创建一个进程需要耗费很大的资源,对于有些业务场景可能需要对进程频繁的创建和删除,所以我们又引进了一个更轻量的进程-线程.

⚡⚡⚡多线程和多进程的联系

线程包含在进程之中,一个进程中至少包含一个或多个线程,对于线程来说它比进程创建和销毁的成本更小,并且一个进程中的多个线程是公用一份系统资源的(硬盘,网络带宽,内存...).

进程是操作系统资源分配的基本单位

线程是操作系统调度执行的基本单位

🌈🌈🌈在Java中使用多线程

==在Java中我们使用Java标准库中的Thread对象实现多线程.==对于实现Java的多线程我们这里有多种方式,这里给大家全部列举出来.

🌈🌈🌈创建线程

⚡⚡⚡⚡实现Thread类

我们可以实现一个继承了Thread类的类,然后重写run方法,在run方法中完成多线程的代码逻辑.然后再main方法中实例化对象,启动线程.

java 复制代码
// 定义一个Thread类,相当于一个线程的模板
class MyThread01 extends Thread {
    // 重写run方法// run方法描述的是线程要执行的具体任务
    @Overridepublic void run() {
        System.out.println("hello, thread.");
    }
}

/**
 * 继承Thread类并重写run方法创建一个线程
 *
 * 
 */
public class Thread_demo01 {
    public static void main(String[] args) {
        // 实例化一个线程对象
        MyThread01 t = new MyThread01();
        // 真正的去申请系统线程,参与CPU调度
        t.start();
    }
}

⚡⚡⚡实现Runnable接口

这里我们也可以通过实现Runnable接口,然后实现run方法,实现多线程.然后将Runnable以参数的形式传递给Thread类,完成线程的创建.

java 复制代码
// 创建一个Runnable的实现类,并实现run方法
// Runnable主要描述的是线程的任务
class MyRunnable01 implements Runnable {
    @Overridepublic void run() {
        System.out.println("hello, thread.");
    }
}
/**
 * 通过继承Runnable接口并实现run方法
 * 
 * 
 */
public class Thread_demo02 {
    public static void main(String[] args) {
        // 实例化Runnable对象
        MyRunnable01 runnable01 = new MyRunnable01();
        // 实例化线程对象并绑定任务
        Thread t = new Thread(runnable01);
        // 真正的去申请系统线程参与CPU调度
        t.start();
    }
}

⚡⚡⚡使用匿名内部类实现线程

由于我们上面的几种创建方式,都是利用继承和接口的形式,通过匿名内部类的学习,我们很容易想到我们完全可以利用匿名内部类实现一个线程,大大简化了代码的复杂性,是一种比较流行的形式.

java 复制代码
/**
 * 通过Thread匿名内部类的方法创建一个线程
 * 
 *
 */
public class Thread_demo03 {public static void main(String[] args) {
        Thread t = new Thread(){
            // 指定线程任务
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        };
        // 真正的申请系统线程参与CPU调度
        t.start();
    }
}

⚡⚡⚡通过Runnable匿名内部类创建一个线程

java 复制代码
**
 * 通过Runnable匿名内部类创建一个线程
 * 
 *
 */
public class Thread_demo04 {public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            // 指定线程的任务
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());                
            }
        });
        // 申请系统线程参与CPU调度
        t.start();
    }
}

⚡⚡⚡利用lambda表达式

不知道大家是否记得.lambda表达式的使用场景,就是这个类只用一次,并且这里必须是函数式接口,才可以使用,这里我们的Runnable就是函数式接口

所以这里我们也可以使用lambda表达式实现一个线程

java 复制代码
/**
 * 通过Lambda表达式的方式创建一个线程
 */
public class Thread_demo05 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            // 指定任务:任务是循环打印当前线程名
            while (true) {
                System.out.println(Thread.currentThread().getName());
                try {
                    // 休眠1000ms
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        // 申请系统线程参与CPU调度
        t.start();
    }
}

🌈🌈🌈Java中多线程的重要属性

这里强调一下==isDaemon()==方法,可以判定是否为后台线程即后台线程不会影响进程的运行,而前台线程结束时会关闭当前的进程的运行.

还有就是getState()获取当前线程的状态,一种有六种状态,这个我们后面再说

🌈🌈🌈启动线程

我们上面说了,可以以来Thread即java的标准库中的类来实现一个线程对象,但是,想要真正启动一个线程还是远远不够的,我们还需要依赖一个方法start(),来启动线程,只有调用了start这个方法,线程才会真正运行,这里还经常会有哦一个面试题:

run方法和start方法有什么区别???

run方法:它是线程的入口,在run方法内部我们完成线程内部要执行的代码,相当于线程的主题内容

start方法:它是线程真正启动的标志,只有当前对象调用了start方法,线程才会真正运行.

特别说明:一个线程,只能调用一次start方法

🌈🌈🌈线程终止

对于一个线程来说,要想终止,只能加快run方法的执行,将run方法结束,来结束线程.这里我们利用interrupt方法终止线程,并且这个方法还会设置一个标志位.

java 复制代码
public class ThreadInterruptExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            try {
                while (!Thread.currentThread().isInterrupted()) {
                    System.out.println("Thread is running...");
                    Thread.sleep(1000); // 模拟一些工作
                }
            } catch (InterruptedException e) {
                System.out.println("Thread was interrupted!");
            }
            System.out.println("Thread is exiting.");
        });

        thread.start(); // 启动线程

        try {
            Thread.sleep(2500); // 等待2.5秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        thread.interrupt(); // 请求中断线程
    }
}

这里就是在main方法中通过interrupt方法终止线程thread,然后开始执行main中的剩余代码.

特别说明interrupt方法执行之后,还会将sleep的线程唤醒,并且清空标志位

java 复制代码
public class ThreadInterruptExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            try {
                while (true) {
                    System.out.println("Thread is running...");
                    Thread.sleep(1000); // 模拟阻塞状态
                }
            } catch (InterruptedException e) {
                System.out.println("Thread was interrupted!");
                return; // 退出线程
            }
        });

        thread.start(); // 启动线程

        try {
            Thread.sleep(2500); // 主线程等待2.5秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        thread.interrupt(); // 请求中断线程
    }
}

我们创建了一个线程,该线程在无限循环中运行,并每1秒打印一次消息。

线程在循环中调用sleep(1000),模拟阻塞状态。

在主线程中,我们让线程运行2.5秒。

然后,我们调用线程的interrupt()方法,这会请求中断线程。

当线程的sleep()方法被中断时,会抛出InterruptedException。这个异常会清除线程的中断状态,并且线程会退出。

⚡⚡⚡线程等待

对于多个线程来货,CPU遵循的原则是随机调度,抢占式执行.,然后后面的线程就会进入我们无法控制线程的执行,但是我们控制线程的结束顺序,即我们让后结束的线程等待先结束的线程,这里把将后结束的线称为等待状态,我们一般叫做-阻塞状态.这种阻塞状态直到被等待线程执行完毕之后才能开始执行.
那么我们如何控制线程的结束顺序呢,一般通过join这个方法来控制,比如说现在有两个线程,a,b我们在a线程中调用一个b.join()此时就会使b线程先执行,a线程陷入阻塞状态,直到b执行完毕,a才能开始执行.

java 复制代码
public class Main {
    public static void main(String[] args) {
        Thread worker = new Thread(new Runnable() {
            public void run() {
                System.out.println("Worker thread is running.");
                try {
                    Thread.sleep(2000); // 模拟工作线程执行任务需要2秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Worker thread has finished.");
            }
        });

        worker.start(); // 启动工作线程

        try {
            worker.join(); // 主线程等待工作线程完成
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Main thread is continuing after worker thread has finished.");
    }
}

这里的join就是主线程在等待worker执行完毕之后,主线程才开始执行,主线程在这个过程中处于阻塞状态.

⚡⚡⚡join()方法

对于这个方法来说,它可以决定线程的等待状态,然而,如果被等待线程如果出现了一些bug,那么等待线程就会一直处于等待状态,即"死等状态",但是显然这种状态并不是我们想要的,所以join方法就给我们提供了两种版本,有参数和无参数版本,无参数即可能陷入死等状态,有参数即可以设置等待的时间单位为毫秒,时间结束不管被等待线程有没有执行完毕,都会结束这个join方法.

🌈🌈🌈线程的状态

对于进程来说,大致分为两种状态,就绪状态和阻塞状态.
就绪状态:正在运行,或者随时可以调到CPU上执行.
阻塞状态:线程处于等待状态,不能立刻执行

然而对于线程来说,他的状态就远远不止这两种了~~

线程一共有六种状态,这里给大家列举出来

NEW

此时线程的Thread对象已经创建完成,但是还没有启动线程,即没有调用start方法,此时的状态称为NEW,可以利用getState方法获取线程的状态

java 复制代码
public class Test {
        public static void main(String[] args) {
            Thread worker = new Thread(new Runnable() {
                public void run() {
                    System.out.println("Worker thread is running.");
                    try {
                        Thread.sleep(2000); // 模拟工作线程执行任务需要2秒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("Worker thread has finished.");
                }
            });


            System.out.println( worker.getState());
            worker.start(); // 启动工作线程

            try {
                worker.join(); // 主线程等待工作线程完成
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("Main thread is continuing after worker thread has finished.");
        }
}

运行结果:

⚡⚡⚡TERMINATED

此时的线程已将结束了,但是Thread类对象还没有销毁...

java 复制代码
public class Main {
    public static void main(String[] args) {
        Thread worker = new Thread(new Runnable() {
            public void run() {
                System.out.println("Worker thread is running.");
                try {
                    Thread.sleep(2000); // 模拟耗时操作
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt(); // 重新设置中断状态
                }
                System.out.println("Worker thread has finished.");
            }
        });

        worker.start(); // 启动线程

        while (!worker.getState().equals(Thread.State.TERMINATED)) {
            System.out.println("Main thread is waiting for worker thread to finish.");
            try {
                Thread.sleep(1000); // 每1秒检查一次
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); // 重新设置中断状态
            }
        }

        System.out.println("Worker thread is in TERMINATED state.");
    }
}

⚡⚡⚡BLOCKED

这是由于锁竞争导致的阻塞,这里我们还没有引入锁所以先跳过

⚡⚡⚡RUNNABLE

就绪状态:正在运行或者随时可以调度到CPU执行

java 复制代码
public class Test {
        public static void main(String[] args) {
            Thread worker = new Thread(new Runnable() {
                public void run() {
                    System.out.println( Thread.currentThread().getState());
                    System.out.println("Worker thread is running.");
                    try {
                        Thread.sleep(2000); // 模拟工作线程执行任务需要2秒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("Worker thread has finished.");
                }
            });


            //System.out.println( worker.getState());
            worker.start(); // 启动工作线程

            try {
                worker.join(); // 主线程等待工作线程完成
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("Main thread is continuing after worker thread has finished.");
        }
}

运行结果

⚡⚡⚡TIMED_WAITING和WAITING

这两种状态其实就是我们上面提到的一个是处于有时间的暂时等待,另一个则是无限制的死等,即可以利用join的两个版本实现这两个状态.对于有时间的,也可以用sleep检验...

🌈🌈🌈🌈线程安全

在多线程的概念中,线程安全是非常重要的一个知识点,一定要认真巩固,那么线程安全从何而来呢,这里就是因为多线程编程的底层原理,随机调度,抢占式执行,在这样的机制下就会导致线程安全的问题,为了让大家了解的更清楚,给大家举一个例子.

java 复制代码
public class MyThreadDemo1 {
    public static int count=0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
           for(int i=0;i<50000;i++){
               count++;
           }
        });


        Thread t2=new Thread(()->{
           for(int i=0;i<50000;i++){
               count++;
           }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();


        System.out.println(count);
    }
}

这里运行出来的结果就并非是100000,原因是内存中的count值没有及时更新

这里的count++其实不仅仅是一步操作,对于CPU来说其实一共有三步操作:

  • load:将数据从内存中加载到寄存器中
  • add:寄存器中的数值执行++操作
  • save:保存sount的值并更新内存中的count的值

然而对于多线程来说,随时都可能被其他线程占据cpu的运行资源,由于cpu的最小执行单位是指令,所以起码每次执行终止都会让当前的指令执行完毕.

这样的机制就会导致线程安全问题,下面给大家画图演示一下.

此时就会出现t2虽然count值已经加一,但是没有及时更新内存(方块)中的count值,所以导致后面t1加载count时还是0所以后面的count的值就是1

相关推荐
痞老板A小安装C43 小时前
redis的大key和热key问题解决方案
数据库·redis·bootstrap
飞升不如收破烂~3 小时前
Redis的String类型和Java中的String类在底层数据结构上有一些异同点
java·数据结构·redis
feilieren3 小时前
DataGrip 连接 Redis、TongRDS
数据库·redis·缓存
液态不合群3 小时前
Redis中常见的数据类型及其应用场景
数据库·redis·wpf
孙克旭_3 小时前
第三章 分布式缓存Redis
redis·分布式·缓存
Allen Bright3 小时前
Jedis存储一个-以String的形式的对象到Redis
数据库·redis·缓存
Allen Bright3 小时前
Jedis连接池的操作
java·redis
Allen Bright4 小时前
Jedis存储一个以byte[]的形式的对象到Redis
数据库·redis·缓存
NiNg_1_2345 小时前
Redis中的zset用法详解
数据库·redis·缓存
.Ayang5 小时前
微服务介绍
网络·安全·网络安全·微服务·云原生·架构·安全架构