1 引言

线程是进程中可独立执行的最小单位,也是CPU资源分配的基本单位

线程的四个基本属性:

属性

描述

编号id

线程的唯一标识

名称

线程的名字,默认“Thread-编号id”,可自定义

类别

分为守护线程用户线程,可以通过setDaemon(true)设置为守护线程

优先级

表示希望哪个线程优先执行,Java中优先级取值范围是1~10,默认5

2 Java线程对象Thread常用的方法

1 start()

表示启动线程,让CPU调度器调度开启线程持行run()方法。而直接调用run()方法只是运行当前run方法,不会开启线程。

附上源码:

public synchronized void start() {
      
        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();

通过源码,不难发现start()方法实际上是调用start0()方法启动线程的,而start0()native方法。

简单说下native的原理及实现机制:

native修饰的表示本地方法,一个Native Method就是一个java调用非java代码(如C代码)的接口,Java中的Native Method并不提供实现体。

当一个类第一次被使用到时,这个类的字节码会被加载到内存,并且只会会被载一次。在这个被加载的字节码的入口维持着一个该类所有方法描述符的list,这些方法描述符包含这样一些信息:方法代码存于何处,它有哪些参数,方法的描述符(public之类)等等。

如果一个方法描述符内有native,这个描述符块将有一个指向该方法的实现的指针。这些实现在一些DLL文件内,但是它们会被操作系统加载到Java程序的本地方法栈。当一个带有本地方法的类被加载时,其相关的DLL并未被加载,因此指向方法实现的指针并不会被设置。只有当本地方法被调用时,这些DLL才会被加载,这是通过调用java.system.loadLibrary()实现的。

此外,不可不说的是,本地方法意味着和平台有关,因此使用了native的程序可移植性都不太高,并且本地方法是有开销的,它丧失了Java的很多优势。

2 sleep()

线程暂停阻塞等待一段时间,时间过了就继续。该方法是不释放锁的。

附源码:

public static native void sleep(long millis) throws InterruptedException

同样的,sleep()也是本地方法,底层也是通过C代码调用操作系统实现的

3 yield()

t.yield()表示t线程主动让出CPU执行权进入就绪态,所有线程重新争抢CPU(可能出现又是t线程抢到CPU执行权)。

附源码:

public static native void yield();

4 join()

t2线程正在运行,这时在t2线程中调用 t1.join() ,这是让线程t2等待线程t1执行结束再运行。

此方法可以让线程按顺序持行。自己线程中调用自己的join()方法是无实际意义的。

附源码:

public final void join() throws InterruptedException {
       join(0);
   }
   
   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;
           }
       }
   }

join(long timeout)方法底层调用的是Object类的wait(long timeout)方法。

public final native void wait(long timeout) throws InterruptedException;

Object类的wait()方法是释放锁的。

3 Java线程运行状态

java thread没有启动 java thread start run_java thread没有启动


过程分析:

  1. new 的状态(NEW)
  2. start() 之后线程到达就绪状态等待CPU调度,CPU调度了 就是运行状态。(RUNNABLE)
  3. 调用了 TImedWaiting 就回到就绪状态,结束之后自动到运行状态(TIMED_WAITING)
  4. 调用 Waiting 就绪然后持行相应的方法到运行(WAITING)
  5. 调用 Blocked 等待进入同步代码快的锁。(BLOCKED)
  6. 结束状态Teminated(TERMINATED)

线程被挂起:正在CPU中调度的线程,CPU把该线程丢出去,持行其他线程,这就是线程被挂起。之后又回来持续该线程。(每个CPU每次只能持行一个线程)

4 Java线程调度原理

我们知道,在任意时刻,操作系统的CPU 只能执行一条机器指令,并且每个线程只有获取到 CPU 的使用权后,才可以执行指令(也就是说在任意时刻,只有一个线程占用 CPU,处于运行的状态)。
多线程并发运行实际上是指多个线程轮流获取 CPU 使用权,分别执行各自的任务。
线程的调度由 JVM 负责。
线程调度模型分为两类:时间片轮转调度模型和抢占式调度模型。JVM采用的是抢占式调度模型。

  • 时间片轮转调度模型

时间片轮转调度模型是让所有线程轮流获取 CPU 使用权,并且为每个线程分配相同的占用 CPU 的时间片,时间片用完了则线程自动放弃CPU。

  • 抢占式调度模型

JVM 采用的是抢占式调度模型,根据优先级来选择占用CPU,优先级越高线程被持行的几率就越大,如果线程的优先级都一样,那就随机选择一个线程,并让该线程占用 CPU。也就是如果我们同时启动多个线程,并不能保证它们能轮流获取到均等的时间片。
如果我们的程序想干预线程的调度过程,最简单的办法就是给每个线程设定一个优先级。

5 线程创建的四种方式

5.1 继承Thread类创建线程

/**
 * @author Carson
 * @date 2020/4/3 10:44
 */
public class MyThread {
    public static void main(String[] args) {
        new ThreadGenerator().start();
    }
}

class ThreadGenerator extends Thread {
    @Override
    public void run() {
        System.out.println("Starting thread>>>>>>>>>>>>>>>>>>>>>>");
    }
}

该方式的优缺点在于:

  • 优点:编写简单
  • 缺点:线程类已经继承了Thread,不能再继承别的类。

5.2 实现Runnable接口创建线程

/**
 * @author Carson
 * @date 2020/4/3 10:44
 */
public class MyThread {
    public static void main(String[] args) {
        new Thread(new ThreadGenerator()).start();
    }
}

class ThreadGenerator implements Runnable {
    @Override
    public void run() {
        System.out.println("Starting thread>>>>>>>>>>>>>>>>>>>>>>");
    }
}

当然,在使用Java8及更高版本时,可以直接使用Lambda表达式创建:

/**
 * @author Carson
 * @date 2020/4/3 11:22
 */
public class MyThread {
    public static void main(String[] args) throws Exception {
        new Thread(() -> {
            System.out.println("Starting thread>>>>>>>>>>>>>>>>>>>>>>");
        }).start();
    }
}

5.3 使用Callable和Future创建线程

/**
 * @author Carson
 * @date 2020/4/3 10:44
 */
public class MyThread {
    public static void main(String[] args) throws Exception {
        ThreadGenerator threadGenerator = new ThreadGenerator();
        FutureTask futureTask = new FutureTask(threadGenerator);
        new Thread(futureTask).start();
    }
}

class ThreadGenerator implements Callable {
    @Override
    public Object call() throws Exception {
        System.out.println("Starting thread>>>>>>>>>>>>>>>>>>>>>>");
        return true;
    }
}

同样的,也可以直接使用Lambda表达式创建:

/**
 * @author Carson
 * @date 2020/4/3 11:22
 */
public class MyThread {
    public static void main(String[] args) {
        FutureTask<Boolean> futureTask = new FutureTask<>(() -> {
            System.out.println("Starting thread>>>>>>>>>>>>>>>>>>>>>>");
            return true;
        });
        new Thread(futureTask).start();
    }
}

实现Callable接口和实现Runnable接口的不同点在于:

  • call()方法可以有返回值
  • call()方法可以声明并抛出异常

5.4 使用线程池,例如用Executor框架

/**
 * @author Carson
 * @date 2020/4/3 11:22
 */
public class MyThread {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(6);
        /* execute方法不关心返回值。 */
        executorService.execute(new ThreadGenerator());
        /* submit方法有返回值-->Future */
        executorService.submit(new ThreadGenerator());
    }
}

class ThreadGenerator implements Runnable {
    @Override
    public void run() {
        System.out.println("Starting thread>>>>>>>>>>>>>>>>>>>>>>");
    }
}

在开发中推荐使用线程池的方式,用线程池管理线程既高效风险也较低。