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

  1. 中断线程
  2. void Thread.interrupt()

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

  1. 判断是否被中断
  2. boolean Thread.isInterrupted()

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

  1. 判断是否被中断,并清除当前中断状态
  2. 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 "){@Overridepublic 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可以有多种用法:

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

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

  1. java学习笔记15--多线程编程基础2

    本文地址:http://www.cnblogs.com/archimedes/p/java-study-note15.html,转载请注明源地址. 线程的生命周期 1.线程的生命周期 线程从产生到消亡 ...

  2. Java多线程学习(二)synchronized关键字(1)

    转载请备注地址: https://blog.csdn.net/qq_34337272/article/details/79655194 Java多线程学习(二)将分为两篇文章介绍synchronize ...

  3. (硅谷课堂项目)Java开发笔记4:前端基础知识(二)

    文章目录 (硅谷课堂项目)Java开发笔记4:前端基础知识(二) 一.NPM 1.NPM简介 1.1.什么是NPM 1.2.NPM工具的安装位置 2.使用npm管理项目 2.1.创建文件夹npm 2. ...

  4. java学习笔记14--多线程编程基础1

    本文地址:http://www.cnblogs.com/archimedes/p/java-study-note14.html,转载请注明源地址. 多线程编程基础 多进程 一个独立程序的每一次运行称为 ...

  5. Java多线程笔记(零):进程、线程与通用概念

    前言 不积跬步,无以至千里:不积小流,无以成江海.在学习Java多线程相关的知识前,我们首先需要去了解一点操作系统的进程.线程以及相关的基础概念. 进程 通常,我们把一个程序的执行称为一个进程.反过来 ...

  6. java多线程nullpointerexception_温故而知新!越是基础越容易被忽略,java最全基础知识,附赠资料...

    一.hashMap与hashTable与ConcurrentHashMap: 1.HashMap是继承自AbstractMap类,而HashTable是继承自Dictionary类.不过它们都同时实现 ...

  7. Java并发编程:JMM和volatile关键字

    Java内存模型 随着计算机的CPU的飞速发展,CPU的运算能力已经远远超出了从主内存(运行内存)中读取的数据的能力,为了解决这个问题,CPU厂商设计出了CPU内置高速缓存区.高速缓存区的加入使得CP ...

  8. Java多线程 -- 深入理解JMM(Java内存模型) --(五)锁

    [转载自并发编程网 – ifeve.com 原文链接:http://ifeve.com/tag/jmm/] 锁的释放-获取建立的happens before 关系 锁是java并发编程中最重要的同步机 ...

  9. WY的Java学习笔记(1)基础

    Day01.[环境变量,HelloWorld,变量,常量] 2023年2月23日,算是第一天学习,感觉还好,效率很高. 第一章.Java语言前言 1.Java的介绍 1.计算机编程语言:电脑能看懂的语 ...

  10. Java 多线程 笔记 转自http://www.cnblogs.com/lwbqqyumidi/p/3804883.html

    多线程作为Java中很重要的一个知识点, 一.线程的生命周期及五种基本状态 关于Java中线程的生命周期,首先看一下下面这张较为经典的图: 上图中基本上囊括了Java中多线程各重要知识点.掌握了上图中 ...

最新文章

  1. 单步调试 step into/step out/step over 区别
  2. 用偷梁换柱法清除腾讯QQ迷你首页的方法
  3. 虚拟函数的静态决议 和 RTTI 小例子
  4. ant design 预览图片_AntD框架的upload组件上传图片时遇到的一些坑
  5. 字段与属性 c# 1613532992
  6. 2008北京奥运会歌曲推荐
  7. 如何修改php的网页文件,php如何修改php文件内容
  8. python上下文管理器细读
  9. 计算机毕业设计中ASP.NET数据源控件
  10. ASP.NET文件上传大小的限制解决方案
  11. springboot之idea多模块
  12. 453.最小移动次数使数组元素相等
  13. 动图体积太大怎么缩小?教你一招快速压缩gif
  14. chrome谷歌浏览器任务栏图标变白色解决方法
  15. 【Java框架】CSFramework框架的应用——简易聊天室
  16. Stata:面板分位数回归
  17. ubuntu安装nvidia显卡驱动注意事项以及关闭ubuntu内核自动更新
  18. Stacked Hourglass Networks - 堆叠沙漏网络结构详解
  19. 关于团队敏捷流程的思考
  20. Vue 加载 SVG 图片文件

热门文章

  1. 利用反射技术动态执行方法时如何取引用类参数的值
  2. [19/06/08-星期六] CSS基础_表格表单
  3. Tomcat启动命令行窗口中文乱码
  4. K个排序链表的合并(Hard)
  5. Python day17 模块介绍1(time,random)
  6. 最常用标准库函数 (转)
  7. DOM 其他一些特性
  8. 常见服务器返回的错误代码(转)
  9. Shiny 版混合线性模型的建模遍历
  10. 当文科生遇见R语言,照样玩得转