JAVA基础 -- 并发编程Ⅰ

305 阅读7分钟

定义

进程:操作系统资源分配的最小单位;
线程:CPU调度的最小单位

启动

启动线程的方式有且仅有两种方式(这也是JDK Thread源码注释标注的):
1、Clazz extend Thread,然后Clazz.start();
2、Clazz implements Runnable,然后交给一个Thread运行;
image.png

状态

Java中线程的状态分为6种
1、初始(NEW):新创建了一个线程对象,但还没调用start()方法;
2、运行(RUNNABLE):Java线程种将就绪(ready)和运行中(running)两种状态笼统的成为“运行”;
* 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)*
3、阻塞(BLOCKED):表示线程阻塞于锁;
4、等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断);
5、超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回;
6、终止(TERMINATED):表示该线程已经执行完毕。 image.png

线程终止

1、我们可以看到Thread类中,有一些标注过期的方法,比如stop()、destroy()方法等,在android的源码中,这些方法不仅标注为过期,而且直接抛出了一个运行时异常,可以证明这些终止线程的方法都是不友好甚至有很多弊端的方法,我们尽量不可使用。因为这些方法都是较为粗暴的直接停止线程,借用JDK对stop()方法的注释: 这个方法最初是为了强制一个线程停止,并抛出ThreadDeath的异常,它本质上是不安全的。

2、interrupt()方法:设置此线程的中断标志,该方法并不会对线程做出真实的中断操作,只是将该线程的状态设置为中断状态,具体的中断逻辑,需要我们自己程序根据具体逻辑自行判断是否要中断线程;当在此线程调用interrupt()方法后,会导致wait()、wait(long)、join()、join(long)、sleep(long)、sleep(long, int)此类的方法抛出InterruptedException异常,并清除中断状态;

3、isInterrupted()方法与interrupted()方法的区别:

  • isInterrupted()方法是普通方法,interrupted()方法是静态方法;
  • isInterrupted()判断该线程的中断状态,interrupted()方法同样是判断该线程的中断状态,其次该线程在判断该线程的中断状态后,会清除该线程的中断状态(例:如果interrupted()方法被连续调用两次,第二次调用将返回false)
public class Test {

    public static class TestThread extends Thread {
        @Override
        public void run() {
            // 1、isInterrupted()方法判断中断状态
            while (!isInterrupted()) {
                try {
                    TestThread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    // 收到中断异常,该线程的中断状态会被清除,在这里调用 isInterrupted() 将获得false
                    // 需要再次触发中断
                    interrupt();
                }
            }
            // 2、interrupted()方法判断中断状态
            // while (!Thread.interrupted()) {
                    // 再次调用Thread.interrupted()方法后,将获得 false
            // }
        }
    }

    public static void main(String[] args) {
        TestThread testThread = new TestThread();
        testThread.start();
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        testThread.interrupt();
    }
}

wait()、notify()、notifyAll()方法

1、wait()方法根据上述状态图可知,会使当前线程进入等待状态,直到另一个线程调用notify()或notifyAll(),白话就是当前线程必须拥有此对象锁,线程wait()后释放该锁的所有权,并等待另一个线程通知在此锁对象上等待的线程唤醒,通过调用notify()方法或notifyAll()方法,然后线程等待直到它可以重新获得锁的所有权并恢复执行。

2、notify()和notifyAll()都会唤醒在该锁对象上等待的线程,区别就是,notify()方法是随机唤醒一个该对象锁上等待的线程,notifyAll()方法会唤醒所有该对象锁上等待的线程;唤醒的线程并不是立马开始工作,是先进入就绪状态,在当前线程放弃对该对象锁的所有权后,唤醒的线程进行锁的竞争,获得锁的所有权的线程恢复执行;所以,一般来说,常使用notifyAll()方法唤醒线程,如果能确保锁对象上等待的线程有且仅有一个,使用notify()亦可;

3、wait()会catch住InterruptedException获得中断信号,在获得异常抛出后,该线程的中断状态将被清除

public class WaitTest {

    private final static Object lock = new Object();

    public static class TestThread extends Thread {
        @Override
        public void run() {
            while (!isInterrupted()) {
                synchronized (lock) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        interrupt();
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        TestThread testThread = new TestThread();
        testThread.start();
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        lock.notifyAll();
    }
}

yield()方法

1、yield()方法根据上述状态图所示,调用yield()方法后,线程会从运行中(running)状态 转换为 就绪(ready)状态;
2、yield()方法是提示CPU调度器当前线程愿意让出CPU时间片,CPU会再次进行调度选择线程进行执行,该线程依旧可能会被选中;
3、yield()方法平时开发过程中很少适合使用的场景,运用场景更多用于调试或测试目的,可能有助于重现由于竞争条件导致的错误;

join()方法

1、join()方法根据上述状态图所示,调用join()方法后,当前线程会从运行中(running)状态进入等待(waiting)状态;
2、join()方法根据JDK对该API的解释为等待该线程死亡,这里比较绕,需要理解的是指在当前调用x.join()方法的线程(主线程)上,x.join()后面的代码,要等到x线程(子线程)执行结束后才能继续执行;
3、面试点:如何使三个线程按顺序执行;使用join()方法就是其中一个答案;
4、join()方法同样catch住了InterruptedException获得中断信号,是可中断的;
5、join()方法实现原理实际使用的是wait()和notifyAll(),看JDK1.8 join()方法源码;

// 等同于 synchronized(this)
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) {
        // 进入等待
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

成员方法加了synchronized关键字,说明这个锁的对象是this(该线程对象本身),也就是说,主线程持有了子线程对象的锁;
6、有了wait()必然会有notify(),什么时候才会notify,这个在jvm源码里有:

static void ensure_join(JavaThread* thread) { 
    Handle threadObj(thread, thread->threadObj()); 
    ObjectLocker lock(threadObj, thread); 
    thread->clear_pending_exception(); 
    java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED); 
    java_lang_Thread::set_thread(threadObj(), NULL); 
    // 子线程执行结束后,唤醒thread对象锁上等待的所有线程
    lock.notify_all(thread); 
    thread->clear_pending_exception(); 
}

到这里就知道了,java中是什么时候调用notify的,到目前为止,整个join()原理就梳理清楚了;

sleep()方法

1、sleep()方法是一个静态方法,根据上述状态图可知,调用sleep()方法,是当前正在执行的线程由运行中(running)状态进入等待(waiting)状态;
2、sleep()方法JDK对于该方法的注释为,使当前正在只想的线程休眠(暂停执行)指定的毫秒数,线程进入休眠不会释放锁资源的所有权;
3、sleep()方法同样catch住了InterruptedException获得中断信号,是可中断的;

总结

1、线程中断常使用interrupt()进行线程的中断信号设置,成员方法isInterrupted()和静态方法interrupted()都可判断线程中断信号,区别是静态方法interrupted()判断中断信号后会清除;
2、wait()、notify()、notifyAll()常使用在生产者消费者设计模式中,notify()方法唤醒一个在该锁对象上休眠的线程,而notifyAll()则唤醒所有在该锁对象上休眠的线程;
3、yield()方法平时开发中使用较少,可以简单理解为让出CPU执行权,让CPU重新进行线程调度;
4、join()方法则是在一个线程需要在另一个线程执行完成后接着执行,也即是线程间有序执行时使用;
5、sleep()方法使当前正在运行的线程进入休眠,与wait()方法不同的是,调用sleep()方法进入等待后,线程不会释放所持有的锁资源,而wait()方法则会释放该线程所持有的锁资源,唤醒后重新竞争锁资源;