小工具知识网

当前位置: 首页 > 行业知识 > 【JavaEE初阶 -- 多线程】

行业知识

【JavaEE初阶 -- 多线程】

2026-11-14 15:29:43 来源:互联网转载

认识线程(Thread)+Thread类及常见方法

  • 1.认识线程(Thread)
    • 1.1 线程
    • 1.2 进程和线程的关系和区别
    • 1.3 Java的线程和操作系统线程的关系
    • 1.4 创建线程
    • 2. Thread类及常用的方法
      • 2.1 Thread的常见构造方法
      • 2.2 Thread的几个常见属性
      • 2.3 启动一个线程-start()
      • 2.4 终止一个线程
      • 2.5 等待线程(结束)
      • 2.6 获取当前线程引用
      • 2.7 线程的状态
      • 2.8 休���当前线程

        1.认识线程(Thread)

        1.1 线程

        重点:

        • 进程是系统分配资源(CPU、硬盘、内存等)的基本单位
        • 线程是系统调度执行的基本单位

          多进程编程时进程在进行频繁的创建和销毁的时候,开销比较大(资源的申请和释放上)。所以,线程就是在进程的基础上,做出了改进,保持了独立调度执行,省去了 分配、释放资源带来的额外开销,多线程并发编程是必须的:

          • 单核CPU想要提高算力,就需要提升到多核CPU,而并发编程能够更加充分利用多核CPU资源。
          • 在某些情况(等待IO),也需要用到并发编程。
          • 虽然多进程也能实现并发编程,但是线程比进程更轻量。
          • 创建、销毁、调度线程 比 创建、销毁、调度进程更快。
          • 一个线程就是一个“执行流”,每个线程之间都可以按照顺序执行自己的代码,多个线程之间同时执行着多份代码。

            如上图,是一个进程有一个PCB,但是实际上,一个进程可以有多个PCB,意味着这个线程包含了一个线程组包含了多个线程。

            • 操作系统进行多任务调度本质上是在调度PCB,线程在系统中的调度规则和进程是一样的。线程的PCB中也有状态、优先级、上下文和记账信息。
              • PCB中一个属性:内存指针,多个线程的PCB的内存指针,指向的是同一个内存空间。意味着只是创建了第一个线程的时候需要从系统分配资源,后续的线程就不必分配,直接共用前面的那份资源就可以了。除内存之外,文件描述符表(操作硬盘)也是多个线程共用一份的。
              • 把能够资源共享的这些线程,分成组,称为线程组,线程组也是进程的一部分。

                1.2 进程和线程的关系和区别

                假如有这么一个方案:两个房间和桌子,两个老铁吃100只鸡。

                1. 多进程:创建新的进程 就需要申请更多的资源(2 房间桌子,一人吃50)
                2. 多线程:房间和桌子没有增加,吃鸡的人增加了,两个人一人吃50,仍然能提高效率,且资源开销更小。
                3. 引入更多的线程:即人多了,吃的鸡少了速度更快了,此时他们共享同一份资源。
                4. 问题1:人越多越好吗,当引入的线程达到一定的数量之后,接着引入线程就提升不了,人多桌子坐不下。即当线程数越来越多的时候,线程之间就会互相竞争,CPU的资源(CPU的核心是有限的),非但不会提高效率,反而会增加调度的开销。即并不是线程越多越好。
                5. 问题2:重点:线程之间会起冲突,假如2个人同时想吃同一个鸡腿,就会发生冲突。线程之间起了冲突,就可能导致代码中出现一些逻辑上的错误,即线程的安全问题。
                6. 问题3:共享资源也会的问题,当一个线程如果抛出异常,没有处理好,就可能会导致整个进程被终止。

                进程和线程的关系和区别:

                小结:

                1. 进程是包含线程的
                2. 每个线程,也是一个独立的执行流,可以执行一些代码,并且单独的参与到CPU调度中(状态、上下文、优先级、记账信息,每个线程都有自己的一份)
                3. 每个进程,有自己的资源,进程中的线程共用这一份资源(内存空间和文件描述符表)
                • 进程是资源分配的基本单位,线程是系统调度执行的基本单位
                  1. 同一个进程中的线程之间,可能会干扰。从而引起线程安全问题。
                  2. 进程和线程之间,不会相互影响。如果同一个进程中的某个线程,抛出异常,是可能会影响到其他线程,会把整个进程的所有线程都异常终止。
                  3. 线程也不是越多越好,如果线程太多了,调度的开销可能非常明显。

                  1.3 Java的线程和操作系统线程的关系

                  在Java中编写代码一般是使用多线程并发编程,系统提供了多线程编程的api。在Java标准库,把这些api封装了,在代码中就可以使用了。

                  注意:随机调度:抢占式执行。

                  1. 一个线程什么时候被调度到CPU上执行,时机是不确定的
                  2. 一个线程什么时候从CPU上下来给别人让位,时机也是不确定的

                  1.4 创建线程

                  //后面一般是把”跑起来“的程序 称为”进程“  没有运行起来的程序(exe),称为”可执行文件“//1.创建一个自己的类,继承Threadclass MyThread extends Thread {    //这里的Thread类,不需要导包就能直接使用,因为在Java标准库中,有一个特殊的包java.lang(默认包含)    //此处的run方法,不需要手动调用,会在合适的时机(线程创建好了之后),被jvm自动调用执行    // 这种风格的函数,称为”回调函数(callback)“    // 方法重写 本质上 是让自己能够对现有的类进行扩展    @Override    public void run() {        //run方法就是该线程的入口方法        System.out.println("hello word");    }}// 一个.java文件中,只能有一个public的类//这个类如果没有public包级作用于. 就是只能在当前包里被其他的类使用public class ThreadDemo1 {    //一个进程中,至少会有一个线程,这个进程中的第一个线程,也就称为”主线程“    // mian方法,也就是主线程的入口法    public static void main(String[] args) {        // 2.根据刚才的类,创建出示例(线程实例,才是真正的线程)        Thread t = new MyThread();        // 3.调用Thread的start方法,才会真正的调用系统api,在系统内核中创建出线程  线程就会执行上面写好的run方法        t.start();    }}
                  1. 创建一个自己的类来继承Thread类,重写run方法
                  class MyThread2 extends Thread {    @Override    public void run() {        // 如果加上throws,修改了方法签名,此时就无法构成”重写“了        //父类的run,没有throws这个异常,子类重写的时候,也就不能有throws异常        while (true) {            //每秒钟(每次sleep唤醒的时间)先执行mian还是先执行thread,随机调度,抢占式执行            System.out.println("hello word");            try {                //没有处理异常的话 在sleep 1000的过程中可能会被提前唤醒                Thread.sleep(1000);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }}public class TreadDemo2 {    //主线程调用start方法后,即立即往下执行打印了,同时,内核就要通过刚才的线程api构建出线程出来,并且执行run    //由于创建线程本身也有开销(虽然开销比创建进程低,但是也不是0),    //第一轮打印,在创建线程开销本身影响下,导致hellothread会比hello main略慢一点 先打印mian概率更大不是100%    //mian线程其实是第一个线程  ,进程创建第一线程开销最大的,剩下的线程开销都比较小,但不是0    public static void main(String[] args) { //执行一个线程        Thread t = new MyThread2();        t.start();  //调用start创建线程之后,沿着main方法继续执行,打印hellomain        // 另外一路  进入到线程的run方法,打印hello word        // 真正运行程序,可以看到两个循环都在执行        // 这两个线程,就是两个独立的执行流        while (true) {            System.out.println("hello main");            try {                Thread.sleep(1000);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }}
                  1. 实现Runnable接口,重写run方法
                  class MyThread3 implements Runnable {    @Override    public void run() {        while (true) {            System.out.println("hello runnable");            try {                Thread.sleep(1000);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }}public class ThreadDemo3 {    public static void main(String[] args) {        //只是一段可执行的代码        Runnable runnable = new MyThread3();        //还是要搭配Thread类,才能真正在系统中创建出线程,这种写法,就是把线程 和要执行的任务进行了解耦合        Thread t = new Thread(runnable);        t.start();        while (true) {            System.out.println("hello main");            try {                Thread.sleep(1000);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }}
                  1. 继承Thread,重写run,但是使用的是匿名内部类:
                  //继承Thread,重写run,但是使用匿名内部类,在类里面定义的类,该类没有名字,意味着不能重复使用public class ThreadDemo4 {    public static void main(String[] args) {        // t执行的实例是Thread的子类        Thread t = new Thread() {  //{}定义一个类,这个类继承Thread,可以定义子类的属性和方法            //此处的主要目的就是重写run方法            @Override            public void run() {                while (true) {                    System.out.println("hello thread");                    try {                        Thread.sleep(1000);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            }        };        t.start();        while (true) {            System.out.println("hello main");            try {                Thread.sleep(1000);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }}
                  1. 实现Runnable类,重写run,匿名内部类
                  public class ThreadDemo5 {    public static void main(String[] args) {        //Thread构造方法的参数,填写了Runnable的匿名内部类        Thread t = new Thread(new Runnable() {            @Override            public void run() {                while (true) {                    System.out.println("hello runnable");                    try {                        Thread.sleep(1000);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            }        });        t.start();        while (true) {            System.out.println("hello main");            try {                Thread.sleep(1000);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }}
                  1. 重点推荐/使用:使用lambda表达式:
                  public class TreadDemo6 {    public static void main(String[] args) {        //() -> 这里的()前面应该有一个函数名,此处作为匿名函数,就没有名字        Thread t = new Thread(() -> {           while (true) {               System.out.println("hello thread");               try {                   Thread.sleep(1000);               } catch (InterruptedException e) {                   e.printStackTrace();               }           }        });        t.start();        while (true) {            System.out.println("hello main");            try {                Thread.sleep(1000);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }}

                  2. Thread类及常用的方法

                  2.1 Thread的常见构造方法

                  在Java中自己创建的线程,默认按照Thread-0 1 2 3…

                  线程之间的名字是可以重复的,同一个工作需要多个线程完成时,都可以起一样的名字。

                  2.2 Thread的几个常见属性

                  属性: -------------------------获取方法

                  • ID ----------------getld()
                  • 名称--------------getName()
                  • 状态--------------getState()
                  • 优先级-----------getPriority()
                  • 是否后台线程----isDaemon()
                  • 是否存活---------isAlive()
                  • 是否被终止------isInterrupted()

                    2.3 启动一个线程-start()

                    调用start创建出新的线程,本质上是start调用系统的api,来完成创建线程的操作。

                    class MyThread extends Thread {    @Override    public void run() {        //run方法就是该线程的入口方法        System.out.println("hello word");    }}public class ThreadDemo1 {    //一个进程中,至少会有一个线程,这个进程中的第一个线程,也就称为”主线程“    // mian方法,也就是主线程的入口法    public static void main(String[] args) {        // 2.根据刚才的类,创建出示例(线程实例,才是真正的线程)        Thread t = new MyThread();        // 3.调用Thread的start方法,才会真正的调用系统api,在系统内核中创建出线程  线程就会执行上面写好的run方法        t.start();        //t.run();  // 这个操作还是在main线程打印hello word    }}

                    start和run的区别:

                    • start()是创建一个新的线程
                    • run() 这个操作还是在main主线程

                      2.4 终止一个线程

                      1. 怎么让线程提前终止?核心就是:让run方法能够提前结束,取决于 具体代码实现方式。

                        run方法和main方法执行顺序是不确定的。

                      public class ThreadDemo12 {    private static boolean isQuit = false;    public static void main(String[] args) {        //boolean isQuit = false;  局部变量用不了        //lambda表达式 ,变量捕获(lambda表达式/匿名内部类是可以访问到外面的局部变量)        // 捕获的变量得是 final,但是final不能修改 所以这里不能使用局部变量        Thread t = new Thread(() -> {            //lambda表达式,本质上是 函数式接口 =》 匿名内部类            //内部类 访问外部类的成员 是可以的,所以这一过程不受到 变量捕获 的影响            while (!isQuit) {                System.out.println("我是一个线程,工作中");                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    e.printStackTrace();                }            }            System.out.println("线程工作完毕");        });        t.start();        try {            Thread.sleep(3000);        } catch (InterruptedException e) {            e.printStackTrace();        }        System.out.println("让t线程结束");        isQuit = true;    }}

                      通过Thread.currentThread().isInterrupted() 使用一个interrupt方法,来修改刚才标志位的值。

                      在cath语句中,通过处理线程:

                      • 1)让线程立刻结束 ,加上break
                      • 2)让线程不结束, 不加break
                      • 3)让线程执行一些逻辑之后再结束,即写一些代码再加break

                        另外通过catch语句对异常进行处理:

                      • 尝试自动恢复
                      • 记录日志(将异常信息记录到文件中)
                      • 发出警报
                      • 也有少数的正常的业务逻辑,会依赖到catch(如文件操作中,通过catch来结束循环之类的)
                        public class ThreadDemo13 {    public static void main(String[] args) {        Thread t = new Thread(() -> {            while (!Thread.currentThread().isInterrupted()){                System.out.println("我是一个线程,正在工作中");                //在执行sleep的过程中,调用interrupt,大概率sleep休眠时间还没到,被提前唤醒了                //提前唤醒之后,会做两件事:1.抛出InterruptedException(被catch获取到。)2.清除Thread对象的isinterrupt标志位                // 通过interrupt方法已经把标志位设置为true了,但是sleep提前唤醒操作,就把标志位又设为false,此时循环继续执行                //想要结束,只需要在catch中加上break                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    e.printStackTrace();  //打印异常                    //加上break,抛出异常之后,线程也会结束                    break;                }            }            System.out.println("线程执行完毕");        });        t.start();        try {            Thread.sleep(3000);        } catch (InterruptedException e) {            e.printStackTrace();        }        //使用一个interrupt方法,来修改刚才标志位的值        System.out.println("让 t 线程结束");        t.interrupt();    }}

                        2.5 等待线程(结束)

                        多个线程的执行顺序是不确定的,是随机调度,抢占式执行。虽然线程底层的调度是无序的,但是在应用程序中,通过一些api来影响线程执行的顺序。比如join(),影响 线程结束 的先后顺序。

                        public class ThreadDemo14 {    public static void main(String[] args) throws InterruptedException {        Thread t = new Thread(() ->{            for (int i = 0; i  
                        • 典型情况:使用多线程并行开发进行一系列的计算,用一个线程阻塞等待上述计算,等待所有的线程都计算完了,最终这个线程汇总结果。
                        • 主线程通过join等待,这两线程结束,t和t2都执行结束之后,此时主线程才能继续往下执行,才能打印结果和计算时间。
                          // 让主线程创建一个新的线程,由新线程负责完成一系列的运算(1+2+。。+1000)由主线程负责获取到最终结果public class ThreadDemo15 {    // t 线程把计算的结果放到result中    private static long result = 0;    public static void main(String[] args) throws InterruptedException {        Thread t = new Thread(() -> {            // 如果不加一个临时变量来保存每次计算的值,会出现线程安全问题导致每次计算的结果不一样            long tmp = 0;            for (long i = 1; i                 tmp += i;            }            result += tmp;        });        Thread t2 = new Thread(() - {            /*try {                // 如果把join加到开头,就是先执行t,t2被阻塞,等t执行完再执行t2,此时是串行执行                t.join();            } catch (InterruptedException e) {                e.printStackTrace();            }*/            long tmp = 0;            for (long i = 50_0000_0001L; i                 tmp += i;            }            result += tmp;        });        t.start(); // start就是立即的在内核中创建出一个新的线程,且和之前的线程是“并发执行”        t2.start();        long beg = System.currentTimeMillis();        // 使用join就会严格按照t执行结果作为等待的条件        // 主线程通过join等待这两个线程结束,t和t2都执行结束之后,此时主线程才能继续执行        // 如果把join加在这末尾,t和t2并发执行,没啥区别        t.join();        t2.join();        long end = System.currentTimeMillis();        // 不加 t.join() 如果主线程直接就打印result,此时得到的结果是无法预期的        // 由于线程之间的执行顺序不确定,主线程打印的result可能是还没有开始计算的初始值        // 也有可能是计算过程中间结果,也可能是t线程计算完最终结果        //加了t.join()之后,结果一定是t线程执行结束的结果        System.out.println("result = "+result);        System.out.println("time= "+ (end - beg)+" ms");    }}    @Override    public void run() {        // 获取到线程的引用,直接使用this        System.out.println((this.getId() + "," + this.getName()));    }}public class ThreadDemo16 {    public static void main(String[] args) throws InterruptedException {        MyThread5 t1 = new MyThread5();        MyThread5 t2 = new MyThread5();        t1.start();        t2.start();        Thread.sleep(1000);        System.out.println(t1.getId()+","+t1.getName());        System.out.println(t2.getId()+","+t2.getName());    }}    public static void main(String[] args) {        Thread t1 = new Thread(() - {            Thread t = Thread.currentThread();            System.out.println(t.getId()+","+t.getName());        });        Thread t2 = new Thread(() -> {            Thread t = Thread.currentThread();            System.out.println(t.getId()+","+t.getName());        });        t1.start();        t2.start();    }}

                          2.7 线程的状态

                          • 就绪:这个线程可以去CPU上执行了(也包含在正在CPU上执行)
                          • 阻塞:这个线程暂时不方便去CPU上执行,Java中,针对阻塞状态又进行了细分
                            1. NEW :Thread对象已经创建好了,但是还没有调用start方法在系统中创建线程
                            2. TERMINATED: Thread对象仍然存在,但是系统内部的线程已经执行完了
                            3. RUNNABLE :就绪状态,表示这个线程正在CPU上执行,或者排队等待调度
                            4. TIMED_WAITING: 指定时间的阻塞,就在到达一定的时间之后自动解除阻塞

                              使用sleep会进入这个状态,使用带有超时时间的join也会

                            5. WAITING : 不带时间的阻塞(***等),必须要满足一定的条件,才会解除阻塞,

                              join或者wait都会进入WAITING

                            6. BLOCKED : 由于锁竞争,引起的阻塞(即会引起线程安全)
                            • 如果发现某个进程卡住了,就可以使用jconsole这个工具(在JDK中的bin目录下),查看这个进程中的一些重要线程的状态和调用栈,通过状态,就可以判断线程是否阻塞,以及阻塞原因。

                              2.8 休眠当前线程

                              public class ThreadDemo18 {    public static void main(String[] args) throws InterruptedException {        Thread t = new Thread(() -> {            for (int i = 0; i                                                 

上一篇:怎么看主机ipc

下一篇:ThinkPHP 3.2源码深度解析,探索热门PHP框架的奥秘