简栈

拥抱AI,持续成长

SimpleDateFormat是Java提供的一个格式化和解析日期的工具类,日常开发中应该经常会用到,但是由于它是线程不安全的,多线程公用一个SimpleDateFormat实例对日期进行解析或者格式化会导致程序出错。

代码示例演示

写一段小Demo来模拟多线程下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
package com.vernon.test.demo.jdk.text;
import java.text.ParseException;
import java.text.SimpleDateFormat;
/**
* Created with vernon-test
* Description:
* User:chenyuan
* Date:2019/3/20 Time:2:03 PM
*/
public class SimpleDateFormatCase {
//1、创建单例实例
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
//2、创建多个线程,并启动
for (int i = 0; i < 10; ++i) {
Thread thread = new Thread(new Runnable() {
public void run() {
try {
//3、使用单例日期实例解析文本
System.out.println(sdf.parse("2017-12-13 15:17:27"));
} catch (ParseException e) {
e.printStackTrace();
}
}
});
//4、启动线程
thread.start();
}
}
}
控制台正常的情况: 运气好~
1
2
3
4
5
6
7
8
9
10
11
12
Connected to the target VM, address: '127.0.0.1:57434', transport: 'socket'
Wed Dec 13 15:17:27 CST 2017
Wed Dec 13 15:17:27 CST 2017
Wed Dec 13 15:17:27 CST 2017
Wed Dec 13 15:17:27 CST 2017
Wed Dec 13 15:17:27 CST 2017
Wed Dec 13 15:17:27 CST 2017
Wed Dec 13 15:17:27 CST 2017
Wed Dec 13 15:17:27 CST 2017
Wed Dec 13 15:17:27 CST 2017
Wed Dec 13 15:17:27 CST 2017
Disconnected from the target VM, address: '127.0.0.1:57434', transport: 'socket'
控制台非正常的情况 运气不好~
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
Connected to the target VM, address: '127.0.0.1:57756', transport: 'socket'
Exception in thread "Thread-2" Exception in thread "Thread-0" java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.vernon.test.demo.jdk.text.SimpleDateFormatCase$1.run(SimpleDateFormatCase.java:23)
at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.vernon.test.demo.jdk.text.SimpleDateFormatCase$1.run(SimpleDateFormatCase.java:23)
at java.lang.Thread.run(Thread.java:748)
Wed Dec 13 15:17:27 CST 2017
Wed Dec 13 15:17:27 CST 2017
Wed Dec 13 15:17:27 CST 2017
Fri Dec 12 15:17:27 CST 2217
Thu Dec 13 15:17:27 CST 2012
Wed Dec 13 15:17:27 CST 2017
Wed Dec 13 15:17:27 CST 2017
Fri Jun 09 15:17:27 CST 5881628
Disconnected from the target VM, address: '127.0.0.1:57756', transport: 'socket'

通过IntelliJ IDEA的功能查看一下SimpleDateFormat的一个类关系图:

http://static.cyblogs.com/WX20200116-175032.png

阅读全文 »

Map家族介绍

我们都知道HashMap是线程不安全的,但是HashMap的使用频率在所有Map中确实属于比较高的。因为它可以满足我们大多数的场景了。

看一眼Map家族的关系图:

http://static.cyblogs.com/WX20200117-161643@2x.png

Map是一个接口,我们常用的实现类有HashMapLinkedHashMapTreeMapHashTable

HashMap

HashMap根据key的·值来保存value,需要注意的是,HashMap不保证遍历的顺序和插入的顺序是一致的。HashMap允许有一条记录的keynull,但是对值是否为null不做要求。

HashTable

HashTable类是线程安全的,它使用synchronize来做线程安全,全局只有一把锁,在线程竞争比较激烈的情况下hashtable的效率是比较低下的。因为当一个线程访问hashtable的同步方法时,其他线程再次尝试访问的时候,会进入阻塞或者轮询状态,比如当线程1使用put进行元素添加的时候,线程2不但不能使用put来添加元素,而且不能使用get获取元素。所以,竞争会越来越激烈。相比之下,ConcurrentHashMap使用了分段锁技术来提高了并发度,不在同一段的数据互相不影响,多个线程对多个不同的段的操作是不会相互影响的。每个段使用一把锁。所以在需要线程安全的业务场景下,推荐使用ConcurrentHashMap,而HashTable不建议在新的代码中使用,如果需要线程安全,则使用ConcurrentHashMap,否则使用HashMap就足够了。

LinkedHashMap
阅读全文 »

前言

要想深入了解Java并发编程,就要先理解好Java内存模型,而要理解Java内存模型又不得不从硬件、计算机内存模型说起,本文从计算机内存模型产生的原因、解决的问题谈起,然后再对Java模型进行介绍,最后对计算机内存模型和Java内存模型进行总结,希望大家看完本文之后有所收获!

CPU工作过程及出现的问题

CPU执行过程

大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行的时候,又免不了要和数据打交道,而计算机上面的临时数据,是储存在主存中的。

计算机内存包括高速缓存主存

我们知道CPU执行指令的速度比从主存读取数据和向主存写入数据快很多,所以为了高效利用CPU,CPU增加了**高速缓存(cache)**来匹配CPU的执行速度,最终程序的执行过程如下

  1. 首先会将数据从主存中复制一份到CPU的高速缓存中
  2. 当CPU执行计算的时候就可以直接从高速缓存中读取数据和写入数据
  3. 当运算结束后,再将高速缓存的数据更新到主存中
缓存一致性问题

上面的执行过程在单线程情况下并没有问题,但是在多线程情况下就会出现问题,因为CPU如果含有多个核心,则每个核心都有自己独占高速缓存,如果出现多个线程同时执行同一个操作,那么结果是无法预知。例如2个线程同时执行i++,假设i的初始值是0,那么我们希望2个线程执行完成之后i的值变为2,但是事实会是这样吗?
http://static.cyblogs.com/WX20200212-224624@2x.png

阅读全文 »

如何理解Linux的上下文切换

  • Linux 是一个多任务操作系统,它支持同时运行的任务数量远大于 CPU 个数。其实这些任务没有真正的同时运行,是因为系统在很短的时间内,将 CPU 轮流分配给它们,造成多任务同时运行的错觉。
  • 而在每个任务运行前,CPU 都需要知道任务从哪里加载、从哪里开始运行,需要系统事先设置好 CPU 寄存器程序计数器CPU 寄存器是 CPU 内置的容量小、速度极快的内存。而程序计数器则是用来存储 CPU 正在执行的指令位置、或即将执行的下一条指令位置。它们都是 CPU 在运行任务前必须依赖的环境,也被叫做 CPU 上下文
  • 上下文切换,就是先把前一个任务的 CPU 上下文保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳到程序计数器所指的新位置,运行新任务。而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。

根据任务的不同,CPU 的上下文切换可以分为几个不同的场景,也就是:进程上下文切换、线程上下文切换、中断上下文切换。

进程上下文切换

1、用户空间与内核空间

Linux 按照特权等级,把进程的运行空间分为内核空间和用户空间,分别对应着 CPU 特权等级的 Ring 0Ring 3

  • 内核空间(Ring 0)具有最高权限,可以直接访问所有资源。
  • 用户空间(Ring 3)只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入内核中才能访问这些特权资源。
  • 进程既可以在用户空间运行,又可以在内核空间运行。在用户空间运行时被称为进程的用户态,而陷入内核空间的时候,被称为进程的内核态。

2、系统调用

从用户态到内核态的转变,需要通过系统调用来完成。比如查看文件时,需要执行多次系统调用:open、read、write、close等。系统调用的过程如下:

  • 首先,把 CPU 寄存器里原来用户态的指令位置保存起来
  • 为了执行内核代码,CPU 寄存器需要更新为内核态指令的新位置,最后跳转到内核态运行内核任务。
  • 系统调用结束后,CPU 寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程
  • 所以,一次系统调用的过程,其实是发生了两次 CPU 上下文切换。
阅读全文 »

前言

上一篇文章介绍了多线程的概念及synchronized的使用方法《synchronized的使用(一)》,但是仅仅会用还是不够的,只有了解其底层实现才能在开发过程中运筹帷幄,所以本篇探讨synchronized的实现原理及锁升级(膨胀)的过程。

synchronized实现原理

synchronized是依赖于JVM来实现同步的,在同步方法和代码块的原理有点区别。

同步代码块

我们在代码块加上synchronized关键字

1
2
3
4
5
public void synSay() {
synchronized (object) {
System.out.println("synSay----" + Thread.currentThread().getName());
}
}

编译之后,我们利用反编译命令javap -v xxx.class查看对应的字节码,这里为了减少篇幅,我就只粘贴对应的方法的字节码。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public void synSay();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: getfield #2 // Field object:Ljava/lang/String;
4: dup
5: astore_1
6: monitorenter
7: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
10: new #4 // class java/lang/StringBuilder
13: dup
14: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
17: ldc #6 // String synSay----
19: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: invokestatic #8 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
25: invokevirtual #9 // Method java/lang/Thread.getName:()Ljava/lang/String;
28: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
31: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
37: aload_1
38: monitorexit
39: goto 47
42: astore_2
43: aload_1
44: monitorexit
45: aload_2
46: athrow
47: return
Exception table:
from to target type
7 39 42 any
42 45 42 any
LineNumberTable:
line 21: 0
line 22: 7
line 23: 37
line 24: 47
LocalVariableTable:
Start Length Slot Name Signature
0 48 0 this Lcn/T1;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 42
locals = [ class cn/T1, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4

可以发现synchronized同步代码块是通过加monitorentermonitorexit指令实现的。
每个对象都有个**监视器锁(monitor) **,当monitor被占用的时候就代表对象处于锁定状态,而monitorenter指令的作用就是获取monitor的所有权,monitorexit的作用是释放monitor的所有权,这两者的工作流程如下:
monitorenter

阅读全文 »

前言

零拷贝(英语:Zero-copy)技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。

零拷贝操作减少了在用户空间与内核空间之间切换模式的次数

举例来说,如果要读取一个文件并通过网络发送它,传统方式下如下图,传统的I/O操作进行了4次用户空间与内核空间的上下文切换,以及4次数据拷贝。其中4次数据拷贝中包括了2次DMA拷贝和2次CPU拷贝。通过零拷贝技术完成相同的操作,减少了在用户空间与内核空间交互,并且不需要CPU复制数据。

http://static.cyblogs.com/浅析零拷贝技术-1.png

linux中零拷贝技术

Linux系统的“用户空间”和“内核空间”

从Linux系统上看,除了引导系统的BIN区,整个内存空间主要被分成两个部分:内核空间(Kernel space)、用户空间(User space)。

“用户空间”和“内核空间”的空间、操作权限以及作用都是不一样的。

内核空间是Linux自身使用的内存空间,主要提供给程序调度、内存分配、连接硬件资源等程序逻辑使用;用户空间则是提供给各个进程的主要空间。

阅读全文 »

前提

这篇文章主要分析一下Introspector(内省)的用法。Introspector是一个专门处理JavaBean的工具类,用来获取JavaBean里描述符号,常用的JavaBean的描述符号相关类有BeanInfoPropertyDescriptorMethodDescriptorBeanDescriptorEventSetDescriptorParameterDescriptor。下面会慢慢分析这些类的使用方式,以及Introspector的一些特点。

JavaBean是什么

JavaBean是一种特殊(其实说普通也可以,也不是十分特殊)的类,主要用于传递数据信息,这种类中的方法主要用于访问私有的字段,且方法名符合某种命名规则(字段都是私有,每个字段具备SetterGetter方法,方法和字段命名满足首字母小写驼峰命名规则)。如果在两个模块之间传递信息,可以将信息封装进JavaBean中,这种对象称为值对象(Value Object)或者VO。这些信息储存在类的私有变量中,通过SetterGetter方法获得。JavaBean的信息在Introspector里对应的概念是BeanInfo,它包含了JavaBean所有的Descriptor(描述符),主要有PropertyDescriptorMethodDescriptor(MethodDescriptor里面包含ParameterDescriptor)、BeanDescriptorEventSetDescriptor

属性Field和属性描述PropertiesDescriptor的区别

如果是严格的JavaBean(Field名称不重复,并且Field具备SetterGetter方法),它的PropertyDescriptor会通过解析SetterGetter方法,合并解析结果,最终得到对应的PropertyDescriptor实例。所以PropertyDescriptor包含了属性名称和属性的SetterGetter方法(如果存在的话)。

内省Introspector和反射Reflection的区别

  • Reflection:反射就是运行时获取一个类的所有信息,可以获取到类的所有定义的信息(包括成员变量,成员方法,构造器等)可以操纵类的字段、方法、构造器等部分。可以想象为镜面反射或者照镜子,这样的操作是带有客观色彩的,也就是反射获取到的类信息是必定正确的。
  • Introspector:内省基于反射实现,主要用于操作JavaBean,基于JavaBean的规范进行Bean信息描述符的解析,依据于类的SetterGetter方法,可以获取到类的描述符。可以想象为“自我反省”,这样的操作带有主观的色彩,不一定是正确的(如果一个类中的属性没有SetterGetter方法,无法使用内省)。

常用的内省相关类

主要介绍一下几个核心类所提供的方法。

阅读全文 »

Linux整体架构图

我们先来看一张Linux整体架构图。

http://static.cyblogs.com/3433091-63269eb8f87c2bb9.png

系统调用

​ 系统调用时操作系统的最小功能单位。根据不同的应用场景,不同的Linux发行版本提供的系统调用数量也不尽相同,大致在240-350之间。这些系统调用组成了用户态跟内核态交互的基本接口,例如:用户态想要申请一块20K大小的动态内存,就需要brk系统调用,将数据段指针向下偏移,如果用户态多处申请20K动态内存,同时又释放呢?这个内存的管理就变得非常的复杂。

库函数

​ 库函数就是屏蔽这些复杂的底层实现细节,减轻程序员的负担,从而更加关注上层的逻辑实现。它对系统调用进行封装,提供简单的基本接口给用户,这样增强了程序的灵活性,当然对于简单的接口,也可以直接使用系统调用访问资源,例如:open()write()read()等等。库函数根据不同的标准也有不同的版本,例如:glibc库,posix库等。

Shell

Shell顾名思义,就是外壳的意思。就好像把内核包裹起来的外壳。它是一种特殊的应用程序,俗称命令行。为了方便用户和系统交互,一般一个Shell对应一个终端,呈现给用户交互窗口。当然Shell也是编程的,它有标准的shell语法,符合其语法的文本叫Shell脚本。很多人都会用Shell脚本实现一些常用的功能,可以提高工作效率。

为什么要区分用户态与内核态?

阅读全文 »

淘宝根据自身业务需求研发了TDDL(Taobao Distributed Data Layer)框架,主要用于解决分库分表场景下的访问路由(持久层与数据访问层的配合)以及异构数据库之间的数据同步,它是一个基于集中式配置的JDBC DataSource实现,具有分库分表Master/Salve动态数据源配置等功能。

就目前而言,许多大厂也在出一些更加优秀和社区支持更广泛的DAL层产品,比如Hibernate Shards、Ibatis-Sharding等。TDDL位于数据库和持久层之间,它直接与数据库建立交道,如图所示:

http://static.cyblogs.com/20160601111503650.png

​ 淘宝很早就对数据进行过分库的处理,上层系统连接多个数据库,中间有一个叫做DBRoute的路由来对数据进行统一访问。DBRoute对数据进行多库的操作、数据的整合,让上层系统像操作一个数据库一样操作多个库。但是随着数据量的增长,对于库表的分法有了更高的要求,例如,你的商品数据到了百亿级别的时候,任何一个库都无法存放了,于是分成2个、4个、8个、16个、32个……直到1024个、2048个。好,分成这么多,数据能够存放了,那怎么查询它?这时候,数据查询的中间件就要能够承担这个重任了,它对上层来说,必须像查询一个数据库一样来查询数据,还要像查询一个数据库一样快(每条查询在几毫秒内完成),TDDL就承担了这样一个工作。在外面有些系统也用DAL(数据访问层) 这个概念来命名这个中间件。下图展示了一个简单的分库分表数据查询策略:

http://static.cyblogs.com/20160601111544711.png

TDDL的主要优点:

1
2
3
4
5
6
7
8
9
10
1、数据库主备和动态切换
2、带权重的读写分离
3、单线程读重试
4、集中式数据源信息管理和动态变更
5、剥离的稳定jboss数据源
6、支持mysql和oracle数据库
7、基于jdbc规范,很容易扩展支持实现jdbc规范的数据源
8、无server,client-jar形式存在,应用直连数据库
9、读写次数,并发度流程控制,动态变更
10、可分析的日志打印,日志流控,动态变更12345678910

TDDL的体系架构

TDDL其实主要可以划分为3层架构,分别是Matrix层、Group层和Atom层。

**Matrix层:**用于实现分库分表逻辑,底层持有多个Group实例。而Group层和Atom共同组成了动态数据源

阅读全文 »

前言

Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。本文旨在对锁相关源码(本文中的源码来自JDK 8和Netty 3.10.6)、使用场景进行举例,为读者介绍主流锁的知识点,以及不同的锁的适用场景。

Java中往往是按照是否含有某一特性来定义锁,我们通过特性将锁进行分组归类,再使用对比的方式进行介绍,帮助大家更快捷的理解相关知识。下面给出本文内容的总体分类目录:

img

1. 乐观锁 VS 悲观锁

乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。在Java和数据库中都有此概念对应的实际应用。

先说概念。对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。

而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。

乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

img

阅读全文 »
0%