Isaac Li

码农 + 测试


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

Java 并发编程之美学习笔记之基础篇二

发表于 2018-12-19 | 分类于 Java

引言

upload successful

多线程或并发编程是Java体系中的重点和难点,新购入 《Java 并发编程之美》,本博以笔记的形式记录知识要点。


Java 中共享变量的内存可见性问题

Java 内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫工作内存,线程读写变量时操作的是自己工作内存中的变量。

Synchronized 关键字

Synchronized 块是 Java 提供的一种原子性内置锁,Java 中的每个对象都可以把它当作一个同步锁来使用。

Synchronized 的使用会导致上下文切换。

Synchronized 块的内存语义是把在 Synchronized 块内使用到的变量从线程的工作内存中清除,这样在 Synchronized 块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出 Synchronized 块的内存语义是把在 Synchronized 块内对共享变量的修改刷新到主内存。

除可以解决共享变量内存可见性问题外,Synchronized 经常被用来实现原子性操作。

Volatile 关键字

Synchronized 锁太笨重,因为它会带来线程上下文的切换开销。对于解决内存可见性问题,Java 还提供了一种弱形式的同步,也就是使用 Volatile 关键字。该关键字可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为 Volatile 时,线程在写入变量时不会把值缓存在寄存器或其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。

Volatile 虽然提供了可见性保证,但并不保证操作的原子性。

那么一般什么时候才使用 Volatile 关键字呢?

  • 写入变量值不依赖变量的当前值时。因为如果依赖当前值,将是获取——计算——写入三步操作,这三步操作不是原子性的,而 Volatile 不保证原子性。

  • 读写变量值时没有加锁。因为加锁本身已经保证了内存可见性,这时候不需要把变量声明为 Volatile 的。

Java 中的 CAS 操作

Java 提供的非阻塞的 volatile 关键字可以解决共享变量的可见性问题,但不能解决读——改——写等的原子性问题。CAS 即 Compare and Swap,其是 JDK 提供的非阻塞原子性操作。

CAS 操作有个经典的 ABA 问题,ABA 问题的产生是因为变量的状态值产生了环形转换,就是变量的值可以从 A 到 B,然后再从 B 到 A。如果变量的值只能朝一个方向转换,不构成环形,就不会存在问题。JDK 中的 AtomicStampedReference 类给每个变量的状态值都配备了一个时间戳,从而避免了 ABA 问题的产生。

Unsafe 类

JDK 的 rt.jar 包中的 Unsafe 类提供了硬件级别的原子性操作,Unsafe 类中的方法都是 native 方法,它们使用 JNI 的方式访问本地 C++ 实现库。

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
import sun.misc.Unsafe;

public class TestUnSafe {
//获取 Unsafe 的实例
static final Unsafe unsafe = Unsafe.getUnsafe();

//记录变量 state 在类 TestUnSafe 中的偏移值
static final long stateOffset;

private volatile long state =0;

static {
try{
//获取 state 变量在类 TestUnSafe 中的偏移值
stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));
}catch (Exception e) {
System.out.println (e.getLocalizedMessage());
throw new Error(e);
}
}

public static void main(String[] args) {
//创建实例,并且设置 state 值为1
TestUnSafe test = new TestUnSafe();

Boolean sucess = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
System.out.println(sucess);
}

}

这段代码执行结果如下

upload successful

查看 Unsafe 源码

upload successful
代码判断是不是 Bootstrap 类加载器加载的 localClass,很明显 TestUnSafe.class 是使用 AppClassLoader 加载的,所以这里直接抛了异常。

如果真的想要实例化 Unsafe 类,可以使用反射来获取 Unsafe 类实例方法。

Java指令重排序

Java 内存模型允许编译器和处理器对指令重排序以提高运行性能,并且只会对不存在数据依赖性的指令重排序。

重排序在多线程下会导致非预期的程序执行结果,而使用 volatile 修饰 ready 就可以避免重排序和内存可见性问题。

写 volatile 变量时,可以确保 volatile 写之前的操作不会被编译器重排序到 volatile 写之后。读 volatile 变量时,可以确保 volatile 读之后的操作不会被编译器重排序到 volatile 读之前。

锁的概述

悲观锁指对数据被外界修改持保守态度,认为数据很容易就会被其它线程修改,所以在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态。

乐观锁是相对悲观锁来说的,它认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排它锁,而是在进行数据提交更新时,才会正式对数据冲突与否进行检测。

乐观锁并不会使用数据库提供的锁机制,一般在表中添加 version 字段或者使用业务状态来实现。乐观锁直到提交时才锁定,所以不会产生任何死锁。

根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁。公平锁表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的。而非公平锁则在运行时闯入。

在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销。

根据锁只能被单个线程持有还是能被多个线程共同持有,锁可以分为独占锁和共享锁。独占锁是一种悲观锁,共享锁则是一种乐观锁。

Synchronized 内部锁是可重入锁。可重入锁的原理是在锁内部维护一个线程标示,用来标示该锁目前被哪个线程占用,然后关联一个计数器。一开始计数器值为0,说明该锁没有被任何线程占用。当一个线程获取了该锁时,计数器的值会变成1,这时其他线程再来获取该锁时会发现锁的所有者不是自己而被阻塞挂起。但是当获取了该锁的线程再次获取锁时发现锁拥有者是自己,就会把计数器值加1,当释放锁后计数器值-1.当计数器值为0时,锁里面的线程标示被重置为 null,这时候被阻塞的线程会被唤醒来竞争获取该锁。

自旋锁当前线程在获取锁时,如果发现锁已经被其它线程占用,它不马上阻塞自己,在不放弃CPU使用权的情况下,多次尝试获取。自旋锁是使用CPU时间换取线程阻塞与调度的开销。

Java 并发编程之美学习笔记之基础篇

发表于 2018-12-18 | 分类于 Java

引言

upload successful

多线程或并发编程是Java体系中的重点和难点,新购入 《Java 并发编程之美》,本博以笔记的形式记录知识要点。


并发编程线程基础

进程和线程关系

一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。

程序计数器用来记录线程当前要执行的指令地址。

每个线程都有自己的栈资源,用于存储该线程的局部变量,这些局部变量是该线程私有的,其他线程是访问不了的,除此之外栈还用来存放线程的调用栈帧。

堆里面主要存放使用 new 操作创建的对象实例。

方法区用来存放 JVM 加载的类、常量及静态变量等信息,也是线程共享的。

线程的创建与运行

Java 中有三种线程创建方式,分别为实现 Runnable 接口的 run 方法,继承 Thread 类并重写 run 的方法,使用 FutureTask 方式。

使用继承方式的好处是:在 run 方法内获取当前线程直接使用 this 就可以了,无须使用 Thread.currrentThread() 方法。不好的地方是 Java 不支持多继承。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码。而 Runnable 则没有这个限制。Runnable 只能使用主线程里面被声明为 final 的变量。

上面两种方式都有一个缺点,就是任务没有返回值。Futuretask 方式可以拿到返回值。

线程通知与等待

当一个线程调用一个共享变量的 wait() 方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回

  • 其他线程调用了该共享对象的 notify() 或 notifyAll() 方法。
  • 其他线程调用了该线程的 interrupt()方法。
  • 该线程抛出 InterruptedException 异常返回。

Note: 这里要注意的是区分共享对象和线程

另外需要注意的是,如果调用 wait() 方法时没有事先获取该对象的监视器锁,则调用 wait() 方法时调用线程会抛出 IllegalMonitorStateException 异常。

当前线程调用 wait() 方法后只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的。

让线程睡眠的 sleep 方法

Thread 类中有一个静态的 sleep 方法,当一个执行中的线程调用了这个方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的

让出 CPU 执行权的 yield 方法

Thread 类中有一个静态的 yield 方法,当一个线程调用 yield 方法时,当前线程会让出CPU使用权,然后处于就绪状态,线程调度器会从线程就绪队列里获取一个线程优先级最高的线程。

线程死锁

死锁的产生必须具备以下四个条件

  • 互斥条件
  • 请求并持有条件
  • 不可剥夺条件
  • 环路等待条件

造成死锁的原因其实和申请资源的顺序有很大关系,使用资源申请的有序性原则就可以

ThreadLocal

ThreadLocal 是 JDK 包提供的,它提供了线程本地变量,如果你创建了一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是本地内存里的变量,从而避免了线程安全问题。

ThreadLocal 不支持继承性

InheritableThreadLocal 类继承自 ThreadLocal 类,提供了一个特性,就是让子线程能访问到父线程中设置的本地变量。

CV 计算机视觉学习笔记之滤波

发表于 2018-12-17 | 分类于 CV

概述

噪声对图像处理的影响很大,它影响图像处理的输入、采集和处理等各个环节以及输出结果。因此,在进行其它的图像处理前,需要对图像进行去噪处理。

从统计学的观点来看,凡是统计特征不随时间变化的噪声称为平稳噪声,而统计特征随时间变化的噪声称为非平稳噪声。幅值基本相同,但是噪声出现的位置是随机的,称为椒盐噪声;如果噪声的幅值是随机的,根据幅值大小的分布,有高斯型和瑞利型两种,分别称为高斯噪声和瑞利噪声。

椒盐噪声:出现位置是随机的,但噪声的幅值是基本相同的。
高斯噪声:出现位置是固定的(每一点),但噪声的幅值是随机的。

常见的去噪处理有均值滤波,中值滤波,灰度最小方差均值滤波,K近邻平滑滤波,对称近邻均值滤波,西戈玛平滑滤波等。

均值滤波

均值滤波器可以归为低通滤波器,是一种线性滤波器,其输出为邻域模板内的像素的简单平均值,主要用于图像的模糊和降噪。

均值滤波器的概念非常的直观,使用滤波器窗口内的像素的平均灰度值代替图像中的像素值,这样的结果就是降低图像中的“尖锐”变化。这就造成,均值滤波器可以降低噪声的同时,也会模糊图像的边缘。均值滤波器的处理结果是过滤掉图像中的“不相关”细节,其中“不相关”细节指的是:与滤波器模板尺寸相比较小的像素区域。

权系数矩阵模板

upload successful

upload successful

upload successful

优点: 算法简单,计算速度快;

缺点: 它不能很好地保护图像细节,在图像去噪的同时也破坏了图像的细节部分,从而使图像变得模糊,不能很好地去除噪声点。

中值滤波

中值滤波也是消除图像噪声最常见的手段之一,特别是消除椒盐噪声,中值滤波的效果要比均值滤波更好。中值滤波是跟均值滤波唯一不同是,不是用均值来替换中心每个像素,而是将周围像素和中心像素排序以后,取中值。

中值滤波的设计思想:因为噪声的出现(如椒盐噪声),使改点的像素比周围的像素亮(暗)许多,那么对像素点周围的像素进行由小到大的排列后,最明或最暗的像素一定是排在两边。取排在中间位置上像素值的灰度值替换待处理像素值,就可以达到去除噪声的目的。

upload successful

upload successful

左边是添加概率为0.2的椒盐噪声,右边是原图。下面是使用常规的中值滤波和自适应中值滤波器后的处理结果

upload successful

高斯滤波

高斯滤波器是一种线性滤波器,能够有效的抑制噪声,平滑图像。其作用原理和均值滤波器类似,都是取滤波器窗口内的像素的均值作为输出。其窗口模板的系数和均值滤波器不同,均值滤波器的模板系数都是相同的为1;而高斯滤波器的模板系数,则随着距离模板中心的增大而系数减小。所以,高斯滤波器相比于均值滤波器对图像个模糊程度较小。

模拟人眼,关注中心区域,有效去除高斯噪声。

权系数矩阵模板
upload successful

upload successful

upload successful


引用

图像噪声的抑制——均值滤波、中值滤波、对称均值滤波

图像处理基础(2):自适应中值滤波器(基于OpenCV实现)

Python-OpenCV——进阶操作一网打尽

Docker 下安装配置 Cloud9

发表于 2018-12-15 | 分类于 Linux

安装配置 Cloud9 的原因

日常工作中可能需要使用 Web IDE 进行代码展示或存储,经过查询,Eclipse Che 和 Cloud9 是两款优秀的 Web IDE,先选择 Cloud9 进行安装试用。

在寻找 Docker 仓库中发现了 kdelfour/cloud9-docker,试用过程中发现下列几个问题

  • 没有安装 JDK,不能进行 Java 代码编译运行
  • 系统是 Ubuntu 14.04.2 LTS
  • 默认端口号是 80

为了解决这些问题,开始搭建符合自己需求的 Cloud9 容器

安装步骤

一. 安装 Docker CE (社区版)
  1. Install required packages. yum-utils provides the yum-config-manager utility, and device-mapper-persistent-data and lvm2 are required by the devicemapper storage driver.
1
yum install -y yum-utils  device-mapper-persistent-data  lvm2
  1. Use the following command to set up the stable repository. You always need the stable repository, even if you want to install builds from the edge or test repositories as well.
1
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
  1. Install the latest version of Docker CE, or go to the next step to install a specific version:
1
yum install docker-ce
  1. Start Docker and check docker version
1
2
systemctl start docker
docker -v
二. 获取 centos7 容器的镜像
  1. 配置国内镜像仓库
1
2
echo "DOCKER_OPTS=\"--registry-mirror=http://hub-mirror.c.163.com\"" >> /etc/default/docker
service docker restart
  1. 查找 centos7 镜像
1
docker search centos
  1. 下载镜像
1
docker pull centos
三. 安装 OpenJDK
  1. 启动 centos 容器

    –network=host 让容器使用主机的 IP 地址
    -v 配置路径 workspace 是 cloud9 将要使用的路径

1
2
3
4
5
6
7
8
9
10
11
12
13
docker run -it -d --name cloud9 --network=host -v /data/workspace/:/workspace/ centos
docker exec -it cloud9 bash
yum update
yum -y install which
yum -y install java-1.8.0-openjdk java-1.8.0-openjdk-devel

cat > /etc/profile.d/java8.sh <<EOF
export JAVA_HOME=$(dirname $(dirname $(readlink $(readlink $(which javac)))))
export PATH=\$PATH:\$JAVA_HOME/bin
export CLASSPATH=.:\$JAVA_HOME/jre/lib:\$JAVA_HOME/lib:\$JAVA_HOME/lib/tools.jar
EOF

source /etc/profile.d/java8.sh
四. 安装编译 Cloud9
  1. 安装编译环境
1
yum install -y build-essential g++ curl libssl-dev apache2-utils git libxml2-dev sshfs
  1. 安装 Node.js
1
2
curl -sL https://rpm.nodesource.com/setup_11.x | bash -
yum install -y nodejs
  1. 安装 Cloud9
1
2
3
4
5
git clone https://github.com/c9/core.git /cloud9
cd /cloud9
scripts/install-sdk.sh

mkdir -p /etc/supervisor/conf.d/
  1. 配置 cloud9.conf 文件
vi /etc/supervisor/conf.d/cloud9.conf


[program:cloud9]
command = node /cloud9/server.js --listen 0.0.0.0 --port 80 -w /workspace
directory = /cloud9
user = root
autostart = true
autorestart = true
stdout_logfile = /var/log/supervisor/cloud9.log
stderr_logfile = /var/log/supervisor/cloud9_errors.log
environment = NODE_ENV="production"

node 启动参数说明:

–settings Settings file to use 设置启动配置文件
–help Show command line options.帮助
-t Start in test mode 启动测试模式
-k Kill tmux server in test mode 在测试模式中杀死tmux终端
-b Start the bridge server - to receive commands from the cli [default: false]
-w Workspace directory 设置工作目录
–port Port 端口号
–debug Turn debugging on 启动调试模式
–listen IP address of the server 侦听ip,默认应该是0.0.0.0
–readonly Run in read only mode 只读模式,不能进行修改文件等操作
–packed Whether to use the packed version. 使用打包好的版本
–auth Basic Auth username:password 登陆时时使用http auth认证 设置用户名密码
–collab Whether to enable collab. 是否设置collab
–no-cache Don’t use the cached version of CSS 不进行缓存

  1. 设置自启动

Supervisor 是一个用 Python 写的进程管理工具,可以很方便的用来启动、重启、关闭进程(不仅仅是 Python 进程)。除了对单个进程的控制,还可以同时启动、关闭多个进程,比如很不幸的服务器出问题导致所有应用程序都被杀死,此时可以用 supervisor 同时启动所有应用程序而不是一个一个地敲命令启动。

1
2
yum install python-setuptools -y
easy_install supervisor

启动界面

upload successful

upload successful


Supervisor 安装与配置可参考:

Supervisord 官网

Supervisor安装与配置

使用 supervisor 管理进程

Java 类加载器

发表于 2018-12-12 | 分类于 Java

一、类加载机制

当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过加载、连接、初始化三个步骤来对该类进行初始化

upload successful

a. 加载

加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个Class文件获取,这里既可以从ZIP包中读取(比如从jar包和war包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将JSP文件转换成对应的Class类)。

b. 验证

这一阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。主要包括四种验证,

  • 文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面的三个验证都是基于方法区的存储结构进行的。
  • 元数据验证:对类的元数据信息进行语义校验(其实就是对类中的各数据类型进行语法校验),保证不存在不符合Java语法规范的元数据信息。
  • 字节码验证:该阶段验证的主要工作是进行数据流和控制流分析,对类的方法体进行校验分析,以保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。
  • 符号引用验证:这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段中发生该转化,后面会有讲解),主要是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。

c. 准备

准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。注意这里所说的初始值概念,比如一个类变量定义为:

public static int v = 8080;
实际上变量v在准备阶段过后的初始值为0而不是8080,将v赋值为8080的putstatic指令是程序被编译后,存放于类构造器方法之中,这里我们后面会解释。
但是注意如果声明为:

public static final int v = 8080;
在编译阶段会为v生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将v赋值为8080。

d. 解析

解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是class文件中的:

CONSTANT_Class_info
CONSTANT_Field_info
CONSTANT_Method_info
等类型的常量。

下面我们解释一下符号引用和直接引用的概念:

符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

e. 初始化

初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。

初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证方法执行之前,父类的方法已经执行完毕。p.s: 如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。

类初始化的时机

当Java程序首次通过下面的 6种 方式来使用某个类或接口时,系统就会初始化该类或接口,也称为主动初始化。
触发类加载的条件:

  1. 创建类的实例。
    • 使用 new 来创建实例
    • 通过反射创建实例
    • 通过反序列化来创建实例
  2. 调用类的类变量(静态属性),或为该类变量赋值。
  3. 调用类的静态方法。
  4. 通过class文件反射创建对象。
    例如:Class.forName(“Person”);
  5. 初始化一个子类的时候,该子类的所有父类都会被初始化。
  6. java虚拟机启动时被标记为启动类的类,就是 main 方法所在的类。

注意以下几种情况不会执行类初始化:

  • 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
  • 定义对象数组,不会触发该类的初始化。
  • 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
  • 通过类名获取Class对象,不会触发类的初始化。
  • 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
  • 通过ClassLoader默认的loadClass方法,也不会触发初始化动作。
  • 在编译时能够确定下来的 final修饰的静态变量(编译常量)不会对类进行初始化。
  • 在编译时无法确定下来的 final修饰的静态变量(运行时常量)会对类进行初始化。

二. 类加载器

类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。

基本上所有的类加载器都是 java.lang.ClassLoader类的一个实例。

java.lang.ClassLoader类介绍

java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例。除此之外,ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。

类加载器的树状组织结构

Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:

  • 引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。
  • 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录(JAVA_HOME/jre/ext/*.jar或java.ext.dirs路径下的内容)。该类加载器在此目录里面查找并加载 Java 类。由sun.misc.Launcher$ExtClassLoader实现
  • 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。由sun.misc.Launcher$AppClassLoader实现

除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。

除了引导类加载器之外,所有的类加载器都有一个父类加载器。通过 表 1中给出的 getParent()方法可以得到。对于系统提供的类加载器来说,系统类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是引导类加载器;对于开发人员编写的类加载器来说,其父类加载器是加载此类加载器 Java 类的类加载器。因为类加载器 Java 类如同其它的 Java 类一样,也是要由类加载器来加载的。一般来说,开发人员编写的类加载器的父类加载器是系统类加载器。类加载器通过这种方式组织起来,形成树状结构。树的根节点就是引导类加载器。图 1中给出了一个典型的类加载器树状组织结构示意图,其中的箭头指向的是父类加载器。

图 1. 类加载器树状组织结构示意图

upload successful

代码清单 1演示了类加载器的树状组织结构。

清单 1. 演示类加载器的树状组织结构

1
2
3
4
5
6
7
8
9
10
public class ClassLoaderTree { 

public static void main(String[] args) {
ClassLoader loader = ClassLoaderTree.class.getClassLoader();
while (loader != null) {
System.out.println(loader.toString());
loader = loader.getParent();
}
}
}

类加载器的代理模式

类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。在介绍代理模式背后的动机之前,首先需要说明一下 Java 虚拟机是如何判定两个 Java 类是相同的。Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。比如一个 Java 类 com.example.Sample,编译之后生成了字节代码文件 Sample.class。两个不同的类加载器 ClassLoaderA和 ClassLoaderB分别读取了这个 Sample.class文件,并定义出两个 java.lang.Class类的实例来表示这个类。这两个实例是不相同的。对于 Java 虚拟机来说,它们是不同的类。试图对这两个类的对象进行相互赋值,会抛出运行时异常 ClassCastException。

了解了这一点之后,就可以理解代理模式的设计动机了。代理模式是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用 java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object类,而且这些类之间是不兼容的。通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。

不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间。

加载类的过程

在前面介绍类加载器的代理模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用 defineClass来实现的;而启动类的加载过程是通过调用 loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。

方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;方法 defineClass()抛出的是 java.lang.NoClassDefFoundError异常。

类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。

线程上下文类加载器

线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。

前面提到的类加载器的代理模式并不能解决 Java 应用开发中会遇到的类加载器的全部问题。Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers包中。这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了 JAXP SPI 的 Apache Xerces所包含的 jar 包。SPI 接口中的代码经常需要加载具体的实现类。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory类中的 newInstance()方法用来生成一个新的 DocumentBuilderFactory的实例。这里的实例的真正的类是继承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的实现所提供的。如在 Apache Xerces 中,实现的类是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而问题在于,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。

线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。

Class.forName

Class.forName是一个静态方法,同样可以用来加载类。该方法有两种形式:Class.forName(String name, boolean initialize, ClassLoader loader)和 Class.forName(String className)。第一种形式的参数 name表示的是类的全名;initialize表示是否初始化类;loader表示加载时使用的类加载器。第二种形式则相当于设置了参数 initialize的值为 true,loader的值为当前类的类加载器。Class.forName的一个很常见的用法是在加载数据库驱动的时候。如 Class.forName(“org.apache.derby.jdbc.EmbeddedDriver”).newInstance()用来加载 Apache Derby 数据库的驱动。


引用

深入探讨 Java 类加载器

Anaconda 下安装 pyecharts

发表于 2018-12-07 | 分类于 Python

克隆github库安装

因为我的系统里同时安装了 python2 python3,所以安装 pyecharts 颇费周折

git clone https://github.com/pyecharts/pyecharts.git
cd pyecharts
pip3 install -r requirements.txt
python3 setup.py install

RNN and LSTM (转)

发表于 2018-12-06 | 分类于 Deep Learning

原文见 杨健程 RNN and LSTM

本处引用仅供学习使用

upload successful

upload successful

upload successful

upload successful

upload successful

upload successful

upload successful

upload successful

upload successful

upload successful

upload successful

upload successful

upload successful


引用

Recurrent Neural Networks Tutorial

Understanding LSTM Networks

jupyter notebook 与 Nginx 集成

发表于 2018-11-13 | 分类于 Linux

1. 配置 jupyter notebook

vim /roo/.jupyter/jupyter_notebook_config.py

编辑 C.NotebookApp.base_url

upload successful

注意:这里一定要给个路径,不能直接使用 / 作为路径!原因是 nginx 需要根据路径进行解析,如果使用 / 作为路径,nginx的配置会非常麻烦

2. 配置 Nginx

        location ^~ /ipython/ {
        proxy_pass  http://www.domain.com:port;     
        proxy_set_header    Referer   http://www.isaac-li.com:9527;  
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        # WebSocket support
        proxy_http_version 1.1;
        proxy_set_header Upgrade "websocket";
        proxy_set_header Connection "Upgrade";
        proxy_read_timeout 86400;
        proxy_redirect off;

    }


    location ~* /(api/kernels/[^/]+/(channels|iopub|shell|stdin)|terminals/websocket)/? {
       proxy_pass  http://www.domain.com:port;
       proxy_set_header    Referer   http://www.isaac-li.com:9527;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        # WebSocket support
        proxy_http_version 1.1;
        proxy_set_header Upgrade "websocket";
        proxy_set_header Connection "Upgrade";
        proxy_read_timeout 86400;
        proxy_redirect off;
} 

www.domain.com:port 需要根据实际情况换成自己的域名和端口号,端口号为 jupyter notebook 服务端口

Nginx 配置问题

发表于 2018-11-13 | 分类于 Linux

在配置Nginx时遇到了一类配置问题,这类问题很容易被忽略,但是影响很大

在proxy_pass 配置里 URL路径最后是否有 / 直接影响了 Nginx 的解析路径,要特别注意。

location /proxy/ {
    proxy_pass http://127.0.0.1/;
}
代理到URL:http://127.0.0.1/

location /proxy/ {
    proxy_pass http://127.0.0.1;
}
代理到URL:http://127.0.0.1/proxy/

Hexo 与 Nginx 的集成

发表于 2018-11-06 | 分类于 Linux

使用 Hexo 时最麻烦的事情应该是贴图了,问题主要出现在如何把图片的网络请求路径解析成实际存储路径这个过程中。本文记录了使用 Nginx 集成 Hexo 解决图片显示问题的技术步骤。

1. 编辑 Post

使用 Hexo Admin 插件进行 Post 编辑时直接可以直接使用 Ctrl+C/Ctrl+V 贴图,成功后会有如下信息

upload successful
这时图片是存放在 Hexo目录下的子目录 Source/images 目录中

2. 发布 Post

使用 Hexo g 命令发布 Post,这时图片会存在 Hexo目录下的子目录 public/images 目录中

3. 配置 Nginx

根据 Hexo Admin 插件命名图片的格式,在 nginx.conf 文件中配置如下,把 png 类型的图片请求路径映射到 public/images/目录下

upload successful

上述方法实测有效。现在发布 Post,只需要运行 Hexo g 命令即可。

12…5
Isaac Li

Isaac Li

44 日志
9 分类
36 标签
Links
  • deeplearning.ai
  • AI初学者
  • fast.ai
  • IBM Developer
  • Java Anti-Patterns
  • Tim Dettmers's blog
  • colash's blog
  • 阮一峰的网络日志
  • Dylan's blog
  • shartoo's blog
  • DATAQUEST
  • Java技术驿站
  • w3schools
  • 在线正则表达式测试
© 2018 Isaac Li
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4