Java中的线程 Krains 2020-08-24

2020-09-10 18:36:04 浏览数 (1)

Java内存模型

Java线程之间的通信由Java内存模型(简称JMM)控制,从抽象的角度来说,JMM定义了线程和主内存之间的抽象关系。

JMM的抽象示意图:

  • 所有的共享变量都存在主内存中
  • 每个线程都保存了一份该线程使用到的共享变量的副本
  • 如果线程A与线程B之间要通信的话,必须经历下面两个步骤
    • 线程A将本地内存A中更新过的共享变量刷新到主内存中
    • 线程B到主内存中去读取线程A之前已经更新过的共享变量

那么怎么知道这个共享变量的被其他线程更新了呢?这就是JMM的功劳了,也是JMM存在的必要性之一。JMM通过控制主内存与每个线程的本地内存之间的交互,来提供内存可见性保证

Java中的volatile关键字可以保证多线程操作共享变量的可见性以及禁止指令重排序,synchronized关键字不仅保证可见性,同时也保证了原子性(互斥性)。在更底层,JMM通过内存屏障来实现内存的可见性以及禁止重排序。为了程序员的方便理解,提出了happens-before,它更加的简单易懂,从而避免了程序员为了理解内存可见性而去学习复杂的重排序规则以及这些规则的具体实现方法。

内存模型的三大特性(如何保证?待补充)

  • 原子性
  • 可见性
  • 有序性

synchronized能够保证三大特性,volatile能够保证可见性和有序性

Java线程生命周期

Java线程生命周期与操作系统中的进程生命周期定义有所不同。

状态名称

说明

NEW

初始状态,Thread对象被创建,但是还没有调用start()方法

RUNNABLE

运行状态,Java中将运行和就绪态统称为运行态

BLOCKED

阻塞状态,线程获取不到锁资源而进入阻塞状态

WAITING

等待状态,进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)

TIME_WAITING

超时等待状态,不同于等待状态,可以在指定的时间自行返回

TERMINATED

终止状态,表示当前线程已经执行完毕

纠错:左上角Object.join()应为Thread.join()

创建线程的三种方式

方法一:继承Thread,覆写run()方法

方法二:实现Runnable接口,然后交给Thread执行

例子:

代码语言:javascript复制
    @Test
    public void test1(){
        Thread t1 = new Thread("t1"){
          @Override
          public void run(){
              System.out.println(1);
          }
        };
        t1.start();
    }

    @Test
    public void test2(){
        // 使用Lambda接口简化类的创建
        Runnable task = () -> System.out.println(2);
        Thread t2 = new Thread(task, "t2");
        t2.start();
    }

区别

  • 方法1把线程和任务合并在一起,方法2将两者分开了
  • Runnable更容易与线程池等高级 API 配合
  • Runnable让任务类脱离了Thread继承体系,更灵活

方法三:FutureTask配合Thread,FutureTask 能够接收 Callable 类型的参数,Callable也是一个函数式接口,只有一个call()方法,创建好FutureTask任务交给Thread执行,它用来处理有返回结果的情况

使用例子:

代码语言:javascript复制
        // 创建任务对象,指定返回结果类型
        FutureTask<String> task = new FutureTask<>(()->{
            Thread.sleep(1000);
            return "sss";
        });

        // 新建线程去执行任务
        new Thread(task, "thread1").start();

        // 调用者线程阻塞,直到task任务执行结束返回结果
        String result = task.get();
        System.out.println(result);

Thread类

常用方法

代码语言:javascript复制
// 启动一个新线程运行run方法
start();  

// 线程运行时的代码
run();

// 等待 调用 该方法的线程结束,当前线程才继续执行
join();

// 最多等待n毫秒
join(long n);
    
// 获取当前正在 执行 的线程
currentThread();

// 让当前执行的线程休眠n毫秒
sleep(n);

// 提示线程调度器让出当前线程对CPU的使用
yield();

// 打断线程,调用sleep、wait、join的线程会进入阻塞状态,可用该方法打断阻塞状态的线程,并抛出异常和清除打断标记
// 如果线程正在运行,打断标记为真
interrupt();

关于start()的两个引申问题

  1. 反复调用同一个线程的start()方法是否可行?
  2. 假如一个线程执行完毕(此时处于TERMINATED状态),再次调用这个线程的start()方法是否可行?

要分析这两个问题,我们先来看看start()的源码:

代码语言:javascript复制
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) {

        }
    }
}

看不到对threadStatus的修改,通过端点调试,两个问题的答案都是不可行,在调用一次start()之后,threadStatus的值会改变(threadStatus !=0),此时再次调用start()方法会抛出IllegalThreadStateException异常。

变量的线程安全分析

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

  • 如果它们没有共享,则线程安全
  • 如果被共享了
    • 对变量只有读操作,则线程安全
    • 对变量有读写操作,则这段代码是临界区,需要考虑线程安全问题

局部变量是否线程安全?

  • 局部变量是线程安全的,因为每个线程都创建了一份栈帧,局部变量存在局部变量表中,不是共享的
  • 但局部变量引用的对象则未必,如果该对象逃离了方法的作用范围,则需要考虑线程安全问题。

参考链接

[1].http://concurrent.redspider.group/article/02/6.html

0 人点赞