zoukankan      html  css  js  c++  java
  • [Java多线程]多线程基础

    Java线程基础

    创建线程与执行线程:start与run

    • start方法启动线程,执行线程的run方法

    创建线程

    方法一:直接使用Thread

    // 创建线程并给它起个名字
    Thread t = new Thread("t1") {
        @Override
        public void run() {}
    }
    // 启动线程
    t.start();
    
    • Thread类是Java对线程模型的抽象
    • 创建线程,只是创建了线程对象,还没有交给操作系统调度;启动线程才是把线程交给操作系统的任务调度器,分配时间片,执行...

    方法二:实现Runnable接口

    // (1) 实现Runnable接口
    Runnable runnable = new Runnable() {
    	public void run() {}
    }
    // (2) lambda 方式简化
    Runnable runnable = () -> log.info("lambda");
    
    // 创建线程对象
    Thread t = new Thread( runnable ,"t2");
    // 启动线程
    t.start();
    
    • 这种方式把任务线程分开,把逻辑上的业务和具体的实现解耦,更加灵活
    • Runnable接口和线程池的使用结合的更好

    方法三:FutureTask配合Thread

    // 创建任务对象
    FutureTask<Integer> task = new FutureTask<>(() -> {
        log.debug("hello");
        return 100;
    });
    
    // 创建线程,启动任务
    new Thread(task,"t1").start();
    // get返回线程执行结果,等待返回结果时主线程阻塞
    log.debug("结果是:{}",task.get());
    
    • FutureTask实现了Runnable接口和Callable接口,后者允许通过get()方法返回线程执行得结果;这种方式和前两种的区别就在于它实现了Callable接口,能够获得线程的返回值。
    • 注意,当任务的状态完成时,get()方法才会返回或者抛异常,如果任务未完成,get()方法会阻塞并直到任务完成

    线程的生命周期

    java的线程本质上就是操作系统的线程,它们是一一对应的。不同的语言对操作系统的线程的封装都是类似的。下图是一个通用的线程生命周期模型:初始状态,可运行状态,运行状态,休眠状态,终止状态。

    image-20211212155327259

    • 初始状态:线程在编程语言层面被创建,但是还不允许被分配CPU,在操作系统层面,真正的线程还没有创建
    • 可运行状态:线程准备就绪,可分配CPU执行,操作系统的线程已经被创建
    • 运行状态:有空闲的CPU,操作系统将CPU分配给一个可运行状态的线程,被分配到CPU的线程状态就变成了运行状态
    • 休眠状态:运行状态的线程如果调用一个阻塞的 API(例如以阻塞方式读文件)或者等待某个事件(例如条件变量),那么线程的状态就会转换到休眠状态,同时释放 CPU 使用权,休眠状态的线程永远没有机会获得 CPU 使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态。
    • 终止状态****:线程执行完或者出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期结束了。

    JVM对其中的可运行状态和运行状态做了封装,对Java来说,只有RUNNABLE状态,它包含了可运行状态和运行状态;同时休眠状态做了细化,分为BLOCKED(阻塞状态)WAITING(无时限等待)和 TIMED_WAITING(有时限等待),也就是说只要 Java 线程处于这三种状态之一,那么这个线程就永远没有 CPU 的使用权。

    img

    其中,理解几个状态之间的转换条件是关键!

    • RUNNABLE 和 BLOCKED的状态转换

    只有一个条件,就是线程等待synchronized的排他锁时,线程的状态会从RUNNABLE转换为BLOCKED。synchrobized修饰的方法,代码块同一时刻只允许一个线程执行,其他线程只能等待,这时,等待的线程会从RUNNABLE转换到BLOCKED状态。也就是线程等待进入同步区域的时候,线程会进入BLOCKED状态。

    注意,我们通常说调用阻塞式API也会阻塞线程,但是这种阻塞式操作系统层面的,对于Java线程来说,它还是处于RUNNABLE状态。JVM不关心操作系统层面的调度相关状态,在JVM看来,等待CPU的使用权(操作系统对应的线程状态为可执行状态)与等待IO(操作系统层面对应休眠状态)没有区别,都是在等待某个资源,所以都是RUNNABLE状态。

    所以Java里的RUNNABLE类比操作系统里的线程状态,包含就绪,运行和阻塞。

    • RUNNABLE和WAITING的状态转换

    WAITING状态的线程不会被分配CPU时间片,他们需要等待其他线程显示地唤醒。有三个场景会触发这种转换:

    1. 获得synchronized锁的线程,调用无参Object.wait()方法。
    2. 调用无参数的Thread.join()方法。
    3. 调用LockSupport.park()方法。
    • RUNNABLE和TIMED_WAITING状态转换

    处于TIMED_WAITING状态也不会被分配CPU时间片,不过无需等待被其他线程显示唤醒,在一定时间之后他们会被系统自动唤醒。有五种场景会触发这种转换

    1. 调用带超时参数的Thread.sleep(long millis)
    2. 获得synchronized锁的线程,调用带参数的Object.wait(long timeout)
    3. 调用带超时参数的Thread.join(long millis)
    4. 调用带超时参数的LockSupport.parkNanos(Object blocker,long deadline)
    5. 调用带超时参数的LockSupport.parkUntil(long deadline)

    基本上都是带超时参数的方法,会触发该状态转换

    • NEW到RUNNABLE状态

    Java刚创建完Thread对象就是NEW状态,NEW状态的线程不会被操作系统调度。调用Thread.start()方法会让线程进入RUNNABLE状态,也就是交给操作系统调度。

    • RUNNABLE到TERMINATED
    1. 线程执行完了
    2. 线程执行过程中发生异常

    如果想主动终止线程,需要调用interrupt方法,该方法会通知线程,线程可以通过异常或者主动检测来处理线程终止。

    还有个方法stop,不过该方法会直接杀死线程,不给线程"善后"的机会。试想,如果该线程持有锁,而此时给他直接stop掉,那么别的线程岂不是永远都无法请求到这个锁了。所以该方法已经@Deprecated

    interrupt()终止的方式

    1. 通过异常的方式通知线程终止

    当线程 A 处于 WAITING、TIMED_WAITING 状态时,如果其他线程调用线程 A 的 interrupt() 方法,会使线程 A 返回到 RUNNABLE 状态,同时线程 A 的代码会触发 InterruptedException 异常。上面我们提到转换到 WAITING、TIMED_WAITING 状态的触发条件,都是调用了类似 wait()、join()、sleep() 这样的方法,我们看这些方法的签名,发现都会 throws InterruptedException 这个异常。这个异常的触发条件就是:其他线程调用了该线程的 interrupt() 方法。

    当线程 A 处于 RUNNABLE 状态时,并且阻塞在 java.nio.channels.InterruptibleChannel 上时,如果其他线程调用线程 A 的 interrupt() 方法,线程 A 会触发 java.nio.channels.ClosedByInterruptException 这个异常;而阻塞在 java.nio.channels.Selector 上时,如果其他线程调用线程 A 的 interrupt() 方法,线程 A 的 java.nio.channels.Selector 会立即返回。

    1. 线程主动检测方式

    如果线程处于 RUNNABLE 状态,并且没有阻塞在某个 I/O 操作上,例如中断计算圆周率的线程 A,这时就得依赖线程 A 主动检测中断状态了。如果其他线程调用线程 A 的 interrupt() 方法,那么线程 A 可以通过 isInterrupted() 方法,检测是不是自己被中断了。

    while(this.isInterrupted()) {
        // 处理中断
    }
    

    sleep与yield

    sleep

    • 调用sleep会让当前线程从running进入Timed Waiting状态(阻塞)
    • 其他线程可以使用interrupt方法大端正在sleep的线程,这是sleep方法会抛出InterruptedException
    • 睡眠结束后的线程还要重新竞争CPU的使用权

    yield

    • 调用yield会让线程从running进入runnable就绪状态,也就是退回到就绪状态,让出当前线程 的CPU时间片,重新竞争CPU的使用权
    • 具体的实现依赖于操作系统的任务调度器

    线程优先级

    • 线程优先级会提示调度器优先调度该线程,但仅仅是一个提示,调度器可以忽略它
    • 针对频繁阻塞的(休眠或者IO)的线程设置较高优先级,针对计算密集型的任务设置较低的优先级,防止处理器被它独占
    • 只有CPU非常繁忙时,优先级高的线程会获得更多的时间片
    • 优先级为1-10,值越小优先级越小,默认为5

    守护线程(Dameon)

    守护线程是在程序运行的时候在后台提供一种通用服务的线程,比如GC垃圾回收线程。它的特点是当所有非守护线程结束时,程序终止,同时会杀死进程中所有守护线程。

    • 创建线程时,设置setDaemon(true)即可创建守护线程,注意一定要在执行线程的start方法之前设置
    public class SimpleDaemons implements Runnable {
        @Override
        public void run() {
            try {
                while (true) {
                    TimeUnit.MILLISECONDS.sleep(100);
                    System.out.println(Thread.currentThread() + " " + this);
                }
            } catch (Exception e) {
                System.out.println("sleep() interrupted");
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 10; i++) {
                Thread daemon = new Thread(new SimpleDaemons());
                daemon.setDaemon(true);
                daemon.start();
            }
            System.out.println("all daemon started");
    //        TimeUnit.MILLISECONDS.sleep(1); // 主线程很快执行完了,守护线程还没来得及执行,整个进程就结束了
            TimeUnit.MILLISECONDS.sleep(150); // 守护线程在100ms后执行了一次,然后主线程结束,进程退出
        }
    }
    -------------------
    all daemon started
    Thread[Thread-4,5,main] _1_thread._7_daemon.SimpleDaemons@5f4e76b6
    Thread[Thread-2,5,main] _1_thread._7_daemon.SimpleDaemons@144add34
    Thread[Thread-7,5,main] _1_thread._7_daemon.SimpleDaemons@8cde255f
    Thread[Thread-11,5,main] _1_thread._7_daemon.SimpleDaemons@11cc546a
    Thread[Thread-5,5,main] _1_thread._7_daemon.SimpleDaemons@21b013b5
    ...
    
    • 当所有非守护线程都结束了,守护线程就被动地结束了,甚至连finally都可以不执行;它是被动地,很“不靠谱”,所以千万不要把IO,文件等重要的操作给他,因为它没办法优雅地回收。
    public class DaemonDontRunFinally {
        public static void main(String[] args) {
            Thread t1 = new Thread(new DaemonWithFinally());
            t1.setDaemon(true);
            t1.start();
        }
    }
    class DaemonWithFinally implements Runnable{
        @Override
        public void run() {
            try {
                System.out.println("Starting Daemon");
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                System.out.println("Exiting via InterruptedException");
            } finally {
                System.out.println("this should always run?");
            }
        }
    }
    ----------
    Starting Daemon
    

    线程里的finally语句都没来得及执行,守护线程就结束了。

    • 可以定制ThreadFactory,生成一组守护线程
    /**
     * 定制生成的Thread
     */
    public class DaemonThreadFactory implements ThreadFactory {
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setDaemon(true);
            return t;
        }
    }
    
    • 守护线程产生的新线程也是守护线程
    public class Daemons {
        public static void main(String[] args) throws InterruptedException {
            Thread t = new Thread(new Daemon());
            t.setDaemon(true);
            t.start();
            System.out.println("t.isDaemon():" + t.isDaemon());
            // 让主线程执行得慢一点,这样守护线程才能体现效果,要不然,守护线程在主线程执行完就结束了,看不到效果
            TimeUnit.MILLISECONDS.sleep(10);
        }
    }
    class Daemon implements Runnable {
        private Thread[] threads = new Thread[10];
        @Override
        public void run() {
            // 创建10个子线程,没有标注子线程是守护线程
            for (int i = 0; i < threads.length; i++) {
                threads[i] = new Thread(new DaemonSpawn());
                threads[i].start();
                System.out.println("DaemonSpawn " + i + " started.");
            }
            for (int i = 0; i < threads.length; i++) {
                System.out.println("thread[" + i + "] isDaemon():" + threads[i].isDaemon());
            }
            while (true) {
                Thread.yield();
            }
        }
    }
    class DaemonSpawn implements Runnable {
        @Override
        public void run() {
            while (true) {
                Thread.yield();
            }
        }
    }
    ----------
    t.isDaemon():true
    DaemonSpawn 0 started.
    DaemonSpawn 1 started.
    DaemonSpawn 2 started.
    DaemonSpawn 3 started.
    DaemonSpawn 4 started.
    DaemonSpawn 5 started.
    DaemonSpawn 6 started.
    DaemonSpawn 7 started.
    DaemonSpawn 8 started.
    DaemonSpawn 9 started.
    thread[0] isDaemon():true
    thread[1] isDaemon():true
    thread[2] isDaemon():true
    thread[3] isDaemon():true
    thread[4] isDaemon():true
    thread[5] isDaemon():true
    thread[6] isDaemon():true
    thread[7] isDaemon():true
    thread[8] isDaemon():true
    thread[9] isDaemon():true
    

    发现守护线程创建的子线程,虽然没有标注是守护线程,但是确实是守护线程

    • 应用场景
      • 为其他线程提供服务支持,比如GC
      • 在任何情况下,程序结束守护线程必须正常关闭

    线程协作

    join

    • 线程调用join方法,就要等待该线程执行完
    public static void main() {
        Thread t1 = new Thread(() -> {
            log.debug("开始");
            TimeUnit.SECONDS.sleep(1);
            log.debug("结束");
        },"t1");
        t1.start();
        t1.join();
        log.debug("main end");
    }
    

    上面的代码中,Main线程中创建了t1子线程,t1.join()表示阻塞main线程,等待t1线程执行完,才会继续执行下面的代码。

    • join(long t)是一个重载的方法,表示最多等待t毫秒,如果线程执行不完,也继续执行

    join()其实就是join(0),它在jdk里的实现使用了wait()方法

    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) {
            // 查询线程是否结束,若线程还是活着的,调用wait方法,释放锁,进入阻塞状态,直到线程结束,调用notifyAll
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);// 如果wait超时,线程会进入就绪状态,继续执行下面代码
                now = System.currentTimeMillis() - base;
            }
        }
    

    JDK注释上写:As a thread terminates the this.notifyAll method is invoked

    当线程结束了,会调用this.notifyAll唤醒线程

    wait,notify,notifyAll

    interrupt方法与两阶段终止

    graph TD
    w("while(ture)") --yes--> b(料理后事)
    

    interrupt方法

    Thread类里提供了3个interrupt相关的方法

    /**给线程发送中断信号*/
    public void interrupt();
    /*判断线程是否收到中断信号,不会清楚中断标记*/
    public boolean isInterrupted();
    /*判断线程是否收到中断信号,调用该方法后清除中断标记*/
    public static native boolean interrupted();
    

    两阶段终止

    我们希望线程安全,快速,可靠地终止。如果线程终止时没有及时关闭资源,释放锁等等“善后”措施,那么就不是可靠地终止。

    Java提供一种中断机制,来实现两阶段终止模式

    两阶段模式,简单概括:第一阶段,主线程T1向线程T2发送终止指令,第二阶段,线程T2响应终止指令

    其中T1向T2发送终止指令在Java中就是用中断机制实现的。

    public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                while (true) {
                    if (Thread.currentThread().isInterrupted()) {
                        log.debug("被打断了, 退出循环");
                        break;
                    }
                }
            }, "t1");
            t1.start();
            Thread.sleep(1000);
            log.debug("interrupt");
            t1.interrupt();
            Thread.sleep(1000);
            log.debug("t1.isInterrupted():" + t1.isInterrupted());
        }
    
    21:05:16.415 c.Test12 [main] - interrupt
    21:05:16.419 c.Test12 [t1] - 被打断了, 退出循环
    21:05:17.432 c.Test12 [main] - t1.isInterrupted():false
    

    主线程调用t1.interrupt()就是发送中断信号给t1线程,t1线程检测出isInterrupted(),即做出响应中断的处理。这个发送中断信号--响应的过程,其实可以理解为线程有一个boolean类型的线程终止标志位。当调用t1.interrupt()的时候,就是将该标志位置为true,线程响应后,该标志位还原。

    对于这种非阻塞的正常线程,我们主动地调用 if (Thread.currentThread().isInterrupted())来检测线程中断,那么对于阻塞状态的中断如何处理呢。

    阻塞状态中断

    img

    线程优雅地结束,从线程状态的角度可以理解为线程从RUNNABLE状态,运行到TERMINATED状态。那么如果线程处于阻塞(休眠)状态,线程中断如何响应呢?

    • 如果线程调用了wait,join,sleep等方法,使线程进入WAITING,TIMED_WAITING状态,那么调用interrupt方法会由JVM抛出InterruptedException,再由代码响应中断。这也说明InterruptedException是CheckledException是有道理的,就是要让调用者主动地去处理中断。注意:抛异常的同时还会重置终止标志符。
    • 如果线程等待内置锁,进入BLOCKED状态,那么没办法,只能等待线程拿到锁之后再响应中断。但是Lock类中提供了lockInterruptibly方法允许等待锁的时候响应中断。
    Thread t1 = new Thread(() -> {
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            log.info("isInterrupted :" + Thread.currentThread().isInterrupted());
            e.printStackTrace();
        }
    }, "t1");
    t1.start();
    TimeUnit.SECONDS.sleep(1);
    log.info("t1.interrupt()");
    t1.interrupt();
    
    21:45:29.490 c.TestInterrupted1 [main] - t1.interrupt()
    21:45:29.490 c.TestInterrupted1 [t1] - isInterrupted :false
    java.lang.InterruptedException
    	at java.lang.Thread.sleep(Native Method)
    	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
    	at cn.itcast.test.TestInterrupted1.lambda$main$0(TestInterrupted1.java:13)
    	at cn.itcast.test.TestInterrupted1$$Lambda$1/0x0000000000000000.run(Unknown Source
    

    上面的例子说明了,线程执行sleep方法进入TIMED_WAITING状态,响应interrupt后,变为RUNNABLE状态,然后继续执行直到终止TERMINATED状态。

    注意:JVM抛出异常时,会重置终止标识符,所以如果需要维持终止标识符的话,还需要在catch块中手动触发一下中断Thread.currentThread().interrupt()

    下面的例子来自https://time.geekbang.org/column/article/95847。

    它的背景是要监控系统每2秒采集一次数据上报。注意它的终止方法,除了调用线程的interrupt方法,还自定义了一个终止标志符。其作用和Thread的终止标识符一致,区别在于,这种方式能够起到“双保险”。如果run方法中调用第三方类库,而线程中断时,我们并不知道第三方类库会如何响应中断,假如它响应后没有重置标志符,那么我们的线程就不能正常终止。所以建议自定义标志符,这样不影响我们的线程中断。

    class Proxy {
      //线程终止标志位
      volatile boolean terminated = false;
      boolean started = false;
      //采集线程
      Thread rptThread;
      //启动采集功能
      synchronized void start(){
        //不允许同时启动多个采集线程
        if (started) {
          return;
        }
        started = true;
        terminated = false;
        rptThread = new Thread(()->{
          while (!terminated){
            //省略采集、回传实现
            report();
            //每隔两秒钟采集、回传一次数据
            try {
              Thread.sleep(2000);
            } catch (InterruptedException e){
              //重新设置线程中断状态
              Thread.currentThread().interrupt();
            }
          }
          //执行到此处说明线程马上终止
          started = false;
        });
        rptThread.start();
      }
      //终止采集功能
      synchronized void stop(){
        //设置中断标志位
        terminated = true;
        //中断线程rptThread
        rptThread.interrupt();
      }
    }
    

    线程池的终止

    ExecutorService提供两种终止:shutdown正常关闭,shutdownNow清醒关闭。

    shutdown正常关闭,更加安全,线程会等队列中的任务都执行完再结束,相应的,关闭速度就慢一点;

    shutdownNow关闭速度快,但是不安全,它会把尝试取消正在执行中的任务,并返回所有已提交但未开始的任务,讲这些任务返回。它的风险就在于,可能正在执行得任务不支持关闭,且也不会作为队列中的任务返回,存在丢失的风险。

    public static void main(String[] args) throws InterruptedException {
        	// 线程数为1
            ExecutorService executorService = Executors.newFixedThreadPool(1);
            executorService.execute(new Task1("t1"));
            executorService.execute(new Task2("t2"));
            executorService.execute(new Task2("t3"));
            TimeUnit.MILLISECONDS.sleep(900);
            // TimeUnit.MILLISECONDS.sleep(1100);
            List<Runnable> unExecTasks = executorService.shutdownNow();
            System.out.println(unExecTasks);
        }
    
    	// Task1执行1s
        static class Task1 implements Runnable {
            private String threadName;
            public Task1(String threadName) {
                this.threadName = threadName;
            }
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            public String toString() {
                return this.threadName;
            }
        }
    	// Task2执行3s
        static class Task2 implements Runnable {
            private String threadName;
            public Task2(String threadName) {
                this.threadName = threadName;
            }
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            public String toString() {
                return this.threadName;
            }
        }
    
    [t2, t3]
    java.lang.InterruptedException
    	at java.lang.Thread.sleep(Native Method)
    	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
    	at cn.itcast.test.TestExecutorShutdown$Task1.run(TestExecutorShutdown.java:34)
    	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    	at java.lang.Thread.run(Thread.java:823)
    

    上面的例子显示了shutdownNow的使用。Task1需要执行1s,Task2需要执行3s。线程池里只有一个线程。当主线程sleep900ms时,shurdownNow,看到除了t1任务没有返回。也就是t1任务正在执行,而t2和t3任务都在队列里,所以返回t2和t3任务。

  • 相关阅读:
    【LeetCode OJ】Remove Element
    【LeetCode OJ】Remove Duplicates from Sorted Array
    【LeetCode OJ】Swap Nodes in Pairs
    【LeetCode OJ】Merge Two Sorted Lists
    【LeetCode OJ】Remove Nth Node From End of List
    【LeetCode OJ】Two Sum
    【LeetCode OJ】Majority Element
    最长公共子序列问题
    php fopen与file_get_contents的区别
    PHP 技巧集合
  • 原文地址:https://www.cnblogs.com/SimonZ/p/15669859.html
Copyright © 2011-2022 走看看