万隆的笔记 万隆的笔记
博文索引
笔试面试
  • 在线学站

    • 菜鸟教程 (opens new window)
    • 入门教程 (opens new window)
    • Coursera (opens new window)
  • 在线文档

    • w3school (opens new window)
    • Bootstrap (opens new window)
    • Vue (opens new window)
    • 阿里开发者藏经阁 (opens new window)
  • 在线工具

    • tool 工具集 (opens new window)
    • bejson 工具集 (opens new window)
    • 文档转换 (opens new window)
  • 更多在线资源
  • Changlog
  • Aboutme
GitHub (opens new window)
博文索引
笔试面试
  • 在线学站

    • 菜鸟教程 (opens new window)
    • 入门教程 (opens new window)
    • Coursera (opens new window)
  • 在线文档

    • w3school (opens new window)
    • Bootstrap (opens new window)
    • Vue (opens new window)
    • 阿里开发者藏经阁 (opens new window)
  • 在线工具

    • tool 工具集 (opens new window)
    • bejson 工具集 (opens new window)
    • 文档转换 (opens new window)
  • 更多在线资源
  • Changlog
  • Aboutme
GitHub (opens new window)
  • JUC介绍
  • 并发编程核心概念与主要内容
  • Java线程创建与使用
    • 继承Thread类
    • 实现Runnable接口
    • 使用Callable和FutureTask接口
    • 使用线程池
    • Thread常用方法
  • 线程生命周期
  • synchronized关键字
  • wait与notify
  • 线程中断-interrupt
  • 线程优雅关闭
  • JMM内存模型
  • volatile关键字
  • final关键字
  • Lock
  • JUC并发编程
2022-05-06
目录

Java线程创建与使用

# Java线程创建与使用

Java中创建线程的方式有以下四种:

  • 继承Thread类
  • 实现Runnable接口
  • 使用Callable接口和FutureTask
  • 使用线程池

关于源码部分,版本为JDK 1.8

# 继承Thread类

集成Thread类,重写run方法,执行没有返回值(void)。

public class TheadDemo{
  public static void main(String[] args) {
    MyThread myThread = new MyThread();
    // 调用start()方法开启线程
    myThread.start();
  }
}

/**
* 继承Thread类,重写run方法
*/
class MyThread extends Thread{
  public void run(){
    for(int i = 0; i < 100; i++){
      System.out.println(Thread.currentThread().getName() + " 线程跑起来了");
    }
  }
}

# start源码

/**
* 调用这个方法线程会开始执行,JVM会负责调用线程的run方法
* 两个线程同时运行:当前线程(它从对start方法的调用中返回)和另一个线程(它执行它的run方法)。
* 多次启动一个线程是不合法的。特别是,线程在完成执行后可能不会重新启动。
*/
public synchronized void start() {
  // ......
  start0();
  // ......
}

private native void start0();

# 实现Runnable接口

实现Runnable接口,实现run方法,接口的实现类的实例作为Thread的target作为参数传入带参的Thread构造函数,通过调用start()方法启动线程。

public class RunnableDemo {
  public static void main(String[] args) {
    MyRunnale myRunable = new MyRunnale(); 
    // 实现类的实例作为Thread的targe带入构造函数,调用start()方法开启线程
    new Thread(myRunable).start();
  }
}

/**
* 实现Runnable,实现run方法
*/
class MyRunnale implements Runnable{
  @Override
  public void run() {
    for(int i = 0; i < 100; i++){
      System.out.println(Thread.currentThread().getName() + " 线程跑起来了");
    }
  }
}

# runnable原理

// 1.分配一个新的Thread对象。名称形式为“Thread-”+n,其中n为整数。
public Thread(Runnable target) {
  init(null, target, "Thread-" + nextThreadNum(), 0);
}

// 2.用当前的AccessControlContext初始化一个线程。
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize) {
  init(g, target, name, stackSize, null, true);
}

// 3.初始化线程,找到target的赋值操作
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
  // ......
  
  // 赋值操作,private Runnable target;
  this.target = target;
  
  // ......
}

/**
* 4.真正调用的地方。
* 如果这个线程是使用一个单独的Runnable运行对象构造的,那么这个Runnable对象的run方法就会被调用;
* 否则,此方法不执行任何操作并返回。Thread的子类应该重写这个方法。
*/
@Override
public void run() {
  if (target != null) {
    target.run();
  }
}

# 与继承Thread类对比

查看源码我们可以知道

  1. 继承Thread的原理是由子类重写了Thread类的run(),当调用start()时,JVM会调用线程的run()方法。
  2. 实现Runnable接口则是通过构造函数传入Runnable的引用,最后赋值给了线程中的target成员变量,最后在run()方法被调用。run()方法内部判断成员变量target的引用是否为空,不为空时执行的就是实现Runable接口时重写的run方法。

两者的优劣:

  1. 继承Thread可以直接使用Thread类中的方法,代码编写简单。缺点是由于Java只能单继承,如果已经有了父类,就不能用这种方法
  2. 实现Runnable接口则摆脱了单继承的限制,子类只需要实现接口,并通过Thread构造线程即可。缺点也很明显,需要先获取到线程对象后,才能使用Thread中的方法。

# 使用Callable和FutureTask接口

# Callable接口

JDK 5.0 新增了Callable接口,该接口与Runnable接口非常相似,Callable 接口的主要特征如下:

  • 声明了一个call方法。执行器运行任务时,该方法会被执行器执行。
  • 该方法有返回结果,必须返回声明中指定类型的对象。
  • 可能抛出异常。call()方法可以抛出任何一种校验异常。可以实现自己的执行器并重载afterExecute()方法来处 理这些异常。
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

Callable接口与Runnable接口类似,两者都是为其实例可能由另一个线程执行的类设计的。然而,Runnable接口的run方法不返回结果,也不能抛出检查异常。此外,Executors类包含将其他常见形式转换为Callable类的实用方法。

# Future接口

Future接口是JUC下的一个重要接口,Future表示异步计算的结果。提供了一些方法来检查计算是否完成、等待其完成以及检索计算的结果。结果只能在计算完成时使用get方法进行检索,必要时阻塞直到准备就绪。取消由cancel方法执行。提供了其他方法来确定任务是正常完成还是被取消。一旦计算完成,计算就不能取消。

FutureTask是Future接口实现类之一,该类提供了一个Future的基本实现 。可用于包装Callable或Runnable对象,既可以作Runnable被线程执行,又可以作为Futrue得到Callable的返回值

FutureTask.png

# 实现步骤

  1. 创建一个实现Callable接口的实现类
  2. 实现call方法,将此线程需要执行的操作声明在call()中
  3. 创建callable接口实现类的对象
  4. 将此callable的对象作为参数传入到FutureTask构造器中,创建FutureTask的对象
  5. 将FutureTask对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用star方法(也可以使用Executor)
  6. 获取callable接口中call方法的返回值
public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable myCallable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
        // 启动线程,执行callable的业务
        new Thread(futureTask).start();
        // 同步等待callable的返回值
        System.out.println(futureTask.get());
        System.out.println("main线程运行结束");
    }
}

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for(int i = 1; i <= 100; i++){
            sum +=i;
        }
        Thread.sleep(5000);
        return sum;
    }
}

还可以通过Executor,实现执行器并重载afterExecute()

public static void main(String[] args) throws ExecutionException, InterruptedException {
  MyCallable myCallable = new MyCallable();

  ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5,
                                                       1, TimeUnit.SECONDS,
                                                       new ArrayBlockingQueue<>(10)) {
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
      // 如果在call方法执行过程中有错误,则可以在此处进行处理
      System.out.println("任务执行完毕 " + r);
    }
  };

  Future<Integer> future = executor.submit(myCallable);
  System.out.println(future.get());
  executor.shutdown();
}

# 原理解析

有了Runnable,为什么还要有Callable接口?

我们假设一共有四个程序需要执行,第三个程序时间很长。如果使用Runnable接口实现会按照顺序依次执行,会等第三个程序执行完毕,才去执行第四个。而如果使用Callable接口,我们可以把时间长的第三个程序单独开启一个线程去执行,第1、2、4 线程执行不受影响。比如,主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务,主线程就去做其他的事情,过一会儿才去获取子任务的执行结果

生活中的例子:

  • 老师上着课口渴了,去买水不合适。讲课线程继续,老师可以单起个线程找班长帮忙买水,水买回来了放桌上,老师需要的时候再去get。
  • 4个同学,A算1+20、B算21+30、C算31*到40、D算41+50,C的计算量有点大,那么可以使用FutureTask单起个线程给C计算,我先汇总ABD结果,最后等C计算完了再汇总C,拿到最终结果
  • 高考,会做的先做,不会的放在后面做。

注意事项:

  • get( )方法建议放在最后一行,防止线程阻塞。获取结果的常见方式

    // 1.不见不散,执行到该行,线程开始阻塞,直到获得计算结果
    Object o = futureTask.get();
    // 2.过时不候,一个可遇见的阻塞时间
    Object o2 = futureTask.get(2L, TimeUnit.SECONDS);
    // 3.使用轮询, 解决阻塞(不推荐,建议使用CompletableFuture)
    // - 如果想要异步获取结果,通常都会以轮询的方式去获取结果,尽量不要阻塞,但是轮询的方式会消耗无畏的CPU资源,而且也不见得能及时地得到计算的结果
    while(true){
      if(futureTask.isDone()){
        System.out.println("使用轮询来解决阻塞,值为:"+futureTask.get());
        break;
      }else{
        System.out.println("阻塞中.....");
      }
    }
    
  • 一个FutureTask,多个线程调用call( )方法只会调用一次

  • 如果需要调用call方法多次,则需要多个FutureTask

# 使用线程池

通过线程池创建线程

public class ThreadPool {

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for(int i = 0; i < 10; i++)
        {
            RunnableThread thread = new RunnableThread();
            executorService.execute(thread);
        }
        // 关闭线程池
        executorService.shutdown();
    }

}

class RunnableThread implements Runnable {
    @Override
    public void run() {
        System.out.println("通过线程池方式创建的线程:" + Thread.currentThread().getName() + " ");
    }
}

# Thread常用方法

# 获取和设置Thread信息

方法名称 说明
getName()/setName(String name) 获取/设置线程的名称
currentThread Thread类的静态方法,返回当前正在执行的线程对象的引用
getId() 返回线程的标识符。该标识符是在钱程创建时分配的一个正整数。在线程的整个生命周期中是唯一且无法改变的。
getState() 返回线程的状态
getPriority()/setPriority() 获取或设置线程的优先级,线程默认优先级是5(范围是1-10)。 线程优先级高仅仅表示线程获取的CPU时间的几率高,往往需要多次运行的时候才能看到想要的效果。

线程两种调度模型

  1. 分时调度模式:所有线程轮流使用CPU的使用权,平均分配每个线程占有CPU的时间片。
  2. 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,则随机选择一个。优先级高的线程获取的CPU时间片相对多一些 (Java使用抢占式调度模型)

# 线程控制

方法名称 说明
sleep(long millis) Thread类的静态方法,将线程的执行暂停ms时间 (休眠线程,不释放锁)
join()/join(int millis) 当前线程暂停,等待指定的线程执行结束后,当前线程再继续 (相当于插队加入),可以设置固定时间
yield() 让出cpu的执行权(礼让线程)
isDaemon()/setDaemon() 获取或设置Thread对象的守护条件
interrupt() 中断目标线程,给目标线程发送一个中断信号,线程被打上中断标记
interrupted() 判断目标线程是否被中断,但是将清除线程的中断标记
isinterrupted() 判断目标线程是否被中断,不会清除中断标记
setUncaughtExceptionHandler() 当线程执行出现未校验异常时,该方法用于建立未校验异常的控制器。

守护线程

守护线程区别于用户线程,是程序在运行时在后台提供一种通用服务的线程。垃圾回收线程就是典型的守护线程。

守护线程拥有自动结束自己生命周期的特性,非守护线程却没有。如果垃圾回收线程是非守护线程,当JVM 要退出时,由于垃圾回收线程还在运行着,导致程序无法退出,这就很尴尬。

上次更新: 5/30/2023, 12:05:21 AM
线程生命周期

线程生命周期→

最近更新
01
2025
01-15
02
Elasticsearch面试题
07-17
03
Elasticsearch进阶
07-16
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式