Java实习生面试复习(五):Thread线程学习

我是一名很普通的双非大三学生。接下来的几个月内,我将坚持写博客,输出知识的同时巩固自己的基础,记录自己的成长和锻炼自己,备战2021暑期实习面试!奥利给!!

多线程也是面试中必问的点,是必备的基础技能。

Thread(线程)的概念

首先我们要知道线程的概念是什么,在继续往下说,大家可以先闭上眼回忆一下:

线程(Thread)是并发编程的基础,也是程序执行的最小单元,它依托进程而存在。一个进程中可以包含多个线> 程,多线程可以共享一块内存空间和一组系统资源,因此线程之间的切换更加节省资源、更加轻量化。

线程的状态有哪些?

线程的状态在 JDK 1.5 之后以枚举的方式被定义在 Thread 的源码中,它总共包含以下 6 个状态:

    public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }

六种状态分别为:

  • NEW,新建状态,线程被创建出来,但尚未启动时的线程状态
  • RUNNABLE,就绪状态,表示可以运行的线程状态,它可能正在运行,或者是在排队等待操作系统给它分配 CPU 资源;
    • 比如Thread.start方法就是将线程从NEW状态 转换成 RUNNABLE 状态。
  • BLOCKED,阻塞等待锁的线程状态,表示处于阻塞状态的线程正在等待监视器锁
    • 比如等待执行 synchronized 代码块或者使用 synchronized 标记的方法。
  • WAITING,等待状态,一个处于等待状态的线程正在等待另一个线程执行某个特定的动作。
    • 比如,一个线程调用了Object.wait()方法,那它就在等待另一个线程调用Object.notify()Object.notifyAll() 方法。
  • TIMED_WAITING,计时等待状态,和上者类似,只是多了一个超时时间。
    • 比如调用了有超时时间设置的方法 Object.wait(long timeout)Thread.join(long timeout) 等这些方法时,它才会进入此状态;
  • TERMINATED,终止状态,表示线程已经执行完成。

但是这 6 种状态并不是线程所有的状态,只是在 Java 源码中列举出的 6 种状态, Java 线程的处理方法都是围绕这 6 种状态的。

线程的工作模式

线程的工作模式:

  • 首先要new Thread()创建线程,此时线程的状态是NEW,表示线程创建成功但没有运行。
  • 然后再调用线程的 start() 方法,此时线程就从 NEW(新建)状态变成了 RUNNABLE(就绪)状态。
  • 此时线程会判断要执行的方法中有没有 synchronized 修饰的代码块或方法,如果有并且其他线程也在使用此锁,那么线程就会变为 BLOCKED(阻塞等待)状态,当其他线程使用完此锁之后,线程会继续执行剩余的方法。
  • 当在遇到 Object#wait、Thread#join、LockSupport#park 这些方法时,线程会变为 WAITING(等待状态)状态,如果是带了超时时间的等待方法,那么线程会进入 TIMED_WAITING(计时等待)状态
  • 当有其他线程执行了 notify() 或 notifyAll() 方法之后,线程被唤醒继续执行剩余的业务方法,直到方法执行完成为止,此时整个线程的流程就执行完了。
    执行流程如下图所示:
    线程的工作模式
    这里要区分开BLOCKEDWAITING 的区别:
    虽然二者都有等待的含义,但还是区别很大的,首先它们状态形成的调用方法不同,其次BLOCKED可以理解为当前线程还处于活跃状态,只是在阻塞等待其他线程使用完某个锁资源;而WAITING则是因为自身调用了Object.wait()Thread.join()LockSupport.park()而进入等待状态,只能等待其他线程执行某个特定的动作才能被继续唤醒,比如当线程因为调用了Object.wait()而进入WAITING状态之后,则需要等待另一个线程执行Object.notify()、Object.notifyAll() 才能被唤醒。

线程的使用

创建线程的方式

有幸听过一节马哥的公开课小马哥慕课直播,感兴趣的可以看看,视频很不错,上和下加一起有六个小时在第12分钟的时候,有说到其实创建线程的方式从本质上说,只有一种,那就是Thread.run(),因为不管你是继承Thread,还是实现Runnable接口,我们可以看源码:

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

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

运行线程的方式有两种,一种当用Runnable接口时间,thread.run会判断target在决定调用谁的run,而线程thread实际是重写了该run方法。怎么用就不我例举了吧。
说到这里,可能有同学就疑惑了,明明我平常写代码调的是start()不是run()啊?这里看这篇文章java中多线程执行时,为何调用的是start()方法而不是run()方法我在补充两点:

  • start()方法不能被多次调用,多次调用会抛出java.lang.IllegalStateException;而run()方法可以进行多次调用,因为它只是一个普通的方法。

线程常见的方法

线程的优先级

优先级代表线程执行的机会的大小,优先级高的可能先执行,低的可能后执行,在 Java 源码中,优先级从低到高分别是 1 到 10,线程默认 new 出来的优先级都是 5,源码如下:

// 最低优先级
public final static int MIN_PRIORITY = 1;

// 普通优先级,也是默认的
public final static int NORM_PRIORITY = 5;

// 最大优先级
public final static int MAX_PRIORITY = 10;

在程序中我们可以通过 Thread.setPriority() 来设置优先级

join方法

在一个线程中调用其他线程对象的join()方法时 ,这时候当前线程会让出执行权给其他线程,直到 其他线程执行完或者过了超时时间之后再继续执行当前线程,join() 源码如下:

    public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            // 其他线程好了之后,当前线程的状态是 TERMINATED,isAlive 返回 false
            // NEW false
            // RUNNABLE true
            while (isAlive()) {
                // 等待其他线程,一直等待
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                // 等待一定的时间,如果在 delay 时间内,等待的线程仍没有结束,放弃等待
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

从源码中可以看出 join() 方法底层还是通过 wait() 方法来实现的。假设我们现在有一道题,保证三个线程的有序执行,让你去实现,想想用join怎么实现?

        Thread thread1 = new Thread(() -> {
            System.out.println("t1执行");
        }, "t1");
        Thread thread2 = new Thread(() -> {
            System.out.println("t2执行");
        }, "t2");
        Thread thread3 = new Thread(() -> {
            System.out.println("t3执行");
        }, "t3");

        thread1.start();
        thread1.join();
        thread2.start();
        thread2.join();
        thread3.start();
        thread3.join();

在想想,在看过join的源码后,你还有什么方法可以保证线程的有序执行?这里在沾两个方法:

        Thread thread1 = new Thread(() -> {
            System.out.println("t1执行");
        }, "t1");
        Thread thread2 = new Thread(() -> {
            System.out.println("t2执行");
        }, "t2");
        Thread thread3 = new Thread(() -> {
            System.out.println("t3执行");
        }, "t3");

        thread1.start();
        while (thread1.isAlive()) {
        }
        thread2.start();
        while (thread2.isAlive()) {
        }
        thread3.start();
        while (thread3.isAlive()) {
        }
        Thread thread1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "执行");
        }, "t1");
        Thread thread2 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "执行");
        }, "t2");
        Thread thread3 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "执行");
        }, "t3");

        threadStartAndWait(thread1);
        threadStartAndWait(thread2);
        threadStartAndWait(thread3);

    public static void threadStartAndWait(Thread thread) {
        System.out.println(Thread.currentThread().getName());
        if (Thread.State.NEW.equals(thread.getState())) {
            thread.start();
        }

        // Java Thread 对象和实际 JVM 执行的OS Thread 不是相同对象
        // JVM Thread 回调 Java Thread.run() 方法
        // 同时 Thread 提供一些 native 方法获取 JVM Thread 状态
        // 当 JVM Thread 执行完之后,自动就notify()了
        while (thread.isAlive()) {  // thread 特殊的object
            // 当线程Thread isAlive() == false 时,thread.wait() 操作会被自动释放
            synchronized (thread) {
                try {
                    //阻塞的是这个对象所在的线程(通常是主线程)
                    thread.wait(); // 到底是谁在通知Thread -> thread.notify();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

yield方法

yield() 为本地方法,源码如下:

    // 当前线程做出让步,放弃当前 cpu,让线程重新选择 cpu,避免线程过度使用 cpu
    // 让步不是不执行,也有可能重新选中自己
    public static native void yield();

yield() 方法表示给线程调度器一个当前线程愿意出让 CPU 使用权的暗示,但是线程调度器可能会忽略这个暗示。
例如下面这段代码:

public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("线程:" + Thread.currentThread().getName() + " I:" + i);
                if (i == 5) {
                    Thread.yield();
                }
            }
        };
        Thread t1 = new Thread(runnable, "T1");
        Thread t2 = new Thread(runnable, "T2");
        t1.start();
        t2.start();
}

当你多次执行这段代码的时候,会发现有时候执行结果会不一样,这是因为如上所说,它是不稳定的。

sleep方法

sleep 也是本地 (native) 方法,意思是当前线程会沉睡多久,但是沉睡时不会释放锁资源,所以沉睡时,其它线程是无法得到锁的。

interrupt(),interrupted() 和 isInterrupted() 的区别

interrupt():将调用该方法的对象所表示的线程标记一个停止标记,并不是真的停止该线程。

interrupted():获取当前线程的中断状态,并且会清除线程的状态标记。是一个是静态方法。

isInterrupted():获取调用该方法的对象所表示的线程的中断状态,不会清除线程的状态标记。是一个实例方法。

简单的中断案例:t1先执行,但是sleep不释放锁资源,在这期间t2等候两秒钟还没拿到锁就中断

public class Test {
    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            testsync();
        },"t1");
        Thread t2 = new Thread(() -> {
            testsync();
        },"t2");
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        t2.start();
        TimeUnit.SECONDS.sleep(2);
        System.out.println("main");
        /**
         * 如果t2两秒钟还拿不到就中断
         */
        t2.interrupt();
    }
    public static void testsync(){
        try {
            /**
             * lockInterruptibly 和 lock的区别,前者会直接抛出异常可以响应中断,后者则不可以
             */
            lock.lockInterruptibly();
            System.out.println(Thread.currentThread().getName());
            TimeUnit.SECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

总结

这篇文章简单介绍了线程的状态以及线程的执行流程,还介绍了BLOCKED(阻塞等待)和WAITING(等待)的区别,start()方法和run()方法的区别,以及thread的几个常见方法

看完这篇文章你能回答出这些问题了吗?

  • BLOCKED(阻塞等待)和 WAITING(等待)有什么区别?
  • start() 方法和 run() 方法有什么区别?
  • 线程的优先级有什么用?该如何设置?
  • 线程的常用方法有哪些?
  • 怎么中断线程?
发布了33 篇原创文章 · 获赞 71 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_39809458/article/details/105025259
今日推荐