上一篇说了类加载器、双亲委派机制、自定义类加载器
一、 问题ask
1. 自定义类加载器的上一层也就是父类加载器是谁
System.out.println(new MyClassLoader().getParent());
输入后果:sun.misc.Launcher$AppClassLoader@18b4aac2
2. 我没有指定parent呀 为什么不是null呢
咱们自定义类加载器继承了ClassLoader,new MyClassLoader()的时候会先走类加载器的结构
// 无参结构 调用了2个参数的结构
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
// 这里指定了parent parent从哪儿来 看getSystemClassLoader()
private ClassLoader(Void unused, ClassLoader parent) {
// 指定parent
this.parent = parent;
// 其余操作
if (ParallelLoaders.isRegistered(this.getClass())) {
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
domains =
Collections.synchronizedSet(new HashSet<ProtectionDomain>());
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
domains = new HashSet<>();
assertionLock = this;
}
}
@CallerSensitive
public static ClassLoader getSystemClassLoader() {
// 返回的scl 看scl怎么初始化的
initSystemClassLoader();
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
// 获取classLoader
scl = l.getClassLoader();
try {
scl = AccessController.doPrivileged(
new SystemClassLoaderAction(scl));
} catch (PrivilegedActionException pae) {
oops = pae.getCause();
if (oops instanceof InvocationTargetException) {
oops = oops.getCause();
}
}
if (oops != null) {
if (oops instanceof Error) {
throw (Error) oops;
} else {
// wrap the exception
throw new Error(oops);
}
}
}
sclSet = true;
}
}
// 间接返回了loader loader 是怎么来的
public ClassLoader getClassLoader() {
return this.loader;
}
// Launcher类初始化的时候 构造方法里初始化了load 默认是appclassloader
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
3. 间接获取零碎类加载器
ClassLoader.getSystemClassLoader(); -> appClassLoader
4. 能够本人指定parent
// 写一个有参的结构 传入一个你想认的爹 而后调用super 把parent传进去就行了
public MyClassLoader(ClassLoader parent) {
super(parent);
}
5. 突破双亲委派? 看一眼 不了解没关系 我也不了解 从别的中央抄过来的
- 重写LoadClass办法
因为双亲委派是在loadClass里边的逻辑指定的 -
什么时候突破 ?
- JDK1.2之前 没有findClass 必须重写loadClass
- ThreadCotextClassLoader 能够实现根底类调用实现类代码,通过thread.setContentClassLoader 指定
- 热启动 热部署
osgi 、tomcat 都有本人的模块指定classloader (能够加载同一类库不同版本)
比方两个WebApplication加载不同版本的同一个类
二、Linking
- verification
对文件格式进行校验 - preparation
给动态变量赋默认值 -
resolution
-
将类、办法 、属性等符号援用解析为间接援用
常量池中的各种符号援用解析为指针、偏移量等内存地址的间接援用。
比方java.lang.Object 他是个符号援用
如果想找他真是的内存数据 须要依据java.lang.Object先去常量池找见这个符号,而后再依据符号找对应的类型,这个就太绕了 ,间接把符号援用解析为间接援用的话 java.lang.Object 就变为0x00012 内存地址 ,间接依据这个地址找类型就能够了
-
三、Initializing
调用初始化代码 <clinit> 给动态成员赋初始值
1. 面试题 输入后果是多少
/**
* @author 木子的昼夜
*/
public class Mr {
public static void main(String[] args) {
System.out.println(T.count);
}
}
class T{
// 成员变量
public static int count = 10;
public static T t = new T();
// 结构
private T(){
count++;
}
}
后果:11
如果赋值和new 对象 换一下地位呢
/**
* @author 木子的昼夜
*/
public class Mr {
public static void main(String[] args) {
System.out.println(T.count);
}
}
class T{
// 成员变量
public static T t = new T();
public static int count = 10;
// 结构
private T(){
count++;
}
}
后果: 10
本人想下这个过程 想不通能够公众号留言 我再进行解答 应该都能够想的通 。。
2. 也就是
- 动态属性 : load->默认值->初始值
- 成员属性: new -> 申请内存->默认值->初始值
3. 这里有个面试题 单例 双重校验
/**
* @author 木子的昼夜
*/
public class Sig {
private static T03 t03;
public static T03 getInstance(){
// 先校验是否是null
if (t03 == null) {
// 等锁
synchronized (T03.class){
// 接着校验是否是null 因为可能多集体等锁
if (t03 == null){
t03 = new T03();
}
}
}
return t03;
}
}
class T03{
}
这个单例模式有什么问题吗 ?
面试官会疯狂的暗示你 加volatile .
接着会问volatile的作用 : 禁止指令重排 保障可见性
这里就是因为 咱们说的 new T03() 的时候 先分配内存 再赋初始值 再赋默认值
如果内存调配好了 另一个线程 if(t03 == null) 就是false了
而后就返回了 如果用t03.count 那他还是0呢
当然 概率很低 然而这是会呈现的
让咱们看一下T03 t03 = new T03();的过程
public class T03 {
public int count =8;
}
public class Test {
public static void main(String[] args) {
T03 t03 = new T03();
}
}
留神:这里须要应用 idea的一个工具->jclasslib ByteCode Viewer 间接搜寻装置即可
- 先运行一下main办法 生成class文件
- 选中Test文件
-
view 视图 找 Show ByteCode By jclasslib
- 看生成过程
0 new #2 <T03> // (1)这句话就是在内存开拓一块空间 count = 0
3 dup
4 invokespecial #3 <T03.<init>> // (2)这句话就是初始化count值 count = 8
7 astore_1 //(3) 这句话 就是把内存空间 地址援用 赋值给t03变量
8 retur
失常状况下 依照(1) (2) (3)的程序执行 是没有任何问题的 然而指令可能重排
可能会呈现 (1) (3)(2) 这种状况 就是咱们上边说的呈现问题的状况 所以要禁止指令重排 volatile
4. JMM 不是接妹妹 是 Java Memory Model
1. 先来一个存储器的层次结构图 来开开胃
2. 为什么会呈现数据不统一 ?
假如线程1应用cpu1 把数据 x 读到了L0、L1、L2中的任何一个中央 这是cpu独享的
线程2 应用cpu2 把数据x 也读到了 cpu2的 L0 、L1、L2的任何一个中央
这时候就是一个数据 在内存中存储着2份了 其中一份批改了 那另一份没改 是不是就有问题了
3.硬件层面怎么来解决这个问题 — 总线锁
在cpu 读取数据 L3–>L2 都要过总线
在cpu1读取x的时候 给总线上一把锁 这时候cpu2不容许读
毛病: 总线锁是锁总线,也就是我cpu2不拜访x 我cpu2去拜访y 也不能拜访 这样不是很正当吧
大家去洗脚了,你找了小丽,而后在门口上了一把锁,凭什么不让我去找小兰。。。
4.硬件层面怎么来解决这个问题 — 一致性协定(各种各样)MESI 、MSI MOSI 、Synaose 、Firefly 、Dragon 等
个别大家聊的时候 是MESI — intel CPU 实现协定
what is MESI ? is this !
- 数据存储在缓存行上 缓存行用额定两位two bit 来标记状态 ,这里须要留神,如果数据夸缓存行了,那就很难用这种形式标记了,就须要应用总线锁了,呀呼嘿嘿
-
这个很难表白 我试着说一下子
1.我是cpu1, 我从主从读取了x ,这时候只有我读没有其余cpu读,我会标记位Exclusive
- 如果我读的时候,还有别的cpu在读,那我就标记位Shared
- 如果我读回来,我做了批改,那我就标记位Modified ,这个时候他人就会变成Invalid
- 如果我读回来,别的cpu不要脸的进行了批改(为啥我批改就不是不要懒 哈哈),那我就标记为Invalid ,这时候如果我要用这个数计算的时候,我会从新从内存读取一下
至于这些状态都是在什么时候变动的,这个学识就大了去了,主板上各种逻辑单元,我也不晓得是什么高科技实现的。
5. 再叙–缓存行
上边说了 缓存行的2bit标记状态 那什么是缓存行呢?
cpu这个家伙呀,在读取数据的时候,是以缓存行为最小单位读取的
比方int x =666; cpu在读取x的时候不会只读取这四个字节,他会读取x及x当前的N个字节
这些个字节总的就叫缓存行,个别缓存行是64字节
缓存行问题:
我是cpu1, 我读取x的时候,会把整个缓存行读取了
我批改了x ,我把缓存行状态改为invalid,其实我没有
批改y z w j 然而如果别的cpu在应用y z w j的话
就须要从新加载一遍
这个问题叫:伪共享 : 位于同一缓存行的两个不同数据被两个CPU锁定,产生相互影响。
这里有一个缓存行对齐的例子:
public class CacheLineTest01 {
static T[] arr = new T[2];
static{
arr[0] = new T();
arr[1] = new T();
}
public static void main(String[] args) throws InterruptedException {
final CountDownLatch cdl = new CountDownLatch(2);
final long count = 1_0000_0000L;
long start = System.currentTimeMillis();
// 起两个线程 别离批改arr[0] arr[1] 对应对象T的属性
// 这个arr很大概率上会在一个缓存行 因为就2个T对象 每个对象就一个Long类型属性 总共不够64字节
new Thread(()->{
for (long i = 0; i <count; i++) {
arr[0].x = i;
}
cdl.countDown();
}).start();
new Thread(()->{
for (int i = 0; i < count; i++) {
arr[1].x = i;
}
cdl.countDown();
}).start();
cdl.await();
long end = System.currentTimeMillis();
System.out.println((end-start)/100);
}
}
class T{
public volatile long x=0L;
}
执行屡次输入后果:
30、29、23、26、27、30
public class CacheLineTest02 {
static T006[] arr = new T006[2];
static{
arr[0] = new T006();
arr[1] = new T006();
}
public static void main(String[] args) throws InterruptedException {
final CountDownLatch cdl = new CountDownLatch(2);
long start = System.currentTimeMillis();
final long count = 1_0000_0000L;
new Thread(()->{
for (long i = 0; i < count; i++) {
arr[0].x = i;
}
cdl.countDown();
}).start();
new Thread(()->{
for (int i = 0; i <count; i++) {
arr[1].x = i;
}
cdl.countDown();
}).start();
cdl.await();
long end = System.currentTimeMillis();
System.out.println((end-start)/100);
}
}
// 加了一个对齐 也就是Padding 这样new2个T006之后 相对不在一个缓存行
// 所以两个cpu批改属性 不会相互影响
class T006 extends Padding{
public volatile long x=0L;
}
class Padding{
long a,b,c,d,e,f,g;
}
执行屡次后果:
14、16、16、14、17、14、15
很显著,第二段代码的执行工夫更快 这就是缓存行对齐对程序效率晋升的作用
能够看图:第一段代码 会走invalid 每次都会去内存拿数据 再进行批改 ,而第二段代码会走Modified不须要去内存再一次拿数据
6. 乱序执行 01
用一句话总结:cpu为了进步执行效率,会在一条指令筹备数据过程中,执行另一条不依赖于前一条指令的指令
能够看一个例子:cpu在执行指令1的时候,指令1 须要去内存拿数据 ,大家晓得内存读取数据耗时至多是cpu的100倍起步,这个工夫cpu等着吗? 不能呀! 那你电脑不卡成狗了吗。
这个工夫cpu会接着去判断下一条指令2,看指令2是否依赖指令1的执行后果,如果依赖,接着看指令3,如果不依赖就执行,顺次往下执行,直到指令1拿回来数据为止
举个例子:
小强做饭,第一道菜是土豆炖牛腩,第二道菜是拍黄瓜
如果是你,你会怎么做?
最容易些想到的是这样:
筹备土豆->筹备牛腩->放锅里->看着它炖熟了->盛进去->筹备黄瓜->拍黄瓜->倒酱汁->拍黄瓜做好了
然而咱们个别不会这么做,咱们跟cpu一样聪慧:
咱们会这样做:
筹备土豆->筹备牛腩->放锅里->判断拍黄瓜这道菜要不要等土豆牛腩好了能力做?->不是->筹备黄瓜->拍黄瓜->倒酱汁->拍黄瓜做好了->在做拍黄瓜的过程中你必定会看着土豆牛腩,避免干锅,如果拍黄瓜过程中土豆牛腩好了,你会先进行拍黄瓜,先去把牛腩捞进去(不然土豆块成土豆汤了),而后再去拍黄瓜
7.乱序执行 02
合并写的概念:
拿生存中的例子就是,小强的土豆炖牛肉好了,能够放上桌让他人吃了,然而他感觉,这顿饭拍黄瓜跟土豆炖牛肉一起吃能力称之为“一顿饭”,留神这里一顿饭在cpu中能够对应一个数据。而后他就俩都做好了,拿一个大托盘,把2道菜合成了“一顿饭” 放上桌,大家吃的不可开交。
学术上的概念大略意思就是: 多个程序对同一个数据x进行操作,cpu执行x=x+1; 筹备把后果写回L3内存,然而他“自作聪明”的发现,后边如同还有一句 x = x+10;所以他就等着x=x+10;这句执行完之后 再把一个最终后果写回L3内存 ,而不是写2次。
合并写的缓冲区WCbuffer 很小很小 只有4个字节
8.乱序执行 证实小程序
import java.util.concurrent.CountDownLatch;
public class TestOrder {
private static int a=0,b=0,x=0,y=0;
public static void main(String[] args) throws InterruptedException {
long count = 0;
for (;;){
count++;
CountDownLatch cdl = new CountDownLatch(1);
CountDownLatch cdlres = new CountDownLatch(2);
// 默认值
a=0;b=0;x=0;y=0;
new Thread(()->{
try {
cdl.await();
a = 1;
x = b;
} catch (InterruptedException e) { }finally {
cdlres.countDown();
}
}).start();
new Thread(()->{
try {
cdl.await();
b = 1;
y = a;
} catch (InterruptedException e) { }finally {
cdlres.countDown();
}
}).start();
cdl.countDown();
cdlres.await();
if (x==0&&y==0){
System.out.println("存在乱序"+",一共执行:"+count+ " 次");
break;
}
}
}
}
如果不重排呈现的后果应该是:
如果呈现x==0 && y == 0 的状况 阐明指令重拍了
想要证实,你就拿着这个程序,跑吧, 跑一会儿 ,要有急躁
看看我执行的次数:40多万次
9.有序性保障
待续。。
有问题欢送 留言,可也在公众号留言(想赢快) :
发表回复