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

发布于:2025-06-25 ⋅ 阅读:(22) ⋅ 点赞:(0)

专栏:JavaEE初阶起飞计划

个人主页:手握风云

目录

一、Thread类及常用方法

2.1. Thread的常见构造方法

2.2. Thread的常见属性

2.3. 启动一个线程

2.4. 中断一个线程

2.5. 等待一个线程

2.6. 休眠当前线程


一、Thread类及常用方法

2.1. Thread的常见构造方法

方法 说明
Thread() 创建线程对象
Thread(Runnable target) 使用Runnable对象创建线程对象
Thread(String name) 创建线程对象并命名
Thread(Runnable target, String name) 使用Runnable对象创建线程对象并命名

        name参数用来给线程取名字。名字叫啥,不影响线程的执行,不同的名字更利于调试。

public class Demo1 {
    public static void main(String[] args) {
        // 创建线程对象
        Thread t1 = new Thread() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello t1");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t1.start();

        // 使用Runnable对象创建线程
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello t2");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });
        t2.start();

        // 创建线程对象并使用name参数命名
        Thread t3 = new Thread("这是我的t3线程") {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello t3");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        t3.start();

        // 使用Runnable对象创建线程并使用name参数命名
        Thread t4 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello t4");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }, "这是我的t4线程");
        t4.start();
    }
}

        如上图所示,我们会发现这里没有main主线程。这是因为start执行完毕后,main方法就执行结束了,对应的主线程也结束了,随之自动销毁。

        对于ThreadGroup线程组,把若干个线程放到同一个组里面,这样的话就可以给每个组里的线程设置相同的属性。但这个并不常用,更多的是用到线程池。

2.2. Thread的常见属性

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

        ID是线程的唯一标识,不同的线程不会重复。获取名称常用于调试和日志记录,帮助开发者了解当前正在执行的线程名称。线程状态可以分为NEW(新建)、RUNNABLE(就绪/运行)、BLOCKED(阻塞)、WAITING(等待)、TIMED_WAITING(超时等待)和TERMINATED(消亡)。线程的优先级是一个整数,表示线程在运行时的重要程度。Java中线程的优先级范围从1到10,其中1是最低优先级,10是最高优先级。

        isDaemon()方法用于判断线程是否为后台线程。后台进程,当线程没运行完,进程可以就结束,无论有多少个后台进程,都无法阻止进程结束。对应的还有前台进程,当线程没运行完,进程就不会结束,当有多个线程时,就得所有线程结束才能结束进程。main线程和自己创建的线程都是前台进程,剩下的都是后台进程。如果一个线程做得任务很重要,这个任务必须要做完,应该设置为前台线程;相反如果任务无关紧要,就可以设置为后台进程。isAlive()方法用于判断线程是否在运行。当一个线程启动后,它会一直运行,直到run()方法执行完毕或者线程被中断。isInterrupted()方法用于判断线程是否被中断。当一个线程被中断时,它的中断状态会被设置为true,可以通过isInterrupted()方法来检查这个状态。

public class Demo2 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (true) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"This is mine");

        t.start();

        System.out.println(t.getId());
        System.out.println(t.getName());
        System.out.println(t.getState());
        System.out.println(t.getPriority());

        System.out.println(t.isDaemon());
        System.out.println(t.isAlive());
        System.out.println(t.isInterrupted());
    }
}

2.3. 启动一个线程

        调用start()方法会真正调用系统中的API,线程跑起来之后就会自动执行到run()。调用start的方法本身就很快,一旦执行start,代码就会自动往下执行,不会产生任何的阻塞等待。

public class Demo2 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("hello thread");
        });
        t.start();
        System.out.println("hello main");
    }
}

        执行结果如上图所示,大部分情况下都是hello main在前,也可能会有例外。这是因为在start之后,main线程和t线程两个执行流,是一个并发执行关系。操作系统对于线程的调度是随机的,如果执行完start恰好被调度出CPU,此时CPU下次执行main还是t就不确定了。

        一个线程对象只能被启动一次。线程执行了start之后,就是就绪状态或者阻塞状态,对于这两种状态,不能再启动了。

public class Demo2 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("hello thread");
        });
        t.start();
        System.out.println("hello main");
        t.start();
    }
}

        总结:使用Interrupt方法时,t线程没有使用sleep等阻塞操作,t的isInterrupted()方法返回true,通过循环条件结束t线程;t线程使用了sleep等阻塞操作,t的isInterrupted()方法也会返回true,但sleep如果被提前唤醒,抛出InterruptedException异常。

2.4. 中断一个线程

        这里不要跟操作系统里的中断搞混,线程的中断准确来说应该叫“打断”或者“终止”。正常情况下,一个线程需要把入口方法执行完,才能够使线程结束。但有时候,我们希望线程能够提前终止,尤其是在线程休眠的时候。这时就需要通过打断线程的操作,也需要线程本身代码做出配合。

  • 通过变量
public class Demo3 {
    public static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (flag) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();

        System.out.println("hello main");
        Thread.sleep(5000);
        flag = false;
        System.out.println("让t线程终止");
    }
}

  • 通过内置的标志位isInterrupted()

        Thread对象中,包含了一个布尔变量,如果为false,说明没有人去尝试终止这个线程;如果为true,说明有人尝试终止。

public class Demo4 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (!t.isInterrupted()) {
                
            }
        });
    }
}

        此时这个代码是有错的,原因如下图所示:变量t未初始化,因为此处针对lambda表达式的定义是在new Thread()之前。

        我们可以使用Thread类内部的静态方法currentThread(),用于获取对当前正在执行的线程对象的引用。

public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            // 当线程t没有被中断时,打印"hello thread"
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("hello thread");
            }
        });
        t.start();
        // 主线程休眠4秒
        Thread.sleep(4000);
        // 中断线程t
        // 把标志位false改为true
        t.interrupt();
    }
}

        这里可能打印的结果有点多,我们也可以让t线程休眠1秒。

public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            // 当线程t没有被中断时,打印"hello thread"
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        // 主线程休眠4秒
        Thread.sleep(5000);
        // 中断线程t
        // 把标志位false改为true
        t.interrupt();
    }
}

        我们发现当线程抛出异常时,但线程并没有终止结束。线程里面有个奇怪的设定:如果线程t正在休眠,此时在main中调用interrupt()方法,就能把sleep提前唤醒。InterruptedException支持sleep提前唤醒,通过一场区分sleep是睡足了还是提前醒了。sleep提前唤醒,触发异常之后,然后sleep就会把isInterrupted标志位给重置为false。

        之所以会有这样奇怪的设定,是为了给程序员留下更多的操作空间。提前唤醒,可能还存在一些“还未完成的工作”,让程序员自行决定线程t是继续执行、立即结束还是稍等一会结束。上面的代码相当于完全忽视了终止请求。

public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            // 当线程t没有被中断时,打印"hello thread"
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // 打印异常调用栈//
                    // e.printStackTrace();
                    // 触发异常就会结束循环,终止线程
                    break;
                }
            }
        });
        t.start();
        // 主线程休眠4秒
        Thread.sleep(5000);
        // 中断线程t
        // 把标志位false改为true
        t.interrupt();
    }
}

        上述几种方式本质上都是线程t自己决定自己是否要终止,相当于main只是给t提供了一个“建议”而不是强制结束。

2.5. 等待一个线程

        有时,我们需要等待⼀个线程完成它的⼯作后,才能进⾏⾃⼰的下⼀步⼯作。例如,张三只有等李四转账成功,才决定是否存钱。线程等待,约定了两个线程结束的先后顺序。

        哪个线程中调用的join,该线程就是等待的一方;join前面的引用,该线程就是被等的一方。

public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    break;
                }
            }
        });
        t.start();

        // 主线程中,可以对线程t进行等待
        System.out.println("主线程等待之前");
        // 由于主线程也可能触发阻塞,也可能抛出InterruptedException异常
        t.join();
        System.out.println("主线程等待之后");
    }
}

        join这个等待是个死等,只要被等的线程没有结束,join都会始终阻塞。如果上面是个死循环,那么主线程就会永远等待t线程。

        上面是t线程等待主线程,也可以让主线程等待t线程。

public class Demo6 {
    public static void main(String[] args) throws InterruptedException {
        // 获取主线程
        Thread mainThread = Thread.currentThread();
        Thread t = new Thread(() -> {
            try {
                System.out.println("t线程等待之前");
                mainThread.join();
                System.out.println("t线程等待之后");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t.start();

        // 主线程等待t线程
        for (int i = 0; i < 10; i++) {
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

        当然也可以让两个线程同时等待对方,但是这样写代码是意义的,会造成两个线程都无法结束,都无法完成对方的等待操作。

        虽然join会触发阻塞,但也不一定会触发,比如在主线程等待t线程之前,t线程已经结束了,此时join就不会阻塞。join默认会是死等,但死等这种情况不太好,如果程序出现了意外,永远等不到结果。所以join方法还有其它的重载版本,可以指定一个最大等待时间(超时时间)。

方法 说明
join() 等待线程结束
join(long millis) 等待线程结束,最多等millis毫秒
join(long millis,int nanos) 更高精度
public class Demo7 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    break;
                }
            }
        });
        t.start();

        System.out.println("等待之前");
        // 等3秒之后就不等了
        t.join(3000);
        System.out.println("等待之后");
    }
}

2.6. 休眠当前线程

        Thread.sleep本质就是让线程的状态变成“阻塞”状态,此线程就不参与CPU调度了。休眠时间到了,线程的状态恢复成就绪状态(不是立即执行),才能参与CPU调度。


网站公告

今日签到

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