Skip to main content

线程的使用

huhxOriginalAbout 12 minjavaThreadConcurrency

在上一篇博客我们讲到什么是线程,这里我们来简单的介绍下线程的使用。

线程的创建

线程的创建有两种方式,一种是继承Thread类,另一种是实现Runnable接口。比较推荐的是实现Runnable接口的这种方式。

Thread本身是实现了Runnable接口的,调用start()方法会新建一个线程(这个逻辑存在于native方法start0中)并让此线程调用run()方法。

继承Thread类

创建线程直接简单的方式就是:通过继承Thread类,重写run()方法。

public class HelloThread extends Thread {
    @Override
    public void run() {
        try {
            TimeUnit.SECONDS.sleep(1);
            System.out.println("Hello from a thread!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        var helloThread = new HelloThread();
        helloThread.start();
        System.out.println("In main thread.");
    }
}

注意:不要用run()来启动新线程(因为没有调用native方法start0启动线程)。它只会在当前线程中串行地执行run()中的代码。

实现Runnable接口

上面是通过继承方式来创建线程的,其实Thread类提供了Runnable的构造函数来创建线程。

public Thread(Runnable target) {
    this(null, target, "Thread-" + nextThreadNum(), 0);
}

构造函数把Runnable参数设置成Thread的局部变量target。之所以不用重写Thread类的run()方法,原因如下:

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

Thread默认的run()方法,如果target有值的话,就会调用target里面的run方法。所以我们只需实现Runnable接口,然后把这个接口作为参数传递给Thread即可。

以下是一个例子:

public class HelloRunnable implements Runnable {

    @Override
    public void run() {
        try {
            System.out.println("Hello from a thread!");
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        var thread = new Thread(new HelloRunnable());
        thread.start();
        System.out.println("In main method.");
    }
}

之所以推崇使用实现Runnable接口这种方式,原因如下:

  • Java 中的类只能单继承却可以多实现
  • Runnable任务与执行该任务的Thread对象分开,更加的通用灵活

线程的暂停

Thread.sleep方法会导致当前线程暂停执行一段指定的时间。这是一种使处理器时间可用于应用程序的其他线程或可能在计算机系统上运行的其他应用程序的有效方法。

当然sleep时间不能保证精确,因为它们受到底层操作系统提供的设施的限制,此外在睡眠期间还可以被中断终止。无论如何,您不能假设调用sleep会将线程暂停指定的时间段。

public class SleepThread {
    public static void main(String[] args) throws InterruptedException {
        var t = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("In t thread, Hello");
        });
        t.start();

        System.out.println("In main thread, Hello");
    }
}

Tips

调用sleep()的时候,锁并没有释放。所以t线程在睡眠期间,是没有释放锁的。

线程的终止

一般来说,线程在执行完毕后就会结束,无须手工关闭。但是Jdk中Thread类提供了一个stop()的方法用以立即终止线程。这个方法被废弃,极不推荐使用stop方法太过于暴力,强行把执行到一半的线程终止,可能会引起一些数据不一致的问题。

以下给出一个例子来加以说明:

public class StopThread {
    public static void main(String[] args) throws InterruptedException {
        var person = new Person("huhx", "hubei");

        var t = new Thread(() -> {
            person.setUsername("linux");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            person.setAddress("beijing");
        });

        t.start();
        Thread.sleep(1000);
        t.stop();

        System.out.printf("username = %s and address = %s%n", person.getUsername(), person.getAddress());
    }
}





 





 









输出结果是:

username = linux and address = hubei

上述例子中,我们在线程t中去修改person实例,在修改完username,我们在main线程调用t.stop()来终止线程t,这导致12行的setAddress没能执行。这就导致了person这个实例只是更新了username,而address没能得到更新。(上述流程,我们使用sleep来模拟现实中的停顿或者费时操作)

在真实的场景下这个问题可能会更严重。比如说转账业务A向B转账,A的钱被扣了,这时转账的线程被终止,钱没能转到对方账户B上面。🥶

线程的中断

上面提到stop()终止线程过于暴力而被打入冷宫,那么在JDK有更好的终止线程的方式吗?答案是有的,那就是线程中断。

在Java中线程中断是一种重要的线程协作机制。它并不会使线程立即退出,而是给线程发送一个通知,告知目标线程有人希望你退出啦!至于目标线程接到通知后如何处理,则完全由目标线程自行决定。

中断机制是使用称为中断状态的内部标志来实现的,与之相关的有以下三个Thread类中的方法:

public void interrupt()

public boolean isInterrupted()

public static boolean interrupted()
  • interrupt()方法设置目标线程的中断状态为true,并通知Jvm中断目标线程
  • isInterrupted()方法返回目标线程的中断状态
  • interrupted()方法返回目标线程的中断状态,并设置当前中断状态为false

下面给出一个例子来说明上述几个方法的使用:

public class InterruptThread {
    public static void main(String[] args) throws InterruptedException {
        var t = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                var interrupted = Thread.interrupted();
                System.out.println(interrupted);
            }
            System.out.println("In t thread, Hello");
        });
        t.start();

        System.out.println("Before interrupt: " + t.isInterrupted());
        t.interrupt();
        System.out.println("After interrupt: " + t.isInterrupted());

        System.out.println("In main thread, Hello");
    }
}

输出结果如下:

Before interrupt: false
false
In t thread, Hello
After interrupt: true
In main thread, Hello

我们创建了一个线程t,在t.interrupt()中断方法之前,使用isInterrupted()查询线程t的中断状态为false,而之后线程t的中断状态为true

t.interrupt()方法被调用,中断正在睡眠当中的线程t,导致线程t抛出InterruptedException异常,我们在catch块中使用Thread.interrupted()静态方法查询当前线程也就是线程t的中断状态,结果为 false。这是因为在java中,任何通过抛出InterruptedException退出的方法(比如sleep()wait()join()方法),都会清除中断标记。

当一个线程被中断,我们是可以在catch语句中自行决定被中断的逻辑,是终止线程还是另做其他都是可以的。上述的例子我们只是简单的打印当前线程的中断状态。

线程wait和notify

为了支持多线程之间的协作,JDK提供了两个非常重要的线程等待wait()方法和通知notify()方法。这两个方法并不是在Thread类中的,而是归属于Object类。

public final void wait () throws InterruptedException 

public final native void notify ()

实际上,只能在synchronzied语句中调用wait()notify()notifyAll()方法。如果在非synchronzied语句中调用这些方法,可以编译但是运行的时候会抛出异常IllegalMonitorStateException

Exception in thread "main" java.lang.IllegalMonitorStateException: current thread is not owner

意思是:这些方法的调用之前必须获得目标对象的一个锁

以下我们用一个事例来说明以上两个方法:

public class WaitNotifyThread {
    public static void main(String[] args) throws Exception {
        var waitThread = new Thread(() -> {
            synchronized (WaitNotifyThread.class) {
                try {
                    System.out.println("before wait: " + LocalDateTime.now());
                    WaitNotifyThread.class.wait();
                    System.out.println("after wait: " + LocalDateTime.now());
                } catch (InterruptedException e) {
                    throw new RuntimeException();
                }
            }
        });
        waitThread.start();

        TimeUnit.SECONDS.sleep(2);
        System.out.println("waitThread state: " + waitThread.getState());

        var notifyThread = new Thread(() -> {
            synchronized (WaitNotifyThread.class) {
                System.out.println("before notify: " + LocalDateTime.now());
                WaitNotifyThread.class.notify();
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("waitThread state: " + waitThread.getState());
                System.out.println("after notify: " + LocalDateTime.now());
            }
        });
        notifyThread.start();
    }
}






 













 













以上程序的输出结果:

before wait: 2023-08-14T21:32:31.104650
waitThread state: WAITING
before notify: 2023-08-14T21:32:33.096438
waitThread state: BLOCKED
after notify: 2023-08-14T21:32:36.099898
after wait: 2023-08-14T21:32:36.100387

main线程中,我们创建线程waitThread并启动。线程waitThread首先获得WaitNotifyThread.class的锁并进入到synchronized块中,调用了wait()方法进入了等待状态,线程被挂起,而锁被释放(这一点很重要,这样其他线程就可以拿到锁来唤醒此线程了🤪),此时waitThread线程处于WAITING状态。

在2秒之后,notifyThread线程被创建运行。由于waitThread已经释放了锁,notifyThread拿到WaitNotifyThread.class的锁并进入synchronized块中。notify()方法被调用,将尝试唤醒在WaitNotifyThread.class上面睡眠的某一个线程(在这个例子中,该线程就是waitThread)。

waitThread接收到notifyThread线程的唤醒,要做的第一件事并不是执行后续的代码,而是要尝试重新获得WaitNotifyThread.class的锁。如果暂时无法获得,waitThread还必须要等待这个锁,此时waitThread处于BLOCKED状态。从上述输出来看waitThread被唤醒还等了3秒才拿到了锁(这是因为notifyThread线程sleep了3秒,后面执行完释放了锁)。

当锁被成功获得后,waitThread线程接着运行,所以有输出:after wait

其实wait方法也有其可以指定超时时间的重载方法,如果修改程序如下所示:

WaitNotifyThread.class.wait(); // before

WaitNotifyThread.class.wait(10); // modified

此时waitThread要么被其他线程通知唤醒,要么过10毫秒自行尝试醒来。而且由于notifyThread还没有创建未占据锁,锁立马被waitThread再次拿到然后继续执行。此时输出结果为:

before wait: 2023-08-14T21:34:40.468331
after wait: 2023-08-14T21:34:40.481307
waitThread state: TERMINATED
before notify: 2023-08-14T21:34:42.456399
waitThread state: TERMINATED
after notify: 2023-08-14T21:34:45.461708

关于notifynotifyAll方法的区别,参考:notify与notifyAll

Tips

wait方法会释放锁,notifynotifyAll方法调用不会释放锁。

线程优先级

线程的优先级将该线程的重要性传递给了调度器,调度器将倾向于让优先级高的线程先得到执行。但是这并不意味着低优先级的线程得不到执行,只是它的执行频率或者机会较低,高优先级线程也可能也会抢占失败。

Jdk有10个优先级,但是与多数操作系统都不能很好的映射(Window上面有7个优先级且不固定,Salaris2312^{31}个优先级)。而且线程的优先级调度和底层操作系统有密切的关系,在各个平台上表现不一,并且这种优先级产生的后果也可能不容易预测,无法精准控制。

优先级的数字越大则优先级越高,它的有效范国在1到10之间。以下是Thread类内置的优先级常量

public final static int MNI_PRIORITY = 1;
public final static int NORM PRIORITY = 5; 
public final static int MAX_PRIORITY = 10;

Warning

在绝大多数时间里,所有线程都应该以默认的优先级运行,试图操纵线程优先级通常是一个错误的选择。

线程join和yield

join方法

join方法允许一个线程等待另一个线程的完成。

public class JoinThread {
    public static void main(String[] args) throws InterruptedException {
        var t = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("In t thread: " + LocalDateTime.now());
        });
        t.start();

        t.join();

        System.out.println("In main thread: " + LocalDateTime.now());
    }
}

上述代码有两个线程,一个是main,另一个是我们自己创建的t。当我们调用t.join()会导致main线程暂停直到t线程结束。所以上面的输出结果是:

In t thread: 2023-08-14T20:25:07.515364
In main thread: 2023-08-14T20:25:07.517288

join还有些带超时参数的重载方法,这些重载方法允许程序员指定等待时间。但是与sleep一样会依赖于操作系统的计时,因此您不应假设join将完全等待您指定的时间。

yield方法

yield方法的作用是放弃当前的CPU资源,将它让给其它的任务去占用CPU执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得CPU时间片。

调用yield静态方法是向调度程序提示当前线程愿意让出其当前对处理器的使用,但是调度程序可以忽略这个提示。

yield是一种启发式尝试,旨在改善线程之间的相对进度,否则会过度利用CPU。它的使用应该与详细的分析和基准测试相结合,以确保它确实具有预期的效果。

以下我们使用一个示例来说明:

public class YieldThread extends Thread {
    YieldThread(String string) {
        super(string);
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName() + ": " + i);
            if (i % 2 == 0) {
                Thread.yield();
            }
        }
    }

    public static void main(String[] args) {
        var t1 = new YieldThread("t1");
        var t2 = new YieldThread("t2");

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

这里我们创建线程t1和线程t2,两个线程运行同样的代码:遍历打印0到10的数字,并在index为偶数的时候,调用Thread.yield()放弃当前的CPU资源。

输出不确定(大部分情况两个线程是交替执行),某一次的结果为:

t1: 0 
t1: 1 
t1: 2 
t2: 0 
t1: 3 
t2: 1 
t1: 4 
t2: 2 
t1: 5 
t2: 3 
t1: 6 
t2: 4 
t1: 7 
t2: 5 
t1: 8 
t2: 6 
t1: 9 
t2: 7 
t2: 8 
t2: 9 

从结果可以得知,刚开始t1线程执行了012,也就是说当线程t1执行到i=0时,打印并调用Thread.yield()让出调度。但是下一次的执行权,还是分给了t1,所以紧接着打印:t1: 1

一般来说,yield方法对于调试或测试目的可能很有用,它可能有助于重现由于竞争条件而导致的错误。在设计并发控制结构(例如java.util.concurrent.locks包中的结构)时,它也可能很有用。但是在应用程序中,使用这种方法很少是合适的。

Tips

调用yield()的时候,锁并没有释放。

守护线程

所谓守护(Daemon)线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收器、JIT线程可以理解为守护线程。

与之对应的是用户线程,它可以认为是系统的工作线程,它会完成这个程序应该要完成的业务操作。当进程中所有的用户线程结束,守护线程会自动销毁。

public class DaemonThread {
    public static void main(String[] args) throws InterruptedException {
        var t = new Thread(() -> {
            int i = 0;
            try {
                while (true) {
                    System.out.println("i = " + i++);
                    TimeUnit.SECONDS.sleep(1);
                }
            } catch (InterruptedException e) {
                throw new RuntimeException();
            }
        });
        t.setDaemon(true);
        t.start();

        TimeUnit.SECONDS.sleep(5);
        System.out.println("Main thread is end.");
    }
}













 






上述我们创建线程t,并设置为守护线程。在线程t中,我们每隔一秒的打印递增数字。而在main线程中,我们sleep5秒之后就会退出。所以这个应用程序一共有两个线程,一个是守护线程t,另一个是用户线程main。所以在等到5秒之后main线程的正常退出,守护线程t也相应的结束了。

程序的输出结果如下:

i = 0
i = 1
i = 2
i = 3
i = 4
Main thread is end.

FAQ

在构建方法里面启动线程?

https://www.baeldung.com/java-thread-constructoropen in new window

notify与notifyAll的区别?

总结

参考