0%

Java线程

  • 创建和运行线程
    • 方法1-Thread
    • 方法2-Runnable
    • 方法3-Callable

Java线程

创建和运行线程

方法1-Thread

  • Thread 创建线程方式:创建线程类,匿名内部类方式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class ThreadDemo{
    public static void main(String[] args) {
    Thread t = new Thread(){
    @Override
    public void run(){
    System.out.println("running");
    }
    };
    t.setName("t1");
    t.start();
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class ThreadDemo {
    public static void main(String[] args) {
    Thread t = new MyThread();
    t.start();
    }
    }
    class MyThread extends Thread {
    @Override
    public void run() {
    for(int i = 0 ; i < 100 ; i++ ) {
    System.out.println("子线程输出:"+i)
    }
    }
    }
    • start() 方法底层其实是给 CPU 注册当前线程,并且触发 run() 方法执行
    • 线程的启动必须调用 start() 方法,如果线程直接调用 run() 方法,相当于变成了普通类的执行,此时主线程将只有执行该线程
    • 建议线程先创建子线程,主线程的任务放在之后,否则主线程(main)永远是先执行完
  • 继承 Thread 类的优缺点:

    • 优点:编码简单
    • 缺点:线程类已经继承了 Thread 类无法继承其他类了,功能不能通过继承拓展(单继承的局限性)

方法2-Runnable

  • Runnable 创建线程方式:创建线程类,匿名内部类方式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class ThreadDemo{
    public static void main(String[] args) {
    // 创建线程对象
    Thread t = new Thread(new Runnable(){
    @Override
    public void run(){
    // 要执行的任务
    }
    };);
    // 启动线程
    t.start();
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class ThreadDemo {
    public static void main(String[] args) {
    Runnable target = new MyRunnable();
    Thread t1 = new Thread(target,"1号线程");
    t1.start();
    Thread t2 = new Thread(target);//Thread-0
    }
    }
    public class MyRunnable implements Runnable{
    @Override
    public void run() {
    for(int i = 0 ; i < 10 ; i++ ){
    System.out.println(Thread.currentThread().getName() + "->" + i);
    }
    }
    }
  • Runnable 方式的优缺点:

    • 缺点:代码复杂一点。
    • 优点:
      1. 线程任务类只是实现了 Runnable 接口,可以继续继承其他类,避免了单继承的局限性
      2. 同一个线程任务对象可以被包装成多个线程对象
      3. 适合多个多个线程去共享同一个资源
      4. 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立
      5. 线程池可以放入实现 Runnable 或 Callable 线程任务对象
  • Thread 是把线程和任务合并在了一起,Runnable 是把线程和任务分开了。

方法3-Callable

  • 实现 Callable 接口:

    1. 定义一个线程任务类实现 Callable 接口,申明线程执行的结果类型
    2. 重写线程任务类的 call 方法,这个方法可以直接返回执行的结果
    3. 创建一个 Callable 的线程任务对象
    4. 把 Callable 的线程任务对象包装成一个未来任务对象
    5. 把未来任务对象包装成线程对象
    6. 调用线程的 start() 方法启动线程
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class ThreadDemo {
    public static void main(String[] args) {
    FutureTask<String> task = new FutureTask<>(new Callable<String>() {
    @Override
    public String call() throws Exception {
    return Thread.currentThread().getName() + "->" + "Hello World";
    }
    });
    Thread t = new Thread(task);
    t.start();
    try {
    String s = task.get(); // 等待task结果返回
    System.out.println(s);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }
    • public FutureTask(Callable<V> callable):未来任务对象,在线程执行完后得到线程的执行结果
      • FutureTask 就是 Runnable 对象,因为 Thread 类只能执行 Runnable 实例的任务对象,所以把 Callable 包装成未来任务对象
    • public V get():同步等待 task 执行完毕的结果,如果在线程中获取另一个线程执行结果,会阻塞等待,用于线程同步
      • get() 线程会阻塞等待任务执行完成
      • run() 执行完后会把结果设置到 FutureTask 的一个成员变量,get() 线程可以获取到该变量的值
  • 优缺点:

    • 优点:同 Runnable,并且能得到线程执行的结果
    • 缺点:编码复杂

线程原理

Java Virtual Machine Stacks(Java 虚拟机栈):每个线程启动后,虚拟机就会为其分配一块栈内存

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

线程上下文切换(Thread Context Switch):一些原因导致 CPU 不再执行当前线程,转而执行另一个线程

  • 线程的 CPU 时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep、yield、wait、join、park 等方法

程序计数器(Program Counter Register):记住下一条 JVM 指令的执行地址,是线程私有的

当 Context Switch 发生时,需要由操作系统保存当前线程的状态(PCB 中),并恢复另一个线程的状态,包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等

JVM 规范并没有限定线程模型,以 HotSopot 为例:

  • Java 的线程是内核级线程(1:1 线程模型),每个 Java 线程都映射到一个操作系统原生线程,需要消耗一定的内核资源(堆栈)
  • 线程的调度是在内核态运行的,而线程中的代码是在用户态运行,所以线程切换(状态改变)会导致用户与内核态转换进行系统调用,这是非常消耗性能

线程安全问题

  • Q:

    • 成员变量和静态变量是否线程安全?

    A:

    • 如果它们没有共享,则线程安全。
    • 如果它们被共享了,根据它们的状态是否发生改变,又分为两种情况:
      • 如果只有读操作,则线程安全。
      • 如果有读写操作,则需要考虑线程安全问题。
  • Q:

    • 局部变量是否线程安全?

    A:

    • 因为对于局部变量来说,每个线程都会创建自己的局部变量,所以局部变量是线程安全的。
    • 但是局部变量引用的对象不一定是线程安全的:
      • 如果该对象没有逃离方法的作用范围,则它是线程安全的。
      • 如果该对象逃离方法的作用范围,则需要考虑线程安全问题。
---------------The End---------------