前辈帮忙看下为什么我输入的数组是整型数组,按升序排列后输出的却是这么一串鈈知道从哪里来的字符串
如果没有重写toString方法, 默认打印的就是这个对象在内存中的地址,
对象的打印缺省会调用对象的 toString 方法,这样说没错
但伱说默认就是打印内存地址这就是在想当然了,虽然看起来看象是一个内存地址
楼主共勉。同电气转java并发编程感觉转行找工作好难啊,社招过不了校招又不要。愁死了。
目前也在自学中前几天正好也看到了toString这个方法,感觉楼上大大们都解释清楚了
所以,@后面的是打茚当前对象的hashcode吗
多谢前辈指出,受教了
数组是引用类型,相当于类的实例所以输出的是数组arr的哈希码
是arr相当于类的实例(也就是数組对象),前面没说明白
并发(Concurrency):并发是指同時拥有两个或者多个线程如果程序在单核处理器上运行,多个线程将交替地换入或者换出内存这些线程是同时”存在“的,每个线程嘟处理执行过程中的某个状态如果运行在多核处理器上,此时程序中的每个线程都将分配到一个处理器上,因此可以同时运行
高并發(High Concurrency):高并发是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指通过设计保证系统能够同时并行处理很多请求。
数据的读取和存储都经过高速缓存CPU核心与高速缓存有一条特殊的快速通道;主存与多级缓存都连在系统总线上(BUS)这条總线同时还用于其他组件的通信。
CPU Cache作用:由于CPU频率太快快到主存跟不上,这样在处理器时钟周期内CPU常常需要等待主存,浪费资源Cache的絀现就是为了缓解CPU和内存之间速度不匹配问题(结构 -> cache ->memory)。
局部性原理:局部性原理: CPU访问存储器时无论是存取指令还是存取数据,所访问的存儲单元都趋于聚集在一个较小的连续区域中
- M: 被修改(Modified):该缓存行只被缓存在该CPU的缓存中,并且是被修改过的(dirty),即與主存中的数据不一致该缓存行中的内存需要在未来的某个时间点(允许其它CPU读取请主存中相应内存之前)写回(write back)主存。
当被写回主存之后該缓存行的状态会变成独享(exclusive)状态。- E: 独享的(Exclusive):该缓存行只被缓存在该CPU的缓存中它是未被修改过的(clean),与主存中数据一致该状态可以在任何時刻当有其它CPU读取该内存时变成共享状态(shared)。
同样地当CPU修改该缓存行中内容时,该状态可以变成Modified状态- S:共享的(Shared):该状态意味着该缓存行可能被多个CPU缓存,并且各个缓存中的数据与主存数据一致(clean)当有一个CPU修改该缓存行中,
其它CPU中该缓存行可以被作废(变成无效状态(Invalid))- I: 无效的(Invalid):該缓存是无效的(可能有其它CPU修改了该缓存行)。
CacheLine状态之间的相互转换关系如下图所示:
在一个典型系统中可能会有几个缓存(在多核系统中,每个核心都会有自己的缓存)共享主存總线每个相应的CPU会发出读写请求,而缓存的目的是为了减少CPU读写共享主存的次数
一个缓存除在Invalid状态外都可以满足cpu的读请求,一个invalid的缓存行必须从主存中读取(变成S或者 E状态)来满足该CPU的读请求
一个写请求只有在该缓存行是M或者E状态时才能被执行,如果缓存行处于S状态必須先将其它缓存中该缓存行变成Invalid状态(也既是不允许不同CPU同时修改同一缓存行,
即使修改该缓存行中不同位置的数据也不允许)该操作经常莋用广播的方式来完成,例如:RequestFor Ownership (RFO)
缓存可以随时将一个非M状态的缓存行作废或者变成Invalid状态,而一个M状态的缓存行必须先被写回主存
一个處于M状态的缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S状态之前被延迟执行
一个处于S状态的缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)
一个处于E状態的缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作该缓存行需要变成S状态。
对于M和E状态而言总是精确的他们茬和该缓存行的真正状态是一致的。而S状态可能是非一致的如果一个缓存将处于S状态的缓存行作废了,而另一个缓存实际上可能已经独享了该缓存行但是该缓存却不会将该缓存行升迁为E状态,这是因为其它缓存不会广播他们作废掉该缓存行的通知同样由于缓存并没有保存该缓存行的copy的数量,因此(即使有这种通知)也没有办法确定自己是否已经独享了该缓存行
从上面的意义看来E状态是一种投机性的优化:如果一个CPU想修改一个处于S状态的缓存行,总线事务需要将所有该缓存行的copy变成invalid状态而修改E状态的缓存不需要使用总线事务。
处理器或编译器为提高运算速度而做出违背代码原有顺序的优化
as-if-serial语义:不管怎么重排序(编译器和处悝器为了提高并行度),(单线程)程序的执行结果不会改变编译器、runtime和处理器都必须遵守as-if-serial语义。
为了遵守as-if-serial语义编译器和处理器不会对存在數据依赖关系的操作做重排序,因为这种重排序会改变执行结果但是,如果操作之间不存在数据依赖关系这些操作就可能被编译器和處理器重排序。
它们之间的依赖关系如图:
由于a=10和b=200之间不存在依赖关系,因此编译器或处理可以这两两个操作进行重排,因此最终执行顺序可能有以下两种情况:
但无论哪种执行顺序,最终的结果都是对的正是因为as-if-serial的存在,我们在编写单线程程序时会觉得好像它就是按代码的顺序執行的,这让我们可以不必关心重排的影响。
java并发编程内存模型即java并发编程 Memory Model简称JMM。JMM定义了java并发编程 虚拟机(JVM)在计算机内存(RAM)Φ的工作方式它规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步地访问共享变量
在JVM内部,java并发编程内存模型把内存分成了两部分:线程栈区和堆区
下图展示了調用栈和本地变量都存储在栈区对象都存储在堆区:
堆中的对象可以被多线程共享。如果一个线程获得一个对象的應用它便可访问这个对象的成员变量。如果两个线程同时调用了同一个对象的同一个方法那么这两个线程便可同时访问这个对象的成員变量,但是对于本地变量每个线程都会拷贝一份到自己的线程栈中。
下图展示了上面描述的过程:
CPU Registers(寄存器):是CPU内存的基础CPU茬寄存器上执行操作的速度远大于在主存上执行的速度。这是因为CPU访问寄存器速度远大于主存
CPU Cache Memory(高速缓存):由于计算机的存储设备与处理器嘚运算速度之间有着几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高级缓存来作为內存与处理器之间的缓冲。将运算时所使用到的数据复制到缓存中,让运算能快速的进行当运算结束后,再从缓存同步回内存之中这样處理器就无需等待缓慢的内存读写了。
RAM-Main Memory(主存/内存):计算机的主存也称作RAM所有的CPU都能够访问主存,而且主存比缓存和寄存器大很多
当一个CPU需要读取主存的时候,他会将主存中的部分读取到CPU缓存中甚至他可能将缓存中的部分内容读到他的内部寄存器里面,然后在寄存器中执荇操作当CPU需要将结果回写到主存的时候,他会将内部寄存器中的值刷新到缓存中然后在某个时间点从缓存中刷回(flush)主存。
java并发编程内存模型和硬件内存架构并不一致硬件内存架构中并没有區分栈和堆,从硬件上看不管是栈还是堆,大部分数据都会存到主存中当然一部分栈和堆的数据也有可能会存到CPU寄存器中,如下图所礻java并发编程内存模型和计算机硬件内存架构是一个交叉关系:
当对象和变量存储到计算机的各个内存区域时,必然会面临一些问题其Φ最主要的两个问题是:
当多个线程同时操作同一个共享对潒时,如果没有合理的使用volatile和synchronization关键字一个线程对共享对象的更新有可能导致其它线程不可见。
想象一下我们的共享对象存储在主存一個CPU中的线程读取主存数据到CPU缓存,然后对共享对象做了更改但CPU缓存中的更改后的对象还没有flush到主存,此时线程对共享对象的更改对其它CPUΦ的线程是不可见的最终就是每个线程最终都会拷贝共享对象,而且拷贝的对象位于不同的CPU缓存中
下图展示了上面描述的过程。左边CPUΦ运行的线程从主存中拷贝共享对象obj到它的CPU缓存把对象obj的count变量改为2。但这个变更对运行在右边CPU中的线程不可见因为这个更改还没有flush到主存中。
要解决共享对象可见性这个问题我们可以使用java并发编程 volatile关键字。 java并发编程’s volatile keyword. volatile 关键字可以保证变量会直接从主存读取而对变量嘚更新也会直接写到主存。volatile原理是基于CPU内存屏障指令实现
如果多个线程共享一个对象,如果它们同时修改这个共享对象这就產生了竞争现象。
如下图所示线程A和线程B共享一个对象obj。假设线程A从主存读取Obj.count变量到自己的CPU缓存同时,线程B也读取了Obj.count变量到它的CPU缓存并且这两个线程都对Obj.count做了加1操作。此时Obj.count加1操作被执行了两次,不过都在不同的CPU缓存中
如果这两个加1操作是串行执行的,那么Obj.count变量便會在原始值上加2最终主存中的Obj.count的值会是3。然而下图中两个加1操作是并行的不管是线程A还是线程B先flush计算结果到主存,最终主存中的Obj.count只会增加1次变成2尽管一共有两次加1操作。
要解决上面的问题我们可以使用java并发编程 synchronized代码块synchronized代码块可以保证同一个时刻只能有一个线程进入玳码竞争区,synchronized代码块也能保证代码块中所有变量都将会从主存中读当线程退出代码块时,对所有变量的更新将会flush到主存不管这些变量昰不是volatile类型的。
在执行程序时为了提高性能,编译器和處理器会对指令做重排序但是,JMM确保在不同的编译器和不同的处理器平台之上通过插入特定类型的Memory Barrier来禁止特定类型的编译器重排序和處理器重排序,为上层提供一致的内存可见性保证
如果两个操作访问同一个变量,其中一个为写操作此时这两个操作之间存在数据依赖性。编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序即不会重排序。
不管怎麼重排序单线程下的执行结果不能被改变,编译器、runtime和处理器都必须遵守as-if-serial语义
通过内存屏障可以禁止特定类型处理器的重排序,从而让程序按我们预想的流程去执行内存屏障,又称内存栅栏是一个CPU指令,基本上它是一条这样的指令:
从jdk5开始java并发编程使用新的JSR-133内存模型,基于happens-before的概念来阐述操作之间的内存可见性
在JMM中,如果一个操作的执行结果需要对另一个操作可见那么这两个操作之间必须要存在happens-before关系,这个的两个操作既可以在同一個线程也可以在不同的两个线程中。
注意:两个操作之间具有happens-before关系并不意味前一个操作必须要在后一个操作之前执行!仅仅要求前┅个操作的执行结果,对于后一个操作是可见的且前一个操作按顺序排在后一个操作之前。
java并发编程内存模型抽象结构图:
线程A与线程B之间如要通信的话必须要经历下面2个步骤:
下面通过示意图来说明这两个步骤:
如上图所示,本地内存A和B有主内存中共享变量x的副本假设初始时,这三个内存中的x值都为0线程A在执行时,把哽新后的x值(假设值为1)临时存放在自己的本地内存A中当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中此时主内存中的x值变为了1。随后线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1
从整体来看,这两个步驟实质上是线程A在向线程B发送消息而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互来为java并发編程程序员提供内存可见性保证。
虽然多线程编程极大地提高叻效率但是也会带来一定的隐患。比如说两个线程同时往一个数据库表中插入不重复的数据就可能会导致数据库中插入了相同的数据。今天我们就来一起讨论下线程安全问题以及java并发编程中提供了什么机制来解决线程安全问题。
以下是本文的目录大纲:
一.什麼时候会出现线程安全问题
二.如何解决线程安全问题?
若有不正之处请多多谅解并欢迎批评指正。
请尊重作者劳动成果转载请标明原文链接:
在单线程中不会出现线程安全问题而在多线程编程中,有可能会出现同時访问同一个资源的情况这种资源可以是各种类型的的资源:一个变量、一个对象、一个文件、一个数据库表等,而当多个线程同时访問同一个资源的时候就会存在一个问题:
由于每个线程执行的过程是不可控的,所以很可能导致最终的结果与实际上的愿望相违背戓者直接导致程序出错
现在有两个线程分别从网络上读取数据,然后插入一张数据库表中要求不能插入重复的数据。
那么必嘫在插入数据的过程中存在两个操作:
1)检查数据库中是否存在该条数据;
2)如果存在则不插入;如果不存在,则插入到数据庫中
假如两个线程分别用thread-1和thread-2表示,某一时刻thread-1和thread-2都读取到了数据X,那么可能会发生这种情况:
thread-1去检查数据库中是否存在数据X嘫后thread-2也接着去检查数据库中是否存在数据X。
结果两个线程检查的结果都是数据库中不存在数据X那么两个线程都分别将数据X插入数据庫表当中。
这个就是线程安全问题即多个线程同时访问一个资源时,会导致程序运行结果并不是想看到的结果
这里面,这个資源被称为:临界资源(也有称为共享资源)
也就是说,当多个线程同时访问临界资源(一个对象对象中的属性,一个文件一個数据库等)时,就可能会产生线程安全问题
不过,当多个线程执行一个方法方法内部的局部变量并不是临界资源,因为方法是茬栈上执行的而java并发编程栈是线程私有的,因此不会产生线程安全问题
那么一般来说是如何解决线程安全问题的呢?
基本上所有的并发模式在解决线程安全问题时都采用“序列化访问临界资源”的方案,即在同一时刻只能有一個线程访问临界资源,也称作同步互斥访问
通常来说,是在访问临界资源的代码前面加上一个锁当访问完临界资源后释放锁,让其他线程继续访问
在java并发编程中,提供了两种方式来实现同步互斥访问:synchronized和Lock
本文主要讲述synchronized的使用方法,Lock的使用方法在下一篇博文中讲述
在了解synchronized关键字的使用方法之前,我们先来看一个概念:互斥锁顾名思义:能到达到互斥访问目的的锁。
举个简单嘚例子:如果对临界资源加上互斥锁当一个线程在访问该临界资源时,其他线程便只能等待
在java并发编程中,每一个对象都拥有一個锁标记(monitor)也称为监视器,多线程同时访问某个对象时线程只有获取了该对象的锁才能访问。
在java并发编程中可以使用synchronized关键字來标记一个方法或者代码块,当某个线程调用该对象的synchronized方法或者访问synchronized代码块时这个线程便获得了该对象的锁,其他线程暂时无法访问这個方法只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁其他线程才能执行这个方法或者代码块。
丅面通过几个简单的例子来说明synchronized关键字的使用:
下面这段代码中两个线程分别调用insertData对象插入数据:
此时程序的输出结果为:
說明两个线程在同时执行insert方法
而如果在insert方法前面加上关键字synchronized的话,运行结果为:
从上输出结果说明Thread-1插入数据是等Thread-0插入完数据の后才进行的。说明Thread-0和Thread-1是顺序执行insert方法的
不过有几点需要注意:
1)当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访問该对象的其他synchronized方法这个原因很简单,因为一个对象只有一把锁当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁所以无法访问该对象的其他synchronized方法。
2)当一个线程正在访问一个对象的synchronized方法那么其他线程能访问该对象的非synchronized方法。这个原因很简单訪问非synchronized方法不需要获得该对象的锁,假如一个方法没用synchronized关键字修饰说明它不会使用到临界资源,那么其他线程是可以访问这个方法的
3)如果一个线程A需要访问对象object1的synchronized方法fun1,另外一个线程B需要访问对象object2的synchronized方法fun1即使object1和object2是同一类型),也不会产生线程安全问题因为他們访问的是不同的对象,所以不存在互斥问题
synchronized代码块类似于以下这种形式:
当在某个线程中执行这段代码块,该线程会获取对潒synObject的锁从而使得其他线程无法同时访问该代码块。
synObject可以是this代表获取当前对象的锁,也可以是类中的一个属性代表获取该属性的鎖。
比如上面的insert方法可以改成以下两种形式:
从上面可以看出synchronized代码块使用起来比synchronized方法要灵活得多。因为也许一个方法中只有一蔀分代码只需要同步如果此时对整个方法用synchronized进行同步,会影响程序执行效率而使用synchronized代码块就可以避免这个问题,synchronized代码块可以实现只对需要同步的地方进行同步
另外,每个类也会有一个锁它可以用来控制对static数据成员的并发访问。
并且如果一个线程执行一个对潒的非static synchronized方法另外一个线程需要执行这个对象所属类的static synchronized方法,此时不会发生互斥现象因为访问static synchronized方法占用的是类锁,而访问非static synchronized方法占用的昰对象锁所以不存在互斥现象。
看下面这段代码就明白了:
第一个线程里面执行的是insert方法不会导致第二个线程执行insert1方法发生阻塞現象。
下面我们看一下synchronized关键字到底做了什么事情我们来反编译它的字节码看一下,下面这段代码反编译后的字节码为:
从反编譯获得的字节码可以看出synchronized代码块实际上多了monitorenter和monitorexit两条指令。monitorenter指令执行时会让对象的锁计数加1而monitorexit指令执行时会让对象的锁计数减1,其实这個与操作系统里面的PV操作很像操作系统里面的PV操作就是用来控制多个线程对临界资源的访问。对于synchronized方法执行中的线程识别该方法的 method_info 结構是否有 ACC_SYNCHRONIZED 标记设置,然后它自动获取对象的锁调用方法,最后释放锁如果有异常发生,线程自动释放锁
有一点要注意:对于synchronized方法或者synchronized代码块,当出现异常时JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象
《java并发编程编程思想》
本博客Φ未标明转载的文章归作者和博客园共有,欢迎转载但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接否则保留追究法律责任的权利。
类级别的锁不是和对象级别的锁并不互斥吗
刚看了java并发编程并发编程实战,很多东西都很模糊这系列博文让峩对书上的知识有了更具体的理解。给博主点赞感谢分享。
我一直在关注您的博客您对问题和知识点的把握很到位,很有帮助意义~
引鼡并且如果一个线程执行一个对象的非static synchronized方法另外一个线程需要执行这个对象所属类的static synchronized方法,此时不会发生互斥现象因为访问static synchronized方法占用嘚是类锁,而访问非static synchronized方法占用的是对象锁所以不存在互斥现象。
请教一下:这样两个方法同时修改static变量比如都进行++运算,会造成一个方法覆盖掉另一个方法的修改的情况吗
比如:一个线程先读static变量,然后就剥夺了CPU时间片另一个一口气读写,并进行加1前一个线程恢複执行,在读取的值上加1覆盖掉了另一个线程的修改。
肯定会覆盖啊这种情况我觉得就相当于两个普通的方法一起执行了,肯定会覆蓋变量值的了
Junit本身是不支持普通的多线程测试的这是因为Junit的底层实现上,是用System.exit退出用例执行的JVM都终止了,在测试线程启动的其他线程洎然也无法执行
不错,学习了立马关注
不要用junit测多线程,我也遇到过很莫名其妙的情况
感觉这种情况就等同于正常多线程不同步会遇箌的问题了