美文网首页
Java多线程笔记(一):JMM与基础关键字

Java多线程笔记(一):JMM与基础关键字

作者: 泊浮目 | 来源:发表于2016-12-31 17:34 被阅读36次

    JMM特性一览

    Java Memory Model的关键技术点都是围绕着多线程的原子性、可见性和有序性来建立的。因此我们首先需要来了解这些概念。

    原子性(Atomicity)

    原子性是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个人操作一旦开始,就不会被其他的线程干扰。

    比如对一个静态全局变量int i,两个线程同时对它赋值,线程A给他赋值1,线程B给它赋值为-1.那么不管这么2个线程以合作方式、何种步调工作,i的值要么是1,要么是-1。线程A和B之间是没有干扰的。这就是原子性的一个特点,不可被中断。

    可见性(Visibility)

    可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。显然,对于串行程序来说,可见性问题是不存在的。因为你在任何一个操作步骤中修改了某个变量,那么在后续的步骤中,读取这个变量的值,一定是修改后的新值。

    有序性(Ordering)

    有序性问题是三个问题中最难理解的。对于一个线程的执行代码而言,我们总是习惯地认为代码的执行是从先往后,依次执行。这么理解也不是说完全错误,因为就一个线程内而言,确实会表现成这样。但是,在并发时,程序的执行可能就会出现乱序。给人直观的感觉就是:写在前面的代码,会在后面执行。然而有序性的问题的原因因为是程序在执行时,可能会进行指令重排,重排后的指令与原指令的顺序未必一致。 那么在这里由于篇幅关系就不在展开介绍,有兴趣的读者可以自行搜索Java指令重排CPU流水线等资料。

    哪些指令不能重排——Happen-Before规则

    虽然Java虚拟机和执行系统会对指令进行一定的重排,但是指令重排是有规则的,并非所有的指令都可以随便改变位置。原则基本包括以下:

    1. 程序顺序原则:一个线程内保证语义的串行性
      a=1;
      b=a+1;
      //第二条语句依赖于第一条执行结果。所以不允许指令重排。
    
    1. volatile规则:volatile变量的写,先发生与读,这保证了volatile变量的可见性。一般用volatile修饰的都是经常修改的对象。
    2. 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
    3. 传递性:A先于B,B先于C,那么A必然先于C
    4. 线程的start()方法先于它的每一个动作
    5. 线程的所有操作先于线程的终结(Thread.join())
    6. 线程的中断(interrupt())先于被中断线程的代码
    7. 对象的构造函数执行、结束先于finalize()方法

    Java多线程

    MultiThreadStates.png

    线程所有的状态都在Thread.State枚举类中定义:

    public enum State {
        /**
        * 表示刚刚创建的线程,这种线程还没开始执行。
        **/
        NEW,
        /**
        * 调用start()方法后,线程开始执行,处于RUNNABLE状态,
        * 表示线程所需要的一切资源以及准备好。
        **/
        RUNNABLE,
        /**
        * 当线程遇到synchronized同步块,就进入了BLOCKED阻塞状态。
        * 这时线程会暂停执行,直到获得请求的锁。
        **/
        BLOCKED,
        /**
        * WAITING和TIMED_WAITING都表示等待状态,他们是区别是WAITING表示进入一个无时间限制的等待
        * TIMED_WAITING会进入一个有时间限制的等待。
        * WAITING的状态正是在等待特殊的事件,如notify()方法。而通过join()方法等待的线程,则是等待目标线程的终止。
        * 一旦等到期望的时间,线程就会继续执行,进入RUNNABLE状态。
        * 当线程执行完后进入TERMINATED状态,表示线程执行结束。
        **/
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }
    

    线程的基本操作

    新建线程

    新建线程很简单。只要使用new关键字创建一个线程对象,并且将其start()起来即可。start()方法额就会新建一个线程并让这个线程执行run()方法。

    常见就是有人直接对一个线程对象执行run()方法,那么只会在当前的线程中串行执行run()中的代码

    最后要说的是,默认的Thread.run()就是直接调用内部的Runnable接口。因此,使用Runnable接口告诉线程该做什么,更为合理。

    终止线程

    Stop()方法是用不得的,会直接终止运行中的线程,并立刻释放锁。比如一个线程写数据到一般被中止,则会写坏。

    那么最简单的方法可以考虑给线程做一个死循环,然后对一个类似Flag的变量进行判断,变量变化时退出循环。JDK所提供的线程中断也是类似于此。

    线程中断

    线程中断是重要的线程协作机制,中断就是让线程停止执行,但这个停止执行非stop()的暴力方式。JDK提供了更安全的支持,就是线程中断。
    线程中断并不会使线程立即停止,而是给线程发送一个通知,告诉目标线程有人希望你退出。至于目标线程接到通知后什么时候停止,完全由目标线程自行决定。这点很重要,如果线程接到通知后立即退出,我们就又会遇到类似stop()方法的老问题。
    与线程有关的三个方法,

    1. 中断线程
      public void Thread.interrupt()
      说明:Thread.interrupt() 是一个实例方法,他通知目标线程中断,也就是设置中断标志位。中断标志位表示当前线程已经被中断了。
    2. 判断是否被中断
      public boolean Thread.isInterrupted()
      说明:Thread.isInterrupted() 也是实例方法,他判断当前线程是否被中断(通过检查中断标志位)
    3. 判断是否被中断,并清除当前中断状态
      public static boolean Thread.interrupted()
      说明:Thread.interrupted() 是静态方法,判断当前线程的中断状态,但同时会清除当前线程的中断标志位状态。

    Thread.sleep()方法会让当前线程休眠若干时间,它会抛出一个interruptedException中断异常。interruptedException是必须被捕获的——当线程在sleep时,如果被中断,这个异常就产生。

    public class InterruptExample {
    
        public static void main(String [] a) throws InterruptedException{
    
            Thread t1 = new Thread("线程小哥 - 1 "){
                @Override
                public void run() {
                    while (true){
                        /**
                         * 必须得判断是否接受到中断通知,如果不写退出方法,也无法将当前线程退出.
                         */
                        if (Thread.currentThread().isInterrupted()){
                            System.out.println(Thread.currentThread().getName() + " Interrupted ... ");
                            break;
                        }
    
                        try {
                            /**
                             * 处理业务逻辑花费10秒.
                             * 而在这时,主线程发送了中断通知,当线程在sleep的时候如果收到中断
                             * 则会抛出InterruptedException,如果在异常中不处理,则线程不会中断.
                             *
                             */
                            Thread.sleep(10000);
                        } catch (InterruptedException e) {
                            System.out.println("线程在睡眠中遭到中断....");
                            /**
                             * 在sleep过程中,收到中断通知,抛出异常.可以直接退出线程.
                             * 但如果还需要处理其他业务,则需要重新中断自己.设置中断标记位.
                             * 这样在下次循环的时候 线程发现中断通知,才能正确的退出.
                             */
                            Thread.currentThread().interrupt();
                        }
    
                        Thread.yield();
                    }
                }
            };
    
            t1.start();
            try {
                /**
                 * 处理业务500毫秒
                 * 然后发送中断通知,此时t1线程还在sleep中.
                 */
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            /**
             * 给目标线程发送中断通知
             * 目标线程中必须有处理中断通知的代码
             * 否则,就算发送了通知,目标线程也无法停止.
             */
            t1.interrupt();
        }
    }
    

    等待(wait)和通知(notify)

    为了支持多线程之间的协作,JDK提供了两个非常重要的等待方法wait()和nofity()方法。这两个方法并不是Thread类中的,而是Object类,这意味着任何对象都可以调用这两个方法。

    这两个方法的签名如下:

    public final void wait() throws InterruptedException
    public final native void notify()
    

    如果一个线程调用了object.wait()方法,那么这个线程就会停止执行而转为等待状态,进入obj对象的等待队列。这个等待队列可能有多个线程,因为系统运行多个线程同时等待同一个对象。其他线程调用obj.notify()方法时,它就会从等待队列中随机选择一个线程并将其唤醒。注意这个选择是不公平的,是随机的。

    object.wait()方法并不是可以随便调用。它必须包含在对应的synchronized语句中。无论是wait还是notify都必须首先获得目标对象的一个监视器 。如下图,显示了wait()和nofity的工作流程细节。其中T1和T2表示两个线程。T1在正确执行wait方法后,首先必须获得object对象的监视器。而wait方法在执行后,会释放这个监视器,这样做的目的使得其他等待object对象上的线程不至于因为T1的休眠而全部无法正常执行。

    waitAndNotifyImagesNo1.png

    线程T2在notify()调用前,也必须获得object的监听器。所幸,此时T1已经释放了这个监视器。因此,T2可以顺利获得object的监视器。接着,T2执行了notify()方法尝试唤醒一个等待线程,这里假设唤醒了T1。T1在被唤醒后,要做的第一件事并不是执行后续的代码,而是要尝试重新获得object的监视器。而这个监视器也正是T1在wait()方法执行前所持有的那个。如果暂时无法获得,T1还必须要等待这个监视器。当监视器顺利获得后,T1才可以真正意义上的继续执行。

    注意::Object.wait()和Thread.sleep()方法都可以让线程等待若干的时间。除了wait()可以被唤醒外,另一个最主要的区别就是wait()方法会释放目标对象的锁,而Thread.sleep()方法不会释放任何资源。

    挂起(suspend)和继续执行(resume)线程

    不推荐使用suspend()去挂起线程 的原因是因为suspend()在导致线程暂停的同时,并不会去释放任何资源。此时,其他任何线程都想访问它暂用的锁时,都会被导致牵连,导致无法正常运行。直到对应的线程上进行了resume()操作,被挂起的线程才能继续,从而其他所有阻塞在相关锁上的线程也可以继续执行。但是,如果resume()操作意外地在suspend()前就执行了,那么被挂起的线程可能就很难有机会被继续执行。并且,更严重的是:它锁占用的锁不会被释放,因此可能会导致整个操作系统工作不正常。而且,对于被挂起的线程,从它的线程上看状态,居然会是Runnable,这是最气的。

    等待线程结束(join)和谦让(yield)

    join的方法签名:

    public final void join () throws InterruptedException //一直阻塞当前线程,直到目标线程执行完毕
    public final synchronized void join (long millis) throws InterruptedException//和之前一样,不过增加了最大等待时间
    
    public static native void yield();
    

    这是一个静态方法,一旦执行,它会使当前线程让出CPU。但要注意,让出CPU并不表示当前线程不执行了。当前线程在让出CPU以后,还会进行CPU资源争夺,但是是否能够再次分配到就要看人品了。

    如果你觉得一个线程不那么重要,或者优先级非常低,而且又害怕它会占用太多的CPU资源,那么可以在适当的时候调用Thread.yield(),给予其他重要线程更多的工作机会。

    关键字volatile

    其作用是防止CPU指令重排和使线程对一个对象的修改令其他线程可见。

    对于Java的内存模型来说,每个volatile会在线程的工作内存从保留一个拷贝,只不过java内存模型通过对volatile变量的添加了特殊机制保证了变量的可见性。线程在修改volatile类型变量以后必须立即保存到主内存,在使用变量前必须从主内存加载数据,同时还做了一些禁止指令重排序的操作。对于各个线程的工作内存(私有内存)来说,存在volatile变量不一致的时刻,但是对于执行引擎来说,通过了上面的几条规则保证了变量是一致的。

    可参考: Java并发编程之volatile关键字解析

    线程安全的概念与synchronized

    并行程序开发的一大关注重点就是线程安全。一般来说,程序并行化就是为了获得更高的执行效率,但前提是,不能以牺牲正确性为代价。如果程序并行化以后,连基本的执行结果都无法保证,那么并行程序本身也就没有任何意义了。

    volatile并不能真正的保障线程安全。它只能确保一个线程修改了数据后,其他线程能够看到这个改动。但当两个线程同时修改某一个数据时,却依然会产生冲突。

    关键字synchronized的作用是实现线程间的同步。它的工作是对同步的代码加锁,使得每一次,只能有一个线程进入同步块,从而保证线程间的安全。

    关键字synchronized可以有多种用法:

    • 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
    • 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
    • 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。

    相关文章

      网友评论

          本文标题:Java多线程笔记(一):JMM与基础关键字

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