陈公子的博客

文字可以宣泄过往,但终究写不出流年

详解CopyOnWriter

示例代码链接 https://github.com/ChenXun1989/study-example
copyOnWriter顾名思义就是写的时候复制,是一种常见的计算机程序设计领域的优化策略。
核心思想是当多个调用者访问同一个资源的时候(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向同一个的资源。
当每个调用者修改资源的时候,系统才会真正复制一份专用副本给该调用者,而其他调用者所见到的最初的资源仍然保持不变.
专用副本修改完毕之后,替代真正的资源。最终所有访问者看到一致的视图。

CopyOnWriterArrayList的实现细节

CopyOnWriterArrayList.get()方法

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

/**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
return get(getArray(), index);
}

/**
*Gets the array. Non-private so as to also be accessible
* from CopyOnWriteArraySet class.
*/
final Object[] getArray() {
return array;
}

/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;

从源码上可以看出CopyOnWriterArrayList底层使用一个由volatile修饰的array数组来存值,
而读一个volatile变量(相当于获取锁),JMM会把线程对应的本地内存置为无效,
从而使得被监视器保护的临界区代码必须要从主内存中去读取共享变量。这样来保证读的变量是最新的。

CopyOnWriterArrayList.add()方法

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

/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
//获取写锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 获取真正的资源
Object[] elements = getArray();
int len = elements.length;
// 复制一份私有副本
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 修改私有副本
newElements[len] = e;
// 私有副本修改完毕之后,替代真正的资源
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}

/**
* Sets the array.
*/
final void setArray(Object[] a) {
array = a;
}

从源码上可以看出CopyOnWriterArrayList里面add方法体现了CopyOnWriter的核心思想,具体步骤如下:

  1. 获取写锁
  2. 从原资源复制一份私有副本
  3. 修改私有副本
  4. 私有副本替换原资源
  5. 释放写锁

copyOnWriter的其他应用

CopyOnWriter是一种常见的计算机程序设计领域的优化策略,例如redis里面的持久化机制
redis处理数据是通过单线程的,但是很多场景我们需要持久化数据,reids则依赖系统的fork()函数的copyOnWriter实现。
redis官方文档:

Whenever Redis needs to dump the dataset to disk, this is what happens:
Redis forks. We now have a child and a parent process.
The child starts to write the dataset to a temporary RDB file.
When the child is done writing the new RDB file, it replaces the old one.
This method allows Redis to benefit from copy-on-write semantics.

liunx系统的fork函数实现方式使用copyOnWriter技术创建子进程。
当父进程创建子进程时,内核只为子进程创建虚拟空间,父子两个进程使用的是相同的物理空间。只有父子进程发生更改时才会为子进程分配独立的物理空间。

copyOnWriter的优缺点

  1. copyOnWriter适用读多写少的场景
  2. 数据复制过程带来了更多得io消耗

RPC框架设计

RPC(Remote Procedure Call Protocol)——远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。
RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。
代码地址:https://github.com/ChenXun1989/netty-rpc

透明化RPC

一个RPC服务调用有以下几个步骤

  1. 服务消费方(client)调用以本地调用方式调用服务
  2. client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体
  3. client stub找到服务地址,并将消息发送到服务端
  4. server stub收到消息后进行解码
  5. server stub根据解码结果调用本地的服务
  6. 本地服务执行并将结果返回给server stub
  7. server stub将返回结果打包成消息并发送至消费方
  8. client stub接收到消息,并进行解码
  9. 服务消费方得到最终结果

透明的RPC测试把2-8步骤全部封装起来,消费方只需要执行第一个步骤,并等待返回结果就行。

透明化RPC具体调用过程

注意要点

  • 动态代理(客户端) 主要实现有两种JDK动态代理和cglib
  • reqeust对象 主要包含 服务方法签名和参数
  • repsone对象 主要包含服务方法处理结果和异常
  • 序列化和反序列化 主要在 reqeust和respone对象,该demo采用protobuf实现
  • socket通讯 该demo采用netty4实现。

IOC容器设计

提供getBean接口,支持依赖注入
示例代码地址: https://github.com/ChenXun1989/ioc-framework

IOC容器主要模块

  • ApplicationContext,IOC容器上下文,持有对象Map
  • BeanFactory 每一个类型(class) 对应一BeanFactory,提供获取类实现的接口,属性注入接口
  • CompentScan 收集相关配置信息,主要分xml和注解扫描两种

IOC容器启动过程

ApplicationContext 创建

1
2
3
4
5

ApplicationContext context=new ApplicationContext("com.chenxun.framework.example.entity");
Student s=(Student) context.getBean("student");
s.test();

CompentScan 收集相关配置信息,解析出被ioc容器托管的class集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

private final List<String > list=new ArrayList<>();
public void scan(String packageName){
String parent= System.getProperty("user.dir");
parent=parent+"/src/main/java";
System.out.println(parent);
String child=packageName.replaceAll("\\.", "/");
File f=new File(parent ,child);
for(File c:f.listFiles()){
if(c.getName().endsWith(".java")){
list.add(packageName+"."+c.getName().substring(0,c.getName().lastIndexOf(".")));
}
}
};
public List<String > getList(){
return list;
}

  • 创建对应类的BeanFactory
  • BeanFactory通过反射创建默认class默认实例对象
  • 把创建完的对象放入ApplicationContext持有的对象Map
  • 遍历beanFactory的Map,调用setProperties接口来完成依赖注入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

private void init() throws ClassNotFoundException{
compentScan.scan(backPackage);
for(String clasName:compentScan.getList()){
Class cls=Class.forName(clasName);
Compent c=(Compent) cls.getAnnotation(Compent.class);
if(c!=null){
String key=c.value();
BeanFactory bf=new SimpleBeanFactory(cls);
beanFactorys.put(cls, bf);
map.put(key, bf.getBean());
}
}
for(Entry<Class, BeanFactory> entry :beanFactorys.entrySet()){
entry.getValue().setProperties(map);
}

}

  • 先创建对象,再做依赖注入
  • 容器里面是原生对象还是代理对象,取决于beanfatory返回的对象实例

mybatis源码分析(一)

sqlSessionFactory对象

sqlSessionFactory 是mybatis 核心配置类,管理mybatis全局配置。

sqlSession接口

一个或者多个sql操作的执行单元。实现一个完整的sql操作。依赖sqlSessionFactory创建

Executor接口

对应jdbc底层一个完整的sql操作。sqlSession 实际通过Executor执行sql操作

MapperProxy类

代理实现mybatis的客户端mapper接口

MapperMethod类

对应客户端代码的mapper接口里面的一个方法。该实例缓存了。

StatementHandler接口

jdbc Statement的装饰器

ResultSetHandler接口

jdbc resultSet的装饰器

mybaits执行orm操作细节

sqlsession的close 方法会关闭底层jdbc connection

mybatis插件机制

mybatis插件是基于代理实现的,具体支持一下四个接口

  • Executor
  • ParameterHandler
  • ResultSetHandler
  • StatemetHandler

java线程状态

java线程有以下几个状态

  • new 初始状态
  • runnable 运行状态(包括就绪和运行)
  • blocked 阻塞状态
  • waiting 等待状态,需要其他线程特定动作唤醒(通知或者中断)
  • time-waiting 超时等待状态,可以在指定时间内返回。
  • terminated 终止状态

创建

java构造线程有两种办法,Thread类和Runnable接口,Thread的类本身实现了Runnable接口。
常见的作法,是通过thread类的public Thread(Runnable target)构造函数来创建线程。

1
2
3
4
5
6
7
8
9

new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub

}
});

thread类的初始话代码

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

private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
if (security != null) {
g = security.getThreadGroup();
}

if (g == null) {
g = parent.getThreadGroup();
}
}

g.checkAccess();

if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
this.stackSize = stackSize;

tid = nextThreadID();
}


从这段代码可以看出以下几点
当前线程为创建线程的父线程
child线程继承parent的Daemon、优先级、和加载资源的contextClassLoader和一个可以继承的ThreadLocal。
Daemon线程(后台线程)的finally块代码不会执行

运行

Thread类的start方法起来运行线程。底层通过调用navate方法来运行线程。
start()方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码
run()方法当作普通方法的方式调用,程序还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码

阻塞

线程执行到monitorenter保护的代码快时,会去获取monitorenter对应的对象锁,如未获取,则该线程进入同步队列,状态为阻塞。
任意对象都拥有自己的monitor,包括class对象。

等待/唤醒

线程等待的方法主要有下面几种

  • object.wait() / object.wait(long)
  • object.join() / object.join(long)
  • lockSupport.park() / lockSupport.park(long)

当线程调用某个对象的wati方法,进入等待。当另一个线程调用该对象的notify()/notifyall()方法,之前等待的线程唤醒。
线程唤醒的方法注意下面几种

  • object.notify()/notifyAll()
  • lockSupport.unpark(Thread)

调用对象wait或者notify方法的前题是获取当前对象的内置锁。

通过wait/notify设计的经典作法
等待方:

  • 获取对象锁
  • 条件检查
  • 为假则等待
  • 为真则执行后面的逻辑
    唤醒方:
  • 获取对象锁
  • 改变条件
  • 通知所有等待在对象的线程

条件变量用volatile修饰来保证可见性

终止

线程对应的run方法运行完毕,则线程终止。不推荐使用stop来终止线程。可以通过状态变量来终止线程。

Buffer详解

java中的nio由channel,buffer,Selector组成核心API。
Buffer是一个继承于object的抽象类,主要有下面几种。

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

基本覆盖了基本类型的支持,看下byteBuffer的实现

1
2
3
4
5
6

// 间接缓冲区
ByteBuffer bf1=ByteBuffer.allocate(16);
// 直接缓冲区
ByteBuffer bf2=ByteBuffer.allocateDirect(16);

ByteBuffer提供两种方式来获取缓存区内存,间接或者直接。
字节缓冲区要么是直接的,要么是非直接的。
如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。
也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),
虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,不会被GC回收。
间接缓冲区与本地操作系统低层次的i/o交互需要一个中间缓冲区来实现,因此性能比直接缓存区要低。
直接缓冲区通过sun.misc.Unsafe类调用jni本地方法来分配内存。
因为绕过jvm,因此分配内存的开销较大,并且需要用户自己当成普通资源来主动释放。
堆外内存的分配

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

DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}

可以看出通过unsafe.allocateMemory方法来分配堆外内存,同时创建一个对应的Cleaner
sun.misc.Cleaner.create方法两个参数。

  • 需要监控的堆内存对象,就是堆外内存关联的对象。
  • 程序释放资源的回调。

当JVM进行GC的时候,如果发现我们监控的对象,不存在强引用了,(Cleaner本身是幽灵引用),
就会调用预先绑定的回调方法,我们一般在回调方法里面释放堆外内存

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

private static class Deallocator
implements Runnable
{
private static Unsafe unsafe = Unsafe.getUnsafe();
private long address;
private long size;
private int capacity;
private Deallocator(long address, long size, int capacity) {
assert (address != 0);
this.address = address;
this.size = size;
this.capacity = capacity;
}
public void run() {
if (address == 0) {
// Paranoia
return;
}
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
}


最终通过unsafe类提供freeMemory来释放之前分配的对外内存。
堆外内存大小可以通过jvm启动参数限制: -XX:MaxDirectMemorySize

并发容器和同步容器

java的同步容器有Vector和Hashtable。
vecotor和hashtbale通过synchronized来锁定对容器状态的访问,例如get方法

1
2
3
4
5
6
7

public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}

同步容器类是线程安全的,但复合操作不一定是线程安全的,需要客户端额外加锁来保证线程安全。

并发容器

同步容器将所有对容器状态的访问都串行化,以实现线程安全。这种性能比较低,
java5.0 引入了并发容器,在java.util.concurrent包下面

ConcurrentHashMap

ConcurrentHashMap采用了分段锁的设计,只有在同一个分段内才存在竞态关系,不同的分段锁之间没有锁竞争。
ConcurrentHashMap是弱一致性的,get,clear都是弱一致性的

synchronized原理解析

synchronized的三种方式

对于普通同步方法,锁的是当前实例对象

1
2
3
4

synchronized void test1(){
//do something
}

对于静态方同步方法,锁的是当前类的Class对象

1
2
3
4
5

static synchronized void test2(){
//do something
}

对于同步方法快,锁的是synchronized括号里的对象

1
2
3
4
5
6
7

void test3(){
synchronized(this){
//do something
}
}

synchronized锁升级

jdk1.6之后,synchronized锁有四种状态,无锁,偏向锁,轻量级锁,重量级。

  • 偏向锁: 存在java对象头里面。
  • 轻量级锁:存在当前线程的栈帧里面
  • 重量级锁: 存在当前对象的monitor对象里面

锁升级过程:

  • 无锁状态到偏向锁,当前线程通过cas修改java对象头的锁标志位。
  • 谝向锁到轻量级锁, 当前线程把java对象头的锁信息copy到栈帧里面
  • 轻量级锁到重量级锁,把栈帧里面的锁信息copy到对象的monitor里面。
  • 偏向锁和轻量级锁采用cas自旋修改锁状态,重量级锁通过线程信号来实现*

volatile原理解析

volatile语义

jsr133规范对volatile语义进行了增强,明确保证了可见性和有序性。

volatile不保证原子性

java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。
Java语言提供了volatile,在某些情况下比锁更加方便。
如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。

Volatile变量修饰符如果使用恰当的话,它比synchronized的使用和执行成本会更低,因为它不会引起线程上下文的切换和调度。

volatile的有序性

jmm为了保证volatile的语义中的有序性,通过内存屏障来禁止指令重排序
JMM基于保守策略的JMM内存屏障插入策略

  • 在每个volatile写操作的前面插入一个StoreStore屏障
  • 在每个volatile写操作的后面插入一个SotreLoad屏障
  • 在每个volatile读操作的后面插入一个LoadLoad屏障
  • 在每个volatile读操作的后面插入一个LoadStore屏障

x86处理器仅仅会对写-读操作做重排序,因此在x86中,JMM仅需在volatile后面插入一个StoreLoad屏障即可正确实现volatile写-读的内存语义

volatile的可见性

对于volatile修饰的变量,jvm虚拟机保证从主内存加载到线程工作内存的值是最新的。
从内存语义的角度来看

  • volatile的写-读与锁的释放-获取有相同的内存效果
  • volatile写和锁的释放有相同的内存语义
  • volatile读与锁的获取有相同的内存语义

当线程释放锁的时候,JMM会把线程对应的本地内存中的共享变量刷新到主内存中
当线程获取锁时,JMM会把线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须要从主内存中去读取共享变量。

volatile的使用前提

  • 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
  • 该变量没有包含在具有其他变量的不变式中。

java内存结构和内存模型

java内存结构

java内存结构大致分为以下几个区域

PC寄存器/程序计数器

严格来说是一个数据结构,用于保存当前正在执行的程序的内存地址,由于Java是支持多线程执行的,所以程序执行的轨迹不可能一直都是线性执行。当有多个线程交叉执行时,被中断的线程的程序当前执行到哪条内存地址必然要保存下来,以便用于被中断的线程恢复执行时再按照被中断时的指令地址继续执行下去。 为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存,这在某种程度上有点类似于“ThreadLocal”,是线程安全的。

Java栈 Java Stack

java栈总是与线程关联在一起的,每当创建一个线程,JVM就会为该线程创建对应的Java栈,在这个Java栈中又会包含多个栈帧(Stack Frame),这些栈帧是与每个方法关联起来的,每运行一个方法就创建一个栈帧,每个栈帧会含有一些局部变量、操作栈和方法返回值等信息。每当一个方法执行完成时,该栈帧就会弹出栈帧的元素作为这个方法的返回值,并且清除这个栈帧,Java栈的栈顶的栈帧就是当前正在执行的活动栈,也就是当前正在执行的方法,PC寄存器也会指向该地址。只有这个活动的栈帧的本地变量可以被操作栈使用,当在这个栈帧中调用另外一个方法时,与之对应的一个新的栈帧被创建,这个新创建的栈帧被放到Java栈的栈顶,变为当前的活动栈。同样现在只有这个栈的本地变量才能被使用,当这个栈帧中所有指令都完成时,这个栈帧被移除Java栈,刚才的那个栈帧变为活动栈帧,前面栈帧的返回值变为这个栈帧的操作栈的一个操作数。

由于Java栈是与线程对应起来的,Java栈数据不是线程共有的,所以不需要关心其数据一致性,也不会存在同步锁的问题。

在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

堆Heap

堆是JVM所管理的内存中最大的一块,是被所有Java线程锁共享的,不是线程安全的,在JVM启动时创建。 堆是存储Java对象的地方,这一点Java虚拟机规范中描述是:所有的对象实例以及数组都要在堆上分配。Java堆是GC管理的主要区域,从内存回收的角度来看,由于现在GC基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代再细致一点有Eden空间、From Survivor空间、To Survivor空间等。

方法区Method Area

方法区存放了要加载的类的信息(名称、修饰符等)、类中的静态常量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当在程序中通过Class对象的getName.isInterface等方法来获取信息时,这些数据都来源于方法区。方法区是被Java线程锁共享的,不像Java堆中其他部分一样会频繁被GC回收,它存储的信息相对比较稳定,在一定条件下会被GC,当方法区要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。方法区也是堆中的一部分,就是我们通常所说的Java堆中的永久区 Permanet Generation,大小可以通过参数来设置,可以通过-XX:PermSize指定初始值,-XX:MaxPermSize指定最大值。

常量池Constant Pool

常量池本身是方法区中的一个数据结构。常量池中存储了如字符串、final变量值、类名和方法名常量。常量池在编译期间就被确定,并保存在已编译的.class文件中。一般分为两类:字面量和应用量。字面量就是字符串、final变量等。类名和方法名属于引用量。引用量最常见的是在调用方法的时候,根据方法名找到方法的引用,并以此定为到函数体进行函数代码的执行。引用量包含:类和接口的权限定名、字段的名称和描述符,方法的名称和描述符

本地方法栈Native Method Stack

本地方法栈和Java栈所发挥的作用非常相似,区别不过是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行Native方法服务。

java内存模型 jmm

JMM规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。
工作内存线程独享,主内存线程共享

0%