diff --git "a/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213Final\345\255\227\346\256\265\345\246\202\344\275\225\346\224\271\345\217\230\345\256\203\344\273\254\347\232\204\345\200\274.md" "b/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213Final\345\255\227\346\256\265\345\246\202\344\275\225\346\224\271\345\217\230\345\256\203\344\273\254\347\232\204\345\200\274.md" new file mode 100644 index 0000000..fceef7c --- /dev/null +++ "b/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213Final\345\255\227\346\256\265\345\246\202\344\275\225\346\224\271\345\217\230\345\256\203\344\273\254\347\232\204\345\200\274.md" @@ -0,0 +1 @@ +# `Java`内存模型`FAQ`(`八`)`Final`字段如何改变它们的值 diff --git "a/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213JSR133\346\230\257\344\273\200\344\271\210.md" "b/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213JSR133\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 0000000..5bf0361 --- /dev/null +++ "b/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213JSR133\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,17 @@ +# `Java`内存模型`FAQ`(`三`)`JSR133`是什么? +[原文](http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html )第三章 +从`1997`年以来,人们不断发现`Java`语言规范的`17`章定义的`Java`内存模型中的一些严重的缺陷。这些缺陷会导致一些使人迷惑的行为(例如`final`字段会被观察到值的改变)和破坏编译器常见的优化能力。 + +`Java`内存模型是一个雄心勃勃的计划,它是编程语言规范第一次尝试合并一个能够在各种处理器架构中为并发提供一致语义的内存模型。不过,定义一个既一致又直观的内存模型远比想象要更难。`JSR133`为`Java`语言定义了一个新的内存模型,它修复了早期内存模型中的缺陷。为了实现`JSR133`,`final`和`volatile`的语义需要重新定义。 + +[完整的语义见](http://www.cs.umd.edu/users/pugh/java/memoryModel),但是正式的语义不是小心翼翼的,它是令人惊讶和清醒的,目的是让人意识到一些看似简单的概念(如同步)其实有多复杂。幸运的是,你不需要懂得这些正式语义的细节——`JSR133`的目的是创建一组正式语义,这些正式语义提供了`volatile`、`synchronzied`和`final如`何工作的直观框架。 + +`JSR 133`的目标包含了: + +保留已经存在的安全保证(像类型安全)以及强化其他的安全保证。例如,变量值不能凭空创建:线程观察到的每个变量的值必须是被其他线程合理的设置的。 +正确同步的程序的语义应该尽量简单和直观。 +应该定义未完成或者未正确同步的程序的语义,主要是为了把潜在的安全危害降到最低。 +程序员应该能够自信的推断多线程程序如何同内存进行交互的。 +能够在现在许多流行的硬件架构中设计正确以及高性能的`JVM`实现。 +应该能提供 安全地初始化的保证。如果一个对象正确的构建了(意思是它的引用没有在构建的时候逸出,那么所有能够看到这个对象的引用的线程,在不进行同步的情况下,也将能看到在构造方法中中设置的`final`字段的值。 +应该尽量不影响现有的代码。 diff --git "a/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213volatile\346\230\257\345\271\262\344\273\200\344\271\210\347\224\250\347\232\204.md" "b/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213volatile\346\230\257\345\271\262\344\273\200\344\271\210\347\224\250\347\232\204.md" new file mode 100644 index 0000000..695cc83 --- /dev/null +++ "b/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213volatile\346\230\257\345\271\262\344\273\200\344\271\210\347\224\250\347\232\204.md" @@ -0,0 +1,29 @@ +# `Java`内存模型`FAQ`(`十`)`volatile`是干什么用的 +`Volatile`字段是用于线程间通讯的特殊字段。每次读`volatile`字段都会看到其它线程写入该字段的最新值;实际上,程序员之所以要定义`volatile`字段是因为在某些情况下由于缓存和重排序所看到的陈旧的变量值是不可接受的。编译器和运行时禁止在寄存器里面分配它们。它们还必须保证,在它们写好之后,它们被从缓冲区刷新到主存中,因此,它们立即能够对其他线程可见。相同地,在读取一个`volatile`字段之前,缓冲区必须失效,因为值是存在于主存中而不是本地处理器缓冲区。在重排序访问`volatile`变量的时候还有其他的限制。 + +在旧的内存模型下,访问`volatile`变量不能被重排序,但是,它们可能和访问非`volatile`变量一起被重排序。这破坏了`volatile`字段从一个线程到另外一个线程作为一个信号条件的手段。 + +在新的内存模型下,`volatile`变量仍然不能彼此重排序。和旧模型不同的时候,`volatile`周围的普通字段的也不再能够随便的重排序了。写入一个`volatile`字段和释放监视器有相同的内存影响,而且读取`volatile`字段和获取监视器也有相同的内存影响。事实上,因为新的内存模型在重排序`volatile`字段访问上面和其他字段(`volatile`或者非`volatile`)访问上面有了更严格的约束。当线程A写入一个`volatile`字段`f`的时候,如果线程`B`读取`f`的话 ,那么对线程`A`可见的任何东西都变得对线程`B`可见了。 + +如下例子展示了`volatile`字段应该如何使用: +``` +class VolatileExample { + int x = 0; + volatile boolean v = false; + public void writer() { + x = 42; + v = true; + } + + public void reader() { + if (v == true) { + //uses x - guaranteed to see 42. + } + } +} +``` +假设一个线程叫做“`writer`”,另外一个线程叫做“`reader`”。对变量`v`的写操作会等到变量`x`写入到内存之后,然后读线程就可以看见`v`的值。因此,如果`reader`线程看到了`v`的值为`true`,那么,它也保证能够看到在之前发生的写入`42`这个操作。而这在旧的内存模型中却未必是这样的。如果`v`不是`volatile`变量,那么,编译器可以在`writer`线程中重排序写入操作,那么`reader`线程中的读取`x`变量的操作可能会看到`0`。 + +实际上,`volatile`的语义已经被加强了,已经快达到同步的级别了。为了可见性的原因,每次读取和写入一个`volatile`字段已经像一个半同步操作了 + +重点注意:对两个线程来说,为了正确的设置`happens-before`关系,访问相同的`volatile`变量是很重要的。以下的结论是不正确的:当线程`A`写`volatile`字段`f`的时候,线程`A`可见的所有东西,在线程`B`读取`volatile`的字段`g`之后,变得对线程`B`可见了。释放操作和获取操作必须匹配(也就是在同一个`volatile`字段上面完成)。 diff --git "a/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213\344\273\200\344\271\210\346\230\257\345\206\205\345\255\230\346\250\241\345\236\213.md" "b/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213\344\273\200\344\271\210\346\230\257\345\206\205\345\255\230\346\250\241\345\236\213.md" new file mode 100644 index 0000000..5f83328 --- /dev/null +++ "b/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213\344\273\200\344\271\210\346\230\257\345\206\205\345\255\230\346\250\241\345\236\213.md" @@ -0,0 +1,31 @@ +# `Java`内存模型`FAQ`(`一`)什么是内存模型 +在多核系统中,处理器一般有一层或者多层的缓存,这些的缓存通过加速数据访问(因为数据距离处理器更近)和降低共享内存在总线上的通讯(因为本地缓存能够满足许多内存操作)来提高`CPU`性能。缓存能够大大提升性能,但是它们也带来了许多挑战。例如,当两个`CPU`同时检查相同的内存地址时会发生什么?在什么样的条件下它们会看到相同的值? + +在处理器层面上,内存模型定义了一个充要条件,“让当前的处理器可以看到其他处理器写入到内存的数据”以及“其他处理器可以看到当前处理器写入到内存的数据”。有些处理器有很强的内存模型(`strong memory model`),能够让所有的处理器在任何时候任何指定的内存地址上都可以看到完全相同的值。而另外一些处理器则有较弱的内存模型(`weaker memory model`),在这种处理器中,必须使用内存屏障(一种特殊的指令)来刷新本地处理器缓存并使本地处理器缓存无效,目的是为了让当前处理器能够看到其他处理器的写操作或者让其他处理器能看到当前处理器的写操作。这些内存屏障通常在`lock`和`unlock`操作的时候完成。内存屏障在高级语言中对程序员是不可见的。 + +在强内存模型下,有时候编写程序可能会更容易,因为减少了对内存屏障的依赖。但是即使在一些最强的内存模型下,内存屏障仍然是必须的。设置内存屏障往往与我们的直觉并不一致。近来处理器设计的趋势更倾向于弱的内存模型,因为弱内存模型削弱了缓存一致性,所以在多处理器平台和更大容量的内存下可以实现更好的可伸缩性 + +“一个线程的写操作对其他线程可见”这个问题是因为编译器对代码进行重排序导致的。例如,只要代码移动不会改变程序的语义,当编译器认为程序中移动一个写操作到后面会更有效的时候,编译器就会对代码进行移动。如果编译器推迟执行一个操作,其他线程可能在这个操作执行完之前都不会看到该操作的结果,这反映了缓存的影响。 + +此外,写入内存的操作能够被移动到程序里更前的时候。在这种情况下,其他的线程在程序中可能看到一个比它实际发生更早的写操作。所有的这些灵活性的设计是为了通过给编译器,运行时或硬件灵活性使其能在最佳顺序的情况下来执行操作。在内存模型的限定之内,我们能够获取到更高的性能。 + +看下面代码展示的一个简单例子: + +``` +ClassReordering { + int x = 0, y = 0; + public void writer() { + x = 1; + y = 2; + } + public void reader() { + int r1 = y; + int r2 = x; + } +} +``` +让我们看在两个并发线程中执行这段代码,读取Y变量将会得到2这个值。因为这个写入比写到X变量更晚一些,程序员可能认为读取X变量将肯定会得到`1`。但是,写入操作可能被重排序过。如果重排序发生了,那么,就能发生对Y变量的写入操作,读取两个变量的操作紧随其后,而且写入到X这个操作能发生。程序的结果可能是`r1`变量的值是`2`,但是`r2`变量的值为`0`。 + +`Java`内存模型描述了在多线程代码中哪些行为是合法的,以及线程如何通过内存进行交互。它描述了“程序中的变量“ 和 ”从内存或者寄存器获取或存储它们的底层细节”之间的关系。`Java`内存模型通过使用各种各样的硬件和编译器的优化来正确实现以上事情。 + +`Java`包含了几个语言级别的关键字,包括:`volatile`, `final`以及`synchronized`,目的是为了帮助程序员向编译器描述一个程序的并发需求。`Java`内存模型定义了`volatile`和`synchronized`的行为,更重要的是保证了同步的`java`程序在所有的处理器架构下面都能正确的运行。 diff --git "a/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213\345\205\266\344\273\226\350\257\255\350\250\200\357\274\214\345\203\217C++\357\274\214\344\271\237\346\234\211\345\206\205\345\255\230\346\250\241\345\236\213\345\220\227.md" "b/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213\345\205\266\344\273\226\350\257\255\350\250\200\357\274\214\345\203\217C++\357\274\214\344\271\237\346\234\211\345\206\205\345\255\230\346\250\241\345\236\213\345\220\227.md" new file mode 100644 index 0000000..4cfc581 --- /dev/null +++ "b/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213\345\205\266\344\273\226\350\257\255\350\250\200\357\274\214\345\203\217C++\357\274\214\344\271\237\346\234\211\345\206\205\345\255\230\346\250\241\345\236\213\345\220\227.md" @@ -0,0 +1,2 @@ +# `Java`内存模型`FAQ`(`二`) 其他语言,像`C++`,也有内存模型吗? +大部分其他的语言,像C和C++,都没有被设计成直接支持多线程。这些语言对于发生在编译器和处理器平台架构的重排序行为的保护机制会严重的依赖于程序中所使用的线程库(例如pthreads),编译器,以及代码所运行的平台所提供的保障。 diff --git "a/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213\345\220\214\346\255\245\344\274\232\345\271\262\344\272\233\344\273\200\344\271\210\345\221\242.md" "b/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213\345\220\214\346\255\245\344\274\232\345\271\262\344\272\233\344\273\200\344\271\210\345\221\242.md" new file mode 100644 index 0000000..5cfca75 --- /dev/null +++ "b/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213\345\220\214\346\255\245\344\274\232\345\271\262\344\272\233\344\273\200\344\271\210\345\221\242.md" @@ -0,0 +1,22 @@ +# `Java`内存模型`FAQ`(`七`)同步会干些什么呢 +同步有几个方面的作用。最广为人知的就是互斥 ——一次只有一个线程能够获得一个监视器,因此,在一个监视器上面同步意味着一旦一个线程进入到监视器保护的同步块中,其他的线程都不能进入到同一个监视器保护的块中间,除非第一个线程退出了同步块。 + +但是同步的含义比互斥更广。同步保证了一个线程在同步块之前或者在同步块中的一个内存写入操作以可预知的方式对其他有相同监视器的线程可见。当我们退出了同步块,我们就释放了这个监视器,这个监视器有刷新缓冲区到主内存的效果,因此该线程的写入操作能够为其他线程所见。在我们进入一个同步块之前,我们需要获取监视器,监视器有使本地处理器缓存失效的功能,因此变量会从主存重新加载,于是其它线程对共享变量的修改对当前线程来说就变得可见了。 + +依据缓存来讨论同步,可能听起来这些观点仅仅会影响到多处理器的系统。但是,重排序效果能够在单一处理器上面很容易见到。对编译器来说,在获取之前或者释放之后移动你的代码是不可能的。当我们谈到在缓冲区上面进行的获取和释放操作,我们使用了简述的方式来描述大量可能的影响。 + +新的内存模型语义在内存操作(读取字段,写入字段,锁,解锁)以及其他线程的操作(`start` 和 `join`)中创建了一个部分排序,在这些操作中,一些操作被称为`happen before`其他操作。当一个操作在另外一个操作之前发生,第一个操作保证能够排到前面并且对第二个操作可见。这些排序的规则如下: +线程中的每个操作`happens before`该线程中在程序顺序上后续的每个操作。 +解锁一个监视器的操作`happens before`随后对相同监视器进行锁的操作。 +对`volatile`字段的写操作`happens before`后续对相同`volatile`字段的读取操作。 +线程上调用`start()`方法`happens before`这个线程启动后的任何操作。 +一个线程中所有的操作都`happens before`从这个线程`join()`方法成功返回的任何其他线程。(注意思是其他线程等待一个线程的`jion()`方法完成,那么,这个线程中的所有操作`happens before`其他线程中的所有操作) +这意味着:任何内存操作,这个内存操作在退出一个同步块前对一个线程是可见的,对任何线程在它进入一个被相同的监视器保护的同步块后都是可见的,因为所有内存操作`happens before`释放监视器以及释放监视器`happens before`获取监视器。 + +其他如下模式的实现被一些人用来强迫实现一个内存屏障的,不会生效: +``` +synchronized (new Object()) {} +``` +这段代码其实不会执行任何操作,你的编译器会把它完全移除掉,因为编译器知道没有其他的线程会使用相同的监视器进行同步。要看到其他线程的结果,你必须为一个线程建立`happens before`关系。 + +重点注意:对两个线程来说,为了正确建立`happens before`关系而在相同监视器上面进行同步是非常重要的。以下观点是错误的:当线程`A`在对象`X`上面同步的时候,所有东西对线程`A`可见,线程`B`在对象`Y`上面进行同步的时候,所有东西对线程`B`也是可见的。释放监视器和获取监视器必须匹配(也就是说要在相同的监视器上面完成这两个操作),否则,代码就会存在“数据竞争”。 diff --git "a/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213\345\234\250\346\226\260\347\232\204Java\345\206\205\345\255\230\346\250\241\345\236\213\344\270\255\357\274\214final\345\255\227\346\256\265\346\230\257\345\246\202\344\275\225\345\267\245\344\275\234\347\232\204.md" "b/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213\345\234\250\346\226\260\347\232\204Java\345\206\205\345\255\230\346\250\241\345\236\213\344\270\255\357\274\214final\345\255\227\346\256\265\346\230\257\345\246\202\344\275\225\345\267\245\344\275\234\347\232\204.md" new file mode 100644 index 0000000..8361e0e --- /dev/null +++ "b/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213\345\234\250\346\226\260\347\232\204Java\345\206\205\345\255\230\346\250\241\345\236\213\344\270\255\357\274\214final\345\255\227\346\256\265\346\230\257\345\246\202\344\275\225\345\267\245\344\275\234\347\232\204.md" @@ -0,0 +1,42 @@ +# `Java`内存模型`FAQ`(`九`)在新的`Java`内存模型中,`final`字段是如何工作的 +一个对象的`final`字段值是在它的构造方法里面设置的。假设对象被正确的构造了,一旦对象被构造,在构造方法里面设置给`final`字段的的值在没有同步的情况下对所有其他的线程都会可见。另外,引用这些`final`字段的对象或数组都将会看到`final`字段的最新值。 + +对一个对象来说,被正确的构造是什么意思呢?简单来说,它意味着这个正在构造的对象的引用在构造期间没有被允许逸出。(参见安全构造技术)。换句话说,不要让其他线程在其他地方能够看见一个构造期间的对象引用。不要指派给一个静态字段,不要作为一个listener注册给其他对象等等。这些操作应该在构造方法之后完成,而不是构造方法中来完成。 +``` +class FinalFieldExample { + final int x; + int y; + static FinalFieldExample f; + public FinalFieldExample() { + x = 3; + y = 4; + } + + static void writer() { + f = new FinalFieldExample(); + } + + static void reader() { + if (f != null) { + int i = f.x; + int j = f.y; + } + } +} +``` +上面的类展示了`final`字段应该如何使用。一个正在执行`reader`方法的线程保证看到`f.x`的值为`3`,因为它是`final`字段。它不保证看到`f.y`的值为`4`,因为`f.y`不是`final`字段。如果`FinalFieldExample`的构造方法像这样: +``` +public FinalFieldExample() { // bad! + x = 3; + y = 4; + // bad construction - allowing this to escape + global.obj = this; +} +``` +那么,从`global.obj`中读取`this`的引用线程不会保证读取到的`x`的值为`3`。 + +能够看到字段的正确的构造值固然不错,但是,如果字段本身就是一个引用,那么,你还是希望你的代码能够看到引用所指向的这个对象(或者数组)的最新值。如果你的字段是`final`字段,那么这是能够保证的。因此,当一个`final`指针指向一个数组,你不需要担心线程能够看到引用的最新值却看不到引用所指向的数组的最新值。重复一下,这儿的“正确的”的意思是“对象构造方法结尾的最新的值”而不是“最新可用的值”。 + +现在,在讲了如上的这段之后,如果在一个线程构造了一个不可变对象之后(对象仅包含`final`字段),你希望保证这个对象被其他线程正确的查看,你仍然需要使用同步才行。例如,没有其他的方式可以保证不可变对象的引用将被第二个线程看到。使用`final`字段的程序应该仔细的调试,这需要深入而且仔细的理解并发在你的代码中是如何被管理的。 + +如果你使用`JNI`来改变你的`final`字段,这方面的行为是没有定义的。 diff --git "a/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213\346\226\260\347\232\204\345\206\205\345\255\230\346\250\241\345\236\213\346\230\257\345\220\246\344\277\256\345\244\215\344\272\206\345\217\214\351\207\215\351\224\201\346\243\200\346\237\245\351\227\256\351\242\230.md" "b/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213\346\226\260\347\232\204\345\206\205\345\255\230\346\250\241\345\236\213\346\230\257\345\220\246\344\277\256\345\244\215\344\272\206\345\217\214\351\207\215\351\224\201\346\243\200\346\237\245\351\227\256\351\242\230.md" new file mode 100644 index 0000000..b4a92cf --- /dev/null +++ "b/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213\346\226\260\347\232\204\345\206\205\345\255\230\346\250\241\345\236\213\346\230\257\345\220\246\344\277\256\345\244\215\344\272\206\345\217\214\351\207\215\351\224\201\346\243\200\346\237\245\351\227\256\351\242\230.md" @@ -0,0 +1 @@ +# `Java`内存模型`FAQ`(`十一`)新的内存模型是否修复了双重锁检查问题? diff --git "a/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213\346\227\247\347\232\204\345\206\205\345\255\230\346\250\241\345\236\213\346\234\211\344\273\200\344\271\210\351\227\256\351\242\230.md" "b/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213\346\227\247\347\232\204\345\206\205\345\255\230\346\250\241\345\236\213\346\234\211\344\273\200\344\271\210\351\227\256\351\242\230.md" new file mode 100644 index 0000000..72bda1b --- /dev/null +++ "b/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213\346\227\247\347\232\204\345\206\205\345\255\230\346\250\241\345\236\213\346\234\211\344\273\200\344\271\210\351\227\256\351\242\230.md" @@ -0,0 +1,9 @@ +# Java内存模型FAQ(五)旧的内存模型有什么问题? +旧的内存模型中有几个严重的问题。这些问题很难理解,因此被广泛的违背。例如,旧的存储模型在许多情况下,不允许`JVM`发生各种重排序行为。旧的内存模型中让人产生困惑的因素造就了JSR-133规范的诞生。 + + +例如,一个被广泛认可的概念就是,如果使用`final`字段,那么就没有必要在多个线程中使用同步来保证其他线程能够看到这个字段的值。尽管这是一个合理的假设和明显的行为,也是我们所期待的结果。实际上,在旧的内存模型中,我们想让程序正确运行起来却是不行的。在旧的内存模型中,`final`字段并没有同其他字段进行区别对待——这意味着同步是保证所有线程看到一个在构造方法中初始化的final字段的唯一方法。结果——如果没有正确同步的话,对一个线程来说,它可能看到一个字段的默认值,然后在稍后的时间里,又能够看到构造方法中设置的值。这意味着,一些不可变的对象,例如`String`,能够改变它们值——这实在很让人郁闷。 + +旧的内存模型允许`volatile`变量的写操作和非`volaitle`变量的读写操作一起进行重排序,这和大多数的开发人员对于`volatile`变量的直观感受是不一致的,因此会造成迷惑。 + +最后,我们将看到的是,程序员对于程序没有被正确同步的情况下将会发生什么的直观感受通常是错误的。`JSR-133`的目的之一就是要引起这方面的注意。 diff --git "a/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213\346\262\241\346\234\211\346\255\243\347\241\256\345\220\214\346\255\245\347\232\204\345\220\253\344\271\211\346\230\257\344\273\200\344\271\210.md" "b/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213\346\262\241\346\234\211\346\255\243\347\241\256\345\220\214\346\255\245\347\232\204\345\220\253\344\271\211\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 0000000..9807134 --- /dev/null +++ "b/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213\346\262\241\346\234\211\346\255\243\347\241\256\345\220\214\346\255\245\347\232\204\345\220\253\344\271\211\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,7 @@ +# `Java`内存模型`FAQ`(`六`)没有正确同步的含义是什么? +没有正确同步的代码对于不同的人来说可能会有不同的理解。在`Java`内存模型这个语义环境下,我们谈到“没有正确同步”,我们的意思是: + +一个线程中有一个对变量的写操作, +另外一个线程对同一个变量有读操作, +而且写操作和读操作没有通过同步来保证顺序。 +当这些规则被违反的时候,我们就说在这个变量上有一个“数据竞争”(`data race`)。一个有数据竞争的程序就是一个没有正确同步的程序。 diff --git "a/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213\351\207\215\346\216\222\345\272\217\346\204\217\345\221\263\347\235\200\344\273\200\344\271\210.md" "b/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213\351\207\215\346\216\222\345\272\217\346\204\217\345\221\263\347\235\200\344\273\200\344\271\210.md" new file mode 100644 index 0000000..3ad5aea --- /dev/null +++ "b/Java/Java\345\206\205\345\255\230\346\250\241\345\236\213FAQ\344\271\213\351\207\215\346\216\222\345\272\217\346\204\217\345\221\263\347\235\200\344\273\200\344\271\210.md" @@ -0,0 +1,8 @@ +# Java内存模型FAQ(四)重排序意味着什么? +在很多情况下,访问一个程序变量(对象实例字段,类静态字段和数组元素)可能会使用不同的顺序执行,而不是程序语义所指定的顺序执行。编译器能够自由的以优化的名义去改变指令顺序。在特定的环境下,处理器可能会次序颠倒的执行指令。数据可能在寄存器,处理器缓冲区和主内存中以不同的次序移动,而不是按照程序指定的顺序。 + +例如,如果一个线程写入值到字段`a`,然后写入值到字段`b`,而且`b`的值不依赖于`a`的值,那么,处理器就能够自由的调整它们的执行顺序,而且缓冲区能够在`a`之前刷新b的值到主内存。有许多潜在的重排序的来源,例如编译器,`JIT`以及缓冲区。 + +编译器,运行时和硬件被期望一起协力创建好像是顺序执行的语义的假象,这意味着在单线程的程序中,程序应该是不能够观察到重排序的影响的。但是,重排序在没有正确同步了的多线程程序中开始起作用,在这些多线程程序中,一个线程能够观察到其他线程的影响,也可能检测到其他线程将会以一种不同于程序语义所规定的执行顺序来访问变量。 + +大部分情况下,一个线程不会关注其他线程正在做什么,但是当它需要关注的时候,这时候就需要同步了。 diff --git "a/Java/Java\347\232\204main\346\226\271\346\263\225\347\233\270\345\205\263\351\227\256\351\242\230.md" "b/Java/Java\347\232\204main\346\226\271\346\263\225\347\233\270\345\205\263\351\227\256\351\242\230.md" new file mode 100644 index 0000000..d755df3 --- /dev/null +++ "b/Java/Java\347\232\204main\346\226\271\346\263\225\347\233\270\345\205\263\351\227\256\351\242\230.md" @@ -0,0 +1,10 @@ +# Java的main方法相关问题 +* 如果`main`方法被声明为`private`会怎么样? + 如果是在`IDE`中运行,比如`Eclipse`中连`Java Application`按钮都没有,所以能通过编译但是不能运行,在命令行中运行应该是会提示`main`方法不是`public`. +* 说明一下`public static void main(String args[])`这段声明里每个关键字的作用 + `public`: `main`方法是`Java`程序运行时调用的第一个方法,因此它必须对`Java`环境可见。所以可见性设置为`public`. + `static`: `Java`平台调用这个方法时不会创建这个类的一个实例,因此这个方法必须声明为`static`。 + `void`: `main`方法没有返回值。 + `String`是命令行传进参数的类型,`args`是指命令行传进的字符串数组。 +* 如果去掉了`main`方法的`static`修饰符会怎样? + 程序能正常编译。运行时会抛NoSuchMethodError异常。 diff --git "a/Java/Java\347\232\204\342\200\235\344\270\200\346\254\241\347\274\226\345\206\231\357\274\214\345\244\204\345\244\204\350\277\220\350\241\214\342\200\235\346\230\257\345\246\202\344\275\225\345\256\236\347\216\260\347\232\204.md" "b/Java/Java\347\232\204\342\200\235\344\270\200\346\254\241\347\274\226\345\206\231\357\274\214\345\244\204\345\244\204\350\277\220\350\241\214\342\200\235\346\230\257\345\246\202\344\275\225\345\256\236\347\216\260\347\232\204.md" new file mode 100644 index 0000000..f4a9f9a --- /dev/null +++ "b/Java/Java\347\232\204\342\200\235\344\270\200\346\254\241\347\274\226\345\206\231\357\274\214\345\244\204\345\244\204\350\277\220\350\241\214\342\200\235\346\230\257\345\246\202\344\275\225\345\256\236\347\216\260\347\232\204.md" @@ -0,0 +1 @@ +# `Java`的”一次编写,处处运行”是如何实现的? diff --git "a/Java/Java\351\207\214\347\232\204\344\274\240\345\274\225\347\224\250\345\222\214\344\274\240\345\200\274\347\232\204\345\214\272\345\210\253\346\230\257\344\273\200\344\271\210.md" "b/Java/Java\351\207\214\347\232\204\344\274\240\345\274\225\347\224\250\345\222\214\344\274\240\345\200\274\347\232\204\345\214\272\345\210\253\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 0000000..a8e7e56 --- /dev/null +++ "b/Java/Java\351\207\214\347\232\204\344\274\240\345\274\225\347\224\250\345\222\214\344\274\240\345\200\274\347\232\204\345\214\272\345\210\253\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,22 @@ +# Java里的传引用和传值的区别是什么 +## 基本概念 +* 传值 +传值就是指将一个值传递到方法的内部。例如`int a = 5`,那么也就是给`int`型变量`a`赋值,值为`5`.如果一个方法,将这个变量传进方法的内部,则进行的就是传值。在`Java`中,有`8`种基本数据类型,它们分别为:`int、long、float、double、char、boolean、short、byte`.这`8`种基本的数据类型作为参数进行传递是,都是进行的传值。·除此之外,还有一种特殊的情况,`String`。本身`String`是一个引用类型,很多人认为在向方法中传递`String`类型参数时,是进行传引用。其实在这里,`String`可以看做为一个包装类,因为`String`其本身就是通过字符数组实现的,那么它在实现参数传递的时候,也就是以`char`型数据的方式进行的,也就是进行传值。 +* 传引用 +`java`中的引用可以初步的理解为地址。也是在`new`了一个对象后,该对象是存在`JVM`的`Heap`区,也就是堆。那么必然有一个地址要指向这个对象的在`JVM`中的位置,那么,指向这个对象的这个地址就可以简单的理解为“引用”。 + +## 实践 +`Java`确实使用对象的引用来做计算的,所有的对象变量都是引用。但是,`Java`在向方法传递参数时传的不是引用,是值。 +以 `swap()` 函数为例: +``` +public void swap(int var1, int var2) +{ + int temp = var1; + var1 = var2; + var2 = temp; +} +``` +当`swap`方法返回时,被当作参数传入的变量仍然保持了原来的值不变。如果我们把传入的`int`型变量改为`Object`型也是一样的,因为`Java`通过传值来传递引用的。 + +## 原因分析 +`Java`复制并传递了“引用”的值,而不是对象。因此,方法中对对象的计算是会起作用的,因为引用指向了原来的对象。但是因为方法中对象的引用是“副本”,所以对象交换就没起作用。交换动作只对方法中的引用副本起作用了,不影响方法外的引用。所以不好意思,方法被调用后,改变不了方法外的对象的引用。如果要对方法外的对象引用做交换,我们应该交换原始的引用,而不是它的副本。 diff --git a/Java/Picture/310.png b/Java/Picture/310.png new file mode 100644 index 0000000..0d20bb8 Binary files /dev/null and b/Java/Picture/310.png differ diff --git a/Java/Picture/3101.png b/Java/Picture/3101.png new file mode 100644 index 0000000..460c064 Binary files /dev/null and b/Java/Picture/3101.png differ diff --git a/Java/Picture/44.png b/Java/Picture/44.png new file mode 100644 index 0000000..8bf4a37 Binary files /dev/null and b/Java/Picture/44.png differ diff --git a/Java/Picture/55.png b/Java/Picture/55.png new file mode 100644 index 0000000..d35a919 Binary files /dev/null and b/Java/Picture/55.png differ diff --git a/Java/Picture/JVMAbstractNeiCunModel.png b/Java/Picture/JVMAbstractNeiCunModel.png new file mode 100644 index 0000000..0a08d75 Binary files /dev/null and b/Java/Picture/JVMAbstractNeiCunModel.png differ diff --git a/Java/README.md b/Java/README.md index 3e1c4b1..f6a94c4 100644 --- a/Java/README.md +++ b/Java/README.md @@ -1,2 +1,21 @@ # Java -* Java多线程之内存可见性 +* `Java`多线程之内存可见性 +* `Java`里的传引用和传值的区别是什么 +* `ArrayList`的`toArray`方法 +* `Java`的`main`方法相关问题 +* `Java`中的`equals`和`==`浅析 +* `equals()`与`hashCode()`方法浅析 +* `Java`的”一次编写,处处运行”是如何实现的 +* 静态类型有什么特点? +* `Java`内存模型`FAQ`(`一`)什么是内存模型 +* `Java`内存模型`FAQ`(`二`) 其他语言,像`C++`,也有内存模型吗? +* `Java`内存模型`FAQ`(`三`)`JSR133`是什么? +* `Java`内存模型`FAQ`(`四`)重排序意味着什么? +* `Java`内存模型`FAQ`(`五`)旧的内存模型有什么问题? +* `Java`内存模型`FAQ`(`六`)没有正确同步的含义是什么? +* `Java`内存模型`FAQ`(`七`)同步会干些什么呢 +* `Java`内存模型`FAQ`(`八`)`Final`字段如何改变它们的值 +* `Java`内存模型`FAQ`(`九`)在新的`Java`内存模型中,`final`字段是如何工作的 +* `Java`内存模型`FAQ`(`十`)`volatile`是干什么用的 +* `Java`内存模型`FAQ`(`十一`)新的内存模型是否修复了双重锁检查问题? +* 深入理解`Java`内存模型(`一`)——基础 diff --git "a/Java/`Java`\344\270\255\347\232\204`equals`\345\222\214`==`\346\265\205\346\236\220.md" "b/Java/`Java`\344\270\255\347\232\204`equals`\345\222\214`==`\346\265\205\346\236\220.md" new file mode 100644 index 0000000..e6454b6 --- /dev/null +++ "b/Java/`Java`\344\270\255\347\232\204`equals`\345\222\214`==`\346\265\205\346\236\220.md" @@ -0,0 +1,149 @@ +# `Java`中的`equals`和`==`浅析 +先看一段`Java`代码: +``` +package test; + +public class HelloWorld { + public static void main(String[] args) { + String str1 = new String("hello"); + String str2 = new String("hello"); + + System.out.println(str1==str2); + System.out.println(str1.equals(str2)); + } +} +``` +输出结果: +``` +false +true +``` +输出结果不一样?==和equals方法之间的区别是什么? + +## 关系操作符“==”到底比较的是什么? +`eg`: +``` +public class Main { + + /** + * @param args + */ + public static void main(String[] args) { + // TODO Auto-generated method stub + + int n=3; + int m=3; + + System.out.println(n==m); + + String str = new String("hello"); + String str1 = new String("hello"); + String str2 = new String("hello"); + + System.out.println(str1==str2); + + str1 = str; + str2 = str; + System.out.println(str1==str2); + } + +} +``` +输出结果为 `true false true` + +`n==m`结果为`true`,这个很容易理解,变量`n`和变量`m`存储的值都为`3`,肯定是相等的。而为什么`str1`和`str2`两次比较的结果不同?要理解这个其实只需要理解基本数据类型变量和非基本数据类型变量的区别。 + +在`Java`中游`8`种基本数据类型: + +  浮点型:`float(4 byte), double(8 byte)`` + +  整型:`byte(1 byte), short(2 byte), int(4 byte) , long(8 byte)`` + +  字符型: `char(2 byte)`` + +  布尔型: `boolean`(`JVM`规范没有明确规定其所占的空间大小,仅规定其只能够取字面值`"true"`和`"false"`) + +对于这`8`种基本数据类型的变量,变量直接存储的是“值”,因此在用关系操作符==来进行比较时,比较的就是 “值” 本身。要注意浮点型和整型都是有符号类型的,而`char`是无符号类型的(`char`类型取值范围为`0~2^16-1`). + +也就是说比如: +``` +  int n=3; + +  int m=3;  +``` +变量`n`和变量`m`都是直接存储的`"3"`这个数值,所以用`==`比较的时候结果是`true`。 + +而对于非基本数据类型的变量,在一些书籍中称作为 引用类型的变量。比如上面的`str1`就是引用类型的变量,引用类型的变量存储的并不是 “值”本身,而是于其关联的对象在内存中的地址。比如下面这行代码: +``` +  String str1; +``` + +这句话声明了一个引用类型的变量,此时它并没有和任何对象关联。 + +而通过`new String("hello")`来产生一个对象(也称作为类`String`的一个实例),并将这个对象和`str1`进行绑定: +``` +  str1= new String("hello"); +``` +那么`str1`指向了一个对象(很多地方也把`str1`称作为对象的引用),此时变量`str1`中存储的是它指向的对象在内存中的存储地址,并不是“值”本身,也就是说并不是直接存储的字符串`"hello"`。这里面的引用和`C/C++`的指针很类似。 + +因此在用`==`对`str1`和`str2`进行第一次比较时,得到的结果是`false`。因此它们分别指向的是不同的对象,也就是说它们实际存储的内存地址不同。 + +而在第二次比较时,都让`str1`和`str2`指向了`str`指向的对象,那么得到的结果毫无疑问是`true`。 +## equals比较的又是什么? +`equals`方法是基类`Object`中的方法,因此对于所有的继承于`Object`的类都会有该方法。为了更直观地理解`equals`方法的作用,直接看`Object`类中`equals`方法的实现。 +源码: +``` +public boolean equals(Object obj) { + return (this == obj); +} +``` +很显然,在`Object`类中,`equals`方法是用来比较两个对象的引用是否相等,即是否指向同一个对象。 + +但是有些朋友又会有疑问了,为什么下面一段代码的输出结果是`true`? +``` +String str1 = new String("hello"); +String str2 = new String("hello"); +System.out.println(str1.equals(str2)); +``` +要知道究竟,可以看一下`String`类的`equals`方法的具体实现,同样在该路径下,`String.java`为`String`类的实现。 + +下面是`String`类中`equals`方法的具体实现: +``` +public boolean equals(Object anObject) { + if (this == anObject) { + return true; + } + if (anObject instanceof String) { + String anotherString = (String)anObject; + int n = value.length; + if (n == anotherString.value.length) { + char v1[] = value; + char v2[] = anotherString.value; + int i = 0; + while (n-- != 0) { + if (v1[i] != v2[i]) + return false; + i++; + } + return true; + } + } + return false; +} +``` +可以看出,`String`类对`equals`方法进行了重写,用来比较指向的字符串对象所存储的字符串是否相等。 + +## 总结 +其他的一些类诸如`Double,Date,Integer`等,都对`equals`方法进行了重写用来比较指向的对象所存储的内容是否相等。 + +  总结来说: + +  1)对于`==`,如果作用于基本数据类型的变量,则直接比较其存储的 “值”是否相等; + +    如果作用于引用类型的变量,则比较的是所指向的对象的地址 + +  2)对于`equals`方法,注意:`equals`方法不能作用于基本数据类型的变量 + +    如果没有对`equals`方法进行重写,则比较的是引用类型的变量所指向的对象的地址; + +    诸如`String、Date`等类对`equals`方法进行了重写的话,比较的是所指向的对象的内容。 diff --git "a/Java/equals()\344\270\216hashCode()\346\226\271\346\263\225\346\265\205\346\236\220.md" "b/Java/equals()\344\270\216hashCode()\346\226\271\346\263\225\346\265\205\346\236\220.md" new file mode 100644 index 0000000..d908385 --- /dev/null +++ "b/Java/equals()\344\270\216hashCode()\346\226\271\346\263\225\346\265\205\346\236\220.md" @@ -0,0 +1,294 @@ +# `equals()`与`hashCode()`方法浅析 + +## 前提 +`java.lang.Object`类中有两个非常重要的方法: +* ```public boolean equals(Object obj)``` +* ```public int hashCode()``` + +`Object`类是类继承结构的基础,所以是每一个类的父类。所有的对象,包括数组,都实现了在`Object`类中定义的方法。 + +## `equals()`方法详解 + +`equals()`方法是用来判断其他的对象是否和该对象相等. + +`equals()`方法在`object`类中定义如下: +``` +public boolean equals(Object obj) { + return (this == obj); +} +``` +很明显是对两个对象的地址值进行的比较(即比较引用是否相同)。但是我们知道,`String 、Math、Integer、Double`等这些封装类在使用`equals()`方法时,已经覆盖了`object`类的`equals()`方法。 + +比如在`String`类中如下: +``` +public boolean equals(Object anObject) { + if (this == anObject) { + return true; + } + if (anObject instanceof String) { + String anotherString = (String)anObject; + int n = count; + if (n == anotherString.count) { + char v1[] = value; + char v2[] = anotherString.value; + int i = offset; + int j = anotherString.offset; + while (n– != 0) { + if (v1[i++] != v2[j++]) + return false; + } + return true; + } + } + return false; +} +``` + +很明显,这是进行的内容比较,而已经不再是地址的比较。依次类推`Math、Integer、Double`等这些类都是重写了`equals()`方法的,从而进行的是内容的比较。当然,基本类型是进行值的比较。 + +它的性质有: + +* 自反性(`reflexive`)。对于任意不为`null`的引用值`x`,`x.equals(x)`一定是`true`。 + +* 对称性(`symmetric`)。对于任意不为`null`的引用值`x`和`y`,当且仅当`x.equals(y)`是`true`时,`y.equals(x)`也是`true`。 + +* 传递性(`transitive`)。对于任意不为`null`的引用值`x`、`y`和`z`,如果`x.equals(y)`是`true`,同时`y.equals(z)`是`true`,那么`x.equals(z)`一定是`true`。 + +* 一致性(`consistent`)。对于任意不为`null`的引用值`x`和`y`,如果用于`equals`比较的对象信息没有被修改的话,多次调用时`x.equals(y)`要么一致地返回`true`要么一致地返回`false`。 + +对于任意不为`null`的引用值`x,x.equals(null)`返回`false`。 + +对于`Object`类来说,`equals()`方法在对象上实现的是差别可能性最大的等价关系,即,对于任意非`null`的引用值`x`和`y`,当且仅当`x`和`y`引用的是同一个对象,该方法才会返回`true`。 + +需要注意的是当`equals()`方法被`override`时,`hashCode()`也要被`override`。按照一般`hashCode()`方法的实现来说,相等的对象,它们的`hash code`一定相等。 + +## `hashcode()` 方法详解 +`hashCode()`方法给对象返回一个`hash code`值。这个方法被用于`hash tables`,例如H`ashMap`。 + +它的性质是: + +在一个`Java`应用的执行期间,如果一个对象提供给`equals`做比较的信息没有被修改的话,该对象多次调用`hashCode()`方法,该方法必须始终如一返回同一个`integer`。 + +如果两个对象根据`equals(Object)`方法是相等的,那么调用二者各自的`hashCode()`方法必须产生同一个`integer`结果。 + +并不要求根据`equals(java.lang.Object)`方法不相等的两个对象,调用二者各自的`hashCode()`方法必须产生不同的`integer`结果。然而,程序员应该意识到对于不同的对象产生不同的`integer`结果,有可能会提高`hash table`的性能。 + +大量的实践表明,由`Object`类定义的`hashCode()`方法对于不同的对象返回不同的`integer`。 + +在`object`类中,`hashCode`定义如下: +``` +public native int hashCode(); +``` + 说明是一个本地方法,它的实现是根据本地机器相关的。当然我们可以在自己写的类中覆盖`hashcode()`方法,比如`String、Integer、Double`等这些类都是覆盖了`hashcode()`方法的。例如在`String`类中定义的`hashcode()`方法如下: + ``` + public int hashCode() { + int h = hash; + if (h == 0) { + int off = offset; + char val[] = value; + int len = count; + + for (int i = 0; i < len; i++) { + h = 31 * h + val[off++]; + } + hash = h; + } + return h; +} + ``` +解释一下这个程序(`String`的`API`中写到):`s[0]* 31^(n-1) + s[1] * 31^(n-2) + … + s[n-1]`使用 `int` 算法,这里 `s[i]` 是字符串的第 `i` 个字符,`n` 是字符串的长度,`^` 表示求幂(空字符串的哈希码为 `0`)。 + +想要弄明白`hashCode`的作用,必须要先知道`Java`中的集合。   +总的来说,`Java`中的集合(`Collection`)有两类,一类是`List`,再有一类是`Set`。前者集合内的元素是有序的,元素可以重复;后者元素无序,但元素不可重复。这里就引出一个问题:要想保证元素不重复,可两个元素是否重复应该依据什么来判断呢? + +这就是`Object.equals`方法了。但是,如果每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了。也就是说,如果集合中现在已经有`1000`个元素,那么第`1001`个元素加入集合时,它就要调用`1000`次`equals`方法。这显然会大大降低效率。 + +于是,`Java`采用了哈希表的原理。哈希(`Hash`)实际上是个人名,由于他提出一哈希算法的概念,所以就以他的名字命名了。哈希算法也称为散列算法,是将数据依特定算法直接指定到一个地址上,初学者可以简单理解,hashCode方法实际上返回的就是对象存储的物理地址(实际可能并不是)。 + +这样一来,当集合要添加新的元素时,先调用这个元素的`hashCode`方法,就一下子能定位到它应该放置的物理位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的`equals`方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。所以这里存在一个冲突解决的问题。这样一来实际调用`equals`方法的次数就大大降低了,几乎只需要一两次。 + +简而言之,在集合查找时,`hashcode`能大大降低对象比较次数,提高查找效率! + +`Java`对象的`eqauls`方法和`hashCode`方法是这样规定的: + + * 1、相等(相同)的对象必须具有相等的哈希码(或者散列码)。 + + * 2、如果两个对象的hashCode相同,它们并不一定相同。 + +以下是`Object`对象`API`关于`equal`方法和`hashCode`方法的说明: + + `If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result. + It is not required that if two objects are unequal according to the equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hash tables.` + +以上`API`说明是对之前`2`点的官方详细说明 +关于第一点,相等(相同)的对象必须具有相等的哈希码(或者散列码),为什么? + +想象一下,假如两个`Java`对象`A`和`B`,`A`和`B`相等(`eqauls`结果为`true`),但`A`和`B`的哈希码不同,则`A`和`B`存入`HashMap`时的哈希码计算得到的`HashMap`内部数组位置索引可能不同,那么`A`和`B`很有可能允许同时存入`HashMap`,显然相等`/`相同的元素是不允许同时存入`HashMap`,`HashMap`不允许存放重复元素。 + +关于第二点,两个对象的`hashCode`相同,它们并不一定相同 + +也就是说,不同对象的`hashCode`可能相同;假如两个`Java`对象`A`和`B`,`A`和`B`不相等(`eqauls`结果为`false`),但`A`和`B`的哈希码相等,将`A`和`B`都存入`HashMap`时会发生哈希冲突,也就是`A`和`B`存放在`HashMap`内部数组的位置索引相同这时`HashMap`会在该位置建立一个链接表,将`A`和`B`串起来放在该位置,显然,该情况不违反`HashMap`的使用原则,是允许的。当然,哈希冲突越少越好,尽量采用好的哈希算法以避免哈希冲突。 + +所以,`Java`对于`eqauls`方法和`hashCode`方法是这样规定的: +* 1.如果两个对象相同,那么它们的`hashCode`值一定要相同; +* 2.如果两个对象的`hashCode`相同,它们并不一定相同(这里说的对象相同指的是用`eqauls`方法比较)。如不按要求去做了,会发现相同的对象可以出现在`Set`集合中,同时,增加新元素的效率会大大下降。 +* 3.`equals()`相等的两个对象,`hashcode()`一定相等;`equals()`不相等的两个对象,却并不能证明他们的`hashcode()`不相等。 + +换句话说,`equals()`方法不相等的两个对象,`hashcode()`有可能相等(我的理解是由于哈希码在生成的时候产生冲突造成的)。反过来,`hashcode()`不等,一定能推出`equals()`也不等;`hashcode()`相等,`equals()`可能相等,也可能不等。 + +在`object`类中,`hashcode()`方法是本地方法,返回的是对象的地址值,而`object`类中的e`quals()`方法比较的也是两个对象的地址值,如果`equals()`相等,说明两个对象地址值也相等,当然`hashcode()`也就相等了;在`String`类中,`equals()`返回的是两个对象内容的比较,当两个对象内容相等时,H`ashcode()`方法根据`String`类的重写代码的分析,也可知道`hashcode()`返回结果也会相等。以此类推,可以知道`Integer、Double`等封装类中经过重写的`equals()`和`hashcode()`方法也同样适合于这个原则。当然没有经过重写的类,在继承了`object`类的`equals()`和`hashcode()`方法后,也会遵守这个原则。 + +## `Hashset、Hashmap、Hashtable与hashcode()`和`equals()`的密切关系 +`Hashset`是继承`Set`接口,`Set`接口又实现`Collection`接口,这是层次关系。那么`Hashset、Hashmap、Hashtable`中的存储操作是根据什么原理来存取对象的呢? + +下面以`HashSet`为例进行分析,我们都知道:在`hashset`中不允许出现重复对象,元素的位置也是不确定的。在`hashset`中又是怎样判定元素是否重复的呢?在`java`的集合中,判断两个对象是否相等的规则是: +* 1.判断两个对象的`hashCode`是否相等 + 如果不相等,认为两个对象也不相等,完毕 + 如果相等,转入`2` + (这一点只是为了提高存储效率而要求的,其实理论上没有也可以,但如果没有,实际使用时效率会大 大降低,所以我们这里将其做为必需的。) + +* 2.判断两个对象用`equals`运算是否相等 + 如果不相等,认为两个对象也不相等 + 如果相等,认为两个对象相等(`equals()`是判断两个对象是否相等的关键) + 为什么是两条准则,难道用第一条不行吗?不行,因为前面已经说了,`hashcode()`相等时,`equals()`方法也可能不等,所以必须用第`2`条准则进行限制,才能保证加入的为非重复元素。 + +例1: +``` +package com.bijian.study; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +public class HashSetTest { + + public static void main(String args[]) { + String s1 = new String("aaa"); + String s2 = new String("aaa"); + System.out.println(s1 == s2); + System.out.println(s1.equals(s2)); + System.out.println(s1.hashCode()); + System.out.println(s2.hashCode()); + Set hashset = new HashSet(); + hashset.add(s1); + hashset.add(s2); + Iterator it = hashset.iterator(); + while (it.hasNext()) { + System.out.println(it.next()); + } + } +} +``` +运行结果: +``` +false +true +96321 +96321 +aaa +``` +这是因为`String`类已经重写了`equals()`方法和`hashcode()`方法,所以`hashset`认为它们是相等的对象,进行了重复添加。 +例2: +``` +package com.bijian.study; + +import java.util.HashSet; +import java.util.Iterator; + +public class HashSetTest { + + public static void main(String[] args) { + HashSet hs = new HashSet(); + hs.add(new Student(1, "zhangsan")); + hs.add(new Student(2, "lisi")); + hs.add(new Student(3, "wangwu")); + hs.add(new Student(1, "zhangsan")); + + Iterator it = hs.iterator(); + while (it.hasNext()) { + System.out.println(it.next()); + } + } +} + +class Student { + int num; + String name; + + Student(int num, String name) { + this.num = num; + this.name = name; + } + + public String toString() { + return num + ":" + name; + } +} +``` +运行结果: +``` +zhangsan +wangwu +lisi +zhangsan +``` +为什么`hashset`添加了相等的元素呢,这是不是和`hashset`的原则违背了呢?回答是:没有。因为在根据`hashcode()`对两次建立的`new Student(1,“zhangsan”)`对象进行比较时,生成的是不同的哈希码值,所以`hashset`把他当作不同的对象对待了,当然此时的`equals()`方法返回的值也不等。 + +为什么会生成不同的哈希码值呢?上面我们在比较`s1`和`s2`的时候不是生成了同样的哈希码吗?原因就在于我们自己写的`Student`类并没有重新自己的`hashcode()`和`equals()`方法,所以在比较时,是继承的`object`类中的`hashcode()`方法,而`object`类中的`hashcode()`方法是一个本地方法,比较的是对象的地址(引用地址),使用`new`方法创建对象,两次生成的当然是不同的对象了,造成的结果就是两个对象的`hashcode()`返回的值不一样,所以`Hashset`会把它们当作不同的对象对待。 + +怎么解决这个问题呢?答案是:在`Student`类中重新`hashcode()`和`equals()`方法。 +``` +class Student { + int num; + String name; + + Student(int num, String name) { + this.num = num; + this.name = name; + } + + public int hashCode() { + return num * name.hashCode(); + } + + public boolean equals(Object o) { + Student s = (Student) o; + return num == s.num && name.equals(s.name); + } + + public String toString() { + return num + ":" + name; + } +} +``` +运行结果: +``` +1:zhangsan +3:wangwu +2:lisi +``` +可以看到重复元素的问题已经消除,根据重写的方法,即便两次调用了`new Student(1,"zhangsan")`,我们在获得对象的哈希码时,根据重写的方法`hashcode()`,获得的哈希码肯定是一样的,当然根据`equals()`方法我们也可判断是相同的,所以在向h`ashset`集合中添加时把它们当作重复元素看待了。 + +重写`equals()`和`hashcode()`小结: + +* 1.重点是`equals`,重写`hashCode`只是技术要求(为了提高效率) +* 2.为什么要重写`equals`呢?因为在`java`的集合框架中,是通过`equals`来判断两个对象是否相等的 +* 3.在`hibernate`中,经常使用`set`集合来保存相关对象,而`set`集合是不允许重复的。在向`HashSet`集合中添加元素时,其实只要重写`equals()`这一条也可以。但当`hashset`中元素比较多时,或者是重写的`equals()`方法比较复杂时,我们只用e`quals()`方法进行比较判断,效率也会非常低,所以引入了h`ashCode()`这个方法,只是为了提高效率,且这是非常有必要的。比如可以这样写: +``` +public int hashCode(){ + return 1; //等价于hashcode无效 +} +``` +这样做的效果就是在比较哈希码的时候不能进行判断,因为每个对象返回的哈希码都是`1`,每次都必须要经过比较`equals()`方法后才能进行判断是否重复,这当然会引起效率的大大降低。 + + + +## 文章参考: + +[在Java中正确地使用equals()和hashCode()方法](http://boxingp.github.io/blog/2015/02/24/use-equals-and-hashcode-methods-in-java-correctly/) + +[Java中equals()与hashCode()方法详解](http://bijian1013.iteye.com/blog/1972404) + +[深入解析Java对象的hashCode和hashCode在HashMap的底层数据结构的应用](http://kakajw.iteye.com/blog/935226) + +[Java hashCode() 和 equals()的若干问题解答](http://www.cnblogs.com/skywang12345/p/3324958.html) diff --git "a/Java/\346\267\261\345\205\245\347\220\206\350\247\243Java\345\206\205\345\255\230\346\250\241\345\236\213\357\274\210\344\270\200\357\274\211\342\200\224\342\200\224\345\237\272\347\241\200.md" "b/Java/\346\267\261\345\205\245\347\220\206\350\247\243Java\345\206\205\345\255\230\346\250\241\345\236\213\357\274\210\344\270\200\357\274\211\342\200\224\342\200\224\345\237\272\347\241\200.md" new file mode 100644 index 0000000..75880e8 --- /dev/null +++ "b/Java/\346\267\261\345\205\245\347\220\206\350\247\243Java\345\206\205\345\255\230\346\250\241\345\236\213\357\274\210\344\270\200\357\274\211\342\200\224\342\200\224\345\237\272\347\241\200.md" @@ -0,0 +1,76 @@ +# 深入理解`Java`内存模型(`一`)——基础 +## 并发编程模型的分类 +在并发编程中,我们需要处理两个关键问题:`线程之间如何通信及线程之间如何同步`(这里的线程是指并发执行的活动实体)。`通信`是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:`共享内存`和`消息传递`。 +在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。 +同步是指程序用于控制不同线程之间操作发生相对顺序的机制。在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。 +`Java`的并发采用的是`共享内存模型`,`Java`线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。 +# `Java`内存模型的抽象 +在`java`中,所有实例域、静态域和数组元素存储在堆内存中,堆内存在线程之间共享(本文使用“共享变量”这个术语代指实例域,静态域和数组元素)。局部变量(`Local variables`),方法定义参数(`java`语言规范称之为`formal method parameters`)和异常处理器参数(`exception handler parameters`)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。 + +`Java`线程之间的通信由`Java`内存模型(本文简称为`JMM`)控制,`JMM`决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,`JMM`定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(`main memory`)中,每个线程都有一个私有的本地内存(`local memory`),本地内存中存储了该线程以读/写共享变量的副本。本地内存是`JMM`的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。`Java`内存模型的抽象示意图如下: +![Java抽象内存模型](Picture/JVMAbstractNeiCunModel.png "java抽象内存模型") +从上图来看,线程`A`与线程`B`之间如要通信的话,必须要经历下面`2`个步骤: +- 首先,线程`A`把本地内存`A`中更新过的共享变量刷新到主内存中去。 +- 然后,线程`B`到主内存中去读取线程`A`之前已更新过的共享变量。 + +下面通过示意图来说明这两个步骤: +![Java共享变量通信步骤](Picture/310.png "Java共享变量通信步骤") +如上图所示,本地内存`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`程序员提供内存可见性保证。 +## 重排序 +在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型: + +- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。 +- 指令级并行的重排序。现代处理器采用了指令级并行技术(`Instruction-Level Parallelism, ILP`)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 +- 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。 + +从`java`源代码到最终实际执行的指令序列,会分别经历下面三种重排序: +![Java编译流程](Picture/3101.png) +上述的`1`属于编译器重排序,`2`和`3`属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题。对于编译器,`JMM`的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,`JMM`的处理器重排序规则会要求`java`编译器在生成指令序列时,插入特定类型的内存屏障(`memory barriers`,`intel`称之为`memory fence`)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。 + +`JMM`属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。 +## 处理器重排序与内存屏障指令 +现代的处理器使用写缓冲区来临时保存向内存写入的数据。写缓冲区可以保证指令流水线持续运行,它可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟。同时,通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,可以减少对内存总线的占用。虽然写缓冲区有这么多好处,但每个处理器上的写缓冲区,仅仅对它所在的处理器可见。这个特性会对内存操作的执行顺序产生重要的影响:处理器对内存的读/写操作的执行顺序,不一定与内存实际发生的读/写操作顺序一致!为了具体说明,请看下面示例: + +| ProcessorA | ProcessorB | +|-----|------| +| a = 1;A1 | b = 2;B1 | +| x = b;A2 | y = a;B2 | + +初始状态:a = b = 0 +处理器允许执行后得到结果:x = y = 0 + +假设处理器`A`和处理器`B`按程序的顺序并行执行内存访问,最终却可能得到`x = y = 0`的结果。具体的原因如下图所示: +![AB执行流程](Picture/44.png) +这里处理器`A`和处理器`B`可以同时把共享变量写入自己的写缓冲区(`A1,B1)`,然后从内存中读取另一个共享变量(`A2,B2)`,最后才把自己写缓存区中保存的脏数据刷新到内存中`(A3,B3)`。当以这种时序执行时,程序就可以得到`x = y = 0`的结果。 + +从内存操作实际发生的顺序来看,直到处理器`A`执行`A3`来刷新自己的写缓存区,写操作`A1`才算真正执行了。虽然处理器A执行内存操作的顺序为:`A1->A2`,但内存操作实际发生的顺序却是:`A2->A1`。此时,处理器A的内存操作顺序被重排序了(处理器`B`的情况和处理器`A`一样,这里就不赘述了)。 + +这里的关键是,由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的顺序可能会与内存实际的操作执行顺序不一致。由于现代的处理器都会使用写缓冲区,因此现代的处理器都会允许对写-读操做重排序。 + +为了保证内存可见性,`java`编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。`JMM`把内存屏障指令分为下列四类: + +|屏障类型 |指令示例| 说明| +|-------|-------|------| +|LoadLoad Barriers |Load1; LoadLoad; Load2| 确保Load1数据的装载,之前于Load2及所有后续装载指令的装载。| +|StoreStore Barriers |Store1; StoreStore; Store2 |确保Store1数据对其他处理器可见(刷新到内存),之前于Store2及所有后续存储指令的存储。| +|LoadStore Barriers |Load1; LoadStore; Store2 |确保Load1数据装载,之前于Store2及所有后续的存储指令刷新到内存。| +|StoreLoad Barriers |Store1; StoreLoad; Load2 |确保Store1数据对其他处理器变得可见(指刷新到内存),之前于Load2及所有后续装载指令的装载。| + +`StoreLoad Barriers`会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。 +`StoreLoad Barriers`是一个“全能型”的屏障,它同时具有其他三个屏障的效果。现代的多处理器大都支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(`buffer fully flush`)。 + +## `happens-before` + +从`JDK5`开始,`java`使用新的`JSR -133`内存模型(本文除非特别说明,针对的都是`JSR- 133`内存模型)。`JSR-133`提出了`happens-before`的概念,通过这个概念来阐述操作之间的内存可见性。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在`happens-before`关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。 与程序员密切相关的`happens-before`规则如下: + +- 程序顺序规则:一个线程中的每个操作,`happens- before` 于该线程中的任意后续操作。 +- 监视器锁规则:对一个监视器锁的解锁,`happens- before` 于随后对这个监视器锁的加锁。 +- `volatile`变量规则:对一个`volatile`域的写,`happens- before` 于任意后续对这`个volatile`域的读。 +- 传递性:如果`A happens- before B`,且`B happens- before C`,那么`A happens- before C`。 +注意,两个操作之间具有`happens-before`关系,并不意味着前一个操作必须要在后一个操作之前执行!`happens-before`仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(`the first is visible to and ordered before the second`)。`happens- before`的定义很微妙,后文会具体说明`happens-before`为什么要这么定义。 + +`happens-before`与`JMM`的关系如下图所示: +![](Picture/55.png) +如上图所示,一个`happens-before`规则通常对应于多个编译器重排序规则和处理器重排序规则。对于`java`程序员来说,`happens-before`规则简单易懂,它避免程序员为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现。 diff --git "a/Java/\351\235\231\346\200\201\347\261\273\345\236\213\346\234\211\344\273\200\344\271\210\347\211\271\347\202\271.md" "b/Java/\351\235\231\346\200\201\347\261\273\345\236\213\346\234\211\344\273\200\344\271\210\347\211\271\347\202\271.md" new file mode 100644 index 0000000..1f6f878 --- /dev/null +++ "b/Java/\351\235\231\346\200\201\347\261\273\345\236\213\346\234\211\344\273\200\344\271\210\347\211\271\347\202\271.md" @@ -0,0 +1 @@ +静态变量是和类绑定到一起的,而不是类的实例对象。每一个实例对象都共享同样一份静态变量。也就是说,一个类的静态变量只有一份,不管它有多少个对象。类变量或者说静态变量是通过`static`这个关键字来声明的。类变量通常被用作常量。静态变量通常通过类名字来进行访问。当程序运行的时候这个变量就会创建直到程序结束后才会被销毁。类变量的作用域和实例变量是一样的。它的初始值和成员变量也是一样的,当变量没被初始化的时候根据它的数据类型,会有一个默认值。类似的,静态方法是属于类的方法,而不是类对象,它的调用并不作用于类对象,也不需要创建任何的类实例。静态方法本身就是`final`的,因为重写只会发生在类实例上,静态方法是和类绑定在一起的,不是对象。父类的静态方法会被子类的静态方法屏蔽,只要原来方法没有声明为`final`。非静态方法不能重写静态方法,也就是说,你不能在子类中把一个静态方法改成实例方法。