《Java并发变成之美》阅读笔记三(第一章 并发编程线程基础)

63 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

1.4 等待线程执行终止的join方法

引用场景

在我们一些情况下,我们需要线程A先完成,然后再完成线程B,如果不加约束的话,两个线程就会抢占式执行,这个是不行的,那我们这个时候就需要用到join了,它能让运行这个方法的线程等待,等待调用它的线程执行完成之后,运行这个方法的线程才能运行。具体的我们可以看一段代码。

public static void main(String[] args) {
    Thread t1 = new Thread(()->{
        for(int i = 0;i<10;i++){
            System.out.println("ThreadI: " + i);
        }
    });
    t1.start();
    for(int i = 0;i<10;i++){
        System.out.println("mainI: " + i);
    }
}
我们可以看到t1和main是共同运行的,一会打印t1里面的内容,一会是打印main函数里面的内容。那我们现在加上join()看看有什么不一样的地方吧
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(()->{
        for(int i = 0;i<10;i++){
            System.out.println("ThreadI: " + i);
        }
    });
    t1.start();
    t1.join();
    for(int i = 0;i<10;i++){
        System.out.println("mainI: " + i);
    }
}

屏幕截图 2022-09-29 145422.jpg 我们可以看到,现在是先打印t1里面的,然后打印main函数的。

我们可以知道,使用这个方法的是main这个线程,调用这个方法的是t1这个线程。

使用join方法的线程会等待,调用join方法的线程。

1.5 让线程睡眠的sleep方法

当一个执行中道德线程调用了Thread的sleep方法后,调用线程会暂时让出指定时间的执行权,但是线程所持有的一些资源是不让出的,比如锁。

private static Object lock = new Object();

public static void main(String[] args) {
    Thread t1 = new Thread(()->{
        try {
            synchronized (lock){
                System.out.println("t1 get lock");
                Thread.sleep(1000);
                System.out.println("t1 lose lock");
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    });
    Thread t2 = new Thread(()->{
        try {
            synchronized (lock){
                System.out.println("t2 get lock");
                Thread.sleep(1000);
                System.out.println("t2 lose lock");
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    });
    t1.start();
    t2.start();
}

从上述代码就可以看出线程在休眠的时候是不会释放锁的。

屏幕截图 2022-09-29 150730.jpg

不管你运行几次,t1和t2都不会交错打印

在指定时间睡眠之后该函数就会正常返回,线程就会处于就绪的状态,然后参与CPU的调度,如果线程在睡眠阶段被其他线程调用了这个线程的interrupt()方法中断该线程,则sleep方法会抛出异常返回。

1.6 让出CPU执行权的yield方法

yield是Thread类中的一个静态方法。当一个线程调用这个函数的时候,就会暗示线程调度器,当前线程想要让出CPU的执行权,让出执行权之后,线程就会处于就绪状态。

这个跟上面介绍的sleep还是有点像的,但是他们两个还是有区别

和sleep的区别

当线程调用sleep这个方法的时候,线程会被阻塞挂起到指定时间结束,在这个期间线程调度器是不会去调度这个线程的

当线程调用yield方法的时候,线程只是让出了自己剩余的时间片,并没有阻塞挂起,只是从执行状态变成就绪状态,线程调度器还是有可能调度这个线程的。

1.7 线程中断

void interrupt()方法

中断线程。

当线程A在运行的时候,线程B调用了线程A的interrupt方法的时候,只是改变了线程A的中断标志位true。线程A并不会受到影响,只是一个标志被改了而已。

public static void main(String[] args) {
    Thread t = new Thread(()->{
        while(!Thread.currentThread().isInterrupted()){
            System.out.println("hello");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    });
    t.start();
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    t.interrupt();
    System.out.println("end");
}

这段代码就是main线程调用了t线程的interrupt,然后改变了while()里面判断的标志,导致了线程t的中断。

还有要注意的是,当线程A在调用wait系列函数,join方法或者sleep方法而被阻塞挂起,这个时候其他线程再调用A的interrupt的时候,就会报错返回

boolean isInterrupted()方法

检测当前线程是否被中断,如果是就返回true,否则就是false

boolean interrupted()方法

检测当前线程是否被中断,如果是就返回true,否则就返回false,跟上面那个方法不一样的是,这个方法如果发现当前线程被中断,就会清除中断标志,并且这个方法是静态方法,可以用Thread直接调用。而且这个方法获得的中断标志是当前线程的,跟调用的实例对象没有关系。

1.8 理解线程上下文切换

在多线程编程中,线程一般都是多于CPU的个数的,而每个CPU同一时刻只能被一个线程使用,但是又为了让用户使用起来感觉所有线程是同时执行的,这个时候就用到了时间片轮转的策略,也就是给每个线程发送一个时间片线程在时间片内调用CPU执行任务,当前线程使用完时间片之后就会让给下一个线程,这个就是上下文切换。

那么当线程再一次执行的时候,它是怎么知道上一次自己执行到哪了呢?

所以在切换上下文的时候需要保存当前线程的执行现场,等下一次再调用的时候就可以复原现场

线程上下文切换的时机有:当前线程的CPU时间片使用完,当前线程被其他线程中断。