美文网首页
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线程基础回顾及内存模型(二)

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