美文网首页
JAVA线程基础回顾及内存模型(二)

JAVA线程基础回顾及内存模型(二)

作者: 啦啦哇哈哈 | 来源:发表于2018-10-22 14:19 被阅读0次

原子性 可见性和有序性

  • 原子性(Atomicity):由JMM直接保证原子性变量操作在上节的read\loadstore\writeuse\assign操作中介绍过了,对于long和double两个64位的非原子性协定知道即可。我们可以认为对基本数据类型的访问读写都是具有原子性的。而要保证一个更大的范围的原子性,JMM提供了lock\unlock来满足需求,语言层面就是使用synchronized关键字。
  • 可见性(Visibility):可见性是指一个线程修改了共享变量的值,其他线程能够立刻得知这个修改。volatile关键字中我们已经讲了这一点。volatilesynchronized都保证了可见性,另一种保证可见性的关键字是final,这是显然的,一个被final修饰的变量一旦被赋值就不能再更改了,这与线程也没有什么关系。
  • 有序性(Ordering):程序代码按照指令顺序执行。
    • 如果在本线程内观察,所有的操作都是有序的,指“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics);如果在一个线程中观察另一个线程,所有的操作都是无序的,指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
    • 提供两个关键字保证有序性:volatile 本身就包含了禁止指令重排序的语义;synchronized保证一个变量在同一个时刻只允许一条线程对其进行lock操作,使得持有同一个锁的两个同步块只能串行地进入。

关于volatile禁止指令重排序保证有序性的介绍,可以参考这篇文章——volatile关键字,举了个单例模式的例子,解释了volatile禁止指令重排序的效果。

public class Singleton {
    private static Singleton uniqueInstance;
    private Singleton(){}
    public static Singleton getInstance(){
        if(uniqueInstance == null){
        // B线程检测到uniqueInstance不为空
            synchronized(Singleton.class){
                if(uniqueInstance == null){
                    uniqueInstance = new Singleton();
                    // A线程被指令重排了,刚好先赋值了;但还没执行完构造函数。
                }
            }
        }
        return uniqueInstance;// 后面B线程执行时将引发:对象尚未初始化错误。
    }
}

如果加上volatile之后,就没有这样的问题了。volatile保证被其修饰的变量不会被编译器重排序,但是其他代码还是可能会被重排序的。


线程控制的几个方法

我们之前只是用start来启动线程。下面再介绍几个API,用来控制线程达到其他的状态

暂停线程

首先是静态sleep()方法:

Thread.sleep(long millis) // 静态方法,让调用这个方法的线程让出 CPU,休眠参数指定的毫秒数

调用这个方法的线程会进入阻塞状态。

其次是实例join()方法:

Thread.join() // 实例方法,分为有参数版本和无参数版本,
              // 调用这个方法的线程会让出 CPU 资源进行等待参数指定的时间(毫秒),如果没有指定参数,
              // 那么会直到这个方法所属的线程对象执行完成后,陷入等待的线程会恢复就绪态,等待 CPU 资源

调用这个方法的线程会进入阻塞状态,CPU会让给这个方法所属的线程对象。

终止线程

run()方法执行完毕是正常的终止线程,但也可以人为调用方法来终止线程的执行。
Thread类有一个stop()方法,这个方法已经被废弃了,是不安全的,具体废弃原因去查文档。可以利用一个boolean变量,这样安全的终止一个线程:

public void run() {
    boolean isFinish = false; // 记录线程任务是否完成
    while (!isFinish) {
        if(/*任务完成*/) {
            break; // 或者 isFinish = true;
        } else {
            // do something ...
        }
    }
}

Thread类中还提供了个interrup()以及相关的一些方法,这是实例方法,可以这样做:

public void run() {
    while (Thread.currentThread().isInterrupted() == false) {
        if (/*任务完成*/) {
            Thread.currentThread().interrupt();
        } else {
            // do something ...
        }
    }
}

详细了解上面的几个方法之前,先知道一个中断标志的概念,中断标志可以理解成线程内部的一个 boolean 类型的字段,其本身不会影响线程的执行,但是和其他方法(下面介绍的会有sleep方法以及wait方法)混用时,就有可能影响线程的执行。

Thread.currentThread() // 静态方法,返回执行当前代码的线程对象引用

Thread.isInterrupted() // 实例方法,返回调用这个方法的线程对象的中断标志(true / false)

Thread.interrupt() // 实例方法,将调用这个方法的线程对象的中断标志设置为 true,
                   // 请注意:线程的中断标志本身不会影响线程的执行

直接用interrup自然比自己去定义boolean更方便,但是我们介绍这个中断标志就是有伏笔的,interrup会把中断标志设置为true,而这和sleep方法一起使用时候,会有异常抛出,我们打开sleep的源码:



从注释中看到,sleep方法调用时候,如果当前线程被中断(它的中断标志是true),那么在抛出异常时候这个中断标志会被清除(将中断标志设置为 false),由此就导致了isInterrupted方法的返回值,可能并不是我们想要的结果。

其他方法

静态yield()方法,提示scheduler,让出调用线程的资源,一定概率。

Thread.yield() // 静态方法,提示线程调度器当前调用这个方法的线程(当前持有 CPU 资源的线程)已经完成任务,
               // 可以让出 CPU 资源了,当然,这只是一种提示,线程调度器可以忽略这种提示,
               // 所以 CPU 资源是否让出并不是一定的,是有一定概率的。

上面介绍的所有方法都是Thread类中的方法,有的是静态的,有的是实例方法,静态的这些方法一般都直接对调用它的线程起到作用,而实例方法,则还对这个方法所属的对象线程有影响。下面我们再来看Object类中一些与线程控制有关的方法。

  • Object.wait()
  • Object.notify()
  • Object.notifyAll()

这些方法全部是实例方法。都是必须要在sychronized方法或者sychronized代码块中才能使用,而且还必须是某个线程已经获取到了这个Object的锁时候,才能调用它的wait,notify,notifyAll。

Object.wait() // 使得调用这个方法的线程释放这个 Object 对象的锁并且陷入无限等待,
              // 直到某个线程调用了这个 Object 对象的 notify 或者 notifyAll 方法
              // 线程被唤醒之后进入就绪状态,等待 CPU 资源
              // 如果当前线程的中断标志为 true,那么会抛出一个 InterruptedException 异常

Object.wait(long timeout) 
// 使得调用这个方法的线程释放这个 Object 对象的锁并且等待参数指定的时间,单位为毫秒
// 直到这个等待的时间段过去、某个线程调用了这个 Object 对象的 notify 或者 notifyAll 方法
// 线程被唤醒之后进入就绪状态,等待 CPU 资源
// 如果当前线程的中断标志为 true,那么会抛出一个 InterruptedException 异常

Object.wait(long timeout, int nanos) 
// 使得调用这个方法的线程释放这个 Object 对象的锁并且等待参数指定的时间,
// 第二个参数是纳秒,提供更加精确的控制
// 直到这个等待的时间段过去、某个线程调用了这个 Object 对象的 notify 或者 notifyAll 方法
// 线程被唤醒之后进入就绪状态,等待 CPU 资源
// 如果当前线程的中断标志为 true,那么会抛出一个 InterruptedException 异常

Object.notify() // 唤醒一个因调用这个 Object 对象的 wait() 方法而陷入等待状态的线程,具体哪个线程未知。

Object.notifyAll() // 唤醒所有因调用这个 Object 对象的 wait() 方法而陷入等待状态的线程。

使用实例还是可以参考这个老哥的文章——wait使用的一个实例——转账余额不足

注意wait和sleep的区别,他们好像都多少有些等待、休眠的意思。但是前者是Object类的实例方法,调用后会释放当前对象锁,并且需要其他线程调用这个对象的notify()或者notifyAll()来唤醒。而后者是Thread类的静态方法,调用后只是让出CPU,并不会释放锁,监控状态一直保持,过了指定时间后它会自动恢复运行状态。

相关文章

  • JAVA线程基础回顾及内存模型(二)

    原子性 可见性和有序性 原子性(Atomicity):由JMM直接保证原子性变量操作在上节的read\load,s...

  • Java多线程目录

    一 Java并发基础介绍二 Thread类三 线程终止四 Java多线程内存模型五 volatile关键字六 sy...

  • java并发编程(四)

    java多线程编程(四) 引言: 内存模型的基础----内存模型相关的基本概念 java内存模型中的顺序一致性--...

  • JAVA线程基础回顾及内存模型(一)

    线程创建的两种方式 继承Thread类 实现Runnable接口 两种方式的区别1.继承Thread类受限于JAV...

  • 高并发Java

    高并发Java(1):前言 高并发Java(2):多线程基础 高并发Java(3):Java内存模型和线程安全 高...

  • 并发编程03-Java内存模型01

    Java内存模型基础并发编程模型的两个关键问题线程之间的通信线程之间的同步Java内存模型的抽象结构从源代码到指令...

  • Java-并发编程知识点总结

    目录: 线程基础 线程池 各种各样的锁 并发容器 原子类 Java 内存模型 线程协作 AQS 框架 一、线程基础...

  • 第12章 Java内存模型与线程

    第12章 Java内存模型与线程 12.3Java内存模型 Java内存模型 [1](Java Memory Mo...

  • 深入理解Java并发内存模型

    Java内存模型是什么 Java 内存模型翻译自Java Memory Model,也称Java多线程内存模型,简...

  • java内存模型

    java内存模型基础 并发编程,两个关键问题:线程通信和线程同步通信机制:共享内存和消息传递 java并发采用共享...

网友评论

      本文标题:JAVA线程基础回顾及内存模型(二)

      本文链接:https://www.haomeiwen.com/subject/qmxszftx.html