Java基础

简单Java基础

接口和抽象类的区别

最大的区别在于:
接口是对象功能的抽象,抽象类是对象本质的抽象。这个是最大区别

img

StringBuilder和StringBuffer

主要有线程安全缓冲区性能三方面的区别

  • 线程安全方面

    • StringBuffer:线程安全的。StringBuilder:线程不安全。因为StringBuffer的所有公开方法都是synchronized修饰的,而StringBuilder并没有sychronized修饰。
  • 缓冲区方面

    StringBuffer每次获取toString都会直接使用缓存区的toStringCache值来构造一个字符串。

    StringBuilder则每次都需要复制一次字符数组,再构造一次字符串

    所以、StringBuffer对缓冲区进行了优化。

  • 性能方面

    由于StringBuilder没有加锁,所以性能要优于StringBuffer

  • 总结:

    StringBuffer适用于多线程操作同一个StringBuffer的场景,StringBuilder在单线程场景下更为合适

Java创建对象的几种方式

  • 通过new关键字来创建
  • 通过反射来创建
  • 通过clone()方法来创建
  • 通过序列化机制来创建

为什么要重写equals方法

因为Object对象中equals方法(源码)中是比较两个对象是否相同,必须两个引用指向统一地址的才会返回true,而一般我们用equals方法来比较对象的内容是否相同,所以一般我们需要重写equals方法。

public class Object {
    ......
        
        public boolean equals(Object obj) {
        return (this == obj);
    }
    ......
}

解决Hash冲突的方法

  • 开放定址法:

    就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入

  • 拉链法

    每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来

  • 再哈希法

    再哈希法又叫双哈希法,有多个不同的Hash函数,当发生冲突时,使用第二个,第三个,….,等哈希函数

  • 建立公共溢出区法

    将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表

深拷贝和浅拷贝

  • 浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象.换言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象.
  • 深拷贝:被复制对象的所有变量都含有与原来的对象相同的值.而那些引用其他对象的变量将指向被复制过的新对象.而不再是原有的那些被引用的对象.换言之.深拷贝把要复制的对象所引用的对象都复制了一遍.

final有哪些用法

  • final修饰的类不可以被继承
  • final修饰的方法不可以被重写
  • final修饰的成员变量不可以被改写,如果修饰应用,那么表示引用的指向不可变,指向的内容可变

Java中的异常

Error:

是程序中无法处理的错误,表示运行应用程序中出现了严重的错误。此类错误一般表示代码运行时JVM出现问题。通常有Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。比如说当jvm耗完可用内存时,将出现OutOfMemoryError。此类错误发生时,JVM将终止线程。非代码性错误。因此,当此类错误发生时,应用不应该去处理此类错误。

Exception:

程序本身可以捕获并且可以处理的异常。

  • 运行时异常(不受捡异常):

    RuntimeException类极其子类表示JVM在运行期间可能出现的错误。编译器不会检查此类异常,并且不要求处理异常,比如用空值对象的引用(NullPointerException)、数组下标越界(ArrayIndexOutBoundException)。此类异常属于不可查异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。

  • 非运行异常(受检异常):

    Exception中除RuntimeException极其子类之外的异常。编译器会检查此类异常,如果程序中出现此类异常,比如说IOException,必须对该异常进行处理,要么使用try-catch捕获,要么使用throws语句抛出,否则编译不通过。

ThreadLocal特性

ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是

  • Synchronized是通过线程等待,牺牲时间来解决访问冲突
  • ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。

正因为ThreadLocal的线程隔离特性,使他的应用场景相对来说更为特殊一些。在android中Looper、ActivityThread以及AMS中都用到了ThreadLocal。当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。

Java assert关键字

Java assert关键字

CAS原理

CAS原理

AQS原理

AQS:AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。
AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包

AQS的核心思想:,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。

AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。

用大白话来说,AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。

**注意:AQS是自旋锁:**在等待唤醒的时候,经常会使用自旋(while(!cas()))的方式,不停地尝试获取锁,直到被其他线程获取成功

实现了AQS的锁有:自旋锁、互斥锁、读锁写锁、条件产量、信号量、栅栏都是AQS的衍生物

img

如图示,AQS维护了一个volatile int state和一个FIFO线程等待队列,多线程争用资源被阻塞的时候就会进入这个队列。state就是共享资源,其访问方式有如下三种:
getState();setState();compareAndSetState();

AQS 定义了两种资源共享方式:
1.Exclusive:独占,只有一个线程能执行,如ReentrantLock
2.Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier

不同的自定义的同步器争用共享资源的方式也不同。

AQS底层使用了模板方法模式

同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):

  1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
  2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
    这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。

自定义同步器在实现的时候只需要实现共享资源state的获取和释放方式即可,至于具体线程等待队列的维护,AQS已经在顶层实现好了。自定义同步器实现的时候主要实现下面几种方法:
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

ReentrantLock为例,(可重入独占式锁):state初始化为0,表示未锁定状态,A线程lock()时,会调用tryAcquire()独占锁并将state+1.之后其他线程再想tryAcquire的时候就会失败,直到A线程unlock()到state=0为止,其他线程才有机会获取该锁。A释放锁之前,自己也是可以重复获取此锁(state累加),这就是可重入的概念。
注意:获取多少次锁就要释放多少次锁,保证state是能回到零态的。

以CountDownLatch为例,任务分N个子线程去执行,state就初始化 为N,N个线程并行执行,每个线程执行完之后countDown()一次,state就会CAS减一。当N子线程全部执行完毕,state=0,会unpark()主调用线程,主调用线程就会从await()函数返回,继续之后的动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。
 在acquire() acquireShared()两种方式下,线程在等待队列中都是忽略中断的,acquireInterruptibly()/acquireSharedInterruptibly()是支持响应中断的。

反射:

反射就是把Java类中的各个成分映射成一个个的Java对象。即在运行状态中,对于任意一个类,都能够知道这个类的所以属性和方法;对于任意一个对象,都能调用它的任意一个方法和属性。这种动态获取信息及动态调用对象方法的功能叫Java的反射机制。

1. 反射机制的功能

Java反射机制主要提供了以下功能:

  • 在运行时判断任意一个对象所属的类。
  • 在运行时构造任意一个类的对象。
  • 在运行时判断任意一个类所具有的成员变量和方法。
  • 在运行时调用任意一个对象的方法。
  • 生成动态代理。

2. 实现反射机制的类

Java中主要由以下的类来实现Java反射机制(这些类都位于java.lang.reflect包中):

  • Class类:代表一个类。 Field类:代表类的成员变量(成员变量也称为类的属性)。

  • Method类:代表类的方法。

  • Constructor类:代表类的构造方法。

  • Array类:提供了动态创建数组,以及访问数组的元素的静态方法。

注解

HashMap

数据结构:

HashMap内部使用链表+数组+红黑树的结构。

插入元素的流程

  1. 判断数组是否为空,为空进行初始化;
  2. 不为空,计算 k 的 hash 值,通过(n - 1) & hash计算应当存放在数组中的下标 index;
  3. 查看 table[index] 是否存在数据,没有数据就构造一个Node节点存放在 table[index] 中;
  4. 存在数据,说明发生了hash冲突(存在二个节点key的hash值一样), 继续判断key是否相等,相等,用新的value替换原数据(onlyIfAbsent为false);
  5. 如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;(如果当前节点是树型节点证明当前已经是红黑树了)
  6. 如果不是树型节点,创建普通Node加入链表中;判断链表长度是否大于 8并且数组长度大于64, 大于的话链表转换为红黑树;
  7. 插入完成之后判断当前节点数是否大于阈值,如果大于开始扩容为原数组的二倍。

HashMap怎么设定初始容量大小:

默认大小是16,负载因子是0.75, 如果自己传入初始大小k,初始化大小为 大于k的 2的整数次方,例如如果传10,大小为16

hash函数如何设计

hash函数是先拿到 key 的hashcode,是一个32位的int值,然后让hashcode的高16位和低16位进行异或操作。

为什么这么设计:

因为key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值。int值范围为**-2147483648~2147483647**,前后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。你想,如果HashMap数组的初始大小才16,用之前需要对数组的长度取模运算,得到的余数才能用来访问数组下标。

源码中模运算就是把散列值和数组长度-1做一个"与"操作,位运算比取余%运算要快。

bucketIndex = indexFor(hash, table.length);

static int indexFor(int h, int length) {
     return h & (length-1);
}

顺便说一下,这也正好解释了为什么HashMap的数组长度要取2的整数幂。因为这样(数组长度-1)正好相当于一个“低位掩码”。“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。以初始长度16为例,16-1=15。2进制表示是00000000 00000000 00001111。和某散列值做“与”操作如下,结果就是截取了最低的四位值。

但这时候问题就来了,这样就算我的散列值分布再松散,要是只取最后几位的话,碰撞也会很严重。更要命的是如果散列本身做得不好,分布上成等差数列的漏洞,如果正好让最后几个低位呈现规律性重复,就无比蛋疼。

右移16位,正好是32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。

为什么这么设计:

  1. 一定要尽可能降低hash碰撞,越分散越好;
  2. 算法一定要尽可能高效,因为这是高频操作, 因此采用位运算;

Java1.8对HashMap做了什么改进

  1. 数组+链表改成了数组+链表或红黑树;
  2. 链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后;
  3. 扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小;
  4. 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;

为什么要改进

  1. 防止发生hash冲突,链表长度过长,将时间复杂度由O(n)降为O(logn);

  2. 因为1.7头插法扩容时,头插法会使链表发生反转,多线程环境下会产生环;

    A线程在插入节点B,B线程也在插入,遇到容量不够开始扩容,重新hash,放置元素,采用头插法,后遍历到的B节点放入了头部,这样形成了环,如下图所示:

    在这里插入图片描述

扩容的时候为什么1.8 不用重新hash就可以直接定位原节点在新数据的位置呢?

这是由于扩容是扩大为原数组大小的2倍,用于计算数组位置的掩码仅仅只是高位多了一个1,怎么理解呢?

扩容前长度为16,用于计算(n-1) & hash 的二进制n-1为0000 1111,扩容为32后的二进制就高位多了1,为0001 1111。

因为是& 运算,1和任何数 & 都是它本身,那就分二种情况,如下图:原数据hashcode高位第4位为0和高位为1的情况;

第四位高位为0,重新hash数值不变,第四位为1,重新hash数值比原来大16(旧数组的容量)

那1.8后HashMap是线程安全的吗?

安琪拉: 不是,在多线程环境下,1.7 会产生死循环、数据丢失、数据覆盖的问题,1.8 中会有数据覆盖的问题,以1.8为例,当A线程判断index位置为空后正好挂起,B线程开始往index位置的写入节点数据,这时A线程恢复现场,执行赋值操作,就把A线程的数据给覆盖了;还有++size这个地方也会造成多线程同时扩容等问题。

LinkedList和ArrayList的区别

ArrayList底层是基于数组实现的,查询效率较高,增删改查的效率较低。

LinkedList底层是基于链表实现的,查询效率较低,增删速度较快

两者都不是线程安全的

concurrentHashMap

ConcurrentHashMap成员变量使用volatile 修饰,免除了指令重排序,同时保证内存可见性,另外使用CAS操作和synchronized结合实现赋值操作,多线程操作只会锁住当前操作索引的节点。

因为在多线程环境下,使用HashMap进行put操作可能会引起死循环,导致cpu利用率接近100%,所以在并发情况下不能使用HashMap

因此针对这一问题:出现了Hashtable 和concurrentHashMap

  • hashtable

    Hashtable使用synchronized来保证线程安全,但在线程竞争激烈的情况下,hashtable的效率非常低下。因为在同一时刻只能有一个线程占有资源,其他线程都处于等待状态。

  • concurrentHashMap

    concurrentHashMap采用分段锁的思想,将数据分成一段一段存储,然后给每一段数据配上一把锁,当一个线程占用锁访问其中一段数据时,其他线程也可以访问别的段数据。

  • 总结

    Hashtable的任何操作都会把整个表锁住,是阻塞的。好处是总能获取最实时的更新,比如说线程A调用putAll写入大量数据,期间线程B调用get,线程B就会被阻塞,直到线程A完成putAll,因此线程B肯定能获取到线程A写入的完整数据。坏处是所有调用都要排队,效率较低
    ConcurrentHashMap 是设计为非阻塞的。在更新时会局部锁住某部分数据,但不会把整个表都锁住。同步读取操作则是完全非阻塞的。好处是在保证合理的同步前提下,效率很高坏处是严格来说读取操作不能保证反映最近的更新。例如线程A调用putAll写入大量数据,期间线程B调用get,则只能get到目前为止已经顺利插入的部分数据。
    应该根据具体的应用场景选择合适的HashMap。

sychronized和ReentrantLock

Synchronized

Java的关键字

为对象,代码块,方法提供线程安全的操作

属于独占式的悲观锁,同时属于可重入锁。

Java中每个对象都有一个monitor对象,加锁就是在竞争monitor对象。对代码块进行加锁是通过在前后分别加上monitorentermonitorexit来实现的

Synchronized的作用范围

  • synchronized作用于成员变量和静态方法时,锁住的是对象的实例,即this对象
  • synchronized作用于静态方法时,锁住的是class实例,因为静态方法属于类
  • synchronized作用于一个代码块时,锁住的是所有代码块中配置的对象

ReentrantLock

Lock接口的实现类,是一个可重入的独占锁

可重入锁指的是允许一个线程对同一资源执行多次加锁操作。

ReentrantLock支持公平锁和非公平锁的实现

ReentrantLock不仅提供了synchronized对锁的操作功能,**还提供了可响应中断锁,可轮询锁请求,定时锁等避免多线程死锁的方法。

sychronized关键字原理

synchronized内部包括ContentionListEntryListWaitSetOnDeckOwner!Owner这6个区域,每个区域都代表锁的不同状态。

  • ContentionList:

    锁竞争队列,所有请求锁的线程都被放在竞争队列中

  • EntryList

    竞争候选列表,在ContentionList中有资格成为候选者来竞争锁资源的线程被移动到了EntryList

  • WaitSet

    等待集合,调用wait方法后被阻塞的线程将被放在WaitSet

  • OnDeck

    竞争候选者,在同一时刻最多只有一个线程在竞争锁资源,该线程的状态被称为OnDeck

  • Owner

    竞争到锁资源的线程被称为Owner状态线程

  • !Owner

    Owner线程释放后,会从Owner的状态变成!Owner

synchronized在收到新的锁请求时首先自旋,如果通过自旋也没有获取锁资源,则将被放入锁竞争队列ContentionList中,Owner

会在释放锁资源的时候,将ContentionList中的部分线程移动到EntryList中,然后将EntryList中的某个线程(一般遵循先进先出规则)设为OnDeck线程。

值得注意的是,Owner并没有将锁资源直接交给OnDeck线程,而是把锁竞争的权利交给OnDeck线程,让OnDeck线程重新竞争锁,该操作牺牲了公平性,但提高了性能。

获取到锁资源的OnDeck线程会变成Owner线程,而未获取到锁资源的线程仍然停留在EntryList中。

Owner线程在被wait方法阻塞后,会被移动到WaitSet队列,直到notify方法或者notifyAll方法通知后被唤醒,从而进入到EntryList,重新竞争锁资源。

ReentrantLock实现原理

ReentrantLock是一个可重入的锁,内部采用AQS来实现

首先看一下AQS中比较重要的属性

exclusiveOwnerThread:独占锁线程,指向了当前获取到锁的线程

state:AQS的核心,AQS就是用这个字段来实现锁的获取和重入,在没有线程获取到锁的时候,锁的状态为0,获取的时候,通过cas对其进行+1,并且每重入一次再 +1,释放一次 -1,具体的后面代码展示

head,tail:AQS维护了一个内部类Node的双向队列,由未获取到锁的线程包装成的Node节点组成,也就是获取锁失败加入队列尾部。

AQS的获取锁过程分析

线程通过CAS将state从0设置为1,如果设置成功,说明获取锁成功,并且将exclusiveOwnerThread指向自己;

当调用lock方法时,公平锁和非公平锁的实现有区别

非公平锁的实现:线程会直接进行CAS操作去设置state,如果成功就获取到锁,如果失败,继续调用NonFairSync的acquire(1)方法

公平锁的实现:线程会直接调用FairSync的accquire(1)方法,这两个方法都调用了AQS的方法

公平锁与非公平锁的区别:

  1. //差别就在这里,多了一个hasQueuedPredecessors方法,这是跟非公平锁唯一的区别
  2. //也就是为什么叫公平锁体现在这里。
  3. //这里判断有没有别的线程比他更早来,如果返回true说明有,肯定就直接获取失败了
  4. //如果没有别他更早来的线程,那么自己就可以去尝试获取锁了

总结一下,AQS中维护了一堆wait线程组成的等待队列,凡是进入了这个队列的线程,之后就会按顺序一个一个获取到锁,执行逻辑,也就是将他们串行化了,那么公平和非公平体现在什么地方呢?

就是tryAcquire这里,公平锁是说新线程进来对于队列中的线程是公平的,如果队列中有等待线程,它就直接往后排,而非公平锁是,新线程对于队列中的等待线程是不公平的,可能存在队列中的头节点释放掉锁之后唤醒下一个线程,结果有一个新的线程进来同时获取锁,这个时候他们机会是平等的,因此说这是非公平锁。

具体的链接:https://blog.csdn.net/yanyan19880509/article/details/52345422

sychronized和lock共同点和区别

sychronized和ReentrantLock的共同点如下:

  • 都用于控制多线程对共享对象的访问

  • 都是可重用锁

  • 都保证了可见性和互斥性

区别:

  • 底层实现:sychronized是Java中的关键字,是由JVM来维护的,是JVM层面的锁 lock是一个类,是java代码层面的锁底层实现的

  • 使用方式不同:sychronized在使用时候,获取锁和释放锁,都是由系统维护的。而使用lock的需要手动获取锁,手动释放锁。

  • 异常的处理方式:sync在线程发生异常时会自动释放锁,不会发生异常死锁,Lock异常时不会自动释放锁,所以需要在finally中实现释放锁。

  • 等待是否可中断:sychronized是不可中断的,除非抛出异常或者正常运行完成,Lock是可以中断的,中断方式有:

    • 调用设置超时方法tryLock(long timeout,timeUnit unit)
    • 调用lockInterruptibly()放到代码块中,然后调用interrupt()方法可以中断
  • 加锁的时候是否可以公平:sync是非公平锁,lock既可以公平也可以不公平

  • 锁可以绑定多个条件

    sync:要么随机唤醒一个线程,要么是唤醒所有等待的线程

    lock:用来实现分组唤醒所需要唤醒的线程,可以精确的唤醒线程。

  • 从锁的实现机制来看

    sync采用的是独占锁,也就是悲观锁的机制

    lock采用的是乐观锁,所谓乐观锁就是每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

volatile关键字

volatile,用来将变量的更新操作通知到其他线程,当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将改变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

img

当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。

而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。

当一个变量定义为volatile之后,将具备两种特性:

​ 1.保证此变量对所有的线程的可见性,这里的“可见性”,如本文开头所述,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,

2.禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。

volatile 性能:

volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

线程池

线程池的结构:

  • 线程池管理器
  • 工作线程
  • 任务接口
  • 任务队列

线程池刚被创建时,只是向系统申请一个用于执行线程队列和管理线程池的线程资源,在调用execute()添加一个任务时,线程池会按照以下流程执行任务:

(1): 如果正在运行的线程数量小于corePoolSize(核心线程数量),线程池就会立刻创建线程并执行该线程任务

(2):如果正在运行的线程数量大于等于corePoolSize,该任务就将被放入阻塞队列中。

(3):在阻塞队列已满且正在运行的线程数量少于maximumPoolSize时,线程池会创建非核心线程立刻执行该线程任务

(4):在阻塞队列已满且正在运行的线程数量大于等于maximumPoolSize时,线程池将拒绝执行该线程任务并抛出RejectExecutionException异常

(5):在线程执行任务完毕后,该任务将被从线程池队列中移除,线程池将从队列中取下一个线程任务继续执行。

(6):在线程处于空闲状态的时间超过keepAliveTime时间时,正在运行的线程数量超过corePoolSize,该线程将会被认定为空闲线程并停止。因此在线程池中所有线程任务都执行完毕后,线程池会收缩到corePoolSize大小。

线程池的拒绝策略:

  • AbortPolicy:直接抛出异常,阻止线程正常运行
  • CallerRunsPolicy:如果被丢弃的线程未关闭,则执行该线程任务
  • DiscardOldestPolicy: 移除线程队列中最早的一个线程任务,并尝试执行当前任务
  • DiscardPolicy:丢弃当前任务而不做任何处理
  • 自定义拒绝策略:继承RejectedExecutionHandler接口

五种常用的线程池:

  • newCachedThreadPool:可缓存的线程池
  • newFixedThreadPool:固定大小的线程池
  • newScheduledThreadPool: 可做任务调度的线程池
  • newSingleThreadExecutor: 单个线程的线程池
  • newWorkStealingPool: 足够大小的线程池

Java线程的创建方式

  • 继承Thread类

  • 实现Runnable接口

  • 通过ExecutorService和Callable实现有返回值的线程

Java中线程调度

两种线程的调度模式:

抢占式调度:

抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。

协同式调度:

协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。

JVM的实现:

JVM规范中规定每个线程都有优先级,且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间。

java使用的线程调度式抢占式调度

Java中线程会按优先级分配CPU时间片运行

线程让出cpu的情况:

  1. 当前运行线程主动放弃CPU,JVM暂时放弃CPU操作(基于时间片轮转调度的JVM操作系统不会让线程永久放弃CPU,或者说放弃本次时间片的执行权),例如调用yield()方法。

  2. 当前运行线程因为某些原因进入阻塞状态,例如阻塞在I/O上。

  3. 当前运行线程结束,即运行完run()方法里面的任务。

进程调度算法

  • 优先调度算法
    • 先来先服务调度算法
    • 短作业(进程)优先调度算法
  • 高优先权优先调度算法
    • 抢占式优先调度算法
    • 非抢占式优先调度算法
    • 高响应比优先调度算法
  • 时间片的轮转调度算法
    • 时间片轮转法
    • 多级反馈队列调度算法

JVM内存结构介绍

  • 虚拟机栈:线程私有的内存区域,每创建一个线程都会对应创建一个Java栈,这个栈中又会对应多个栈帧,栈帧是用来存储方法数据和部分过程结果的数据结构,每调用一个方法就会往栈中创建并压入一个栈帧,每一个方法从调用到最终返回结果的过程,就对应一个栈帧从入栈到出栈的过程。
  • 程序计数器:保存着当前线程执行的虚拟机字节码指令的内存地址。每个线程都会设立一个程序计数器,程序计数器是线程私有的内存区域
  • 本地方法栈:和虚拟机栈的作用相似,不过虚拟机栈是为Java方法服务的,而本地方法栈是为Native方法服务的。
  • 方法区:方法区(Method Area)是用于存储类结构信息的地方,包括常量池、静态变量、构造函数等类型信息,类型信息是由类加载器在类加载时从类文件中提取出来的。方法区同样存在垃圾收集,因为用户通过自定义加载器加载的一些类同样会成为垃圾,JVM会回收一个未被引用类所占的空间,以使方法区的空间达到最小。方法区中还存在着常量池,常量池包含着一些常量和符号引用(加载类的连接阶段中的解析过程会将符号引用转换为直接引用)。方法区是线程共享的。
  • 堆:堆(heap)是存储java实例或者对象的地方,是GC的主要区域,同样是线程共享的内存区域。

总结:

  • 所有线程共享的内存数据区:方法区,堆。而虚拟机栈,本地方法栈和程序计数器都是线程私有的。

  • 存放于栈中的东西如下:

    • 每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象)。对象都存放在堆区中。
    • 每个栈中的数据(基础数据类型和对象引用)都是私有的,其他栈不能访问。
    • 方法的形式参数,方法调用完后从栈空间回收
    • 引用对象的地址,引用完后,栈空间地址立即被回收,堆空间等待GC
  • 存放于堆中的东西如下:

    • 存储的全部是对象,每个对象包含一个与之对应的class信息
    • Jvm只有一个堆区(heap)被所有线程共享,堆区中不存放基本类型和对象引用,只存放对象本身
  • 存放于方法区中的东西如下:

    • 存放线程所执行的字节码指令
    • 跟堆一样.被所有线程共享.方法区包含:所有的class和static变量
    • 常量池位于方法区中

垃圾回收算法

新生代的垃圾回收算法:

  • Serial单线程复制算法
  • ParNew多线程复制算法:默认开启与CPu同等数量的线程进行垃圾回收
  • Parallel Scavenge多线程复制算法:提供了三个参数用于调节、控制垃圾回收的停顿时间及吞吐量,分别是控制最大垃圾收集停顿时间控制吞吐量的大小控制自适应调节策略开启与否的参数。

老年代的垃圾回收算法:

  • CMS多线程标记清除算法
  • Serial Old单线程标记整理算法
  • Parallel Old多线程标记整理算法:在设计上优先考虑吞吐量,其次考虑停顿时间等因素

全区收集算法:

  • G1多线程标记整理算法

CMS垃圾收集器介绍:

CMS主要目的是达到最短的垃圾回收停顿时间,基于线程的标记清除算法实现。

四个阶段:

  • 初始标记:只标记和GC Roots直接关联的对象,速度很快,需要暂停所有工作线程
  • 并发标记:标记GC Root间接关联的对象,不需要暂停工作线程
  • 重新标记:在并发标记过程中用户线程继续执行使得部分对象的状态发生了变化,需要重新标记,需要暂停所有工作线程
  • 并发清除:清除GC Roots 不可达的对象。

引用计数和可达性分析

引用计数法:在为对象添加一个引用时,引用计数加1,在为对象删除一个引用时,引用计数减1,如果引用计数为0,说明此刻该对象没有被引用,可以被回收。引用计数法容易产生循环引用的问题,使得对象不能被回收。

可达性分析:首先定义一些GC Roots对象,然后以这些GC Roots对象作为起点向下搜索,如果在GC Roots和一个对象之间没有可达路径,则称该对象是不可达的,不可达的对象至少经过两次标记才能判断其是否可以被回收,如果在两次标记后,该对象任然不可达的,则将被垃圾回收器回收。

Java内存溢出(OOM)异常排查

OOM:java heap space

**原因1:**当应用程序试图向堆空间添加更多的数据,但堆却没有足够的空间来容纳这些数据时,将会触发OOM: Java heap space异常。需要注意的是:即使有足够的物理内存可用,只要达到堆空间设置的大小限制,此异常仍然会被触发。可以使用参数-Xmx-XX:MaxPermSize设置堆空间的大小

**原因2:**流量/数据量峰值:应用程序在设计之初均有用户量和数据量的限制,某一时刻,当用户数量或数据量突然达到一个峰值,并且这个峰值已经超过了设计之初预期的阈值,那么以前正常的功能将会停止,并触发OOM: Java heap space异常。

**原因3:**内存泄漏:特定的编程错误会导致你的应用程序不停的消耗更多的内存,每次使用有内存泄漏风险的功能就会留下一些不能被回收的对象到堆空间中,随着时间的推移,泄漏的对象会消耗所有的堆空间,最终触发OOM: Java heap space错误。

OOM:GC overhead limit exceeded

**原因:**当应用程序花费超过98%的实践用来做GC并且回收了不到2%的堆内存时,会抛出OOM:GC overhead limit exceeded错误,具体表现就是你的应用几乎耗尽所有可用内存,并且GC多次均未能清理干净。

OOM:Permgen space

Java1.7中会出现这个错误,Java1.8已经采用元空间(MetaSpace)来取代,不会出现这种错误。

Java中堆空间是JVM管理的最大一块内存空间,可以在JVM启动时指定堆空间的大小,其中堆被划分成两个不同的区域:新生代(Young)和老年代(Tenured),新生代又被划分为3个区域:EdenFrom SurvivorTo Survivor

java.lang.OutOfMemoryError: PermGen space错误就表明持久代所在区域的内存已被耗尽。

**原因:首先需要理解Permanent Generation Space的用处是什么。持久代主要存储的是每个类的信息,比如:类加载器引用运行时常量池(所有常量、字段引用、方法引用、属性)字段(Field)数据方法(Method)数据方法代码、**方法字节码等等。我们可以推断出,PermGen的大小取决于被加载类的数量以及类的大小。因此,我们可以得出出现java.lang.OutOfMemoryError: PermGen space错误的原因是:太多的类或者太大的类被加载到permanent generation(持久代)。

解决方案:

  • 1.解决初始化时的OOM

    当在应用程序启动期间触发由于PermGen耗尽引起的OutOfMemoryError时,解决方案很简单。 应用程序需要更多的空间来加载所有的类到PermGen区域,所以我们只需要增加它的大小。 为此,请更改应用程序启动配置,并添加(或增加,如果存在)-XX:MaxPermSize参数,类似于以下示例:

    java -XX:MaxPermSize=512m com.yourcompany.YourClass
    
  • 2.解决Redeploy时的OOM

    分析dump文件:首先,找出引用在哪里被持有;其次,给你的web应用程序添加一个关闭的hook,或者在应用程序卸载后移除引用。你可以使用如下命令导出dump文件:

    jmap -dump:format=b,file=dump.hprof <process-id>
    

    如果是你自己代码的问题请及时修改,如果是第三方库,请试着搜索一下是否存在"关闭"接口,如果没有给开发者提交一个bug或者issue吧。

  • 3.解决运行时OOM

    首先你需要检查是否允许GC从PermGen卸载类,JVM的标准配置相当保守,只要类一创建,即使已经没有实例引用它们,其仍将保留在内存中,特别是当应用程序需要动态创建大量的类但其生命周期并不长时,允许JVM卸载类对应用大有助益,你可以通过在启动脚本中添加以下配置参数来实现:

    -XX:+CMSClassUnloadingEnabled
    

    默认情况下,这个配置是未启用的,如果你启用它,GC将扫描PermGen区并清理已经不再使用的类。但请注意,这个配置只在UseConcMarkSweepGC的情况下生效,如果你使用其他GC算法,比如:ParallelGC或者Serial GC时,这个配置无效。所以使用以上配置时,请配合:

    -XX:+UseConcMarkSweepGC
    

    如果你已经确保JVM可以卸载类,但是仍然出现内存溢出问题,那么你应该继续分析dump文件,使用以下命令生成dump文件:

    jmap -dump:file=dump.hprof,format=b <process-id>
    

    当你拿到生成的堆转储文件,并利用像Eclipse Memory Analyzer Toolkit这样的工具来寻找应该卸载却没被卸载的类加载器,然后对该类加载器加载的类进行排查,找到可疑对象,分析使用或者生成这些类的代码,查找产生问题的根源并解决它。

OOM:Metaspace

**原因:**太多的类或太大的类加载到元空间。

**解决方法:**扩大MetaSpace的空间

OOM:Unable to create new native thread

JVM中的线程完成自己的工作也是需要一些空间的,当有足够多的线程却没有那么多的空间时就会像这样:

OOM:Out of swap space

Java应用程序在启动时会指定所需要的内存大小,可以通过-Xmx和其他类似的启动参数来指定。在JVM请求的总内存大于可用物理内存的情况下,操作系统会将内存中的数据交换到磁盘上去。Out of swap space?表示交换空间也将耗尽,并且由于缺少物理内存和交换空间,再次尝试分配内存也将失败。

OOM:Requested array size exceeds VM limit

Java对应用程序可以分配的最大数组大小有限制。不同平台限制有所不同,但通常在1到21亿个元素之间。

OOM:Out of memory:Kill process or sacrifice child

为了理解这个错误,我们需要补充一点操作系统的基础知识。操作系统是建立在进程的概念之上,这些进程在内核中作业,其中有一个非常特殊的进程,名叫“内存杀手(Out of memory killer)”。当内核检测到系统内存不足时,OOM killer被激活,然后选择一个进程杀掉。

OOM:kill process or sacrifice child

为了理解这个错误,我们需要补充一点操作系统的基础知识。操作系统是建立在进程的概念之上,这些进程在内核中作业,其中有一个非常特殊的进程,名叫“内存杀手(Out of memory killer)”。当内核检测到系统内存不足时,OOM killer被激活,然后选择一个进程杀掉。哪一个进程这么倒霉呢?选择的算法和想法都很朴实:谁占用内存最多,谁就被干掉。

怎么排查线程问题

1. 通过top命令查看当前系统CPU使用情况,定位CPU使用率超过100%的进程ID;
2. 通过ps aux | grep PID命令进一步确定具体的线程信息;
3. 通过ps -mp pid -o THREAD,tid,time命令显示线程信息列表,然后找到耗时的线程ID;
4. 将需要的线程ID转换为16进制格式:printf "%x\n" tid
5. 最后找到线程堆栈信息:jstack pid |grep tid ,其中tid是上面转换后的16进制的线程ID

虚拟机性能监控和故障处理工具

虚拟机进程状况工具 (jps)

jps :JVM Process Status Tool

功能: 列出正在运行的虚拟机进程,并显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一ID

虚拟机统计信息监视工具(jstat)

jstat : JVM Statistics Monitoring Tool

功能:用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程的类装载、内存、垃圾回收、JIT编译等运行数据。

例子:假如需要每250毫秒查询一次进程2764的垃圾收集状况,一共查询20次,那么命令应该是

jstat -gc 2764 250 20

jinfo: Java配置信息工具

功能:实时查看和调整虚拟机各项参数。

jmap: Java内存映像工具

功能:用于生成堆转储快照。如果不想使用jmap命令,要想获得堆转储快照,还有其他方法:

  • 用 -XX:+HeapDumpOnOutOfMemoryError参数,可以让虚拟机在OOM异常出现之后自动生成dump文件
  • 用 -XX:+HeapDumpOnCtrlBreak参数,可以使用[ctrl]+[break]键让虚拟机生成dump文件。
  • 又或者在Linux系统下通过 Kill -3 命令发送进程退出信号“吓唬”一下虚拟机,也能拿到dump文件。

jmap的作用并不仅仅是为了获取dump文件,还可以查询finalize执行队列、Java堆和永久代的详细信息、如空间使用率、当前使用的时哪种收集器等。

jhat: 虚拟机堆转储快照分析工具

功能:配合jmap使用,分析dump文件,但一般不用,因为有比其更优秀的分析工具。

jstack:Java堆栈跟踪工具

jstack命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。生成快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁,死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。

死锁出现的原因

什么是死锁、产生死锁的原因、解决死锁的基本办法、避免死锁、预防死锁、死锁检测、解除死锁

所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。如下图所示:
img

可以归结为两点:

  • 竞争资源

    • 可剥夺资源,是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺,CPU和主存均属于可剥夺性资源;

    • 另一类资源是不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。

    • 产生死锁中的竞争资源之一指的是竞争不可剥夺资源(例如:系统中只有一台打印机,可供进程P1使用,假定P1已占用了打印机,若P2继续要求打印机打印将阻塞)

    • 产生死锁中的竞争资源另外一种资源指的是竞争临时资源(临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁

  • 进程间顺序推进法

    • 若P1保持了资源R1,P2保持了资源R2,系统处于不安全状态,因为这两个进程再向前推进,便可能发生死锁
    • 例如,当P1运行到P1:Request(R2)时,将因R2已被P2占用而阻塞;当P2运行到P2:Request(R1)时,也将因R1已被P1占用而阻塞,于是发生进程死锁

死锁的必要条件

产生死锁的必要条件:

  • 1.互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
  • 2.请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 3.不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
  • 4.环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。

死锁的避免

预防死锁的四点解决办法:

  • 1.资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
  • 2.只要有一个资源的不到分配,也不给这个进程分配其他的资源:(破坏请求保持条件)
  • 3.可剥夺资源:即当某进程获得了部分资源,但得不到其他资源,则释放已占有的资源(破坏不可剥夺条件)
  • 4.资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)

避免死锁:

  • 预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全的状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。
  • 银行家算法:首先需要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源情况。因此,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。安全状态是指至少有一个资源分配序列不会导致死锁。当进程请求一组资源时,假设同意该请求,从而改变了系统的状态,然后确定其结果是否还处于安全状态。如果是,同意这个请求;如果不是,阻塞该进程知道同意该请求后系统状态仍然是安全的。

JVM类加载机制

java文件通过编译器变成了.class文件,接下来类加载器又将这些.class文件加载到JVM中。其中类装载器作用其实就是类的加载,类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。

在什么时候才会启动类加载器?

其实,类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。

从哪个地方去加载.class文件

在这里进行一个简单的分类。例举了5个来源

(1)本地磁盘

(2)网上加载.class文件(Applet)

(3)从数据库中

(4)压缩文件中(ZAR,jar等)

(5)从其他文件生成的(JSP应用)

JVM的类的生命周期

类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

  • 加载

    JVM读取class文件,并且根据class文件描述创建java.lang.Class对象的过程

  • 验证

    用于确保Class文件符合当前虚拟机的要求,保障JVM自身的安全

  • 准备

    主要工作是在方法区中为类变量分配内存空间并设置类中变量的初始值,非final类型的变量会设置为不同数据类型的默认值,在初始化阶段的时候再赋予真实值,final类型的变量会根据实际值在准备阶段就赋予了真实值。

  • 解析:

    会将常量池中的符号引用替换为直接引用

  • 初始化:

    这是类加载机制的最后一步,在这个阶段,java程序代码才开始真正执行。我们知道,在准备阶段已经为类变量赋过一次值。在初始化阶端,程序员可以根据自己的需求来赋值了。一句话描述这个阶段就是执行类构造器< client>()方法的过程。

    在初始化阶段,主要为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

    ①声明类变量是指定初始值

    ②使用静态代码块为类变量指定初始值

    JVM初始化步骤

    1、假如这个类还没有被加载和连接,则程序先加载并连接该类

    2、假如该类的直接父类还没有被初始化,则先初始化其直接父类

    3、假如类中有初始化语句,则系统依次执行这些初始化语句

    类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

    • 创建类的实例,也就是new的方式
    • 访问某个类或接口的静态变量,或者对该静态变量赋值
    • 调用类的静态方法
    • 反射(如 Class.forName(“com.shengsiyuan.Test”))
    • 初始化某个类的子类,则其父类也会被初始化
  • 使用

  • 卸载

类加载器

  • Bootstrap ClassLoader:

    最顶层的加载类,主要加载核心类库,也就是我们环境变量下面%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等

  • Extention ClassLoader:

    扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件

  • AppClassLoader:

    也称为SystemAppClass。 加载当前应用的classpath的所有类。

  • 自定义类加载器

类加载的三种方式。

(1)通过命令行启动应用时由JVM初始化加载含有main()方法的主类。

(2)通过Class.forName()方法动态加载,会默认执行初始化块(static{}),但是Class.forName(name,initialize,loader)中的initialze可指定是否要执行初始化块。

(3)通过ClassLoader.loadClass()方法动态加载,不会执行初始化块。

双亲委派原则

当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。

采用双亲委派的好处:

  • 是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。
  • 双亲委派原则归纳一下就是:可以避免重复加载,父类已经加载了,子类就不需要再次加载更加安全,很好的解决了各个类加载器的基础类的统一问题,如果不使用该种方式,那么用户可以随意定义类加载器来加载核心api,会带来相关隐患。

BIO NIO AIO

BIO 阻塞IO

同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待完成。

采用 BIO 通信模型 的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接。我们一般通过在 while(true) 循环中服务端会调用 accept() 方法等待接收客户端的连接的方式监听请求,请求一旦接收到一个连接请求,通过多线程来支持多个客户端的连接。

当线程数量过多,就会使cpu负载过重,占用大量的资源,不过可以通过 线程池机制 改善,线程池还可以让线程的创建和回收成本相对较低。使用FixedThreadPool 可以有效的控制了线程的最大数量,保证了系统有限的资源的控制,实现了N(客户端请求数量):M(处理客户端请求的线程数量)的伪异步I/O模型(N 可以远远大于 M)

NIO 阻塞IO

NIO使用了多路复用器机制,以socket使用来说,多路复用器(Selector)通过不断轮询各个连接的状态,只有在socket有流可读或者可写时,应用程序才需要去处理它,在线程的使用上,就不需要一个连接就必须使用一个处理线程了,而是只是有效请求时(确实需要进行I/O处理时),才会使用一个线程去处理,这样就避免了BIO模型下大量线程处于阻塞等待状态的情景。

相对于BIO的流,NIO抽象出了新的通道(Channel)作为输入输出的通道,并且提供了缓存(Buffer)的支持,在进行读操作时,需要使用Buffer分配空间,然后将数据从Channel中读入Buffer中,对于Channel的写操作,也需要现将数据写入Buffer,然后将Buffer写入Channel中。

img

NIO 包含下面几个核心的组件:

  • Channel(通道)
  • Buffer(缓冲区)
  • Selector(选择器)

整个NIO体系包含的类远远不止这三个,只能说这三个是NIO体系的“核心API”。我们上面已经对这三个概念进行了基本的阐述,这里就不多做解释了。

AIO 异步IO

Linux操作系统中的五种操作模型

  • 阻塞IO模型

    阻塞 I/O 是最简单的 I/O 模型,一般表现为进程或线程等待某个条件,如果条件不满足,则一直等下去。条件满足,则进行下一步操作。

    举个例子:我们什么也不做,双手一直把着鱼竿,就静静的等着鱼儿咬钩。一旦手上感受到鱼的力道,就把鱼钓起来放入鱼篓中。然后再钓下一条鱼。

  • 非阻塞IO模型

    应用进程与内核交互,目的未达到之前,不再一味的等着,而是直接返回。然后通过轮询的方式,不停的去问内核数据准备有没有准备好。如果某一次轮询发现数据已经准备好了,那就把数据拷贝到用户空间中。

    我们钓鱼的时候,在等待鱼儿咬钩的过程中,我们可以做点别的事情,比如玩一把王者荣耀、看一集《延禧攻略》等等。但是,我们要时不时的去看一下鱼竿,一旦发现有鱼儿上钩了,就把鱼钓上来。

  • IO复用模型

    多个进程的IO可以注册到同一个管道上,这个管道会统一和内核进行交互。当管道中的某一个请求需要的数据准备好之后,进程再把对应的数据拷贝到用户空间中。

    我们钓鱼的时候,为了保证可以最短的时间钓到最多的鱼,我们同一时间摆放多个鱼竿,同时钓鱼。然后哪个鱼竿有鱼儿咬钩了,我们就把哪个鱼竿上面的鱼钓起来。

  • 信号驱动IO模型

    应用进程在读取文件时通知内核,如果某个 socket 的某个事件发生时,请向我发一个信号。在收到信号后,信号对应的处理函数会进行后续处理。

    我们钓鱼的时候,为了避免自己一遍一遍的去查看鱼竿,我们可以给鱼竿安装一个报警器。当有鱼儿咬钩的时候立刻报警。然后我们再收到报警后,去把鱼钓起来。

  • 异步IO模型

    应用进程把IO请求传给内核后,完全由内核去操作文件拷贝。内核完成相关操作后,会发信号告诉应用进程本次IO已经完成。

    我们钓鱼的时候,采用一种高科技钓鱼竿,即全自动钓鱼竿。可以自动感应鱼上钩,自动收竿,更厉害的可以自动把鱼放进鱼篓里。然后,通知我们鱼已经钓到了,他就继续去钓下一条鱼去了。

Java中的代理模式

1、什么是代理模式

代理模式:就是为其他对象提供一种代理以控制对这个对象的访问。

代理可以在不改动目标对象的基础上,增加其他额外的功能(扩展功能)。

2 静态代理

静态代理在使用时,需要定义接口或者父类,被代理对象(目标对象)与代理对象(Proxy)一起实现相同的接口或者是继承相同父类。

静态代理总结:

可以实现在不修改目标对象的基础上,对目标对象的功能进行扩展。

但是由于代理对象需要与目标对象实现一样的接口,所以会有很多代理类,类太多.同时,一旦接口增加方法,目标对象与代理对象都要维护.

可以使用动态代理方式来解决。

动态代理

1.代理对象,不需要实现接口
2.代理对象的生成,是利用JDK的API,动态的在内存中创建代理对象(需要我们指定创建代理对象/目标对象实现的接口的类型)
3.动态代理也叫做:JDK代理,接口代理

Cglib代理

JDK的动态代理机制只能代理实现了接口的类,而不能实现接口的类就不能实现JDK的动态代理,cglib是针对类来实现代理的,他的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理。

Cglib代理,也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能的扩展

Cglib子类代理实现方法:
1.需要引入cglib的jar文件,但是Spring的核心包中已经包括了Cglib功能,所以直接引入Spring-core.jar即可.
2.引入功能包后,就可以在内存中动态构建子类
3.代理的类不能为final,否则报错
4.目标对象的方法如果为final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法.

countDownLatch CyclicBarrier 信号量

countDownLatch

  • countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。
  • 是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

CountDownLatch和CyclicBarrier区别:

  • countDownLatch是一个计数器,线程完成一个记录一个,计数器递减,只能只用一次
  • CyclicBarrier的计数器更像一个阀门,需要所有线程都到达,然后继续执行,计数器递增,提供reset功能,可以多次使用

信号量

你可以认为信号量是一个可以递增或递减的计数器。你可以初始化一个信号量的值为5,此时这个信号量可最大连续减少5次,直到计数器为0。当计数器为0时,你可以让其递增5次,使得计数器值为5。在我们的例子中,信号量的计数器始终限制在[0~5]之间。

显然,信号量并不仅仅是计数器。当计数器值为0时,它们可以使线程等待,即它们是具有计数器功能的锁。

就多线程而言,当一个线程要访问共享资源(由信号量保护)时,首先,它必须获得信号量。如果信号量的内部计数器大于0时,信号量递减计数器,并允许访问共享资源。否则,如果信号量的计数器为0,则信号量将线程置于休眠状态,直到计数器大于0。计数器中的值为0意味着所有共享资源都被其他线程使用,因此希望使用共享资源的线程必须等到有线程空闲(释放信号量)。

java开发设计—七大原则

  • 开闭原则

    当应用需求改变时,在不修改软件实体的源代码或者二进制代码的前提下可以扩展模块的功能,使其满足新的需求。

  • 里氏替换原则

    子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。

  • 依赖倒置原则

    要面向接口编程,不要面向具体的实现编程。

  • 单一职责原则

    一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分。

  • 接口隔离原则

    要为各个类建立它们所需要的专用接口,而不要试图建立一个很庞大的接口供所有依赖它的类去调用。

  • 迪米特原则

    只与你的直接朋友交谈,不跟“陌生人”说话。其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。

  • 合成复用原则

    他要求在软件复用的时候,要尽量使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

阻塞 非阻塞 同步 异步

IO操作

IO分两阶段(一旦拿到数据后就变成了数据操作,不再是IO):
    1.数据准备阶段
    2.内核空间复制数据到用户进程缓冲区(用户空间)阶段

在操作系统中,程序运行的空间分为内核空间和用户空间。
    应用程序都是运行在用户空间的,所以它们能操作的数据也都在用户空间。
    
阻塞IO和非阻塞IO的区别在于第一步发起IO请求是否会被阻塞:
    如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。

一般来讲:
    阻塞IO模型、非阻塞IO模型、IO复用模型(select/poll/epoll)、信号驱动IO模型都属于同步IO,因为阶段2是阻塞的(尽管时间很短)。

同步IO和异步IO的区别就在于第二个步骤是否阻塞:
    如果不阻塞,而是操作系统帮你做完IO操作再将结果返回给你,那么就是异步IO

img

同步和异步IO 阻塞和非阻塞IO

同步和异步IO的概念:

	同步是用户线程发起I/O请求后需要等待或者轮询内核I/O操作完成后才能继续执行

	异步是用户线程发起I/O请求后仍需要继续执行,当内核I/O操作完成后会通知用户线程,或者调用用户线程注册的回调函数

阻塞和非阻塞IO的概念:

	阻塞是指I/O操作需要彻底完成后才能返回用户空间

	非阻塞是指I/O操作被调用后立即返回一个状态值,无需等I/O操作彻底完成

img

img

img

同步与异步(线程间调用)

同步与异步是对应于调用者与被调用者,它们是线程之间的关系,两个线程之间要么是同步的,要么是异步的

	同步操作时,调用者需要等待被调用者返回结果,才会进行下一步操作

	而异步则相反,调用者不需要等待被调用者返回结果,即可进行下一步操作,被调用者通常依靠事件、回调等机制来通知调用者结果

阻塞和非阻塞(线程内调用)

阻塞与非阻塞是对同一个线程来说的,在某个时刻,线程要么处于阻塞,要么处于非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态:

    阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。

    非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值