陈公子的博客

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

class装载

示例代码链接 https://github.com/ChenXun1989/study-example

演示demo

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

public class TestClassLoader {
public static void main(String[] args) {
ClassLoader myClassLoader = new ClassLoader() {};
try {
Class cls = myClassLoader.loadClass(TestClassLoader.class.getName());
System.out.println(myClassLoader.getParent());
System.out.println(cls.getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}


输出结果:

1
2
3

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2

从结果上我们可以得出以下两个结论

  1. 自定义的classloader的parent默认为AppClassLoader
  2. 当classloader加载某个class的时候会委托给parent(父加载器)加载

双亲委托细节

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,
而是把这个请求委派父类加载器去完成。每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,
只有当父加载器反馈自己无法完成这个请求(他的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

双亲委托是如何实现的呢?秘密就在loadClass 这个方法

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,
而是把这个请求委派父类加载器去完成。每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,
只有当父加载器反馈自己无法完成这个请求(他的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

双亲委托是如何实现的呢?秘密就在loadClass 这个方法

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

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}


从code上得出结论如果没有知道class,会先通过父加载起去加载,一直递归上去,如果没有父加载器直接通过bootstrap加载器加载
大致流程如下: 用户自定义的classlaoder – AppClassLoader —ExtClassLoader – bootStrapClassLoader

这么做有什么好处?

如果没有使用双亲委派模型,由各个类加载器自行加载的话,如果用户自己编写了一个称为java.lang.Object的类,
并放在程序的ClassPath中,那系统将会出现多个不同的Object类, Java类型体系中最基础的行为就无法保证。应用程序也将会变得一片混乱

如何破坏双亲委托

双亲委托并不是一个强制约束,只是一种大家都遵守规范,但有些场景会破坏双亲委托(例如SPI)

1
2
3
4
5
6
7

public static void test2(){
ServiceLoader<SPITest> loaders= ServiceLoader.load(SPITest.class);
loaders.forEach(i->{
i.test();
});
}

准确的说spi没有破坏双亲委托,只是绕过了双亲委托获取class,委托机制上层(例如bootstrapClassloader)通过Thread.currentThread().getContextClassLoader()来获取
委托机制里面下层(例如AppClassLoader)加载的类。具体code如下:

1
2
3
4
5
6

public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}

这样做是为了解决一些 上层类依赖下层类的情况,比如jdbc,jdbc相关的类文件在rt.jar中,但是driver对应的实现类是放在我们应用的jar的lib里面,
这样 extClassLoader 就加载不到,但是通过spi机制,rt.jar包中的jdbc相关类就能获取由AppClassLoader 加载的jdbc具体的driver了。
上面讲到 双亲委托机制是通过loaderclass这个方法来实现的,如果我们自定义的classloader覆盖改方法(或者覆盖findClass),就能强行破坏,例如osgi.
在jvm里面每个class的唯一标识符由 classloader全名+class全名构成,因此假设两个独立的classloader,加载一样的class,jvm也会认为是两个独立的class,
tomcat就是此机制来实现webapp目录下不同的web应用的隔离

详解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的使用前提

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