java线程(一)之基本概念

基本概念

  1. 同步和异步
    • 同步和异步通常用来形容一次方法调用。同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。
    • 当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态.这种方式我们称之为并发(Concurrent).
  2. 并发(Concurrency)和并行(Parallelism)
    • 并发:同一时间应对多件事情的能力。并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的。一会儿运行任务A,一会儿执行任务B,系统会不停地在两者间切换。但对于外部观察者来说,即使多个任务之间是串行并发的,也会造成多任务间是并行执行的错觉
    • 并行:同一时间做多件事情的能力。并行是真正意义上的同时执行。当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。真实的并行也只可能出现在多个CPU系统中(比如多核CPU)
  3. 临界区
    • 临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。
  4. 阻塞与非阻塞
    • 一个线程占用了临界区资源 ,那么其他所有需要这个资源的线程就必须在这个临界区中进行等待,等待会导致线程挂起,这种情况就是阻塞

线程的状态

  1. java线程的状态,可以通过ThreadgetState()获取线程的状态,该方法返回一个枚举。

    public enum State {
            NEW,
            RUNNABLE,
            BLOCKED,
            WAITING,
            TIMED_WAITING,
            TERMINATED;
    }
    
  2. 线程状态转换图
    -w1016

  3. 新建状态(New):用new语句创建的线程处于新建状态,此时它和其他Java对象一样,仅仅在堆区中被分配了内存

  4. 就绪状态(Runnable):当一个线程对象创建后,其他线程调用它的start()方法,该线程就进入就绪状态,线程进入线程队列排队,此时该状态线程并未开始执行,它仅表示可以运行了。至于该线程何时运行,取决于线程调度器的调度。Java虚拟机会为它创建方法调用栈和程序计数器。处于这个状态的线程位于可运行池中,等待获得CPU的使用权。进入就绪状态条件。

    • 调用start()方法,进入就绪状态(等待CPU调度)
    • 线程从Running状态转入就绪状态(例如调用yeild()方法)
    • 阻塞状态结束会进入就绪状态
  5. 运行状态(Running): 处于这个状态的线程占用CPU,执行程序代码。只有处于就绪状态(Runnable)的线程才有机会转到运行状态(Running)。当然可能会有某种耗时计算/IO等待的操作/CPU时间片切换等, 这个状态下发生的等待一般是其他系统资源, 而不是锁, Sleep等

  6. 阻塞状态(Blocked):阻塞状态是指线程因为某些原因放弃CPU,暂时停止运行。当线程处于阻塞状态时,Java虚拟机不会给线程分配CPU,直到线程重新进入就绪状态。它才有机会转到运行状态。阻塞状态不能直接转成运行状态,阻塞状态只能重新进入就绪状态

  7. 阻塞状态(线程进入临界区)可分为以下3种:

    • 位于对象等待池中的阻塞状态(Blocked in object’s wait pool):当线程处于运行状态时,如果执行了某个对象的wait()方法,Java虚拟机就会把线程放到这个对象的等待池中。
    • 位于对象锁池中的阻塞状态(Blocked in object’s lock pool):当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,Java虚拟机就会把这个线程放到这个对象的锁池中。
    • 其他阻塞状态(Otherwise Blocked):当前线程执行了sleep()方法,或者调用了其他线程的join()方法,或者发出了I/O请求时(等待IO操作完成),就会进入这个状态。
  8. Waiting:这个状态下是指线程拥有了某个锁之后, 调用了他的wait方法, 等待其他线程/锁拥有者调用 notify / notifyAll以便该线程可以继续下一步操作, 这里要区分 BLOCKEDWATING 的区别, 一个是在临界点外面等待进入, 一个是在临界点里面wait等待别人notify, 线程调用了join方法join了另外的线程的时候, 也会进入WAITING状态, 等待被他join的线程执行结束

  9. TIMED_WAITING:这个状态就是有限的(时间限制)的WAITING, 一般出现在调用wait(long), join(long)等情况下, 另外一个线程sleep后, 也会进入TIMED_WAITING状态。

    • TIMED_WAITING(onobjectmonitor)
    • TIMED_WAITING(parking)
    • TIMED_WAITING(sleeping)
    • WAITING(onobjectmonitor)
  10. TERMINATED(死亡状态):当线程退出run()方法时,就进入死亡状态,该线程结束生命周期。Thread类的isAlive()方法判断一个线程是否活着,当线程处于死亡或新建状态时,该方法返回false,在其余状态下,返回true。

    • 主线程结束后,其他线程不受其影响,不会随之结束。一旦子线程启动起来后,就拥有和主线程相等地位,不受主线程影响。
    • 已死亡的线程是不可能通过start()方法唤醒线程的,否则引发IllegalThreadStateException异常

线程管理

  1. 每个java程序都至少有一个执行线程,当运行程序的时候,JVM将启动这个执行线程来调用程序的main方法
  2. 当一个程序的所有线程都运行完成时(更确切地说,当所有非守护线程都运行完成时候),这个java程序将宣告结束。
  3. 如果初始线程(执行main方法的线程)结束了,其余的线程仍将继续执行直到他们运行结束。如果某一个线程调用了System.exit()指令来结束程序运行,所有的线程都将结束
  4. 在java中,一个线程就是一个对象,对象的创建离不开内存空间的分配。创建一个线程与创建其他类型的java对象所不同的是,JVM会为每个线程分配调用栈所需的内存空间,调用栈用于跟踪java方法间的调用关系以及java代码对Native Code的调用;此外java中的每个线程可能还有一个内核线程(与具体的操作系统有关)与之对应

线程创建与运行

  1. java中创建线程的三种方式

    • extends Thread:
      • 缺点: 不能继承其他类了,因为每条线程都是一个Thread的子类的实例,因此多个线程之间共享数据比较麻烦。
      • 优点: 如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程。
    • implements Runnable:
      • 缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法获取当前线程。不能有返回值,不能抛出checked异常。当RuntimeException从run方法中抛出时,默认行为是在控制台输出堆栈记录并退出程序。
      • Runnable接口是对任务的抽象。多个线程可以共享同一个实例
      • Thread类本身也实现了Runnable接口,因此Thread类及其子类的对象也可以作为target传递给新的线程对象。
    • implements Callable:
      • Runnable的增强版,可以有返回值,可以抛出checked异常
  2. 创建一个Thread实例与创建其他类的实例不同,JVM会为一个Thread实例分配两个调用栈所需要的空间。这两个调用栈一个用于跟踪java代码间的调用关系,另一个用于跟踪java代码对本地代码(Native代码)的调用关系。一个是JVM中线程,另一个是与JVM中的线程相对应的依赖于JVM宿主机操作系统的本地线程。启动一个java线程只需要调用相应的start()方法,线程启动后,当相应的线程被JVM的线程调度器调度到运行时,相应Thread实例的run方法会被JVM调用。

  3. 继承Thread

    public class MyThread extends Thread {
        public static void main(String[] args) {
            MyThread thread = new MyThread();
            thread.start();
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.err.println(i);
    
                try {
                    String threadName = Thread.currentThread().getName();
                    System.out.println(threadName);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }  
    
  4. 实现Runnable接口

    public class MyThread implements Runnable {
        public static void main(String[] args) {
        //重写run()方法(线程的执行体),把Runnable对象包装成Thread对象后再执行
        //new Thread(new Runnable()).start();
            Thread thread = new Thread(new MyThread());
            thread.start();
    
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println(i);
                String name = Thread.currentThread().getName();
                System.out.println(name);
            }
        }
    }
    
  5. 实现Callable接口

    public class MyThread implements Callable<Integer> {
        public static void main(String[] args) throws InterruptedException, ExecutionException {
           //先包装成Runnable,Future的子类FuturTask来包装.
            FutureTask<Integer> fts = new FutureTask<Integer>(new MyThread());
            new Thread(fts).start();
            Integer returnValue = fts.get();
            System.out.println("call()方法的返回值是:" + returnValue);
        }
        @Override
        public Integer call() {
            int sum = 0;
            for (int i = 0; i < 100; i++) {
                System.out.println(i);
                sum += i;
                System.out.println("===================================");
                System.out.println(sum);
            }
            return sum;
        }
    }
    
  6. 异常处理:UncaughtExceptionHandler是对run()没有捕获异常的一种补救措施,但是通常我们不依赖这种方式,因为是线程级别的。通常我们希望在内层就将异常处理掉

    public class ExceptionHandlerTest {
        public static void main(String[] args) {
            Thread t = new Thread() {
                public void run() {
                    Integer.parseInt("A112");
                }
            };
            t.setUncaughtExceptionHandler(new ExceptionHandler());
            t.start();
        }
    }
    
    class ExceptionHandler implements Thread.UncaughtExceptionHandler {
    
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            System.out.println("线程出现异常.....");
            e.printStackTrace();
        }
    }
    

守护线程的创建与运行

  1. Daemon线程的优先级很低,通常当同一个应用程序没有其他的线程运行的时候,守护线程才运行。当守护线程是程序唯一运行的线程时,守护线程执行结束后,JVM也就结束了这个程序。

  2. 守护线程通常被用来做为同一个程序中普通线程的服务提供者,通常是无限循环的,以等待服务请求或者执行线程的任务(典型的守护线程java的垃圾回收器)

  3. setDaemon方法只能在start()方法被调用之前设置,一旦线程开始运行,将不能再修改守护状态。

  4. 通过Thread的setDaemon(true)把某个线程设置为守护线程,必须在该线程启动之前设置,否则会引发IllegalThreadStateException

  5. 判断是否是守护线程使用Thread对象的isDaemon(),前台线程死亡后台进程也随之死亡,JVM的垃圾回收就是典型的后台线程

  6. java中一个线程是否是一个守护线程默认取决于其父线程:默认情况下父线程是守护线程,则子线程也是守护线程,父线程是用户线程,则子线程也是用户线程。父线程与子线程之间的生命周期没有必然关系,java中没有API用于获取一个线程的父线程或者所有子线程。

  7. 案例

    public class DaemonThread implements Runnable {
    
        public static int a = 0;
    
        public void run() {
            for (int k = 0; k < 5; k++) {
                a = a + 1;
            }
        }
    
        public static void main(String[] args) throws Exception {
            Runnable r = new DaemonThread();
            Thread t = new Thread(r);
            t.setDaemon(true);// 设置后台进程,必须在调用start()方法前调用
            t.start();
            for (int i = 0; i < 300; i++) {
                /*
                 * 注意循环体内一定要有实际执行语句,否则编译器或JVM可能优化掉你的这段代码,视这段代码为无效。
                 */
                System.out.print(i);
            }
    
            t.join();
            System.out.println();
            System.out.println(a);
        }
    }
    

线程的属性

  1. 线程的信息

    属性只读类型描述
    ID:线程Idlong用于标识不同的线程,线程运行结束,编号可能被后续创建的线程重新使用
    Name:名称String用于区分不同的线程,默认格式"Thread-线程编号" 设置线程的名称有助于代码调试以及问题定位
    Deamon:守护boolean设置为守护线程,必须在线程启动之前设置
    Priority:优先级int每个线程都有优先级,优先级的高低只和线程获得执行机会的次数多少有关。并非线程优先级越高的就一定先执行,哪个线程的先运行取决于CPU的调度; 默认情况下main线程具有普通的优先级,而它创建的线程也具有普通优先级。线程的优先级用数字来表示,范围从1到10,即Thread.MIN_PRIORITY到Thread.MAX_PRIORITY。一个线程的缺省优先级是5,即Thread.NORM_PRIORITY。
    State:状态保存线程的状态NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED;
  2. 优先级:线程的优先级本质上只是一个给线程调度器的提示信息,并不保证线程按照其优先级的顺序从高到低执行。java中优先级的使用不当或者滥用可能导致某些线程永远无法得到执行(线程饥饿),一般情况下使用默认即可(默认为5)。一个线程的优先级默认为该线程的父线程的优先级

    public class ThreadTest {
        public static void main(String args[]) {
            Thread t1 = new MyThread("T1");
            // 设置优先级为最小
            t1.setPriority(Thread.MIN_PRIORITY);
            Thread t2 = new MyThread("T2");
            // 设置优先级为最大
            t2.setPriority(Thread.MAX_PRIORITY);
            Thread t3 = new MyThread("T3");
            // 设置优先级为最大
            t3.setPriority(Thread.MAX_PRIORITY);
            t1.start();
            t2.start();
            t3.start();
        }
    }
    
    class MyThread extends Thread {
        public String message;
    
        public MyThread(String message) {
            this.message = message;
        }
    
        public void run() {
            for (int i = 0; i < 3; i++)
                //getPriority()获取优先级
                System.out.println(message + " " + getPriority());
        }
    }   
    

线程方法

run与start

  1. 每个线程都有其要执行的任务,java线程的任务处理逻辑入口是run方法,run()由JVM在运行相应的线程时直接调用。run方法只是Thread类的一个普通方法而已(因为Thread实现了Runnable接口,所以run只是一个普通方法而已),如果直接调用Run方法,run其实运行在当前线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,通常情况下我们不应该直接调用run方法。
  2. 要想让JVM运行线程的run方法,首先要启动线程。Thread类的start方法的作用是启动线程,启动一个线程的本质是请求JVM运行相应的线程,而这个线程具体何时运行是由线程调度器决定的,即start方法调用结束并不意味着相应线程已经开始运行(此时线程处于就绪状态),这个线程可能立即运行、稍后运行、甚至从不会运行。一旦启动的线程获取到CPU时间片,就开始运行run方法,run()一旦运行结束此线程随即终止
  3. Thread的start()通知线程调度器此线程已经准备就绪,等待调用线程对象的run()方法。start()调用本地方法(JNI)来实现一个真正的意义上的线程(只有start()成功调用之后,OS才分配线程资源)
  4. 执行start()方法的顺序并不代表线程启动的顺序,执行多次start()方法会出现IllegalThreadStateException,即我们不能通过重新调用一个已经运行结束的线程的start方法来使其重新运行。

sleep

  1. Thread.sleep():线程暂停(休眠),并进入阻塞状态,给其他线程执行机会,不考虑优先级。可以使线程挂起,当线程休眠的时间结束,JVM会给它CPU时间片,继续执行它的指令

  2. sleep()的另一种方式是通过TimeUnit枚举类元素进行调用,以下是TimeUnit的sleep,接收单位秒,最终转换成毫秒。如果休眠中被中断,该方法会立即抛异常,而不需要等到线程休眠时间结束

    TimeUnit.SECONDS.sleep(10);
    源码:
    public void sleep(long timeout) throws InterruptedException {
          if (timeout > 0) {
              long ms = toMillis(timeout);
              int ns = excessNanos(timeout, ms);
              Thread.sleep(ms, ns);
          }
    }
    

yield

  1. yield:线程让步,即让线程让出CPU,进入就绪状态。所以有可能出现的情况,该线程让出CPU进入就绪之后,线程调度器再次将它调度出来了。
  2. yield特点:
    • yield不会将线程转为阻塞状态,给优先级相同或优先级更高的线程执行的机会。
    • yield告诉当前正在执行的线程把运行机会交给优先级相同或优先级更高的线程执行的机会。
    • yield不能保证使得当前正在运行的线程迅速转换到可运行的状态
    • yield仅能使一个线程从运行状态转到可运行状态,而不是等待或阻塞状态
  3. 如果你觉得一个线程不那么重要,或者优先级非常低,而又害怕它会占用过多的CPU资源,那么可以在适当的时候调用Thread.yield(),给予其他重要线程更多的工作机会。

join

  1. 等待线程终止,再执行其他任务,使用join()。当一个线程的join()被调用时,调用它的线程将被挂起,直到这个线程对象完成它的任务。(ps:jannal 可以理解为强制加入,夺得主权)

  2. 当一个线程调用其他线程的join()时,如果使用join(),则不必等到被调用线程终止,如果参数指定的毫米时钟已经达到,它将继续运行。比如主线程中有 thread.join(1000),主线程将挂起,直到满足

    • 线程运行已经完成
    • 时钟已经过去1000毫秒
  3. 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) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }
    
  4. 案例:Thread1Thread2都调用join()Thread1Thread2都是被main主线程所调用,所以此时main主线程将被挂起.直到Thread1Thread2都执行完毕才执行

    class NetWorkConnectionLoader implements Runnable {
        @Override
        public void run() {
            System.out.println("开始网络连接加载" + LocalDateTime.now());
            try {
                TimeUnit.SECONDS.sleep(6);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("结束网络连接加载" + LocalDateTime.now());
        }
    }
    
    public class DataSourceLoader implements Runnable {
    
        @Override
        public void run() {
            System.out.println("开始数据源加载" + LocalDateTime.now());
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("数据源加载结束" + LocalDateTime.now());
        }
    
        public static void main(String[] args) {
            DataSourceLoader DataSourceLoader = new DataSourceLoader();
            Thread thread1 = new Thread(DataSourceLoader);
            NetWorkConnectionLoader NetWorkConnectionLoader = new NetWorkConnectionLoader();
            Thread thread2 = new Thread(NetWorkConnectionLoader);
            thread1.start();
            thread2.start();
    
            try {
                thread1.join();
                thread2.join();
            } catch (InterruptedException e) {
            }
            System.out.println("Main配置加载" + LocalDateTime.now());
        }
    
    }
    

废弃的方法

  1. 由于JVM实现的有些问题,因此Thread类的有些方法已经被废弃
    • resume:使被暂停的线程继续运行
    • suspend:暂停线程运行
    • stop:停止线程运行

线程栈

  1. 获取方法调用栈的方式

    • (new Throwable()).getStackTrace()
    • Thread.currentThread().getStackTrace()
  2. Thread.currentThread().getStackTrace();获取的线程栈是一个数组,数组中的元素是一个java.lang. StackTraceElement

    public final class StackTraceElement implements java.io.Serializable {
        // Normally initialized by VM (public constructor added in 1.5)
        private String declaringClass;//class
        private String methodName;//方法名
        private String fileName;//文件名
    	  private int    lineNumber;//行号
        ......
    } 
    
  3. 案例

    public class ThreadStackTrace {
        public static void main(String[] args) {
            methodStack();
            printThreadDump();
    
        }
    
        private static void methodStack() {
            StackTraceElement[] stackTraceElements = (new Throwable()).getStackTrace();
            for (int i = 0; i < stackTraceElements.length; i++) {
                StackTraceElement stackTraceElement = stackTraceElements[i];
                System.out.printf(
                        "index:%s,className:%s,fileName:%s,methodName:%s,lineNumber:%s\n",
                        i,
                        stackTraceElement.getClassName(),
                        stackTraceElement.getFileName(),
                        stackTraceElement.getMethodName(),
                        stackTraceElement.getLineNumber()
                );
            }
        }
    
        public static void printThreadDump() {
            StringBuffer msg = new StringBuffer();
            for (Map.Entry<Thread, StackTraceElement[]> stackTrace : Thread.getAllStackTraces().entrySet()) {
                Thread thread = stackTrace.getKey();
                StackTraceElement[] stack = stackTrace.getValue();
                if (thread.equals(Thread.currentThread())) {
                    continue;
                }
                msg.append("\n 线程名称:").append(thread.getName()).append("\n");
                for (StackTraceElement element : stack) {
                    msg.append("\t").append(element).append("\n");
                }
            }
            System.out.println(msg.toString());
        }
    }
    index:0,className:cn.jannal.thread.threadstack.ThreadStackTrace,fileName:ThreadStackTrace.java,methodName:methodStack,lineNumber:14
    index:1,className:cn.jannal.thread.threadstack.ThreadStackTrace,fileName:ThreadStackTrace.java,methodName:main,lineNumber:8
    
      
    
    线程名称:Finalizer
       java.lang.Object.wait(Native Method)
       java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
       java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
       java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)
    
    线程名称:Reference Handler
       java.lang.Object.wait(Native Method)
       java.lang.Object.wait(Object.java:502)
       java.lang.ref.Reference$ReferenceHandler.run(Reference.java:157)
    
    线程名称:Signal Dispatcher
    

异常处理

  1. 因为run()方法不支持throws语句,所以当线程对象的run()方法抛出非运行时异常时候,我们必须捕获并处理它。当运行时异常从run方法中抛出时,默认行为是在控制台输出堆栈记录并退出程序。

  2. UncaughtExceptionHandler 是对run()没有捕获异常的一种补救措施,但是通常我们不依赖这种方式,因为是线程级别的。通常我们希望在内层就将异常处理掉

    public class ExceptionHandlerTest {
    	public static void main(String[] args) {
    		Thread t  = new Thread(){
    			public void run(){
    				Integer.parseInt("A112");
    			}
    		};
    		t.setUncaughtExceptionHandler(new ExceptionHandler());
    		t.start();
    	}
    }
    class ExceptionHandler implements UncaughtExceptionHandler{
    
    	@Override
    	public void uncaughtException(Thread t, Throwable e) {
    		System.out.println("线程出现异常.....");
    		e.printStackTrace();
    	}
    }
    
  3. 输出结果(输出顺序不确定,可能先打印,也可能后打印)

    java.lang.NumberFormatException: For input string: "A112"
    	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    	at java.lang.Integer.parseInt(Integer.java:580)
    	at java.lang.Integer.parseInt(Integer.java:615)
    	at cn.jannal.thread.exception.ExceptionHandlerTest$1.run(ExceptionHandlerTest.java:7)
    线程出现异常.....
    

线程组

  1. java使用ThreadGroup来代表线程组,它可以对一批线程进行分类管理。对线程组的控制相当于同时控制这批线程。用户创建的所有线程都属于指定线程组。如果程序没有显式指定线程属于哪个线程组,该线程属于默认线程组。
  2. 将线程加入到指定的线程组:在创建一个Thread实例时,通过传入ThreadGroup对象,即可将该线程放入指定的线程组。
  3. setDaemon(boolean daemon):控制将线程组本身都设置为后台线程组,并不是将它包含的线程设为后台线。如果当它包含的线程都死了,后台线程组就自动销毁了。
  4. setMaxPriorty(优先级):它是设置该线程组的最高优先级,该线程组中已有的线程优先级不会受影响,以后新加入的线程优先级受影响
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值