diff --git a/README.md b/README.md index 89b8bccd..acf8a187 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,23 @@ ![](https://img.shields.io/badge/version-v2.0.0-green.svg) ![](https://img.shields.io/badge/author-Hollis-yellow.svg) ![](https://img.shields.io/badge/license-GPL-blue.svg) + | 主要版本 | 更新时间 | 备注 | | ---- | ---------- | -------------- | +| v4.0 | 2022-05-20 | 知识体系完善,知识点补充| | v3.0 | 2020-03-31 | 知识体系完善,在v2.0的基础上,新增20%左右的知识点
调整部分知识的顺序及结构,方便阅读和理解
通过GitHub Page搭建,便于阅读| | v2.0 | 2019-02-19 | 结构调整,更适合从入门到精通;
进一步完善知识体系;
新技术补充;| | v1.1 | 2018-03-12 | 增加新技术知识、完善知识体系 | | v1.0 | 2015-08-01 | 首次发布 | +Java成神之路全套面试题——围绕成神之路,500多道题,60多万字>>> + +![Java八股](http://www.hollischuang.com/wp-content/uploads/2023/10/640.png) + + +扫码下单后,按照短信提示操作即可。 + 目前正在更新中... 欢迎大家参与共建~ @@ -33,7 +42,7 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer) ### 关于作者 -Hollis,阿里巴巴技术专家,51CTO专栏作家,CSDN博客专家,掘金优秀作者,《程序员的三门课》联合作者,《Java工程师成神之路》系列文章作者;热衷于分享计算机编程相关技术,博文全网阅读量上千万。 +Hollis,阿里巴巴技术专家,51CTO专栏作家,CSDN博客专家,掘金优秀作者,《深入理解Java核心技术》作者,《程序员的三门课》联合作者,《Java工程师成神之路》系列文章作者;热衷于分享计算机编程相关技术,博文全网阅读量上千万。 ### 开源协议 diff --git a/docs/README.md b/docs/README.md index 57416c18..45ea4c4a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,9 +2,13 @@ ![](https://img.shields.io/badge/version-v2.0.0-green.svg) ![](https://img.shields.io/badge/author-Hollis-yellow.svg) ![](https://img.shields.io/badge/license-GPL-blue.svg) +成神之路系列丛书的第一本《深入理解Java核心技术(基础篇)》已经正式出版了,这本书囊括了中基础篇的几乎全部内容,欢迎大家购买品鉴。 + +![](docs/contact/book.jpeg) | 主要版本 | 更新时间 | 备注 | | ---- | ---------- | -------------- | +| v4.0 | 2022-05-20 | 知识体系完善,知识点补充| | v3.0 | 2020-03-31 | 知识体系完善,在v2.0的基础上,新增20%左右的知识点
调整部分知识的顺序及结构,方便阅读和理解
通过GitHub Page搭建,便于阅读| | v2.0 | 2019-02-19 | 结构调整,更适合从入门到精通;
进一步完善知识体系;
新技术补充;| | v1.1 | 2018-03-12 | 增加新技术知识、完善知识体系 | diff --git a/docs/_sidebar.md b/docs/_sidebar.md index fb2a2b59..f12297d8 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -262,7 +262,7 @@ * [什么是泛型](/basics/java-basic/generics.md) - * [类型擦除](/basics/java-basic/type-erasue.md) + * [类型擦除](/basics/java-basic/type-erasure.md) * [泛型带来的问题](/basics/java-basic/generics-problem.md) @@ -400,7 +400,7 @@ * [为什么不能使用BigDecimal的equals比较大小](/basics/java-basic/stop-using-equlas-in-bigdecimal.md) - * 为什么不能直接使用double创建一个BigDecimal + * [为什么不能直接使用double创建一个BigDecimal](/basics/java-basic/stop-create-bigdecimal-with-double.md) * Java 8 @@ -478,11 +478,11 @@ * 线程池原理 - * 为什么不允许使用Executors创建线程池 + * [为什么不允许使用Executors创建线程池](/basics/concurrent-coding/why-not-executors.md) * 线程安全 - * 什么是线程安全 + * [什么是线程安全](/basics/concurrent-coding/thread-safe.md) * 多级缓存和一致性问题 diff --git a/docs/advance/design-patterns/abstract-factory-pattern.md b/docs/advance/design-patterns/abstract-factory-pattern.md index cb0fad47..ab115aa1 100644 --- a/docs/advance/design-patterns/abstract-factory-pattern.md +++ b/docs/advance/design-patterns/abstract-factory-pattern.md @@ -36,7 +36,7 @@ > > Product(具体产品):定义具体工厂生产的具体产品对象,实现抽象产品接口中定义的业务方法。 -本文的例子采用一个汽车代工厂造汽车的例子。假设我们是一家汽车代工厂商,我们负责给奔驰和特斯拉两家公司制造车子。我们简单的把奔驰车理解为需要加油的车,特斯拉为需要充电的车。其中奔驰车中包含跑车和商务车两种,特斯拉同样也包含奔驰车和商务车。 +本文的例子采用一个汽车代工厂造汽车的例子。假设我们是一家汽车代工厂商,我们负责给奔驰和特斯拉两家公司制造车子。我们简单的把奔驰车理解为需要加油的车,特斯拉为需要充电的车。其中奔驰车中包含跑车和商务车两种,特斯拉同样也包含跑车和商务车。 [QQ20160419-1][6] diff --git a/docs/advance/design-patterns/adapter-pattern.md b/docs/advance/design-patterns/adapter-pattern.md index b5db9af9..49384606 100644 --- a/docs/advance/design-patterns/adapter-pattern.md +++ b/docs/advance/design-patterns/adapter-pattern.md @@ -125,7 +125,7 @@ GOF中将适配器模式分为类适配器模式和对象适配器模式。区 public void charge(){ System.out.println("开始给我的GalaxyS7手机充电..."); microUsbInterface.chargeWithMicroUsb(); - System.out.println("开始给我的GalaxyS7手机充电..."); + System.out.println("结束给我的GalaxyS7手机充电..."); } public MicroUsbInterface getMicroUsbInterface() { @@ -203,14 +203,14 @@ GOF中将适配器模式分为类适配器模式和对象适配器模式。区 ============================== 开始给我的GalaxyS7手机充电... 使用MicroUsb型号的充电器充电... - 开始给我的GalaxyS7手机充电... + 结束给我的GalaxyS7手机充电... ============================== 开始给我的Iphone6Plus手机充电... 使用MicroUsb型号的充电器充电... 结束给我的Iphone6Plus手机充电... -上面的例子通过适配器,把一个MicroUsb型号的充电器用来给Iphone充电。从代码层面,就是通过适配器复用了MicroUsb接口及其实现类。在很大程度上福永了已有的代码。 +上面的例子通过适配器,把一个MicroUsb型号的充电器用来给Iphone充电。从代码层面,就是通过适配器复用了MicroUsb接口及其实现类。在很大程度上复用了已有的代码。 ## 优缺点 diff --git a/docs/advance/design-patterns/strategy-pattern.md b/docs/advance/design-patterns/strategy-pattern.md index 29f26245..7ef203f8 100644 --- a/docs/advance/design-patterns/strategy-pattern.md +++ b/docs/advance/design-patterns/strategy-pattern.md @@ -88,7 +88,7 @@ @Override public double calPrice(double bookPrice) { - System.out.println("对于中级会员的折扣为20%"); + System.out.println("对于高级会员的折扣为20%"); return bookPrice * 0.8; } } @@ -149,7 +149,7 @@ } } - //对于中级会员的折扣为20% + //对于高级会员的折扣为20% //高级会员图书的最终价格为:240.0 //对于中级会员的折扣为10% //中级会员图书的最终价格为:270.0 diff --git a/docs/basics/concurrent-coding/concurrent.md b/docs/basics/concurrent-coding/concurrent.md index 4f14ebbf..3685a1bb 100644 --- a/docs/basics/concurrent-coding/concurrent.md +++ b/docs/basics/concurrent-coding/concurrent.md @@ -1,6 +1,6 @@ 并发(Concurrent),在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。 -那么,操作系统视如何实现这种并发的呢? +那么,操作系统是如何实现这种并发的呢? 现在我们用到操作系统,无论是Windows、Linux还是MacOS等其实都是**多用户多任务分时操作系统**。使用这些操作系统的用户是可以“同时”干多件事的。 diff --git a/docs/basics/concurrent-coding/debug-in-multithread.md b/docs/basics/concurrent-coding/debug-in-multithread.md index f51bbc67..056f6e19 100644 --- a/docs/basics/concurrent-coding/debug-in-multithread.md +++ b/docs/basics/concurrent-coding/debug-in-multithread.md @@ -2,7 +2,7 @@ 但是我之前面试过很多人,很多人都知道多线程怎么实现,但是却不知道如何调试多线程的代码,这篇文章我们来介绍下如何调试多线程的代码。 -首先我们写一个多线程的例子,使用继承Runnable接口的方式定义多个线程,并启动执行。 +首先我们写一个多线程的例子,使用实现Runnable接口的方式定义多个线程,并启动执行。 /** * @author Hollis diff --git a/docs/basics/concurrent-coding/thread-safe.md b/docs/basics/concurrent-coding/thread-safe.md new file mode 100644 index 00000000..10374e58 --- /dev/null +++ b/docs/basics/concurrent-coding/thread-safe.md @@ -0,0 +1,119 @@ +# 什么是线程安全 + +线程安全,维基百科中的解释是: + +> 线程安全是编程中的术语,指某个函数、函数库在**并发**环境中被调用时,能够正确地处理**多个线程**之间的**共享变量**,使程序功能正确完成。 + +我们把这个定义拆解一下,我们需要弄清楚这么几点: 1、并发 2、多线程 3、共享变量 + +# 并发 + +提到线程安全,必须要提及的一个词那就是并发,如果没有并发的话,那么也就不存在线程安全问题了。 + +## 什么是并发 + +并发(Concurrent),在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。 + +那么,操作系统视如何实现这种并发的呢? + +现在我们用到操作系统,无论是Windows、Linux还是MacOS等其实都是**多用户多任务分时操作系统**。使用这些操作系统的用户是可以“同时”干多件事的。 + +但是实际上,对于单CPU的计算机来说,在CPU中,同一时间是只能干一件事儿的。为了看起来像是“同时干多件事”,分时操作系统是把CPU的时间划分成长短基本相同的时间区间,即”时间片”,通过操作系统的管理,把这些时间片依次轮流地分配给各个用户使用。 + +如果某个作业在时间片结束之前,整个任务还没有完成,那么该作业就被暂停下来,放弃CPU,等待下一轮循环再继续做.此时CPU又分配给另一个作业去使用。 + +由于计算机的处理速度很快,只要时间片的间隔取得适当,那么一个用户作业从用完分配给它的一个时间片到获得下一个CPU时间片,中间有所”停顿”,但用户察觉不出来,好像整个系统全由它”独占”似的。 + +所以,在单CPU的计算机中,我们看起来“同时干多件事”,其实是通过CPU时间片技术,并发完成的。 + +提到并发,还有另外一个词容易和他混淆,那就是并行。 + +## 并发与并行之间的关系 + +并行(Parallel),当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。 + +Erlang 之父 Joe Armstrong 用一张比较形象的图解释了并发与并行的区别: + + + +并发是两个队伍交替使用一台咖啡机。并行是两个队伍同时使用两台咖啡机。 + +映射到计算机系统中,上图中的咖啡机就是CPU,两个队伍指的就是两个进程。 + +# 多线程 + +## 进程和线程 + +理解了并发和并行之间的关系和区别后,我们再回到前面介绍的多任务分时操作系统,看看CPU是如何进行进程调度的。 + +为了看起来像是“同时干多件事”,分时操作系统是把CPU的时间划分成长短基本相同的”时间片”,通过操作系统的管理,把这些时间片依次轮流地分配给各个用户的各个任务使用。 + +在多任务处理系统中,CPU需要处理所有程序的操作,当用户来回切换它们时,需要记录这些程序执行到哪里。在操作系统中,CPU切换到另一个进程需要保存当前进程的状态并恢复另一个进程的状态:当前运行任务转为就绪(或者挂起、删除)状态,另一个被选定的就绪任务成为当前任务。**上下文切换**就是这样一个过程,他允许CPU记录并恢复各种正在运行程序的状态,使它能够完成切换操作。 + +> 在上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。从这个角度来看,上下文切换有点像我们同时阅读几本书,在来回切换书本的同时我们需要记住每本书当前读到的页码。在程序中,上下文切换过程中的“页码”信息是保存在进程控制块(PCB)中的。PCB还经常被称作“切换帧”(switchframe)。“页码”信息会一直保存到CPU的内存中,直到他们被再次使用。 + +对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。 + +而在多个进程之间切换的时候,需要进行上下文切换。但是上下文切换势必会耗费一些资源。于是人们考虑,能不能在一个进程中增加一些“子任务”,这样减少上下文切换的成本。比如我们使用Word的时候,它可以同时进行打字、拼写检查、字数统计等,这些子任务之间共用同一个进程资源,但是他们之间的切换不需要进行上下文切换。 + +在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。 + +随着时间的慢慢发展,人们进一步的切分了进程和线程之间的职责。**把进程当做资源分配的基本单元,把线程当做执行的基本单元,同一个进程的多个线程之间共享资源** + +拿我们比较熟悉的Java语言来说,Java程序是运行在JVM上面的,每一个JVM其实就是一个进程。所有的资源分配都是基于JVM进程来的。而在这个JVM进程中,又可以创建出很多线程,多个线程之间共享JVM资源,并且多个线程可以并发执行。 + +# 共享变量 + +所谓共享变量,指的是多个线程都可以操作的变量。 + +前面我们提到过,进程视分配资源的基本单位,线程是执行的基本单位。所以,多个线程之间是可以共享一部分进程中的数据的。在JVM中,Java堆和方法区的区域是多个线程共享的数据区域。也就是说,多个线程可以操作保存在堆或者方法区中的同一个数据。那么,换句话说,保存在堆和方法区中的变量就是Java中的共享变量。 + +那么,Java中哪些变量是存放在堆中,哪些变量是存放在方法区中,又有哪些变量是存放在栈中的呢? + +## 类变量、成员变量和局部变量 + +Java中共有三种变量,分别是类变量、成员变量和局部变量。他们分别存放在JVM的方法区、堆内存和栈内存中。 + + /** + * @author Hollis + */ + public class Variables { + + /** + * 类变量 + */ + private static int a; + + /** + * 成员变量 + */ + private int b; + + /** + * 局部变量 + * @param c + */ + public void test(int c){ + int d; + } + } + + +上面定义的三个变量中,变量a就是类变量,变量b就是成员变量,而变量c和d是局部变量。 + +所以,变量a和b是共享变量,变量c和d是非共享变量。所以如果遇到多线程场景,对于变量a和b的操作是需要考虑线程安全的,而对于线程c和d的操作是不需要考虑线程安全的。 + +# 小结 + +在了解了一些基础知识以后,我们再来回过头看看线程安全的定义: + +> 线程安全是编程中的术语,指某个函数、函数库在**并发**环境中被调用时,能够正确地处理**多个线程**之间的**共享变量**,使程序功能正确完成。 + +现在我们知道了什么是并发环境,什么是多个线程以及什么是共享变量。那么只要我们在编写多线程的代码的时候注意一下,保证程序功能可以正确的执行就行了。 + +那么问题来了,定义中说线程安全能够**正确地处理**多个线程之间的共享变量,使程序功能**正确完成**。 + +多线程场景中存在哪些问题会导致无法正确的处理共享变量? 多线程场景中存在哪些问题会导致程序无法正确完成? 如何解决多线程场景中影响『正确』的这些问题? 解决这些问题的各个手段的实现原理又是什么? + + [1]: http://www.hollischuang.com/archives/3029 + [2]: http://www.hollischuang.com/archives/tag/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B \ No newline at end of file diff --git a/docs/basics/concurrent-coding/why-not-executors.md b/docs/basics/concurrent-coding/why-not-executors.md new file mode 100644 index 00000000..9460028a --- /dev/null +++ b/docs/basics/concurrent-coding/why-not-executors.md @@ -0,0 +1,153 @@ +在《[深入源码分析Java线程池的实现原理][1]》这篇文章中,我们介绍过了Java中线程池的常见用法以及基本原理。 + +在文中有这样一段描述: + +> 可以通过Executors静态工厂构建线程池,但一般不建议这样使用。 + +关于这个问题,在那篇文章中并没有深入的展开。作者之所以这么说,是因为这种创建线程池的方式有很大的隐患,稍有不慎就有可能导致线上故障,如:一次Java线程池误用引发的血案和总结( ) + +本文我们就来围绕这个问题来分析一下为什么JDK自身提供的构建线程池的方式并不建议使用?到底应该如何创建一个线程池呢? + +### Executors + +Executors 是一个Java中的工具类。提供工厂方法来创建不同类型的线程池。 + +![][2] + +从上图中也可以看出,Executors的创建线程池的方法,创建出来的线程池都实现了ExecutorService接口。常用方法有以下几个: + +`newFiexedThreadPool(int Threads)`:创建固定数目线程的线程池。 + +`newCachedThreadPool()`:创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果没有可用的线程,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。 + +`newSingleThreadExecutor()`创建一个单线程化的Executor。 + +`newScheduledThreadPool(int corePoolSize)`创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。 + +类看起来功能还是比较强大的,又用到了工厂模式、又有比较强的扩展性,重要的是用起来还比较方便,如: + +
ExecutorService executor = Executors.newFixedThreadPool(nThreads) ;
+
+ +即可创建一个固定大小的线程池。 + +但是为什么我说不建议大家使用这个类来创建线程池呢? + +我提到的是『不建议』,但是在阿里巴巴Java开发手册中也明确指出,而且用的词是『不允许』使用Executors创建线程池。 + + +### Executors存在什么问题 + +在阿里巴巴Java开发手册中提到,使用Executors创建线程池可能会导致OOM(OutOfMemory ,内存溢出),但是并没有说明为什么,那么接下来我们就来看一下到底为什么不允许使用Executors? + +我们先来一个简单的例子,模拟一下使用Executors导致OOM的情况。 + +
/**
+ * @author Hollis
+ */
+public class ExecutorsDemo {
+    private static ExecutorService executor = Executors.newFixedThreadPool(15);
+    public static void main(String[] args) {
+        for (int i = 0; i < Integer.MAX_VALUE; i++) {
+            executor.execute(new SubThread());
+        }
+    }
+}
+
+class SubThread implements Runnable {
+    @Override
+    public void run() {
+        try {
+            Thread.sleep(10000);
+        } catch (InterruptedException e) {
+            //do nothing
+        }
+    }
+}
+
+ +通过指定JVM参数:`-Xmx8m -Xms8m` 运行以上代码,会抛出OOM: + +
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
+    at java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)
+    at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
+    at com.hollis.ExecutorsDemo.main(ExecutorsDemo.java:16)
+
+ +以上代码指出,`ExecutorsDemo.java`的第16行,就是代码中的`executor.execute(new SubThread());`。 + +### Executors为什么存在缺陷 + +通过上面的例子,我们知道了`Executors`创建的线程池存在OOM的风险,那么到底是什么原因导致的呢?我们需要深入`Executors`的源码来分析一下。 + +其实,在上面的报错信息中,我们是可以看出蛛丝马迹的,在以上的代码中其实已经说了,真正的导致OOM的其实是`LinkedBlockingQueue.offer`方法。 + +
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
+    at java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)
+    at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
+    at com.hollis.ExecutorsDemo.main(ExecutorsDemo.java:16)
+
+ +如果读者翻看代码的话,也可以发现,其实底层确实是通过`LinkedBlockingQueue`实现的: + +
public static ExecutorService newFixedThreadPool(int nThreads) {
+        return new ThreadPoolExecutor(nThreads, nThreads,
+                                      0L, TimeUnit.MILLISECONDS,
+                                      new LinkedBlockingQueue<Runnable>());
+
+ +如果读者对Java中的阻塞队列有所了解的话,看到这里或许就能够明白原因了。 + +Java中的`BlockingQueue`主要有两种实现,分别是`ArrayBlockingQueue` 和 `LinkedBlockingQueue`。 + +`ArrayBlockingQueue`是一个用数组实现的有界阻塞队列,必须设置容量。 + +`LinkedBlockingQueue`是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为`Integer.MAX_VALUE`。 + +这里的问题就出在:**不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。**也就是说,如果我们不设置`LinkedBlockingQueue`的容量的话,其默认容量将会是`Integer.MAX_VALUE`。 + +而`newFixedThreadPool`中创建`LinkedBlockingQueue`时,并未指定容量。此时,`LinkedBlockingQueue`就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。 + +上面提到的问题主要体现在`newFixedThreadPool`和`newSingleThreadExecutor`两个工厂方法上,并不是说`newCachedThreadPool`和`newScheduledThreadPool`这两个方法就安全了,这两种方式创建的最大线程数可能是`Integer.MAX_VALUE`,而创建这么多线程,必然就有可能导致OOM。 + +### 创建线程池的正确姿势 + +避免使用Executors创建线程池,主要是避免使用其中的默认实现,那么我们可以自己直接调用`ThreadPoolExecutor`的构造函数来自己创建线程池。在创建的同时,给`BlockQueue`指定容量就可以了。 + +
private static ExecutorService executor = new ThreadPoolExecutor(10, 10,
+        60L, TimeUnit.SECONDS,
+        new ArrayBlockingQueue(10));
+
+ +这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出`java.util.concurrent.RejectedExecutionException`,这是因为当前线程池使用的队列是有边界队列,队列已经满了便无法继续处理新的请求。但是异常(Exception)总比发生错误(Error)要好。 + +除了自己定义`ThreadPoolExecutor`外。还有其他方法。这个时候第一时间就应该想到开源类库,如apache和guava等。 + +作者推荐使用guava提供的ThreadFactoryBuilder来创建线程池。 + +
public class ExecutorsDemo {
+
+    private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
+        .setNameFormat("demo-pool-%d").build();
+
+    private static ExecutorService pool = new ThreadPoolExecutor(5, 200,
+        0L, TimeUnit.MILLISECONDS,
+        new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
+
+    public static void main(String[] args) {
+
+        for (int i = 0; i < Integer.MAX_VALUE; i++) {
+            pool.execute(new SubThread());
+        }
+    }
+}
+
+ +通过上述方式创建线程时,不仅可以避免OOM的问题,还可以自定义线程名称,更加方便的出错的时候溯源。 + +思考题,文中作者说:发生异常(Exception)要比发生错误(Error)好,为什么这么说? + +文中提到的《阿里巴巴Java开发手册》,请关注公众号Hollis,回复:手册。即可获得完整版PDF。 + + [1]: https://mp.weixin.qq.com/s/-89-CcDnSLBYy3THmcLEdQ + [2]: http://www.hollischuang.com/wp-content/uploads/2018/10/15406248096737.jpg \ No newline at end of file diff --git a/docs/basics/java-basic/enum-class.md b/docs/basics/java-basic/enum-class.md index 58ad1bdb..159fe15a 100644 --- a/docs/basics/java-basic/enum-class.md +++ b/docs/basics/java-basic/enum-class.md @@ -11,4 +11,4 @@ Java中定义枚举是使用enum关键字的,但是Java中其实还有一个ja 这个类我们在日常开发中不会用到,但是其实我们使用enum定义的枚举,其实现方式就是通过继承Enum类实现的。 -当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承。 \ No newline at end of file +当我们使用enum来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承。 \ No newline at end of file diff --git a/docs/basics/java-basic/enum-singleton.md b/docs/basics/java-basic/enum-singleton.md index 089bd57a..05164b23 100644 --- a/docs/basics/java-basic/enum-singleton.md +++ b/docs/basics/java-basic/enum-singleton.md @@ -39,14 +39,14 @@ private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { - if (singleton == null) { - synchronized (Singleton.class) { if (singleton == null) { - singleton = new Singleton(); + synchronized (Singleton.class) { + if (singleton == null) { + singleton = new Singleton(); + } + } } - } - } - return singleton; + return singleton; } } diff --git a/docs/basics/java-basic/final-string.md b/docs/basics/java-basic/final-string.md index 920b8e0c..a2e2e05b 100644 --- a/docs/basics/java-basic/final-string.md +++ b/docs/basics/java-basic/final-string.md @@ -23,7 +23,7 @@ s = s.concat("ef"); 所以,一旦一个string对象在内存(堆)中被创建出来,他就无法被修改。而且,String类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。 -如果我们想要一个可秀改的字符串,可以选择StringBuffer 或者 StringBuilder这两个代替String。 +如果我们想要一个可修改的字符串,可以选择StringBuffer 或者 StringBuilder这两个代替String。 ### 为什么String要设计成不可变 @@ -70,7 +70,7 @@ String s2 = s; 当我们在程序中传递一个字符串的时候,如果这个字符串的内容是不可变的,那么我们就可以相信这个字符串中的内容。 -但是,如果是可变的,那么这个字符串内容就可能随时都被修改。那么这个字符串内容就完全可信了。这样整个系统就没有安全性可言了。 +但是,如果是可变的,那么这个字符串内容就可能随时都被修改。那么这个字符串内容就完全不可信了。这样整个系统就没有安全性可言了。 #### 线程安全 diff --git a/docs/basics/java-basic/hash-in-hashmap.md b/docs/basics/java-basic/hash-in-hashmap.md index ddb52259..4cc4f7e6 100644 --- a/docs/basics/java-basic/hash-in-hashmap.md +++ b/docs/basics/java-basic/hash-in-hashmap.md @@ -79,7 +79,7 @@ } -前面我说过,`indexFor`方法其实主要是将hash生成的整型转换成链表数组中的下标。那么`return h & (length-1);`是什么意思呢?其实,他就是取模。Java之所有使用位运算(&)来代替取模运算(%),最主要的考虑就是效率。**位运算(&)效率要比代替取模运算(%)高很多,主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。** +前面我说过,`indexFor`方法其实主要是将hash生成的整型转换成链表数组中的下标。那么`return h & (length-1);`是什么意思呢?其实,他就是取模。Java之所以使用位运算(&)来代替取模运算(%),最主要的考虑就是效率。**位运算(&)效率要比代替取模运算(%)高很多,主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。** 那么,为什么可以使用位运算(&)来实现取模运算(%)呢?这实现的原理如下: diff --git a/docs/basics/java-basic/hashmap-default-capacity.md b/docs/basics/java-basic/hashmap-default-capacity.md index cb561673..423d843f 100644 --- a/docs/basics/java-basic/hashmap-default-capacity.md +++ b/docs/basics/java-basic/hashmap-default-capacity.md @@ -194,7 +194,7 @@ Step 1 怎么理解呢?其实是对一个二进制数依次向右移位,然 ### 扩容 -除了初始化的时候回指定HashMap的容量,在进行扩容的时候,其容量也可能会改变。 +除了初始化的时候会指定HashMap的容量,在进行扩容的时候,其容量也可能会改变。 HashMap有扩容机制,就是当达到扩容条件时会进行扩容。HashMap的扩容条件就是当HashMap中的元素个数(size)超过临界值(threshold)时就会自动扩容。 diff --git a/docs/basics/java-basic/length-of-string.md b/docs/basics/java-basic/length-of-string.md index 7c3fcceb..6b9d37a3 100644 --- a/docs/basics/java-basic/length-of-string.md +++ b/docs/basics/java-basic/length-of-string.md @@ -134,4 +134,4 @@ int 是一个 32 位变量类型,取正数部分来算的话,他们最长可 在运行期,长度不能超过Int的范围,否则会抛异常。 -最后,这个知识点 ,我录制了视频(https://www.bilibili.com/video/BV1uK4y1t7H1/),其中有关于如何进行实验测试、如何查阅Java规范以及如何对javac进行deubg的技巧。欢迎进一步学习。 +最后,这个知识点 ,我录制了视频([点击跳转](https://www.bilibili.com/video/BV1uK4y1t7H1/)) ,其中有关于如何进行实验测试、如何查阅Java规范以及如何对javac进行deubg的技巧。欢迎进一步学习。 diff --git a/docs/basics/java-basic/stop-create-bigdecimal-with-double.md b/docs/basics/java-basic/stop-create-bigdecimal-with-double.md new file mode 100644 index 00000000..ee5eed79 --- /dev/null +++ b/docs/basics/java-basic/stop-create-bigdecimal-with-double.md @@ -0,0 +1,163 @@ +很多人都知道,在进行金额表示、金额计算等场景,不能使用double、float等类型,而是要使用对精度支持的更好的BigDecimal。 + +所以,很多支付、电商、金融等业务中,BigDecimal的使用非常频繁。**但是,如果误以为只要使用BigDecimal表示数字,结果就一定精确,那就大错特错了!** + +在之前的一篇文章中,我们介绍过,使用BigDecimal的equals方法并不能验证两个数是否真的相等([为什么阿里巴巴禁止使用BigDecimal的equals方法做等值比较?][1])。 + +除了这个情况,BigDecimal的使用的第一步就是创建一个BigDecimal对象,如果这一步都有问题,那么后面怎么算都是错的! + +那到底应该如何正确的创建一个BigDecimal? + +**关于这个问题,我Review过很多代码,也面试过很多一线开发,很多人都掉进坑里过。这是一个很容易被忽略,但是又影响重大的问题。** + +关于这个问题,在《阿里巴巴Java开发手册》中有一条建议,或者说是要求: + +![][2] + +这是一条【强制】建议,那么,这背后的原理是什么呢? + +想要搞清楚这个问题,主要需要弄清楚以下几个问题: + +1、为什么说double不精确? 2、BigDecimal是如何保证精确的? + +在知道这两个问题的答案之后,我们也就大概知道为什么不能使用BigDecimal(double)来创建一个BigDecimal了。 + +### double为什么不精确 + +首先,**计算机是只认识二进制的**,即0和1,这个大家一定都知道。 + +那么,所有数字,包括整数和小数,想要在计算机中存储和展示,都需要转成二进制。 + +**十进制整数转成二进制很简单,通常采用"除2取余,逆序排列"即可,如10的二进制为1010。** + +但是,小数的二进制如何表示呢? + +十进制小数转成二进制,一般采用"乘2取整,顺序排列"方法,如0.625转成二进制的表示为0.101。 + +但是,并不是所有小数都能转成二进制,如0.1就不能直接用二进制表示,他的二进制是0.000110011001100… 这是一个无限循环小数。 + +**所以,计算机是没办法用二进制精确的表示0.1的。也就是说,在计算机中,很多小数没办法精确的使用二进制表示出来。** + +那么,这个问题总要解决吧。那么,**人们想出了一种采用一定的精度,使用近似值表示一个小数的办法**。这就是IEEE 754(IEEE二进制浮点数算术标准)规范的主要思想。 + +IEEE 754规定了多种表示浮点数值的方式,其中最常用的就是32位单精度浮点数和64位双精度浮点数。 + +在Java中,使用float和double分别用来表示单精度浮点数和双精度浮点数。 + +所谓精度不同,可以简单的理解为保留有效位数不同。采用保留有效位数的方式近似的表示小数。 + +所以,大家也就知道为什么**double表示的小数不精确**了。 + +接下来,再回到BigDecimal的介绍,我们接下来看看是如何表示一个数的,他如何保证精确呢? + +### BigDecimal如何精确计数? + +如果大家看过BigDecimal的源码,其实可以发现,**实际上一个BigDecimal是通过一个"无标度值"和一个"标度"来表示一个数的。** + +在BigDecimal中,标度是通过scale字段来表示的。 + +而无标度值的表示比较复杂。当unscaled value超过阈值(默认为Long.MAX_VALUE)时采用intVal字段存储unscaled value,intCompact字段存储Long.MIN_VALUE,否则对unscaled value进行压缩存储到long型的intCompact字段用于后续计算,intVal为空。 + +涉及到的字段就是这几个: + + public class BigDecimal extends Number implements Comparable { + private final BigInteger intVal; + private final int scale; + private final transient long intCompact; + } + + +关于无标度值的压缩机制大家了解即可,不是本文的重点,大家只需要知道BigDecimal主要是通过一个无标度值和标度来表示的就行了。 + +**那么标度到底是什么呢?** + +除了scale这个字段,在BigDecimal中还提供了scale()方法,用来返回这个BigDecimal的标度。 + + /** + * Returns the scale of this {@code BigDecimal}. If zero + * or positive, the scale is the number of digits to the right of + * the decimal point. If negative, the unscaled value of the + * number is multiplied by ten to the power of the negation of the + * scale. For example, a scale of {@code -3} means the unscaled + * value is multiplied by 1000. + * + * @return the scale of this {@code BigDecimal}. + */ + public int scale() { + return scale; + } + + +那么,scale到底表示的是什么,其实上面的注释已经说的很清楚了: + +> 如果scale为零或正值,则该值表示这个数字小数点右侧的位数。如果scale为负数,则该数字的真实值需要乘以10的该负数的绝对值的幂。例如,scale为-3,则这个数需要乘1000,即在末尾有3个0。 + +如123.123,那么如果使用BigDecimal表示,那么他的无标度值为123123,他的标度为3。 + +**而二进制无法表示的0.1,使用BigDecimal就可以表示了,及通过无标度值1和标度1来表示。** + +我们都知道,想要创建一个对象,需要使用该类的构造方法,在BigDecimal中一共有以下4个构造方法: + + BigDecimal(int) + BigDecimal(double) + BigDecimal(long) + BigDecimal(String) + + +以上四个方法,创建出来的的BigDecimal的标度(scale)是不同的。 + +其中 BigDecimal(int)和BigDecimal(long) 比较简单,因为都是整数,所以他们的标度都是0。 + +而BigDecimal(double) 和BigDecimal(String)的标度就有很多学问了。 + +### BigDecimal(double)有什么问题 + +BigDecimal中提供了一个通过double创建BigDecimal的方法——BigDecimal(double) ,但是,同时也给我们留了一个坑! + +因为我们知道,double表示的小数是不精确的,如0.1这个数字,double只能表示他的近似值。 + +所以,**当我们使用new BigDecimal(0.1)创建一个BigDecimal 的时候,其实创建出来的值并不是正好等于0.1的。** + +而是0.1000000000000000055511151231257827021181583404541015625。这是因为doule自身表示的只是一个近似值。 + +![][3] + +**所以,如果我们在代码中,使用BigDecimal(double) 来创建一个BigDecimal的话,那么是损失了精度的,这是极其严重的。** + +### 使用BigDecimal(String)创建 + +那么,该如何创建一个精确的BigDecimal来表示小数呢,答案是使用String创建。 + +而对于BigDecimal(String) ,当我们使用new BigDecimal("0.1")创建一个BigDecimal 的时候,其实创建出来的值正好就是等于0.1的。 + +那么他的标度也就是1。 + +但是需要注意的是,new BigDecimal("0.10000")和new BigDecimal("0.1")这两个数的标度分别是5和1,如果使用BigDecimal的equals方法比较,得到的结果是false,具体原因和解决办法参考[为什么阿里巴巴禁止使用BigDecimal的equals方法做等值比较?][1] + +那么,想要创建一个能精确的表示0.1的BigDecimal,请使用以下两种方式: + + BigDecimal recommend1 = new BigDecimal("0.1"); + BigDecimal recommend2 = BigDecimal.valueOf(0.1); + + +这里,留一个思考题,BigDecimal.valueOf()是调用Double.toString方法实现的,那么,既然double都是不精确的,BigDecimal.valueOf(0.1)怎么保证精确呢? + +### 总结 + +因为计算机采用二进制处理数据,但是很多小数,如0.1的二进制是一个无线循环小数,而这种数字在计算机中是无法精确表示的。 + +所以,人们采用了一种通过近似值的方式在计算机中表示,于是就有了单精度浮点数和双精度浮点数等。 + +所以,作为单精度浮点数的float和双精度浮点数的double,在表示小数的时候只是近似值,并不是真实值。 + +所以,当使用BigDecimal(Double)创建一个的时候,得到的BigDecimal是损失了精度的。 + +而使用一个损失了精度的数字进行计算,得到的结果也是不精确的。 + +想要避免这个问题,可以通过BigDecimal(String)的方式创建BigDecimal,这样的情况下,0.1就会被精确的表示出来。 + +其表现形式是一个无标度数值1,和一个标度1的组合。 + + [1]: https://mp.weixin.qq.com/s/iiZW9xr1Xb2JIaRFnWLZUg + [2]: https://www.hollischuang.com/wp-content/uploads/2021/01/16119907257353.jpg + [3]: https://www.hollischuang.com/wp-content/uploads/2021/01/16119945021181.jpg \ No newline at end of file diff --git a/docs/basics/java-basic/syntactic-sugar.md b/docs/basics/java-basic/syntactic-sugar.md index 8c085333..fb2a1471 100644 --- a/docs/basics/java-basic/syntactic-sugar.md +++ b/docs/basics/java-basic/syntactic-sugar.md @@ -268,7 +268,7 @@ Java SE5提供了一种新的类型-Java的枚举类型,关键字`enum`可以 } -通过反编译后代码我们可以看到,`public final class T extends Enum`,说明,该类是继承了`Enum`类的,同时`final`关键字告诉我们,这个类也是不能被继承的。**当我们使用`enmu`来定义一个枚举类型的时候,编译器会自动帮我们创建一个`final`类型的类继承`Enum`类,所以枚举类型不能被继承。** +通过反编译后代码我们可以看到,`public final class T extends Enum`,说明,该类是继承了`Enum`类的,同时`final`关键字告诉我们,这个类也是不能被继承的。**当我们使用`enum`来定义一个枚举类型的时候,编译器会自动帮我们创建一个`final`类型的类继承`Enum`类,所以枚举类型不能被继承。** ### 糖块六 、 内部类 diff --git a/docs/basics/object-oriented/java-pass-by.md b/docs/basics/object-oriented/java-pass-by.md index c05138bc..7d2abf93 100644 --- a/docs/basics/object-oriented/java-pass-by.md +++ b/docs/basics/object-oriented/java-pass-by.md @@ -56,7 +56,7 @@ * 传值调用(值传递) * 在传值调用中,实际参数先被求值,然后其值通过复制,被传递给被调函数的形式参数。因为形式参数拿到的只是一个"局部拷贝",所以如果在被调函数中改变了形式参数的值,并不会改变实际参数的值。 -* 传引用调用(应用传递) +* 传引用调用(引用传递) * 在传引用调用中,传递给函数的是它的实际参数的隐式引用而不是实参的拷贝。因为传递的是引用,所以,如果在被调函数中改变了形式参数的值,改变对于调用者来说是可见的。 * 传共享对象调用(共享对象传递) * 传共享对象调用中,先获取到实际参数的地址,然后将其复制,并把该地址的拷贝传递给被调函数的形式参数。因为参数的地址都指向同一个对象,所以我们称也之为"传共享对象",所以,如果在被调函数中改变了形式参数的值,调用者是可以看到这种变化的。 diff --git a/docs/basics/object-oriented/platform-independent.md b/docs/basics/object-oriented/platform-independent.md index 880d0a44..dd2f0790 100644 --- a/docs/basics/object-oriented/platform-independent.md +++ b/docs/basics/object-oriented/platform-independent.md @@ -118,5 +118,5 @@ Java的平台无关性是建立在Java虚拟机的平台有关性基础之上的 [5]: https://www.hollischuang.com/wp-content/uploads/2019/03/15539291533175.jpg [6]: https://www.hollischuang.com/wp-content/uploads/2019/03/15539297082025.jpg [7]: https://www.hollischuang.com/wp-content/uploads/2019/03/15539303829914.jpg - [8]: http://www.hollischuang.com/wp-content/uploads/2019/03/Jietu20200614-165222.jpg + [8]: https://www.hollischuang.com/wp-content/uploads/2021/06/Jietu20210627-141259-2.jpg [9]: https://www.hollischuang.com/archives/2938 diff --git a/docs/basics/object-oriented/why-pass-by-reference.md b/docs/basics/object-oriented/why-pass-by-reference.md index 81d37c9d..f76bb96f 100644 --- a/docs/basics/object-oriented/why-pass-by-reference.md +++ b/docs/basics/object-oriented/why-pass-by-reference.md @@ -87,6 +87,7 @@ public void pass(User user) { user = new User(); user.setName("hollischuang"); + user.setGender("Male"); System.out.println("print in pass , user is " + user); } @@ -136,4 +137,4 @@ OK,以上就是本文的全部内容,不知道本文是否帮助你解开了 [3]: https://docs.oracle.com/javase/tutorial/java/javaOO/arguments.html [4]: https://en.wikipedia.org/wiki/Evaluation_strategy [5]: https://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value - [6]: https://blog.penjee.com/passing-by-value-vs-by-reference-java-graphical/ \ No newline at end of file + [6]: https://blog.penjee.com/passing-by-value-vs-by-reference-java-graphical/ diff --git a/docs/contact/book.jpeg b/docs/contact/book.jpeg new file mode 100644 index 00000000..73a855ec Binary files /dev/null and b/docs/contact/book.jpeg differ diff --git a/docs/menu.md b/docs/menu.md index f034f328..6f69aef5 100644 --- a/docs/menu.md +++ b/docs/menu.md @@ -2,9 +2,13 @@ ![](https://img.shields.io/badge/version-v2.0.0-green.svg) ![](https://img.shields.io/badge/author-Hollis-yellow.svg) ![](https://img.shields.io/badge/license-GPL-blue.svg) +成神之路系列丛书的第一本《深入理解Java核心技术(基础篇)》已经正式出版了,这本书囊括了中基础篇的几乎全部内容,欢迎大家购买品鉴。 + +![](contact/book.jpeg) | 主要版本 | 更新时间 | 备注 | | ---- | ---------- | -------------- | +| v4.0 | 2022-05-20 | 知识体系完善,知识点补充| | v3.0 | 2020-03-31 | 知识体系完善,在v2.0的基础上,新增20%左右的知识点
调整部分知识的顺序及结构,方便阅读和理解
通过GitHub Page搭建,便于阅读| | v2.0 | 2019-02-19 | 结构调整,更适合从入门到精通;
进一步完善知识体系;
新技术补充;| | v1.1 | 2018-03-12 | 增加新技术知识、完善知识体系 | @@ -319,7 +323,7 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer) * [什么是泛型](/basics/java-basic/generics.md) - * [类型擦除](/basics/java-basic/type-erasue.md) + * [类型擦除](/basics/java-basic/type-erasure.md) * [泛型带来的问题](/basics/java-basic/generics-problem.md) @@ -457,7 +461,7 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer) * [为什么不能使用BigDecimal的equals比较大小](/basics/java-basic/stop-using-equlas-in-bigdecimal.md) - * 为什么不能直接使用double创建一个BigDecimal + * [为什么不能直接使用double创建一个BigDecimal](/basics/java-basic/stop-create-bigdecimal-with-double.md) * Java 8 @@ -535,11 +539,11 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer) * 线程池原理 - * 为什么不允许使用Executors创建线程池 + * [为什么不允许使用Executors创建线程池](/basics/concurrent-coding/why-not-executors.md) * 线程安全 - * 什么是线程安全 + * [什么是线程安全](/basics/concurrent-coding/thread-safe.md) * 多级缓存和一致性问题 diff --git a/pics/book.jpeg b/pics/book.jpeg new file mode 100644 index 00000000..73a855ec Binary files /dev/null and b/pics/book.jpeg differ