【2】线程的创建

  • 继承Thread类创建线程类
  • 通过Runnable接口创建线程类
  • 通过Callable和Future创建线程

1、继承Thread类创建线程类

  • 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
  • 创建Thread类的实例,即创建了线程对象。
  • 调用线程对象的start()方法来启动该线程。
csharp 复制代码
public class ThreadSimple extends Thread {
    int i = 0;

    //重写run方法,run方法的方法体就是现场执行体
    public void run() {
        for (; i < 100; i++) {
            System.out.println(getName() + "  " + i);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "  : " + i);
            if (i == 20) {
                new ThreadSimple().start();
            }
        }
    }
}

2、通过Runnable接口创建线程类

  • 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  • 创建Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  • 调用线程对象的start()方法来启动该线程。
csharp 复制代码
public class ThreadSimple implements Runnable {

    private int i;

    public void run() {
        for (i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 20) {
                ThreadSimple threadSimple = new ThreadSimple();
                new Thread(threadSimple, "新线程1").start();
            }
        }
    }

} 

3、通过Callable和Future创建线程

具体是创建Callable接口的实现类,并实现call()方法。并使用FutureTask类来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程。

我们发现FutureTask类实际上是同时实现了Runnable和Future接口,由此才使得其具有Future和Runnable双重特性。通过Runnable特性,可以作为Thread对象的target,而Future特性,使得其可以取得新创建线程中的call()方法的返回值。

csharp 复制代码
public class ThreadSimple implements Callable<Integer> {


    public static void main(String[] args) {

        Callable<Integer> myCallable = new ThreadSimple();    // 创建myCallable对象
        FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask来包装myCallable对象

        for (int i = 0; i < 31; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                Thread thread = new Thread(ft);   //FutureTask对象作为Thread对象的target创建新的线程
                thread.start();                      //线程进入到就绪状态
            }
        }

        System.out.println("主线程for循环执行完毕..");

        try {
            int sum = ft.get();            //取得新创建的新线程中的call()方法返回的结果
            System.out.println("sum = " + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }

    private int i = 0;

    // 与run()方法不同的是,call()方法具有返回值
    @Override
    public Integer call() {
        int sum = 0;
        for (; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            sum += i;
        }
        return sum;
    }
}

线程的启动

1.start() 方法

1.1 方法含义

启动新线程:通知 JVM 在有空闲的情况下启动线程,本质是请求 JVM 来运行我们的线程,线程何时运行由线程调度器来确定。该线程启动的同时会启动两个线程:第一个是用来执行 start 方法的父线程或主线程,第二个是被创建的子线程。

准备工作:让线程处于就绪状态(已经获得了除 CPU 以外的其他资源,如已经设置了上下文,线程状态,栈等),做完准备工作后,才能被 JVM 或操作系统调度到执行状态获取 CPU 资源,然后才会执行 run 方法。

重复调用 start() :抛出异常 Exception in thread "main" java.lang.IllegalThreadStateException。一旦线程 start 以后,就从 NEW 状态进入到其他状态,比如 RUNNABLE,只有处于 NEW 状态的线程才能调用 start() 方法。

1.2 原理分析

通过 threadStatus 属性来判断是否重复启动并抛出异常,实际的启动方法是 native 方法 start0()。

csharp 复制代码
public class Thread implements Runnable {

    /**
     * 线程状态,初始化为 0,表示还未启
     */
    private volatile int threadStatus = 0;

    public synchronized void start() {
        // 判断线程的状态,也就是判断是否启动,重复启动时抛出 IllegalThreadStateException
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        // 将线程加入线程组
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    // 告知线程组该线程启动失败
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {}
        }
    }

    private native void start0();
}

通过 /src/share/native/java/lang/Thread.c 可知,start0() 方法对应 JVM_StartThread 方法

arduino 复制代码
static JNINativeMethod methods[] = {
    {"start0",           "()V",        (void *)&JVM_StartThread},
};

位于 /src/hotspot/share/prims/jvm.cpp 的 JVM_StartThread 方法中有段注释

less 复制代码
// Since JDK 5 the java.lang.Thread threadStatus is used to prevent
// re-starting an already started thread, so we should usually find
// that the JavaThread is null. However for a JNI attached thread
// there is a small window between the Thread object being created
// (with its JavaThread set) and the update to its threadStatus, so we
// have to check for this

该段注释说自从 JDK5 后 使用 Thread 类的 threadStatus 属性去方式线程重复启动,接下来看下 /src/share/vm/runtime/thread.cpp 中的 start 方法,该方法中判断如果该线程是 Java 线程,则将该线程的状态改为 RUNNABLE。

arduino 复制代码
void Thread::start(Thread* thread) {
  trace("start", thread);
  if (!DisableStartThread) {
    if (thread->is_Java_thread()) {
     // 这里调用 set_thread_status 方法将线程的状态修改为 RUNNALBE
      java_lang_Thread::set_thread_status(((JavaThread*)thread)->threadObj(),
                                          java_lang_Thread::RUNNABLE);
    }
    os::start_thread(thread);
  }
}

2.run() 方法

run() 只是 Thread 类的一个基本方法

typescript 复制代码
public class Thread implements Runnable {
    /** 省略 */

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

3.比较两方法

输出:main 和 Thread-0

scss 复制代码
public class StartAndRunMethod {

    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println(Thread.currentThread().getName());
        runnable.run();
        new Thread(runnable).start();
    }
}

调用 start 方法才是真正意义上启动了一个线程,会经历线程的各个生命周期,如果直接调用 run 方法,则只是普通的调用方法,不会通过子线程去调用。

线程的终止

1.过期的 suspend()、resume()、stop()

这三个方法已经被废除,通过查看 Oracle 官方文档 可以得知。使用 stop() 方法停止线程会释放线程的所有 monitor,该方法在终止一个线程时不会保证线程的资源正常释放,并且抛出 ThreadDeath 异常,通常是没有给予线程完成资源释放工作的机会,因此会导致程序出现数据不同步。suspend() 方法则容易造成死锁,该方法在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入挂起状态。resume() 必须和 suspend() 一起使用,当要恢复目标线程的线程在调用 resume 之前尝试锁定这个 monitor,此时就会导致死锁。

2.volatile 标志位

通过 volatile 修饰的共享变量可以进行线程的终止。

2.1 成功案例

子线程每隔 1 秒输出:持续运行。主线程在 2 秒后将 stop 置为 true,此时子线程 while 循环停止,子线程运行结束。循环只进行了两次。

arduino 复制代码
public class RightVolatileDemo {

    private static volatile boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (!stop) {
                System.out.println("持续运行");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        TimeUnit.SECONDS.sleep(2);
        stop = true;
    }
}

运行结果如下:

持续运行
持续运行

2.2 失败案例

使用 volatile 的局限性,当线程陷入阻塞时,使用 volatile 修饰的变量无法停止线程。

通过生产者消费者例子来演示阻塞情况下 volatile 的局限性,定义一个生产者类实现 Runnable 接口重写 run 方法,在 run 中当 volatile 修饰的 canceled 变量为 false 时,生产者通过 BlockingQueue 的 put 方法不断添加数据,当阻塞队列到达上限时,put 方法会阻塞。定义一个消费者类,通过 needMoreCount 方法判断消费者是否结束消费。

在主函数中初始化一个长度为 10 的阻塞队列,构建生产者和消费者实例,当消费者结束消费时,将生产者的 canceled 属性值改为 true,但是此时生产者仍然在运行,因为生产者线程阻塞在 put 方法。这就是 volatile 标志位的局限性了。

csharp 复制代码
public class WrongVolatileDemo {

    public static void main(String[] args) throws InterruptedException {
        // 定义容量为 10 的阻塞队列
        BlockingQueue<Integer> storage = new ArrayBlockingQueue<>(10);

        // 启动生产者线程
        Thread producerThread = new Thread(new Producer(storage));
        producerThread.start();
        Thread.sleep(1000);

        // 启动消费者
        Consumer consumer = new Consumer(storage);
        while (consumer.needMoreCount()) {
            System.out.println("消费者消费:" + consumer.getStorage().take());
            Thread.sleep(100);
        }
        System.out.println("消费者消费完全结束");

        // 此时生产者不应该继续生产
        Producer.canceled = true;
    }

    /**
     * 生产者
     */
    private static class Producer implements Runnable {

        static volatile boolean canceled = false;
        private BlockingQueue<Integer> storage;

        public Producer(BlockingQueue<Integer> storage) {
            this.storage = storage;
        }

        @Override
        public void run() {
            int count = 1;
            try {
                while (!canceled) {
                    // 如果队列满的话,put 方法会阻塞当前线程
                    storage.put(count);
                    System.out.println("生产者生产:" + count);
                    count++;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("生产者停止运行");
            }
        }
    }

    /**
     * 消费者
     */
    private static class Consumer {

        private BlockingQueue<Integer> storage;

        public Consumer(BlockingQueue<Integer> storage) {
            this.storage = storage;
        }

        public BlockingQueue<Integer> getStorage() {
            return storage;
        }

        public boolean needMoreCount() {
            return Math.random() < 0.95;
        }
    }
}

3.interrupt 方法

interrupt 翻译为中断,中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的 interrupt() 方法对其进行中断操作。

举几个例子来演示 interrupt 的不同用法。

3.1 不带阻塞的中断

该例子是最简单的中断,thread 线程启动后,休眠 1ms 再调用该对象的 interrupt 方法,此时线程中正在执行的循环检测到 Thread.currentThread().isInterrupted() 为 true 结束循环,输出 count 变量的值。

当线程调用自身的 interrupt 方法时,会将中断标记设置为 ture,线程内部循环会通过检查自身是否被中断来结束循环,而 线程内部的 isInterrupted() 方法就能判断线程是否被中断。

arduino 复制代码
public class InterruptThreadWithoutSleep {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            int count = 0;
            // 检查自身是否被中断来结束循环
            while (!Thread.currentThread().isInterrupted()) {
                count++;
            }
            System.out.println(count);
        });
        thread.start();
        Thread.sleep(1);
        // 设置中断标记
        thread.interrupt();
    }
}

3.2 带有阻塞的中断

该例子演示带有 sleep 阻塞的中断方法使用。sleep 方法使用需要抛出 InterruptedException,说明该方法可以响应 interrupt 中断。在线程启动后,该线程会休眠 1s,而主线程在休眠 100ms 后会调用中断方法,此时该线程是处于阻塞状态,在阻塞状态下响应到中断,sleep 方法会抛出 InterruptedException ,但是在抛出该异常前,JVM 会先将该线程的中断标识位清除,然后才抛出 InterruptedException,此时调用 isInterrupted() 方法将会返回 false。

如果在执行过程中,每次循环都会调用 sleep 方法,那么其实可以不需要每次迭代都通过 isInterrupted() 方法检查中断,因为 sleep 方法会响应中断。

arduino 复制代码
public class InterruptThreadWithSleep {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("中断标记:" + Thread.currentThread().isInterrupted());
            }
        });
        thread.start();
        Thread.sleep(100);
        thread.interrupt();
    }
}

运行结果如下:

bash 复制代码
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at com.hncboy.interrupt.InterruptThreadWithSleep.lambda$main$0(InterruptThreadWithSleep.java:15)
    at java.lang.Thread.run(Thread.java:748)
中断标记:false

3.4 interrupt 相关方法

3.4.1 interrupt()

设置中断标记,最终调用 native 的 interrupt0() 方法设置中断标记。

scss 复制代码
public void interrupt() {
    if (this != Thread.currentThread())
        // 权限检查
        checkAccess();

    synchronized (blockerLock) {
        // IO 读写相关
        Interruptible b = blocker;
        if (b != null) {
            interrupt0();
            b.interrupt(this);
            return;
        }
    }
    // 该方法一定会执行
    interrupt0();
}

private native void interrupt0();

找到 interrupt0 方法对应的 JVM_Interrupt 方法,找到该方法代码。

scss 复制代码
JVM_ENTRY(void, JVM_Interrupt(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_Interrupt");

  // Ensure that the C++ Thread and OSThread structures aren't freed before we operate
  oop java_thread = JNIHandles::resolve_non_null(jthread);
  MutexLockerEx ml(thread->threadObj() == java_thread ? NULL : Threads_lock);
  // We need to re-resolve the java_thread, since a GC might have happened during the
  // acquire of the lock
  JavaThread* thr = java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread));
  if (thr != NULL) {
    Thread::interrupt(thr);
  }
JVM_END

找到关键方法 Thread::interrupt 的代码。

arduino 复制代码
void Thread::interrupt(Thread* thread) {
  trace("interrupt", thread);
  debug_only(check_for_dangling_thread_pointer(thread);)
  os::interrupt(thread);
}

找到关键方法 os::interrupt 的代码,此时找到了设置中断标记的方法,Java 中的每个线程都与操作系统的线程一一对应,一个 osthread 就对应 Java 中的一个线程,如果 osthread 没有被设置为中断,则设置中断标记为 true。

scss 复制代码
void os::interrupt(Thread* thread) {
  assert(Thread::current() == thread || Threads_lock->owned_by_self(),
    "possibility of dangling Thread pointer");

  OSThread* osthread = thread->osthread();
  // 如果线程没有被中断 
  if (!osthread->interrupted()) {
    // 设置中断标记为 true
    osthread->set_interrupted(true);
    // More than one thread can get here with the same value of osthread,
    // resulting in multiple notifications.  We do, however, want the store
    // to interrupted() to be visible to other threads before we execute unpark().
    OrderAccess::fence();
    ParkEvent * const slp = thread->_SleepEvent ;
    if (slp != NULL) slp->unpark() ;
  }

  // For JSR166. Unpark even if interrupt status already was set
  if (thread->is_Java_thread())
    ((JavaThread*)thread)->parker()->unpark();

  ParkEvent * ev = thread->_ParkEvent ;
  if (ev != NULL) ev->unpark() ;

}
3.4.2 isInterrupted() 和 interrupted()

返回线程的中断状态。interrupted 为静态方法,两个方法都调用了 isInterrupted 方法,不过传入的参数不一样,true 表示清除中断状态,false 表示不清除。

Thread.interrupted() 在哪个线程里被调用,就返回哪个线程的中断标志。

java 复制代码
public boolean isInterrupted() {
    return isInterrupted(false);
}

public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

private native boolean isInterrupted(boolean ClearInterrupted);
3.4.3 综合例子
arduino 复制代码
public class InterruptComprehensive {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {});

        // 启动线程
        thread.start();
        // 设置中断标志
        thread.interrupt();
        // 获取中断标志,被中断了返回 true
        System.out.println("isInterrupted: " + thread.isInterrupted());
        // 获取中断标志并重置,interrupted 静态方法调用的是执行它的线程,也就是 main 线程,返回 false
        System.out.println("isInterrupted: " + thread.interrupted());
        // 获取中断标志并重置,Main 函数没有没有被中断,返回 false
        System.out.println("isInterrupted: " + Thread.interrupted());
        // 获取中断标志,中断标记没有被清除,返回 true
        System.out.println("isInterrupted: " + thread.isInterrupted());
        thread.join();
        System.out.println("Main thread is over.");
    }
}

运行结果:

vbnet 复制代码
isInterrupted: true
isInterrupted: false
isInterrupted: false
isInterrupted: true
Main thread is over.

3.5 能响应中断的部分方法

有些阻塞方法是不可中断的,例如 I/O 阻塞和 synchronized 阻塞,需要针对某一些锁或某一些 I/O 给出特定的方案。

  • Object.wait()/wait(long)/wait(long, int)
  • Thread.sleep(long)/sleep(long, int)
  • Thread.join()/join(long)/join(long, int)
  • java.util.concurrent.BlockingQueue.take()/put(E)
  • java.util.concurrent.locks.Lock.lockInterruptibly()
  • java.util.concurrent.CountDownLatch.await()
  • java.util.concurrent.CyclicBarrier.await()
  • java.util.concurrent.Exchanger.exchange(V)
  • java.nio.channels.InterruptibleChannel 相关方法
  • java.nio.channels.Selector 相关方法

3.6 InterruptedException 异常处理

3.6.1 传递中断

当在 run 中调用了一个有异常的方法时,该异常应该在方法中用 throws 声明,传递到 run 方法,而不是在方法中捕获,此时可能会造成不可预料的结果。throwInMethod2() 为正确做法,throwInMethod1() 为错误做法。

csharp 复制代码
public class HandleInterruptedException implements Runnable {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new HandleInterruptedException());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }

    @Override
    public void run() {
        while (true) {
            System.out.println("work");
            try {
                throwInMethod2();
            } catch (InterruptedException e) {
                System.out.println("保存日志、停止程序");
                e.printStackTrace();
            }
        }

        /*while (true) {
            System.out.println("go");
            throwInMethod1();
        }*/
    }

    private void throwInMethod2() throws InterruptedException {
        Thread.sleep(2000);
    }

    private void throwInMethod1() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
3.6.2 重新设置中断状态

因为阻塞抛出 InterruptedException 异常后,会清除中断状态。可以在 catch 子语句中调用 Thread.currentThread().interrupt() 方法来恢复设置中断状态,以便于在后续的执行中,依然能够检查到刚才发生了中断。

csharp 复制代码
public class HandleInterruptedException2 implements Runnable {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new HandleInterruptedException2());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }

    @Override
    public void run() {
        while (true) {
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("发生中断,程序运行结束");
                break;
            }
            System.out.println("work");
            reInterrupt();
        }
    }

    private void reInterrupt() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            e.printStackTrace();
        }
    }
}

拓展

实现线程只有一种方式

关于这个问题,我们先不聚焦为什么说创建线程只有一种方式,先认为有两种创建线程的方式,而其他的创建方式,比如线程池或是定时器,它们仅仅是在 new Thread() 外做了一层封装,如果我们把这些都叫作一种新的方式,那么创建线程的方式便会千变万化、层出不穷,比如 JDK 更新了,它可能会多出几个类,会把 new Thread() 重新封装,表面上看又会是一种新的实现线程的方式,透过现象看本质,打开封装后,会发现它们最终都是基于 Runnable 接口或继承 Thread 类实现的。

接下来,我们进行更深层次的探讨,为什么说这两种方式本质上是一种呢?

typescript 复制代码
@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

首先,启动线程需要调用 start() 方法,而 start() 方法最终还会调用 run() 方法,我们先来看看第一种方式中 run() 方法究竟是怎么实现的,可以看出 run() 方法的代码非常短小精悍,第 1 行代码 if (target != null) ,判断 target 是否等于 null,如果不等于 null,就执行第 2 行代码 target.run(),而 target 实际上就是一个 Runnable,即使用 Runnable 接口实现线程时传给Thread类的对象。

然后,我们来看第二种方式,也就是继承 Thread 方式,实际上,继承 Thread 类之后,会把上述的 run() 方法重写,重写后 run() 方法里直接就是所需要执行的任务,但它最终还是需要调用 thread.start() 方法来启动线程,而 start() 方法最终也会调用这个已经被重写的 run() 方法来执行它的任务,这时我们就可以彻底明白了,事实上创建线程只有一种方式,就是构造一个 Thread 类,这是创建线程的唯一方式。

我们上面已经了解了两种创建线程方式本质上是一样的,它们的不同点仅仅在于实现线程运行内容的不同,那么运行内容来自于哪里呢?

运行内容主要来自于两个地方,要么来自于 target,要么来自于重写的 run() 方法,在此基础上我们进行拓展,可以这样描述:本质上,实现线程只有一种方式,而要想实现线程执行的内容,却有两种方式,也就是可以通过 实现 Runnable 接口的方式,或是继承 Thread 类重写 run() 方法的方式,把我们想要执行的代码传入,让线程去执行,在此基础上,如果我们还想有更多实现线程的方式,比如线程池和 Timer 定时器,只需要在此基础上进行封装即可。

实现 Runnable 接口比继承 Thread 类实现线程要好

下面我们来对刚才说的两种实现线程内容的方式进行对比,也就是为什么说实现 Runnable 接口比继承 Thread 类实现线程要好?好在哪里呢?

首先,我们从代码的架构考虑,实际上,Runnable 里只有一个 run() 方法,它定义了需要执行的内容,在这种情况下,实现了 Runnable 与 Thread 类的解耦,Thread 类负责线程启动和属性设置等内容,权责分明。

第二点就是在某些情况下可以提高性能,使用继承 Thread 类方式,每次执行一次任务,都需要新建一个独立的线程,执行完任务后线程走到生命周期的尽头被销毁,如果还想执行这个任务,就必须再新建一个继承了 Thread 类的类,如果此时执行的内容比较少,比如只是在 run() 方法里简单打印一行文字,那么它所带来的开销并不大,相比于整个线程从开始创建到执行完毕被销毁,这一系列的操作比 run() 方法打印文字本身带来的开销要大得多,相当于捡了芝麻丢了西瓜,得不偿失。如果我们使用实现 Runnable 接口的方式,就可以把任务直接传入线程池,使用一些固定的线程来完成任务,不需要每次新建销毁线程,大大降低了性能开销。

第三点好处在于 Java 语言不支持双继承,如果我们的类一旦继承了 Thread 类,那么它后续就没有办法再继承其他的类,这样一来,如果未来这个类需要继承其他类实现一些功能上的拓展,它就没有办法做到了,相当于限制了代码未来的可拓展性。

综上所述,我们应该优先选择通过实现 Runnable 接口的方式来创建线程。

好啦,本课时的全部内容就讲完了,在这一课时我们主要学习了 通过 Runnable 接口和继承 Thread 类等几种方式创建线程,又详细分析了为什么说本质上只有一种实现线程的方式,以及实现 Runnable 接口究竟比继承 Thread 类实现线程好在哪里?学习完本课时相信你一定对创建线程有了更深入的理解。

相关推荐
GGGGGGGGGGGGGG.3 小时前
使用dockerfile创建镜像
java·开发语言
兮动人4 小时前
SpringBoot加载配置文件的优先级
java·spring boot·后端·springboot加载配置
我爱Jack4 小时前
HttpServletRequest 和 HttpServletResponse 区别和作用
java·spring·mvc
yyueshen4 小时前
volatile 在 JVM 层面的实现机制
java·jvm
慕容魏4 小时前
入门到入土,Java学习 day16(算法1)
java·学习·算法
认真的小羽❅4 小时前
动态规划详解(二):从暴力递归到动态规划的完整优化之路
java·算法·动态规划
m0_748254664 小时前
Spring Boot 热部署
java·spring boot·后端
mango02194 小时前
SpringMVC
java
Seven974 小时前
SpringCloud带你走进微服务的世界
java·后端·spring cloud
Vacant Seat5 小时前
图论-实现Trie(前缀树)
java·开发语言·数据结构·图论