JavaEE初阶第三期:解锁多线程,从 “单车道” 到 “高速公路” 的编程升级(一)

发布于:2025-06-28 ⋅ 阅读:(15) ⋅ 点赞:(0)

专栏:JavaEE初阶起飞计划

个人主页:手握风云

目录

一、认识线程

1.1. 概念

1.2. 为什么要使用线程

1.3. 进程和线程的关系

1.4. 多线程模型

二、多线程的创建

2.1. 继承Thread类

2.2. 实现Runnable接口

2.3. 匿名内部类

2.4. lambda表达式


一、认识线程

1.1. 概念

        进程是操作系统进行资源分配和调度的基本单位。简单来说,进程就是程序的一次执行过程。通过进程,用户可以同时运行多个应用程序,提高计算机的利用率和用户体验。同时,进程间的隔离性也增强了系统的稳定性和安全性,一个进程的崩溃通常不会影响到其他进程。

1.2. 为什么要使用线程

        进程虽然说可以实现并发编程的效果,比如写一个服务器来同时处理多个进程,可以进程级别太高,创建或者销毁进程的开销很大。为了优化这些问题,于是引入了线程。

1.3. 进程和线程的关系

        进程包含线程,线程也可以被称为轻量级进程。每个线程都可以独立执行一段逻辑,并且独立在CPU上调度;同一个进程中的多个线程,共享进程的资源,如内存资源、文件描述符表等。当进程已经有了,在进程内部在创建新线程,就把申请资源开销省下来了。

1.4. 多线程模型

        我们假设有一个滑稽老铁去吃100只烧鸡,花费的时间肯定会太长。按照多进程模型,让两个滑稽老铁一人分别吃50只,虽然效率提升了,但我们需要额外申请内存空间。

        但我们如果按照多线程的模型来,让两个滑稽老铁在同一个桌子上来分别吃50只烧鸡,省下了申请资源的开销,效率就会进一步提高。

        但线程并不是越多,效率就越高。当滑稽老铁的数目进一步增多,但桌子就这么大,有可能其中一人吃得好好的,会被其他人挤走。此时非但不会提高效率,效率反而越低了,那此时就只能对机器进行性能优化。

        当线程数目比较少时,也会出现一些问题:

  • 当两个线程尝试操作一个共享的资源时,比如内存中的同一个变量,就可能会发生冲突,从而引起bug。
  • 如果某个线程抛出异常,且没有其他代码把这个异常catch住,就会导致进程内的所有线程会被随之带走,整个进程也会结束。

二、多线程的创建

        多线程编程和多进程编程都是操作系统提供的API,因为Java是一次编译到处运行的,JVM已经把系统差异给屏蔽了,JVM已经把多线程的API进行了较好地封装,但多进程的API封装得比较粗糙。下面就通过代码来感受一下Java中如何使用多线程。

2.1. 继承Thread类

public class Demo1 {
// 定义一个静态内部类MyThread,继承自Thread类
     static class MyThread extends Thread {
         // 重写run方法
         @Override
         public void run() {
             System.out.println("hello thread");
         }
     }

    public static void main(String[] args) {
        // 创建一个MyThread对象
        Thread thread = new MyThread();
        // 启动线程
        thread.start();
    }
}

        Thread类是Java中的核心类,用于表示和管理线程。由于操作系统本身就提供了一组操作线程的函数,但这个函数是C语言版本的。JVM把这些操作系统的函数给封装成了Java版本,到了程序员手中,就成了Thread。使用Thread类,就相当于在使用操作系统的API。run方法是线程执行体,包含了线程需要执行的任务代码。当线程启动时,run方法中的代码将在新的线程中执行,也就是多线程程序的入口。

        在MyThread类里面重写的是run()方法,但在main()方法里调用的却是start()方法,这是因为start()方法是调用系统API,真正在操作系统内部创建一个线程,这个新的线程就会以run()作为入口,执行里面的逻辑,而run()方法就不需要在代码中显式调用。

        但如果我们调用run()方法,虽然也能打印"hello thread",但没有创建新的线程,而是直接在main()方法所在的 “主线程” 里执行了run()方法的逻辑。对于一个进程,至少得有一个线程;对于一个Java程序来说,main()方法,所在的进程至少会有这个线程,这个线程就是主线程。上面的代码就具有一个主线程和一个新创建的线程。

  • sleep静态方法

        Thread.sleep是Thread类中的一个静态方法。当调用这个方法时,当前线程会被阻塞,不执行任何操作,直到指定的毫秒数过去。这个方法可以用于多种场景,比如在循环中添加延迟,或者在执行某些操作之前等待一段时间。

public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Hello Thread!");
    }
}
public class Demo2 {
    public static void main(String[] args) {
        // 创建一个MyThread对象
        Thread thread = new MyThread();
        // 启动线程
        thread.start();

        while (true) {
            System.out.println("Hello Main!");
            // 休眠1000秒
            Thread.sleep(1000);
        }
    }
}

        但此时的代码会显示出错误,原因是我们有受查的异常,必须进行显式处理,可以用throws抛出或者try{}catch{}进行捕获。

//throws
public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个MyThread对象
        Thread thread = new MyThread();
        // 启动线程
        thread.start();

        while (true) {
            System.out.println("Hello Main!");
            // 休眠1000秒
            Thread.sleep(1000);
        }
    }
}

//try{}catch{}
public class Demo2 {
    public static void main(String[] args) {
        // 创建一个MyThread对象
        Thread thread = new MyThread();
        // 启动线程
        thread.start();

        while (true) {
            System.out.println("Hello Main!");
            // 休眠1000秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

        同理,我们在MyTread类里面进行循环打印。但这里只能使用try{}catch{}进行捕获,因为run()方法是重写自父类的方法,如果使用throws抛出不符合父类方法的声明。

        完整代码实现:

public class MyThread extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("Hello Thread!");
            try {
                // 休眠1秒
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // 抛出运行时异常
                throw new RuntimeException(e);
            }
        }
    }
}
public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个MyThread对象
        Thread thread = new MyThread();
        // 启动线程
        thread.start();

        while (true) {
            System.out.println("Hello Main!");
            // 休眠1000秒
            Thread.sleep(1000);
        }
    }
}

        运行结果如下,虽然两个打印分别在不同的while循环中,两个线程属于并发关系,独立在CPU上执行。我们同时也可以看到两个线程不是严格交替执行的,由于两个线程都加了休眠,当1000毫秒到后,哪个线程线程的执行顺序是无法确定的,这是因为操作系统调度的线程是随机不可预测的。

        我们还有更直观的办法来观察多线程:在我们安装的JDK里面的bin目录下,以管理员身份运行jconsole.exe,然后点击连接本地进程Demo2,然后点击线程,就可以看到我们的Thread-0和main两个线程,剩下的线程都是JVM自带的,这些线程进行了一些背后的操作,比如垃圾回收、记录统计信息、记录一些调试信息等。

2.2. 实现Runnable接口

public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个实现了Runnable接口的实例
        Runnable myRunnable = new MyRunnable();
        // Runnable本身没有start方法,需要配合Thread类来使用
        // 创建一个Thread对象,并将myRunnable作为参数传入
        Thread thread = new Thread(myRunnable);
        // 启动线程
        thread.start();
        while (true) {
            System.out.println("Hello Main!");
            Thread.sleep(1000);
        }
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("Hello Tread!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

        运行结果:

        对于第一种继承Thread写法,描述任务的时候,代码是写到Thread子类中的,意味着任务内容与Thread类耦合度高,未来想把这个任务给别的主体来执行。

        对于第二种Runnable接口写法,任务是写到Runnable中,不涉及到任何和“线程”的概念,任务内容和Thread耦合度几乎没有,后序可以把这个任务交给进程或者协程来执行。

2.3. 匿名内部类

public class Demo4 {
    public static void main(String[] args) {
        // 创建一个新的线程
        Thread thread = new Thread() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("Hello Tread!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        
        // 启动线程
        thread.start();
        
        while (true) {
            // 在主线程中输出"Hello Main!"
            System.out.println("Hello Main!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

        此处创建了没有的名字匿名内部类,并且这个类是Thread类的子类,该子类也重写了run()方法,也创建了子类实例,通过thread引用指向子类。这样写可以简化代码。

        运行结果如下:

2.4. lambda表达式

public class Demo5 {
    public static void main(String[] args) {
        // 创建一个新的线程
        Thread thread = new Thread(() -> {
            while (true) {
                System.out.println("Hello Thread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

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

        // 无限循环
        while (true) {
            System.out.println("Hello Main!");
            try {
                // 主线程休眠1秒
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

        运行结果如下:


网站公告

今日签到

点亮在社区的每一天
去签到