资讯专栏INFORMATION COLUMN

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

cyixlq / 1166人阅读

摘要:当线程执行完后进入状态,表示线程执行结束。其中和表示两个线程。但要注意,让出并不表示当前线程不执行了。关键字其作用是防止指令重排和使线程对一个对象的修改令其他线程可见。

JMM特性一览

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

原子性(Atomicity)

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

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

可见性(Visibility)

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

有序性(Ordering)

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

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

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

程序顺序原则:一个线程内保证语义的串行性

  a=1;
  b=a+1;
  //第二条语句依赖于第一条执行结果。所以不允许指令重排。

volatile规则:volatile变量的写,先发生与读,这保证了volatile变量的可见性。一般用volatile修饰的都是经常修改的对象。

锁规则:解锁(unlock)必然发生在随后的加锁(lock)前

传递性:A先于B,B先于C,那么A必然先于C

线程的start()方法先于它的每一个动作

线程的所有操作先于线程的终结(Thread.join())

线程的中断(interrupt())先于被中断线程的代码

对象的构造函数执行、结束先于finalize()方法

Java多线程

线程所有的状态都在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()方法的老问题。
与线程有关的三个方法,

中断线程

void Thread.interrupt()

说明:Thread.interrupt() 是一个实例方法,他通知目标线程中断,也就是设置中断标志位。中断标志位表示当前线程已经被中断了。

判断是否被中断

boolean Thread.isInterrupted()

说明:Thread.isInterrupted() 也是实例方法,他判断当前线程是否被中断(通过检查中断标志位)

判断是否被中断,并清除当前中断状态

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的休眠而全部无法正常执行。

线程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可以有多种用法:

指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。

直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/65184.html

相关文章

  • 《深入理解 Java 内存模型》读书笔记

    摘要:前提深入理解内存模型程晓明著,该书在以前看过一遍,现在学的东西越多,感觉那块越重要,于是又再细看一遍,于是便有了下面的读书笔记总结。同步同步是指程序用于控制不同线程之间操作发生相对顺序的机制。线程之间的通信由内存模型控制。 showImg(https://mmbiz.qpic.cn/mmbiz_jpg/1flHOHZw6RtPu3BNx3zps1JhSmPICRw7QgeOmxOfTb...

    姘存按 评论0 收藏0
  • 《深入理解 Java 内存模型》读书笔记

    摘要:前提深入理解内存模型程晓明著,该书在以前看过一遍,现在学的东西越多,感觉那块越重要,于是又再细看一遍,于是便有了下面的读书笔记总结。同步同步是指程序用于控制不同线程之间操作发生相对顺序的机制。线程之间的通信由内存模型控制。 showImg(https://segmentfault.com/img/remote/1460000013474312?w=1920&h=1271); 前提 《深...

    xuexiangjys 评论0 收藏0
  • 深入理解Java内存模型()——基础

    摘要:线程之间的通信由内存模型本文简称为控制,决定一个线程对共享变量的写入何时对另一个线程可见。为了保证内存可见性,编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。 并发编程模型的分类 在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体)。通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的...

    jsdt 评论0 收藏0
  • 深入理解Java内存模型(七)——总结

    摘要:编译器,和处理器会共同确保单线程程序的执行结果与该程序在顺序一致性模型中的执行结果相同。正确同步的多线程程序的执行将具有顺序一致性程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。 前情提要 深入理解Java内存模型(六)——final 处理器内存模型 顺序一致性内存模型是一个理论参考模型,JMM和处理器内存模型在设计时通常会把顺序一致性内存模型作为参照。JMM和处理器内...

    paney129 评论0 收藏0
  • 后台开发常问面试题集锦(问题搬运工,附链接)

    摘要:基础问题的的性能及原理之区别详解备忘笔记深入理解流水线抽象关键字修饰符知识点总结必看篇中的关键字解析回调机制解读抽象类与三大特征时间和时间戳的相互转换为什么要使用内部类对象锁和类锁的区别,,优缺点及比较提高篇八详解内部类单例模式和 Java基础问题 String的+的性能及原理 java之yield(),sleep(),wait()区别详解-备忘笔记 深入理解Java Stream流水...

    spacewander 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<