Java 线程内存模型的缺陷和增强
Java内存模型-jsr133规范介绍

Java内存模型-jsr133规范介绍最近在看《深⼊理解Java虚拟机:JVM⾼级特性与最佳实践》讲到了线程相关的细节知识,⾥⾯讲述了关于java内存模型,也就是jsr 133定义的规范。
系统的看了jsr 133规范的前⾯⼏个章节的内容,觉得受益匪浅。
废话不说,简要的介绍⼀下java内存规范。
什么是内存规范在jsr-133中是这么定义的A memory model describes, given a program and an execution trace of that program, whetherthe execution trace is a legal execution of the program. For the Java programming language, thememory model works by examining each read in an execution trace and checking that the writeobserved by that read is valid according to certain rules.也就是说⼀个内存模型描述了⼀个给定的程序和和它的执⾏路径是否⼀个合法的执⾏路径。
对于java序⾔来说,内存模型通过考察在程序执⾏路径中每⼀个读操作,根据特定的规则,检查写操作对应的读操作是否能是有效的。
java内存模型只是定义了⼀个规范,具体的实现可以是根据实际情况⾃由实现的。
但是实现要满⾜java内存模型定义的规范。
处理器和内存的交互这个要感谢硅⼯业的发展,导致⽬前处理器的性能越来越强⼤。
⽬前市场上基本上都是多核处理器。
如何利⽤多核处理器执⾏程序的优势,使得程序性能得到极⼤的提升,是⽬前来说最重要的。
⽬前所有的运算都是处理器来执⾏的,我们在⼤学的时候就学习过⼀个基本概念程序 = 数据 + 算法,那么处理器负责计算,数据从哪⾥获取了?数据可以存放在处理器寄存器⾥⾯(⽬前x86处理都是基于寄存器架构的),处理器缓存⾥⾯,内存,磁盘,光驱等。
Java使用newThread和线程池的区别

Java使⽤newThread和线程池的区别1.new Thread的弊端执⾏⼀个异步任务你还只是如下new Thread吗new Thread(new Runnable() {@Overridepublic void run() {// TODO Auto-generated method stub}}).start();说说弊端:a. 每次new Thread新建对象性能差。
b. 线程缺乏统⼀管理,可能⽆限制新建线程,相互之间竞争,及可能占⽤过多系统资源导致死机或oom。
c. 缺乏更多功能,如定时执⾏、定期执⾏、线程中断。
相⽐new Thread,Java提供的四种线程池的好处在于:a. 重⽤存在的线程,减少对象创建、消亡的开销,性能佳。
b. 可有效控制最⼤并发线程数,提⾼系统资源的使⽤率,同时避免过多资源竞争,避免堵塞。
c. 提供定时执⾏、定期执⾏、单线程、并发数控制等功能。
2.Executors提供四种线程池newCachedThreadPool创建⼀个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若⽆可回收,则新建线程。
线程池的规模不存在限制。
newFixedThreadPool 创建⼀个固定长度线程池,可控制线程最⼤并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建⼀个固定长度线程池,⽀持定时及周期性任务执⾏。
newSingleThreadExecutor 创建⼀个单线程化的线程池,它只会⽤唯⼀的⼯作线程来执⾏任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执⾏。
下⾯代码说明:(1). newCachedThreadPool创建⼀个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若⽆可回收,则新建线程。
⽰例代码如下:ExecutorService cachedThreadPool = Executors.newCachedThreadPool();for (int i = 0; i < 10; i++) {final int index = i;try {Thread.sleep(index * 1000);} catch (InterruptedException e) {e.printStackTrace();}cachedThreadPool.execute(new Runnable() {@Overridepublic void run() {System.out.println(index);}});}线程池为⽆限⼤,当执⾏第⼆个任务时第⼀个任务已经完成,会复⽤执⾏第⼀个任务的线程,⽽不⽤每次新建线程。
优化java堆大小的5个技巧

优化Java堆大小的5个技巧摘要:Java堆容量不足可以对性能造成很大影响,这样无疑就给程序带来不可必要的麻烦,本文总结了影响Java堆容量不足的五大原因以及巧妙地去优化?本文作者Pierre是一名有10多年经验的高级系统架构师,他的主要专业领域是Java EE、中间件和JVM技术。
根据他多年的工作实践经验,他发现许多性能问题都是由Java堆容量不足和调优引起的。
下面他将和大家分享非常实用的5个Java堆优化技巧。
1.JVM:对难以理解的东西产生恐惧感千万不要以为,通过配置,调优,就可以排除那些你所不明白的问题。
有些人认为Java程序员不需要知道内部JVM内存管理。
毫无疑问,这种观点明显是错误的,如果想拓宽知识面和提升排除故障能力,你就必须要了解和学习一下JVM内存管理。
对于Java或者是Java EE新手来说,Java Heap调优和故障排除是一项非常有挑战的工作。
下面会提供一些典型的案例场景:客户端环境面临着有规律的OutOfMemoryError错误并且对业务造成了很大的影响。
你的开发团队要在如此大的压力下去解决这个问题,通常会怎么做?1用谷歌搜索引擎找到类似的问题并且你会相信(或假设)你也面临同样的问题。
2你会抓住JVM-Xms和存在OutOfMemoryError异常这几个关键字的例子,然后希望通过这样的案例来快速解决客户端问题。
3最后你会在你环境中使用相同的调优方法。
两天后,问题仍然发生(甚至更糟或者稍微好点)……到底是哪里错了呢?首先,没有摸清问题根源所在?对开发环境没有正确地进行深层面(规格、负载情况等)理解。
网络搜索是一个非常优秀的学习方法和知识分享工具,但是你必须结合自己的实际项目,从根本上进行分析解决。
可能缺乏基本的JVM和JVM内存管理技能,阻止你把所有的点给连接起来。
今天讲的第一条技巧是帮助你理解基本的JVM原则及其与众不同的内存空间。
这些知识都是相当重要的,它可以帮助你做出有效的调优策略、更加正确合理的预测将来会产生的影响、提前知道未来需要做哪些调优工作。
Java线程知识深入解析

Java线程知识深入解析一般来说,我们把正在计算机中执行的程序叫做"进程"(Process) ,而不将其称为程序(Program)。
所谓"线程"(Thread),是"进程"中某个单一顺序的控制流。
新兴的操作系统,如Mac,Windows NT,Windows95等,大多采用多线程的概念,把线程视为基本执行单位。
线程也是Java中的相当重要的组成部分之一。
甚至最简单的Applet也是由多个线程来完成的。
在Java中,任何一个Applet 的paint()和update()方法都是由AWT(Abstract Window Toolkit)绘图与事件处理线程调用的,而Applet 主要的里程碑方法——init(),start(),stop()和destory() ——是由执行该Applet的应用调用的。
单线程的概念没有什么新的地方,真正有趣的是在一个程序中同时使用多个线程来完成不同的任务。
某些地方用轻量进程(Lightweig ht Process)来代替线程,线程与真正进程的相似性在于它们都是单一顺序控制流。
然而线程被认为轻量是由于它运行于整个程序的上下文内,能使用整个程序共有的资源和程序环境。
作为单一顺序控制流,在运行的程序内线程必须拥有一些资源作为必要的开销。
例如,必须有执行堆栈和程序计数器在线程内执行的代码只在它的上下文中起作用,因此某些地方用"执行上下文"来代替"线程"。
线程属性为了正确有效地使用线程,必须理解线程的各个方面并了解Java 实时系统。
必须知道如何提供线程体、线程的生命周期、实时系统如何调度线程、线程组、什么是幽灵线程(Demo nThread)。
(1)线程体所有的操作都发生在线程体中,在Java中线程体是从Thread类继承的run()方法,或实现Runnable接口的类中的run()方法。
Java多线程编程的使用方法及性能优化技巧

Java多线程编程的使用方法及性能优化技巧多线程编程是现代计算机应用开发中一个重要的领域。
在Java编程中,使用多线程可以提高程序的性能和并发处理能力。
本文将介绍Java多线程编程的使用方法以及一些性能优化技巧。
1. Java多线程编程的基本概念和使用方法Java中的多线程编程是通过创建Thread对象或实现Runnable接口来实现的。
以下是一些基本的使用方法:1.1 创建一个Thread对象可以通过继承Thread类或实现Runnable接口来创建一个Thread对象。
继承Thread类的方法需要重写run()方法,在run()方法中编写线程的逻辑代码。
实现Runnable接口的方法需要实现run()方法,并将其作为参数传递给Thread对象。
下面是一个示例代码:```javaclass MyThread extends Thread {public void run() {// 线程的逻辑代码}}class MyRunnable implements Runnable {public void run() {// 线程的逻辑代码}}public class Main {public static void main(String[] args) {// 使用继承Thread类的方式创建线程对象MyThread t1 = new MyThread();t1.start();// 使用实现Runnable接口的方式创建线程对象 MyRunnable runnable = new MyRunnable();Thread t2 = new Thread(runnable);t2.start();}}```1.2 线程的生命周期在Java多线程编程中,线程有以下几个状态:创建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和终止(Terminated)。
可以通过调用Thread类的start()方法来启动线程,使其进入就绪状态。
JVM学习笔记 Java内存模型

JVM学习笔记Java内存模型一Java内存模型1、运行时数据区域1.1 程序计数器程序计数器(Program Counter Register)是一块较小的内存空间,可以看成是当前线程所执行的字节码的行号指示器。
字节码解释器工作时就是通过改变这个计数器的值来选取吓一跳需要执行的字节码指令。
线程私有,每条线程都需要有一个独立的程序计数器,各条线程之间互不影响,独立存储。
如果线程正在执行一个Java Method,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的Native Method,这个计数器的值为空(Undefined)。
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError的区域。
1.2 Java虚拟机栈Java虚拟机栈(Java Virtual Machine Stacks)线程私有,生命周期与线程相同。
虚拟机栈描述的Java方法执行的内存模型: 每个方法在执行的同时,都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表存放了编译期可知的各种基本数据类型(boolean, byte, char, short, int, float, long ,double), 对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)其中64位长度的long,double类型的数据会占用2个局部变量空间(slot),其余的数据类型只占用1 个。
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,这个方法在运行期间不会改变局部变量表的大小。
Java多线程编程线程大总结

要理解线程调度的原理,以及线程执行过程,必须理解线程栈模型。 线程栈是指某时刻时内存中线程调度的栈信息,当前调用的方法总是位于栈顶。线程栈的内容是 随着程序的运行动态变化的,因此研究线程栈必须选择一个运行的时刻(实际上指代码运行到什 么地方)。 下面通过一个示例性的代码说明线程(调用)栈的变化过程。
注意:对 Java 来说,run()方法没有任何特别之处。像 main()方法一样,它只是新线程知道调 用的方法名称(和签名)。因此,在 Runnable 上或者 Thread 上调用 run 方法是合法的。但并 不启动新的线程。
四、例子 1、实现 Runnable 接口的多线程例子 /** * 实现 Runnable 接口的类 * * @author leizhimin 2008-9-13 18:12:10 */ public class DoSomething implements Runnable {
}
public void run() { for(int i = 0;i<5;i++){ for(long k= 0; k <100000000;k++); System.out.println(this.getName()+" :"+i); }
}
public static void main(String[] args) { Thread t1 = new TestThread("阿三"); Thread t2 = new TestThread("李四"); t1.start(); t2.start();
private String name;
影响JavaEE性能的十大问题

影响JavaEE性能的十大问题本文作者是一名有10多年经验的高级系统架构师,他的主要专业领域是Java EE、中间件和JVM技术。
他在性能优化和提升方面也有很深刻的见解,下面他将和大家分享一下常见的10个影响Java EE性能问题。
1.缺乏正确的容量规划容量规划是一个全面的和发展的过程标准,预测当前和未来的IT环境容量需求。
制定合理的容量规划不仅会确保和跟踪当前IT生产能力和稳定性,同时也会确保新项目以最小的风险部署到现有的生产环境中。
硬件、中间件、JVM、调整等在项目部署之前就应该准备好。
2.Java EE中间件环境规范不足“没有规矩,不成方圆”。
第二个比较普遍的原因是Java EE中间件或者基础架构不规范。
在项目初始,新平台上面没有制定合理的规范,导致系统稳定性差。
这会增加客户成本,所以花时间去制定合理的Java EE中间件环境规范是必须的。
这项工作应与初始容量规划迭代相结合。
3.Java虚拟机垃圾回收过度各位对“ng.OutOfMemoryError”这个错误信息是不是很熟悉呢?由于JVM的内存空间过度消耗(Java堆、本机堆等)而抛出的异常。
垃圾收集问题并不一定会表现为一个OOM条件,过度的垃圾收集可以理解成是JVM GC线程在短时间里进行轻微或超量收集集合数据而导致的JVM暂停时间很长和性能下降。
可能有以下几个原因:与JVM的负载量和应用程序内存占用量相比,Java堆可能选择的太小。
JVM GC策略使用不合理。
应用程序静态或动态内存占用量太大,不适合在32位JVM上使用。
JVM OldGen随着时间推移,泄漏越来越严重,而GC在几个小时或者几天后才发现。
JVM PermGen空间(只有HotSpot VM)或本机堆随着时间推移会泄露是一个非常普遍的问题;OOM的错误往往是观察一段时间后,应用程序进行动态调动。
YoungGen和OldGen的比例空间与你的应用程序不匹配。
Java堆在32位的VM上太大,导致本机堆溢出,具体可以表现为OOM试着去链接一个新的Java EE应用程序、创建一个新的Java线程或者需要计算本地内存分配任务。
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
Java 线程/内存模型的缺陷和增强Java在语言层次上实现了对线程的支持。
它提供了Thread/Runnable/ThreadGroup等一系列封装的类和接口,让程序员可以高效的开发Java多线程应用。
为了实现同步,Java提供了synchronize关键字以及object的wait()/notify()机制,可是在简单易用的背后,应藏着更为复杂的玄机,很多问题就是由此而起。
一、Java内存模型在了解Java的同步秘密之前,先来看看JMM(Java Memory Model)。
Java被设计为跨平台的语言,在内存管理上,显然也要有一个统一的模型。
而且Java语言最大的特点就是废除了指针,把程序员从痛苦中解脱出来,不用再考虑内存使用和管理方面的问题。
可惜世事总不尽如人意,虽然JMM设计上方便了程序员,但是它增加了虚拟机的复杂程度,而且还导致某些编程技巧在Java语言中失效。
JMM主要是为了规定了线程和内存之间的一些关系。
对Java程序员来说只需负责用synchronized同步关键字,其它诸如与线程/内存之间进行数据交换/同步等繁琐工作均由虚拟机负责完成。
如图1所示:根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。
每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。
图1 Java内存模型示例图线程若要对某变量进行操作,必须经过一系列步骤:首先从主存复制/刷新数据到工作内存,然后执行代码,进行引用/赋值操作,最后把变量内容写回Main Memory。
Java语言规范(JLS)中对线程和主存互操作定义了6个行为,分别为load,save,read,write,assign和use,这些操作行为具有原子性,且相互依赖,有明确的调用先后顺序。
具体的描述请参见JLS第17章。
我们在前面的章节介绍了synchronized的作用,现在,从JMM的角度来重新审视synchronized关键字。
假设某条线程执行一个synchronized代码段,其间对某变量进行操作,JVM会依次执行如下动作:(1) 获取同步对象monitor (lock)(2) 从主存复制变量到当前工作内存(read and load)(3) 执行代码,改变共享变量值(use and assign)(4) 用工作内存数据刷新主存相关内容(store and write)(5) 释放同步对象锁(unlock)可见,synchronized的另外一个作用是保证主存内容和线程的工作内存中的数据的一致性。
如果没有使用synchronized关键字,JVM不保证第2步和第4步会严格按照上述次序立即执行。
因为根据JLS中的规定,线程的工作内存和主存之间的数据交换是松耦合的,什么时候需要刷新工作内存或者更新主内存内容,可以由具体的虚拟机实现自行决定。
如果多个线程同时执行一段未经synchronized保护的代码段,很有可能某条线程已经改动了变量的值,但是其他线程却无法看到这个改动,依然在旧的变量值上进行运算,最终导致不可预料的运算结果。
二、DCL失效这一节我们要讨论的是一个让Java丢脸的话题:DCL失效。
在开始讨论之前,先介绍一下LazyLoad,这种技巧很常用,就是指一个类包含某个成员变量,在类初始化的时候并不立即为该变量初始化一个实例,而是等到真正要使用到该变量的时候才初始化之。
例如下面的代码:代码1由于LazyLoad可以有效的减少系统资源消耗,提高程序整体的性能,所以被广泛的使用,连Java的缺省类加载器也采用这种方法来加载Java类。
在单线程环境下,一切都相安无事,但如果把上面的代码放到多线程环境下运行,那么就可能会出现问题。
假设有2条线程,同时执行到了if(res == null),那么很有可能res被初始化2次,为了避免这样的Race Condition,得用synchronized关键字把上面的方法同步起来。
代码如下:代码2现在Race Condition解决了,一切都很好。
N天过后,好学的你偶然看了一本Refactoring的魔书,深深为之打动,准备自己尝试这重构一些以前写过的程序,于是找到了上面这段代码。
你已经不再是以前的Java菜鸟,深知synchronized过的方法在速度上要比未同步的方法慢上100倍,同时你也发现,只有第一次调用该方法的时候才需要同步,而一旦res初始化完成,同步完全没必要。
所以你很快就把代码重构成了下面的样子:代码3这种看起来很完美的优化技巧就是Double-Checked Locking。
但是很遗憾,根据Java的语言规范,上面的代码是不可靠的。
造成DCL失效的原因之一是编译器的优化会调整代码的次序。
只要是在单个线程情况下执行结果是正确的,就可以认为编译器这样的“自作主张的调整代码次序”的行为是合法的。
JLS在某些方面的规定比较自由,就是为了让JVM有更多余地进行代码优化以提高执行效率。
而现在的CPU大多使用超流水线技术来加快代码执行速度,针对这样的CPU,编译器采取的代码优化的方法之一就是在调整某些代码的次序,尽可能保证在程序执行的时候不要让CPU的指令流水线断流,从而提高程序的执行速度。
正是这样的代码调整会导致DCL的失效。
为了进一步证明这个问题,引用一下《DCL Broken Declaration》文章中的例子:设一行Java代码:Objects[i].reference = new Object();经过Symantec JIT编译器编译过以后,最终会变成如下汇编码在机器中执行:0206106A mov eax,0F97E78h0206106F call01F6B210;为Object申请内存空间; 返回值放在eax中02061074 mov dword ptr [ebp],eax ; EBP 中是objects[i].reference的地址; 将返回的空间地址放入其中; 此时Object尚未初始化02061077 mov ecx,dword ptr [eax] ; dereference eax所指向的内容; 获得新创建对象的起始地址02061079 mov dword ptr [ecx],100h ; 下面4行是内联的构造函数0206107F mov dword ptr [ecx+4],200h02061086 mov dword ptr [ecx+8],400h0206108D mov dword ptr [ecx+0Ch],0F84030h可见,Object构造函数尚未调用,但是已经能够通过objects[i].reference获得Object对象实例的引用。
如果把代码放到多线程环境下运行,某线程在执行到该行代码的时候JVM或者操作系统进行了一次线程切换,其他线程显然会发现msg对象已经不为空,导致Lazy load的判断语句if(objects[i].reference == null)不成立。
线程认为对象已经建立成功,随之可能会使用对象的成员变量或者调用该对象实例的方法,最终导致不可预测的错误。
原因之二是在共享内存的SMP机上,每个CPU有自己的Cache和寄存器,共享同一个系统内存。
所以CPU可能会动态调整指令的执行次序,以更好的进行并行运算并且把运算结果与主内存同步。
这样的代码次序调整也可能导致DCL失效。
回想一下前面对Java内存模型的介绍,我们这里可以把Main Memory 看作系统的物理内存,把Thread Working Memory认为是CPU内部的Cache和寄存器,没有synchronized 的保护,Cache和寄存器的内容就不会及时和主内存的内容同步,从而导致一条线程无法看到另一条线程对一些变量的改动。
结合代码3来举例说明,假设Resource类的实现如下:Class Resource{ Object obj;}即Resource类有一个obj成员变量引用了Object的一个实例。
假设2条线程在运行,其状态用如下简化图表示:图2现在Thread-1构造了Resource实例,初始化过程中改动了obj的一些内容。
退出同步代码段后,因为采取了同步机制,Thread-1所做的改动都会反映到主存中。
接下来Thread-2获得了新的Resource实例变量res,由于没有使用synchronized保护所以Thread-2不会进行刷新工作内存的操作。
假如之前Thread-2的工作内存中已经有了obj实例的一份拷贝,那么Thread-2在对obj执行use操作的时候就不会去执行load操作,这样一来就无法看到Thread-1对obj的改变,这显然会导致错误的运算结果。
此外,Thread-1在退出同步代码段的时刻对ref和obj执行的写入主存的操作次序也是不确定的,所以即使Thread-2对obj执行了load操作,也有可能只读到obj的初试状态的数据。
(注:这里的load/use均指JMM 定义的操作)有很多人不死心,试图想出了很多精妙的办法来解决这个问题,但最终都失败了。
事实上,无论是目前的JMM还是已经作为JSR提交的JMM模型的增强,DCL都不能正常使用。
在William Pugh的论文《Fixing the Java Memory Model》中详细的探讨了JMM的一些硬伤,更尝试给出一个新的内存模型,有兴趣深入研究的读者可以参见文后的参考资料。
如果你设计的对象在程序中只有一个实例,即singleton的,有一种可行的解决办法来实现其LazyLoad:就是利用类加载器的LazyLoad特性。
代码如下:这里ResSingleton只有一个静态成员变量。
当第一次使用ResSingleton.res的时候,JVM才会初始化一个Resource实例,并且JVM会保证初始化的结果及时写入主存,能让其他线程看到,这样就成功的实现了LazyLoad。
除了这个办法以外,还可以使用ThreadLocal来实现DCL的方法,但是由于ThreadLocal的实现效率比较低,所以这种解决办法会有较大的性能损失,有兴趣的读者可以参考文后的参考资料。
最后要说明的是,对于DCL是否有效,个人认为更多的是一种带有学究气的推断和讨论。
而从纯理论的角度来看,存取任何可能共享的变量(对象引用)都需要同步保护,否则都有可能出错,但是处处用synchronized又会增加死锁的发生几率,苦命的程序员怎么来解决这个矛盾呢?事实上,在很多Java开源项目(比如Ofbiz/Jive等)的代码中都能找到使用DCL的证据,我在具体的实践中也没有碰到过因DCL 而发生的程序异常。