Stephanie Tang's Blog


  • 首页

  • 归档

  • 标签

HTTPS的工作原理

发表于 2020-04-19

当你打开浏览器,访问某个网站,如果网址旁有个小锁,代表访问的网址是安全的,反之不安全。当我们没有看到那个小锁的小图标的时候,需要提高警惕,不要随意输入个人重要的资料。所有的银行和支付相关的网站都是100%使用HTTPS的。

google

我们为什么需要HTTPS?

主要有三个原因:

  1. 保护隐私(Privacy):所有信息都是加密传播,第三方无法窃听数据。如果使用HTTP明文传输数据的话,很可能被第三方劫持数据,那么所输入的密码或者其他个人资料都被暴露在他人面前,后果可想而知。
  2. 数据完整性(Integraty):一旦第三方篡改了数据,接收方会知道数据经过了篡改,这样便保证了数据在传输过程中不被篡改 —— 数据的完整性。
  3. 身份认证(Identification):第三方不可能冒充身份参与通信,因为服务器配备了由证书颁发机构(Certificate Authority,简称CA)颁发的安全证书,可以证实服务器的身份信息,防止第三方冒充身份。(也有少数情况下,通信需要客户端提供证书,例如银行系统,需要用户在登录的时候,插入银行提供给用户的USB,就是需要客户端提供证书,用来验证客户的身份信息。)

HTTPS是什么?SSL/TLS是什么?

  • HTTP协议(HyperText Transfer Protocol,超文本传输协议)是大家最熟悉的一种协议,它是一个用于传输超媒体文档的协议,它位于OSI网络协议模型的应用层。但是它是明文传输协议,是非常不安全的,容易被人篡改和窃取数据。

  • SSL(Secure Socket Layer) —— 网景(Netscape)公司设计的主要用于web的安全传输协议。它位于TCP传输层协议和应用层协议之间。(它没有被划分在OSI网络协议模型中,且认为它是应用层的子层。)

  • 众所周知,网景公司20世纪90年代在和微软的竞争中最终败下阵来,之后网景公司将SSL协议的管理权转交给IETF(Internet Engineering Task Force, www.ietf.org)。于是IETF将SSL作了标准化,重新命名为TLS(Transport Layer Security)。在1999年,TLS 1.0诞生了(其实也就是SSL 3.1)。

  • HTTPS(HyperText Transfer Protocol Secure)是建立在SSL/TLS协议之上,信息通信通过SSL/TLS进行加密,最后一个S就是Secure的缩写,代表安全的意思,HTTPS = HTTP + SSL/TLS。

HTTP v.s. HTTPS

SSL/TLS发展史

SSL

TLS 1.0/1.1/1.2

2013年各大浏览器才开始支持TLS1.2

2018年3月发布了TLS 1.3

  • 实际上现代的浏览器已经基本不使用SSL,使用的都是TLS,SSL 3.0于2015年已经寿终正寝 —— 各大浏览器也不支持了。但是由于SSL这个术语存在的时间太长,很多地方还是广泛的使用它,但是要清楚其实它说的是TLS。

  • 有调查显示现在绝大部分浏览器(> 99.5%)都使用TLS 1.2或者TLS 1.3。只有不足1%的浏览器仍然使用TLS 1.0或者TLS 1.1。

  • TLS 1.2仍然是主流协议(本文写于2020年初),相信将来逐渐TLS 1.3将会作为主流协议。

  • 很多浏览器将会开始不支持TLS 1.0和1.1:

    • Google将在Chrome 72中不推荐使用TLS 1.0和1.1,而Chrome 81之后将会完全不支持。
    • Mozilla的Firefox,微软的Edge和IE以及苹果的Safari都会分别于2020年逐渐移除对TLS 1.0和1.1的支持。
  • 那么一些还在使用TLS 1.0和1.1的网站就得被迫升级到TLS 1.2或者TLS 1.3。

  • 要关闭浏览器对TLS 1.0和1.1的支持,可以在Internet选项中修改:

Internet选项

SSL/TLS的工作原理

需要理解SSL/TLS的工作原理,我们需要掌握加密算法。加密算法有两种:对称加密和非对称加密:

  • 对称加密:通信双方使用相同的密钥进行加密。特点是加密速度快,但是缺点是需要保护好密钥,如果密钥泄露的话,那么加密就会被别人破解。常见的对称加密有AES,DES算法。

  • 非对称加密:它需要生成两个密钥:公钥(Public Key)和私钥(Private Key)。公钥顾名思义是公开的,任何人都可以获得,而私钥是私人保管的。相信大多程序员已经对这种算法很熟悉了:我们提交代码到github的时候,就可以使用SSH key:在本地生成私钥和公钥,私钥放在本地.ssh目录中,公钥放在github网站上,这样每次提交代码,不用麻烦的输入用户名和密码了,github会根据网站上存储的公钥来识别我们的身份。公钥负责加密,私钥负责解密;或者,私钥负责加密,公钥负责解密。这种加密算法安全性更高,但是计算量相比对称加密大很多,加密和解密都很慢。常见的非对称算法有RSA。

SSL/TLS是利用了对称加密和非对称加密的特点。先来看下整个SSL/TLS的握手过程,之后我们再分步骤详细解读,每一步都干了些什么。

SSL/TLS握手

  1. 当TCP建立连接之后,TLS握手的第一步由客户端发起,发送ClientHello的消息到服务器。
    ClientHello消息包含:
  • 客户端支持的SSL/TLS版本
  • 客户端支持的加密套件(Cipher Suites)
  • 会话Idsession id(如果有的值的话,服务器端会复用对应的握手信息,避免短时间内重复握手)
  • 随机数client-random

延伸阅读:
加密套件名如:“TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA256”,这么长的名字看着有点晕吧,不用怕,其实它的命名非常规范,格式很固定。基本的形式是“密钥交换算法-服务身份验证算法-对称加密算法-握手校验算法”。
握手过程中,证书签名使用的RSA算法,如果证书验证正确,再使用ECDHE算法进行密钥交换,握手后的通信使用的是AES256的对称算法分组模式是GCM。验证证书签名合法性使用SHA256作哈希算法检验。相关的算法的用处将在后文中详解。

  1. 然后服务器端在收到这个ClientHello,从中选择服务器支持的版本和套件,发送ServerHello消息:
  • 服务器所能支持的最高SSL/TLS版本
  • 服务器选择的加密套件
  • 随机数server-random
  • 会话Idsession id(用于下次复用当前握手的信息,避免短时间内重复握手。)

随后服务器发送服务器的安全证书(含公钥)。

如果需要客户端也提供证书的话,还会发出客户端证书请求(Client Certificate Request),只有少数金融机构才需要客户端也提供客户端证书。

此后客户端发送Server Hello Done消息表示Hello阶段完成。

  1. 客户端收到ServerHello后,会对收到的证书进行验证。

我们来看一下为什么可以通过CA(Certificate Authority,证书颁发机构)签发的证书来确认网站的身份?

当我们安装操作系统或者浏览器的时候,会安装一组可信任的CA(根证书CA包括GlobalSign、GeoTrust、Verisign等)列表。根CA如GlobalSign就在我们的可信任的CA列表里,你的浏览器或者操作系统含有GlobalSign的公钥。

先来看一下Google的证书,当你访问Google的时候,Google会发给你它的证书。证书中包含颁发机构的签名以及服务器的公钥。

Google证书

浏览器首先用哈希函数对明文信息的摘要做哈希得到一个哈希值(用到的就是证书中的签名哈希算法SHA256),然后用根CA的公钥对根证书的签名作解密得到另一个哈希值(用到的算法就是RSA非对称算法),如果两个哈希值相等则说明证书没有被篡改过。当然还需校验证书中服务器名称是否合法以及验证证书是否过期.

签名的过程

这样就免受中间人攻击了,因为假如有中间人修改了证书的内容(如将证书中的公钥替换成自己的公钥),那么将获得不同的哈希值,从而两个哈希值不匹配导致验证失败。如果要绕过这个机制,中间人必须要也替换签名,使签名也相匹配。而做到这一点就需要破解到了根证书的密钥(而这是不可能的,中间人必然会失败)。浏览器会出现以下画面,告诉你正在遭受中间人攻击,因为证书被篡改了:

不信任网站

那聪明的你肯定也想到了,如果你开发了一个系统还在测试阶段,还没有正式申请一张证书,那么你可以为服务器自签名一张证书,然后将证书导入客户端的CA信任列表中。

信任链机制

Google证书

可以看到证书路径是GlobalSign Root CA-R2 -> GTS CA 1O1->*.google.com。因为我们的浏览器信任GlobalSign Root CA,根据信任链机制,你相信了根CA颁发的证书,也要相信它签名的子CA颁发的证书,也要相信子CA签名的子子CA的证书…而我们通过一级级的校验,如果从根证书到最下层的证书都没有被篡改过,我们就相信最下层的这个服务器证书是合法的。所以在这个机制中,你就需要无条件的相信根证书的颁发机构。

如果通过验证,客户端生成一个随机数pre-master,用于密钥交换过程。

  1. 密钥交换过程:客户端用第三步中服务器的证书中拿到服务器的公钥,用这个公钥加密(算法是加密套件中的密钥交换算法,譬如ECDHE算法)生成密文发送给服务器。

  2. 客户端用server-random + client-random + pre-master一起计算出对称密钥master secret。

  3. 服务器收到第四步的信息之后,用服务器的私钥对密文进行解密得到密钥pre-master。

因为只有服务器有私钥,可以针对客户端发出的加密过的信息进行解密得到pre-master,这样就保证了只有服务器和客户端知道pre-master。服务器端也可以用server-random + client-random + pre-master一起计算出对称密钥master secret。

现在客户端和服务器均有密钥master secret了,后面就可以用它来进行加密和解密了。

为什么不能只用一个pre-master作为之后加密的对称密钥?
虽然只有服务器有私钥,能够解密pre-master呀,但仅用它作为master secret是不够安全的,这是因为要以防客户端的pre-master并不是随机数的情况。加上另外两个随机数client-random以及server-random(而这两个随机数和时间有相关性),这样就能保证最后生成的master secret一定是随机数。

  1. 客户端用master secret加密了一条握手完成的消息发送给服务器。

  2. 服务器端也回发了一条用master secret加密的握手完成的消息。

  3. 当两方都收到对方发送的握手消息之后,也成功解密后,就可以用master secret愉快的开始数据加密和解密了。

综上,整个握手过程主要是通过一系列步骤通过非对称加密的算法交换得到了master secret,这个步骤通常需要几百毫秒,但是就是这一顿猛操作之后使得只有服务器和客户端知道master secret。之后的通信又利用了高效的对称算法对所有信息进行加密和解密,虽然加密和解密也需要耗时耗流量,不过信息是完全不可能被别人篡改和破解的,这一点损耗还是值得的。

参考

  • https://www.wst.space/ssl-part1-ciphersuite-hashing-encryption/
  • https://www.wst.space/ssl-part-2-diffie-hellman-key-exchange/
  • https://www.wst.space/ssl-part-3-certificate-authority/
  • https://www.wst.space/ssl-part-4-tls-handshake-protocol/
  • https://howhttps.works
  • http://www.moserware.com/2009/06/first-few-milliseconds-of-https.html
  • https://quguang.wang/post/geektime-network-protocol-https/
  • https://www.rrfed.com/2017/02/03/https/
  • https://segmentfault.com/a/1190000009486161

缓存模式以及缓存的数据一致性

发表于 2020-04-13

缓存由于其高性能,支持高并发的特性,在高并发的项目中不可或缺。被大家广泛使用的有Redis,Memcached等。本文主要探讨几种常见的缓存的读写模式,以及如何来保证缓存和数据库的数据一致性。

Cache-Aside

Cache-Aside可能是项目中最常见的一种模式。它是一种控制逻辑都实现在应用程序中的模式。缓存不和数据库直接进行交互,而是由应用程序来同时和缓存以及数据库打交道。Cache-Aside的名字正体现了这个模式,Cache在应用的一旁(aside)。

读数据时

  1. 程序需要判断缓存中是否已经存在数据。
  2. 当缓存中已经存在数据(也就是缓存命中,cache hit),则直接从缓存中返回数据
  3. 当缓存中不存在数据(也就是缓存未命中,cache miss),则先从数据库里读取数据,并且存入缓存,然后返回数据

Cache Aside Read

写数据时,我们可以有以下两种策略:

第一种策略:

  1. 更新数据库
  2. 更新缓存

但这种策略有线程安全的问题,可能出现缓存和数据库不一致。试想有两个写的线程,线程A和线程B

  1. A写数据库
  2. B后于A写数据库
  3. B写缓存
  4. A写缓存
  5. 缓存和数据库中的数据不一致,缓存中的是脏数据

要解决线程安全的问题,我们可以加锁,不过实现起来比较麻烦,因此我们不考虑这种写策略,而使用第二种策略。

第二种策略:

  1. 更新数据库
  2. 删除缓存中对应的数据

Cache Aside Write

那么这种写策略会有线程安全的问题吗?有,试想一下有两个线程,线程A读,线程B写

  1. A读数据,由于未命中那么从数据库中取数据
  2. B写数据库
  3. B删除缓存
  4. A由于网络延迟比较慢,将脏数据写入缓存

但是这种情况可能性非常的小,需要同时满足很多条件,近乎不太可能发生,所以我们一般都采用这种写策略。另外可以对缓存中的数据设置合适的过期时间,即使发生的脏数据的情况,也不会发生很长时间。

应用场景

应用于缓存不支持Read-Through/Write-Through的系统。

优点

  • 缓存仅仅保存被请求的数据,属于懒加载模式(Lazy Loading),和下文的Write-Through模式相比,避免了任何数据都被写入缓存造成缓存频繁的更新。

缺点

  • 当发生缓存未命中的情况时,则会比较慢,因为要经过三个步骤:查询缓存,从数据库读取,写入缓存。

  • 复杂的逻辑都在应用程序中,如果实现微服务,多个微服务中会有重复的逻辑代码

Read-Through/Write-Through

这种模式中,应用程序将缓存作为主要的数据源,而数据库对于应用程序是透明的,更新数据库和从数据库的读取的任务都交给缓存来代理了,所以对于应用程序来说,简单很多。

Read-Through

由缓存配置一个读模块,它知道如何将数据库中的数据写入缓存。在数据被请求的时候,如果未命中,则将数据从数据库载入缓存。

Read Through

Write-Through

缓存配置一个写模块,它知道如何将数据写入数据库。当应用要写入数据时,缓存会先存储数据,并调用写模块将数据写入数据库。

Write Through

应用场景

Read Through/Write Through适用于写入之后经常被读取的应用。

优点

  • 缓存不存在脏数据

  • 相比较Cache-Aside懒加载模式,读取速度更高,因为较少因为缓存未命中而从数据库中查找

  • 应用程序的逻辑相对简单

缺点

  • 对于总是写入却很少被读取的应用,那么Write-Through会非常浪费性能,因为数据可能更改了很多次,却没有被读取,白白的每次都写入缓存造成写入延迟。

除了Write-Through以外,我们还有另外的两种写模式可以和Read-Through一起来配合使用,分别是Write-Back和Write-Around。

Write-Back

又叫做Write-Behind。和Write-Through写入的时机不同,Write-Back将缓存作为可靠的数据源,每次都只写入缓存,而写入数据库则采用异步的方式,比如当数据要被移除出缓存的时候再存储到数据库或者一段时间之后批量更新数据库。

Write Back

应用场景

读写效率都非常好,写的时候因为异步存储到数据库,提升了写的效率,适用于读写密集的应用。

优点

  • 写入和读取数据都非常的快,因为都是从缓存中直接读取和写入。

  • 对于数据库不可用的情况有一定的容忍度,即使数据库暂时不可用,系统也整体可用,当数据库之后恢复的时候,再将数据写入数据库。

缺点

  • 有数据丢失的风险,如果缓存挂掉而数据没有及时写到数据库中,那么缓存中的有些数据将永久的丢失了

Write-Around

和Write-Through不同,更新的时候只写入数据库,不写入缓存,结合Read-Through或者Cache-Aside使用,只在缓存未命中的情况下写缓存。

Write Around

应用场景

适合于只写入一次而很少被读取的应用。

优点

  • 相比较Write-Through写入的时候的效率较高,如果数据写入后很少被读取,缓存也不会被没用到的数据占满。

缺点

  • 如果数据会写入多次,那么可能存在缓存和数据库不一致

参考

  • https://shahriar.svbtle.com/Understanding-writethrough-writearound-and-writeback-caching-with-python
  • https://www.cnblogs.com/rjzheng/p/9041659.html
  • https://docs.microsoft.com/en-us/azure/architecture/patterns/cache-aside
  • https://codeahoy.com/2017/08/11/caching-strategies-and-how-to-choose-the-right-one/
  • https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/Strategies.html
  • https://dzone.com/articles/using-read-through-amp-write-through-in-distribute
  • https://coolshell.cn/articles/17416.html

单例设计模式

发表于 2018-03-01

单例设计模式(Singleton Design Pattern)指的是实现一个类,在应用中只有唯一一个实例。但现在已经成为业界共识,它成为了反模式的代表。这篇文章先讨论实现单例模式的几种方式,然后讨论为什么它是一种反模式。以及它的应用场景。

我们先来讨论单例模式的几种实现方法。

饿汉式(Eager Initialization)

1
2
3
4
5
6
7
8
9
10
public class EagerInitializationSingleton {
private static final EagerInitializationSingleton instance = new EagerInitializationSingleton();

//private constructor to avoid client applications to use constructor
private EagerInitializationSingleton(){}

public static EagerInitializationSingleton getInstance(){
return instance;
}
}

第一种写法不算很复杂,因为声明了static和final,在类加载的时候就实例化了,另外将constructor声明为private,也就是不能使用new来创建实例,而只能使用getInstance()来得到唯一的实例,从而保证了只有一个实例。

但是这种方法的坏处显而易见,它在类加载的时候就初始化了,对于从来没有用过但是创建又需要很多资源的对象,这无疑是一种浪费。于是就有了懒汉式(lazy initialization)。

双检锁(Double Checked Locking)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Double-checked Locking Lazy Initialization
public class DoubleCheckedLockSingleton {

private volatile static DoubleCheckedLockSingleton singleton;

private DoubleCheckedLockSingleton() {}

public static DoubleCheckedLockSingleton getInstance() {
if(singleton == null) {
synchronized(DoubleCheckedLockSingleton.class) {
if(singleton == null) {
singleton = new DoubleCheckedLockSingleton();
}
}
}
return singleton;
}
}

第二种双检锁是一种懒汉式——只有调用getInstance()的时候才实例化对象。但逻辑比饿汉式复杂许多,让我们来仔细看一看。
getInstance()方法中使用了双检锁,这么做避免了同步整个getInstance()方法,因为只是在第一次实例化时需要同步,创建好之后,就可以直接返回已经创建好的singleton了。又是懒汉式,又不需要同步整个getInstance()方法影响性能,开销不算太大。
然而这里还有个坑,注意这里的volatile,尽管有了synchronized,但如果没有声明volatile,这个看似完美的方法还是会失效。
话说因为singleton = new DoubleCheckedLockSingleton()这个操作实际上含有三个步骤

(1) 给singleton对象分配内存
(2) 调用构造器来初始化成员变量
(3) 将singleton对象指向分配好的内存空间(执行这一步之后singleton != null)

然而JMM中指令是存在重排序的,也就是不能保证(2)和(3)的次序。
假设两个线程同时进入第一层if(singleton == null),假设线程A首先进入了synchronized block,但是执行完singleton = new DoubleCheckedLockSingleton()之后,仅仅只完成了步骤(3),singleton对象指向分配好的内存空间(并没有完成构造器的初始化工作),然后返回。线程B这时候进入synchronized block,看见singleton已经不等于null了,于是也返回了,然后使用这个没有真正初始化完成的实例,便会有不可预知的问题。
所以这里加上volatile之后禁止指令重排,根据happens-before原则,写操作将会先行发生于后面发生的读操作,也就是线程B一定读到的是线程A实例化完整的实例。
但是请注意,即使是使用了volatile,在Java 5之前仍旧是有问题的,Java 5之前的JMM有bug,并不能保证禁止重排序。
所以对于这么多坑的一种写法,我们明白了原理就算了,能避免就避免吧。

静态内部类(Static Inner Class)

1
2
3
4
5
6
7
8
9
10
11
public class InnerClassSingleton {
private static class SingletonHolder {
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}

private InnerClassSingleton (){}

public static final InnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

这种写法也是懒汉式的,同时读取实例的时候不会进行同步,没有性能缺陷,也不依赖JDK版本。完美!

Enum

1
2
3
4
5
public enum EnumSingleton {

INSTANCE;

}

这种写法太简单了!有没有!它也是懒汉式的,天然的保证了线程安全,天然的不需要同步,不过当然需要Java 5以上的版本啦(Java 5之前也得有enum才行啊!),也是《Effective Java》上推荐的方法。

性能比较

这里有一篇关于几种写法的性能比较:Fastest Thread-safe Singleton in Java

但是没有出乎意料,对于懒汉式的写法中,静态内部类 > 双检锁 > synchronized getInstance方法(本文没有讨论,因为性能实在是堪忧)。这里没有比较enum,留给有兴趣的童鞋吧。

如果一定要实现一个单例设计模式,就我而言,我会采用enum的写法。

为什么单例模式是一个反模式?

那么讨论完单例模式的几种实现方法,我们再来说明为什么它已然已经成为业界共识的反模式了。纳尼?前面铺垫那么多,不是白讨论了吗?!虽然是浪费了很多口舌,但是单例模式依然有他的应用场景,我们后面会说。

首先我们要搞清楚两个概念:单例模式和单例。前者是一种设计模式,它的特征是:1)私有的构造器,2)通过Singleton.getInstance()方法获得该类的唯一实例。而单例指的是一个类有且只有一个实例,可以通过依赖注入来实现。Spring中,我们常用的service或者dao通常就是典型的单例,它们通过BeanFactory来进行依赖注入,来保证应用中引用的都是一个类的同一个实例。

要注意的是,Spring中默认的singleton scope和严格意义上单例还是有区别的,来自Spring的文档中指出:

When a bean is a singleton, only one shared instance of the bean will be managed and all requests for beans with an id or ids matching that bean definition will result in that one specific bean instance being returned.

也就是说你在Spring中定义了singleton的使用范围,并不能保证你的bean在应用中是绝对的唯一实例,因为这个bean通常有公有的构造器,你完全可以绕过Spring而new一个实例。

单例模式之所以是一个反模式,是因为:

  • 它通常是一个复杂的object,维护着一个全局的状态,而全局的状态使得结果不可预测
  • 由于是私有构造器,单元测试变得困难

应用场景

基本的原则就是不要自己实现单例模式,然而依然有场景(为数不多的)适合使用单例模式,譬如logging:

  • 客户代码需要一个全局的logging service来发送请求去写log
  • 多个监听器(listener)可以注册同一个logging service
  • 虽然不同的应用会有不同的输出,它们注册监听器的方法是一样的,它们可以定制监听器来实现不同的配置,客户代码不需要知道log在哪里或如何写log的

参考:

  • 如何正确地写出单例模式
  • The “Double-Checked Locking is Broken” Declaration
  • Fastest Thread-safe Singleton in Java
  • Singletons are Pathological Liars
  • Where Have All the Singletons Gone?
  • Root Cause of Singletons

深入理解Java中的ThreadLocal

发表于 2017-12-02

ThreadLocal的实现原理

  1. 每个Thread中都维护有一个ThreadLocalMap

    1
    2
    3
    4
    5
    6
    7
    8
    public class Thread implements Runnable {
    ...
    /* ThreadLocal values pertaining to this thread. This map is maintained
    * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    ...
    }
  2. 这个ThreadLocalMap的key是ThreadLocal实例本身,value就是每个线程要独立使用的Object。

    1
    2
    3
    4
    5
    6
    7
    public class ThreadLocal<T> {
    ...
    void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    ...
    }
  3. 每个Thread要获得Object的时候,首先会调用getMap(Thread t)找到当前线程的ThreadLocalMap,然后使用ThreadLocal实例作为key,找到对应的value,也就是要找的Object。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public class ThreadLocal<T> {
    public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    ThreadLocalMap.Entry e = map.getEntry(this);
    if (e != null)
    return (T)e.value;
    }
    return setInitialValue();
    }

    ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
    }

    private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
    map.set(this, value);
    else
    createMap(t, value);
    return value;
    }
    }
  4. 这样每个线程都有独立使用的Object,就不会有线程之间相互干扰的问题,也不需要同步,因为不存在线程共享。

ThreadLocal的内存泄漏问题

从源代码中,我们可以知道ThreadLocalMap.Enty对key(也就是ThreadLocal实例)是WeakReference,那么GC会将ThreadLocal回收,然而Entry的value不会被回收。但是如果线程及时结束,因为ThreadLocalMap和Thread的生命周期是一样的,所以ThreadLocalMap也会被回收,也就不会有内存泄漏的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static class ThreadLocalMap {

/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
...
}

问题就出在假如使用了线程池,线程池中的线程完成任务后还会继续放回线程池而不会结束,这样就造成了泄漏:value中的Object虽然不会再被访问到(因为作为key的ThreadLocal已经被回收了),但是由于ThreadLocalMap.Entry对value还存在强引用,所以value不会被回收,而Thread又一直存活,这样value就永远无法被回收了。

解决ThreadLocal内存泄漏的最佳实践

解决方案一:不在有线程池的web应用中使用ThreadLocal

解决方案二:如果一定要在有线程池的web应用中使用ThreadLocal,那么在线程执行完任务之前,要调用ThreadLocal.remove()来清除value

1
2
3
4
5
6
7
try {
threadLocal.set(value);
...
}
finally {
threadLocal.remove();
}

参考:

  • 深入分析 ThreadLocal 内存泄漏问题
  • Threading stories: ThreadLocal in web applications
  • https://github.com/dropwizard/metrics/issues/742
  • https://wiki.apache.org/tomcat/MemoryLeakProtection

深入理解Java中的ThreadLocal

发表于 2017-12-02

ThreadLocal的实现原理

  1. 每个Thread中都维护有一个ThreadLocalMap

    1
    2
    3
    4
    5
    6
    7
    8
    public class Thread implements Runnable {
    ...
    /* ThreadLocal values pertaining to this thread. This map is maintained
    * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    ...
    }
  2. 这个ThreadLocalMap的key是ThreadLocal实例本身,value就是每个线程要独立使用的Object。

    1
    2
    3
    4
    5
    6
    7
    public class ThreadLocal<T> {
    ...
    void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    ...
    }
  3. 每个Thread要获得Object的时候,首先会调用getMap(Thread t)找到当前线程的ThreadLocalMap,然后使用ThreadLocal实例作为key,找到对应的value,也就是要找的Object。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public class ThreadLocal<T> {
    public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    ThreadLocalMap.Entry e = map.getEntry(this);
    if (e != null)
    return (T)e.value;
    }
    return setInitialValue();
    }

    ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
    }

    private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
    map.set(this, value);
    else
    createMap(t, value);
    return value;
    }
    }
  4. 这样每个线程都有独立使用的Object,就不会有线程之间相互干扰的问题,也不需要同步,因为不存在线程共享。

ThreadLocal的内存泄漏问题

从源代码中,我们可以知道ThreadLocalMap.Enty对key(也就是ThreadLocal实例)是WeakReference,那么GC会将ThreadLocal回收,然而Entry的value不会被回收。但是如果线程及时结束,因为ThreadLocalMap和Thread的生命周期是一样的,所以ThreadLocalMap也会被回收,也就不会有内存泄漏的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static class ThreadLocalMap {

/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
...
}

问题就出在假如使用了线程池,线程池中的线程完成任务后还会继续放回线程池而不会结束,这样就造成了泄漏:value中的Object虽然不会再被访问到(因为作为key的ThreadLocal已经被回收了),但是由于ThreadLocalMap.Entry对value还存在强引用,所以value不会被回收,而Thread又一直存活,这样value就永远无法被回收了。

解决ThreadLocal内存泄漏的最佳实践

解决方案一:不在有线程池的web应用中使用ThreadLocal

解决方案二:如果一定要在有线程池的web应用中使用ThreadLocal,那么在线程执行完任务之前,要调用ThreadLocal.remove()来清除value

1
2
3
4
5
6
7
try {
threadLocal.set(value);
...
}
finally {
threadLocal.remove();
}

参考:

  • 深入分析 ThreadLocal 内存泄漏问题
  • Threading stories: ThreadLocal in web applications
  • https://github.com/dropwizard/metrics/issues/742
  • https://wiki.apache.org/tomcat/MemoryLeakProtection

为什么SimpleDateFormat不是线程安全的

发表于 2017-12-01

为什么SimpleDateFormat不是线程安全的

我们用一个简单的例子来说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.text.DateFormat;
import java.text.SimpleDateFormat;

public class SimpleDateFormatNotThreadSafe {

private static final DateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd");

public static void main (String[] args) {

for(int i = 1 ; i < 10; i++) {
Thread t = new MyThread(String.valueOf(i));
t.start();
}
}

static class MyThread extends Thread {

MyThread(String threadName) {
super(threadName);
}

@Override
public void run() {
try{
System.out.println("Created Thread " + getName() + ", "+ FORMAT.parse("2016-01-01"));
} catch( Exception e){
e.printStackTrace();
}
}
}

}

基于是多线程程序,线程运行的先后不可预知。但是奇怪的事情发生了,连可预知的FORMAT.parse("2016-01-01")也出现了奇怪的结果。

以下是几个可能出现的错误结果:

错误结果1

1
2
3
4
5
6
7
8
9
10
Created Thread 2, Wed Jan 01 00:00:00 HKT 2200
Created Thread 6, Fri Jan 01 00:00:00 HKT 2016
Created Thread 7, Fri Jan 01 00:00:00 HKT 2016
Created Thread 8, Fri Jan 01 00:00:00 HKT 2016
Created Thread 4, Fri Jan 01 00:00:00 HKT 2016
Created Thread 3, Fri Jan 01 00:00:00 HKT 2016
Created Thread 9, Fri Jan 01 00:00:00 HKT 2016
Created Thread 0, Wed Jan 01 00:00:00 HKT 2200
Created Thread 1, Fri Jan 01 00:00:00 HKT 2016
Created Thread 5, Fri Jan 01 00:00:00 HKT 2016

错误结果2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Created Thread 1, Sat May 26 09:34:08 HKT 164390675
Created Thread 3, Fri Jan 01 00:00:00 HKT 2016
Created Thread 4, Fri Jan 01 00:00:00 HKT 2016
Created Thread 0, Fri Jan 01 00:00:00 HKT 42016
Created Thread 5, Fri Jan 01 00:00:00 HKT 2016
Created Thread 6, Fri Jan 01 00:00:00 HKT 2016
Created Thread 2, Fri Jan 01 00:00:00 HKT 42016
Created Thread 7, Fri Jan 01 00:00:00 HKT 2016
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1110)
at java.lang.Double.parseDouble(Double.java:540)
at java.text.DigitList.getDouble(DigitList.java:168)
at java.text.DecimalFormat.parse(DecimalFormat.java:1321)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2088)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455)
at java.text.DateFormat.parse(DateFormat.java:355)
at com.examples.tutorials.SimpleDateFormatNotThreadSafe$MyThread.run(SimpleDateFormatNotThreadSafe.java:27)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1110)
at java.lang.Double.parseDouble(Double.java:540)
at java.text.DigitList.getDouble(DigitList.java:168)
at java.text.DecimalFormat.parse(DecimalFormat.java:1321)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1793)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455)
at java.text.DateFormat.parse(DateFormat.java:355)
at com.examples.tutorials.SimpleDateFormatNotThreadSafe$MyThread.run(SimpleDateFormatNotThreadSafe.java:27)

错误结果3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java.lang.NumberFormatException: For input string: ".11E.111E"
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1250)
at java.lang.Double.parseDouble(Double.java:540)
at java.text.DigitList.getDouble(DigitList.java:168)
at java.text.DecimalFormat.parse(DecimalFormat.java:1321)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1793)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455)
at java.text.DateFormat.parse(DateFormat.java:355)
at com.examples.tutorials.SimpleDateFormatNotThreadSafe$MyThread.run(SimpleDateFormatNotThreadSafe.java:27)
java.lang.NumberFormatException: For input string: ".11E.111E1"
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1250)
at java.lang.Double.parseDouble(Double.java:540)
at java.text.DigitList.getDouble(DigitList.java:168)
at java.text.DecimalFormat.parse(DecimalFormat.java:1321)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1793)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455)
at java.text.DateFormat.parse(DateFormat.java:355)
at com.examples.tutorials.SimpleDateFormatNotThreadSafe$MyThread.run(SimpleDateFormatNotThreadSafe.java:27)
Created Thread 1, Fri Jan 01 00:00:00 HKT 2016
Created Thread 6, Fri Jan 01 00:00:00 HKT 2016
Created Thread 3, Fri Jan 01 00:00:00 HKT 2016
Created Thread 5, Fri Jan 01 00:00:00 HKT 2016
Created Thread 4, Fri Jan 01 00:00:00 HKT 2016
Created Thread 7, Fri Jan 01 00:00:00 HKT 2016
Created Thread 8, Fri Jan 01 00:00:00 HKT 2016
Created Thread 9, Fri Jan 01 00:00:00 HKT 2016

这正是因为SimpleDateFormat是非线程安全所导致的,这应该是设计者的错误。但是基于向后兼容,JDK中一直没有将它改为线程安全。然后Java 8引入了新的类解决了这一问题,下面会进行详细阐述。
从SimpleteDateFormat的源代码中可以知道,parse(..)的时候,首先会calender.clear(),然后会calender.set(..)。如果一个线程parse(..)还没有返回,另一个线程也进入了parse(..)并进行了calender.clear(),那么第一个线程将会得到意想不到的结果。

所以在SimpleDateFormat的文档中,也做了说明:

Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.

如何写出线程安全的date formatter

针对写出线程安全的date formatter,在Java 8之前主要有三种方法:

  • 每个线程使用单独的DateFormat实例
  • 使用synchronized进行同步
  • 使用ThreadLocal

每个线程使用单独的DateFormat实例

在每个线程里,创建一个新的DateFormat实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class SimpleDateFormatThreadSafe {

public Date convertStringToDate(String dateString) throws ParseException {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
return df.parse(dateString);
}

}

这种方法虽然实现起来很简单,但是效率非常的低。

使用synchronized进行同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class SimpleDateFormatThreadSafeSynchronized {

private static final DateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd");

public Date convertStringToDate(String dateString) throws ParseException {
Date result;
synchronized(FORMAT) {
result = FORMAT.parse(dateString);
}
return result;
}

}

使用ThreadLocal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.text.DateFormat;
import java.text.SimpleDateFormat;

public class ThreadLocalThreadSafe {

private static final ThreadLocal<DateFormat> FORMAT = new ThreadLocal<DateFormat>(){

@Override
protected DateFormat initialValue(){
return new SimpleDateFormat("yyyy-MM-dd");
}
};

public Date convertStringToDate(String dateString) throws ParseException {
return FORMAT.get().parse(dateString);
}

}

以上三种方法的性能测试

有人针对以上的三种方法进行了性能测试,性能排序为:

使用ThreadLocal > 使用synchronized进行同步 > 每个线程使用单独的DateFormat实例

但是需要指出的是ThreadLocal的性能是在有线程池的情况下,假如没有线程池,和每个线程单独new一个DateFormat实例是没有区别的

性能排序

所以我建议,多线程环境下,最好不用使用SimpleDateFormat,无论哪种方法都会写一些多余的代码,性能也不见得好。

线程安全的Date Formatter

在Java 8之前的Java版本,我们可以考虑使用线程安全的类

  • apache commons中的FastDateFormat
  • joda-time中的DateTimeFormat

Java 8中的DateTimeFormatter

Java 8中引入了新的包java.time,完美的解决了线程不安全的问题。

将字符串转换成java.time.LocalDate:

1
2
3
4
5
6
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

//convert String to LocalDate
LocalDate localDate = LocalDate.parse("2016-01-01", formatter);

System.out.println(localDate);

从今天开始写博客

发表于 2017-11-30

多年以前的我会写博客,会记录当下的心情,会和朋友们在博客上打趣互捧。后来,大家渐渐的不玩博客了,我有段时间还在坚持写,甚至会自己买域名维护网站,尽管没有什么人来看,那时候仅仅是默默的记录着自己的足迹,所思所想。
这么多年过去了,大家从博客转向了微博,又从微博转向了朋友圈,似乎所感所想变得越来越精炼,大家也没有兴趣没有耐性再去看什么长篇大论了。这个时代已经变成“快文化”为主导了。
其实,很多时候很多话都想写下来,但微博和朋友圈似乎并不是一个好的出口。首先朋友圈的“快文化”似乎并不能完整清晰的表达我们的想法,几张p过的食物照片,几张到此一游的相片,只能代表我吃了,我去了,我有钱,我有闲,有品位,大家来赞我吧,大家来羡慕我吧,这样的社交文化太肤浅。其次朋友圈对我而言太过“overwelming”,我本性不喜欢被太多人关注私生活,我讨厌过多的社交,似乎与知心的三五好友互通往来,知道对方发生了什么,在做着什么,这样的方式更适合我。我也不想被赞和评论束缚,这有悖于我想记录文字的初衷,别人看不看与我无关,别人喜不喜欢与我无关。我只是单纯的想给自己留下点足迹,至少多年以后翻开看,原来那个时候我是这样想的啊,原来那个时候我竟然做成了这件事啊。或许,从现在开始我不应该犯懒了。
这个博客,会有很多是关于程序员的世界,会有关于我和我的家庭,会有游记,会有影评,还会有书评。
话不多说,关键在于坚持。

Stephanie Tang

Stephanie Tang

7 日志
11 标签
© 2020 Stephanie Tang
由 Hexo 强力驱动
主题 - NexT.Muse