Java语法
字符型常量和字符串常量的区别
- 形式上:字符常量是单引号引起的一个字符;字符串常量是双引号引起的若干个字符
- 含义上:字符常量相当于一个整形值(ASCII值),可以参与表达式运算;字符串常量代表一个地址值
- 占内存大小 字符常量只占2个字节,字符串常量占若干个字节
Java泛型
泛型类
public class Generic<T> {
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey() {
return key;
}
}
泛型接口
public interface Generator<T> {
public T method();
}
class GeneratorImpl<T> implements Generator<T> {
@Override
public T method() {
return null;
}
}
class GeneratorImpl<T> implements Generator<String> {
@Override
public String method() {
return "hello";
}
}
泛型方法
public <E> void printArray(E[] inputArray) { }
泛型理解
Java内部类
成员内部类
内部类如何使用外部类的属性和方法
当创建一个内部类的时候,无形中就与外围类有了一种联系,依赖这种联系,就可以无限制地访问外围类的元素
- 如果需要利用一个外部类的引用则需要外部类对象.this来生成外部类对象的引用
- 如果需要创建某个内部类对象,必须要利用外部类的对象通过.new来创建内部类
public class OuterClass {
private int number = 100;
public class InnerClass {
private int number = 200;
private static int inner_i = 100; // 内部类不允许定义静态变量
public void paint() {
int number = 500;
System.out.println(number);
System.out.println(this.number);
// 通过外部类名加this的方式访问外部类成员属性
System.out.println(OuterClass.this.number);
}
}
// 推荐使用getInnerClass()来获取成员内部类,尤其是该内部类的构造函数无参数时
public InnerClass getInnerClass() {
return new InnerClass();
}
public static void main(String[] args) {
// 注意创建内部类对象分为两个步骤
OuterClass outer = new OuterClass();
OuterClass.InnerClass in = outer.new InnerClass();
System.out.println(outer.getInnerClass().hashCode());
System.out.println(in.hashCode());
}
}
局部内部类
在方法内定义的内部类,与局部变量相似,在局部内部类前不加修饰符public
或private
,其范围为定义它的代码块。
注意:局部内部类中不可定义静态变量,可以访问外部类的局部变量(即方法内的变量),但是变量必须是final的(JDK8以后就没有这个要求了)
public class OuterClassTest {
private int s = 100;
private int out_i = 1;
public void f(final int k) {
final int s = 200;
final int j = 10;
class Inner {
int s = 300; // 可以定义与外部类同名的变量
Inner(int k) {
inner_f(k);
}
int inner_i = 100;
void inner_f(int k) {
System.out.println(out_i);
System.out.println(k); // 可以访问外部类的局部变量(形参) 但是必须是final
System.out.println(s);
System.out.println(this.s);
System.out.println(OuterClassTest.this.s);
}
}
new Inner(k);
}
public static void main(String[] args) {
OuterClassTest out = new OuterClassTest();
out.f(3);
}
}
在类外不可直接生成局部内部类,要想使用局部内部类时需要生成对象,对象调用方法,在方法中才能调用其局部内部类。
静态内部类
public class OuterStatic {
private static int i = 1;
private int j = 10;
public static void outer_f1() {
}
public void outer_f2() {
}
static class Inner {
static int inner_i = 100;
int inner_j = 200;
static void inner_f1() {
System.out.println("Outer.i" + i); // 静态内部类只能访问外部类的静态成员
outer_f1(); // 静态变量和静态方法
}
// 静态内部类不能访问外部类的非静态成员
void inner_f2() {
// System.out.println("Outer.j" + j);
// outer_f2();
}
}
public void outer_f3() {
System.out.println(Inner.inner_i);
Inner.inner_f1();
Inner inner = new Inner();
inner.inner_f2();
}
}
生成(new)一个静态内部类不需要外部类成员,这是静态内部类和成员内部类的区别
内部类实现真正的多继承
public class InnerClassAdvantage extends Person{
@Override
public void run() {
System.out.println("person run or machine run");
}
class InnerClass implements Machine{
@Override
public void run() {
System.out.println("Inner class implement interface");
InnerClassAdvantage.this.run();
}
}
public InnerClass getInnerClassInstance() {
return new InnerClass();
}
public static void main(String[] args) {
new InnerClassAdvantage().getInnerClassInstance().run();
}
}
abstract class Person {
abstract void run();
}
interface Machine {
void run();
}
匿名内部类
匿名内部类必须要继承一个父类或者实现一个接口,当然也仅能只继承一个父类或者实现一个接口
public class anonymousClass {
public void test(Bird bird) {
System.out.println(bird.getName() + " can fly " + bird.fly() + "米");
}
public static void main(String[] args) {
anonymousClass anonymousClass = new anonymousClass();
anonymousClass.test(new Bird() {
@Override
public int fly() {
return 1000;
}
});
}
}
abstract class Bird {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public abstract int fly();
}
JUC
AQS
可重入锁
- synchronized 隐式锁
- Lock(ReentrantLock)显式锁
乐观锁VS悲观锁
乐观锁和悲观锁,体现了看待线程同步的不同角度。
悲观锁
认为在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改,Java中synchronized和Lock的实现类都是悲观锁。
乐观锁
乐观锁在Java中是通过使用无锁编程来实现的,最常采用的是CAS算法,原子类就是采用的是CAS自旋实现的
- 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确
- 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
CAS
CAS全称为Compare And Swap,是一种无锁的算法,在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。
CAS算法涉及到三个操作数
- 需要读写的内存值V
- 进行比较的值A
- 要写入的新值B
当且仅当V的值等于A时,CAS通过原子方式利用新值B来更新V的值,否则不会执行任何操作
观察AtomicInteger类可以发现,
- Unsafe是获取并且操作内存中的数据,unsafe.getAndAddInt()方法来实现自旋锁
- valueOffset是存储value在AtomicInteger的偏移量
- value是存储AtomicInteger的值,需要使用volatile来描述,保证其在线程间是可见的
其中整个比较 + 更新的操作封装在compareAndSwapInt()方法中,在JNI里是借助于一个CPU指令完成的,是属于原子操作
CAS的问题
- ABA问题,在操作值的时候,检查内存值是否发生来变化,没有发生变化才更新内存值,但是如果变化是先从A变成了B,然后又变成了A,那么CAS进行检查的时候会发现值没有发生变化,但是实际上是有变化的。ABA问题一般的解决方案是每次变量更新的时候把版本号加一
JDK1.5以后采用AtomicStampedReference
首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。 - 循环时间大
如果CAS操作长时间不成功,会导致其一直处于自旋的状态,给CPU带来非常大的开销。 - 只能保证一个变量的原子操作
CAS能够保证原子操作,但是对于多个共享变量操作时,是无法保证操作的原子性的。
AtomicReference
类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。
类相关
default关键字
jdk1.8之后可以在interface中添加default实现方法
- 如果两个interface的default了一个方法则需要在多实现类中重载方法
- 类大于接口
package JavaSE.interfaceDefault;
/**
* @program: JavaLife
* @author: JiaLe Hu
* @create: 2020-12-15 16:36
**/
public interface InterfaceDefault1 {
default void helloWorld() {
System.out.println("Hello World");
}
}
package JavaSE.interfaceDefault;
public interface interfaceDefault2 {
default void helloWorld() {
System.out.println("Hello World");
}
}
package JavaSE.interfaceDefault;
/**
* @program: JavaLife
* @author: JiaLe Hu
* @create: 2020-12-15 16:37
**/
public class MyImplement implements InterfaceDefault1, interfaceDefault2 {
public static void main(String[] args) {
MyImplement my = new MyImplement();
my.helloWorld();
}
// 这里需要重载冲突的方法
@Override
public void helloWorld() {
System.out.println("I come from MyImplement");
}
}
package JavaSE.interfaceDefault;
/**
* @program: JavaLife
* @author: JiaLe Hu
* @create: 2020-12-15 16:40
**/
public class MyImplement2 extends MyImplement implements interfaceDefault2{
// 会优先选择 extends 类的方法实现
public static void main(String[] args) {
MyImplement2 myImplement2 = new MyImplement2();
myImplement2.helloWorld();
}
}
反射
反射是框架设计的灵魂
Java反射机制是在运行状态中,对于任意一个类,都能知道这个类的所有属性和方法;对于任意一个对象,都能调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
Invoke
Invoke 用来执行某个对象的目标方法
Invoke()方法主要分为两个部分:访问控制检查和调用MethodAccessor.invoke实现方法执行
MethodAccessor有两种选择,一个是Java实现的,另一个是native code实现的,Java实现的版本在初始化时需要较多时间,但是长久来说性能较好;nativa版本正好相反。
让Java方法在被反射调用时,开头若干次使用native版,等反射调用次数超过阈值时则生成一个专用的MethodAccessor实现类,生成其中的invoke()方法的字节码,以后对该Java方法的反射调用就会使用Java版。
内置类比较
String StringBuilder StringBuffer
- 可变与不可变 String是final是不可变的,StringBuilder和StringBuffer都是继承于AbstractStringBuilder都是可变的
- 线程安全 String是不可修改的,StringBuffer对方加了同步锁是线程安全的,StringBuilder没有对方法添加同步锁,所以是非线程安全的
- 执行效率 String执行效率是最慢的,StringBuffer执行效率差别不大,StringBuilder是执行效率最高的
HashMap
JDK7中的HashMap底层实现
不管是1.7,还是1.8,HashMap的实现框架都是哈希表+链表的组合方式
最后一个变量modCount
,记录了map新增或删除k-v做出的调整的次数,其主要作用,是对Map的iterator()操作做一致性校验,如果在iterator
操作的过程中,map的数值有修改,直接抛出ConcurrentModificationException
异常。
变量存在着关系如下
threshold = table.length * loadFactor;
当执行put()操作放入一个新的值时,如果map中已经存在对应的key,则作替换即可,若不存在,则会首先判断size>=threshold
是否成立,这是决定哈希table是否扩容的重要因素。
put()操作
- 特殊的key值处理,key为null
- 计算table中目标bucket的下标
- 指定目标bucket,遍历Entry结点链表,若找到key相同的Entry结点,则做替换
- 若未找到目标Entry结点,则新增一个Entry结点
put()
是有返回值的
- 如果key已经存在,则返回的是旧的value值
- 如果key不存在,则返回null值
特殊key值处理
- HashMap中,是允许key,value都为null的,且key为null只存一份,多次存储会将旧value值覆盖
- key为null的存储位置,都统一放在下标为0的bucket,即:table[0]位置的链表
- 如果是第一次对key=null做put操作,将会在table[0]的位置新增一个Entry结点,使用头插法做链表插入
扩容
- 扩容后大小是扩容前的2倍
- 数据搬迁,从旧的table迁到扩容后的新table。为避免碰撞过多,先决策是否需要对每一个Entry链表结点重新hash,然后根据hash值计算得到bucket下标,然后使用头插法做结点迁移
如何计算bucket下标
若两个对象逻辑相等,那么他们的hashCode一定相等,反之却不一定成立。
取模的逻辑
static int indexFor (int h, int length) {
return h & (length - 1);
}
将table的容量与hash值做“与”运算,得到哈希table的bucket下标。
哈希表的大小控制在2的次幂
原因1:降低发生碰撞的概率,使散列更均匀。根据key的hash值计算bucket的下标位置时,使用“与”运算公式:h & (length-1),当哈希表长度为2的次幂时,等同于使用表长度对hash值取模(不信大家可以自己演算一下),散列更均匀;
原因2:表的长度为2的次幂,那么(length-1)的二进制最后一位一定是1,在对hash值做“与”运算时,最后一位就可能为1,也可能为0,换句话说,取模的结果既有偶数,又有奇数。设想若(length-1)为偶数,那么“与”运算后的值只能是0,奇数下标的bucket就永远散列不到,会浪费一半的空间。
fail-fast策略
在系统设计中,当遇到可能会诱导失败的条件时立即上报错误,快速失效系统往往被设计在立即终止正常操作过程,而不是尝试去继续一个可能会存在错误的过程。
在HashMap中,我们前面提到的modCount
域变量,就是用于实现hashMap中的fail-fast。出现这种情况,往往是在非同步的多线程并发操作。
在对Map的做迭代(Iterator)操作时,会将modCount域变量赋值给expectedModCount
局部变量。在迭代过程中,用于做内容修改次数的一致性校验。若此时有其他线程或本线程的其他操作对此Map做了内容修改时,那么就会导致modCount
和expectedModCount
不一致,立即抛出异常ConcurrentModificationException
。
JDK8中的HashMap底层实现
哈希表中因为hash碰撞而产生的链表结构,如果数据量很大,那么产生碰撞的几率很增加,这带来的后果就是链表的长度也一直在增加,相对比于JDK7,HashMap在JDK8中做链表结构做了优化(但是仍然线程不安全),在一定的条件下将链表转化为红黑树,提升查询效率
put()操作
在进一步分析put()操作前,先说明一下:除了底层存储结构有调整,链表结点的定义也由Entry类转为了Node类
三个重要的参数
// 一个桶的树化阈值
// 当桶中元素个数超过这个值时,需要使用红黑树节点替换链表节点
static final int TREEIFY_THRESHOLD = 8;
//一个树的链表还原阈值
//当扩容时,桶中元素个数小于这个值,就会把树形的桶元素 还原(切分)为链表结构
static final int UNTREEIFY_THRESHOLD = 6;
//哈希表的最小树形化容量
//当哈希表中的容量大于这个值时,表中的桶才能进行树形化
//否则桶内元素太多时会扩容,而不是树形化
static final int MIN_TREEIFY_CAPACITY = 64;
扩容操作
什么场景下会触发扩容?
- 哈希table为null或长度为0
- Map中存储的k-v对数量超过阈值
threshold
- 链表中的长度超过了
TREEIFY_THRESHOLD=8
,但表长度却小于MIN_TREEIFY_CAPACITY=64
一般扩容分为2步:
- 对哈希表长度的扩展
- 讲old table中的数据搬到new table上(将old table.length & hash如果为0则位置不变,不为0为旧位置 + old table.length)
...
// 前面已经做了第1步的长度拓展,我们主要分析第2步的操作:如何迁移数据
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 若只有一个结点,则原地存储
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// "lo"前缀的代表要在原bucket上存储,"hi"前缀的代表要在新的bucket上存储
// loHead代表是链表的头结点,loTail代表链表的尾结点
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 以oldCap=8为例,
// 0001 1000 e.hash=24
// & 0000 1000 oldCap=8
// = 0000 1000 --> 不为0,需要迁移
// 这种规律可发现,[oldCap, (2*oldCap-1)]之间的数据,
// 以及在此基础上加n*2*oldCap的数据,都需要做迁移,剩余的则不用迁移
if ((e.hash & oldCap) == 0) {
// 这种是有序插入,即依次将原链表的结点追加到当前链表的末尾
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
// 需要搬迁的结点,新下标为从当前下标往前挪oldCap个距离。
newTab[j + oldCap] = hiHead;
}
}
}
}
}
get()操作
先根据key计算hash值,进一步计算得到哈希table的目标index,若此bucket上为红黑树,则再红黑树上查找,若不是红黑树,遍历链表。
HashTable
- HashTable线程安全,HashMap线程不安全;
- HashMap的key、value都可为null,且value可多次为null,key多次为null时会覆盖。当HashTable的key、value都不可为null,否则直接NPE(NullPointException)。
- 初始值和扩容方式不同。HashTable的初始值为11,扩容为原大小的2*d+1。容量大小都采用奇数且为素数,且采用取模法,这种方式散列更均匀。但有个缺点就是对素数取模的性能较低(涉及到除法运算)
HashTable的线程安全
put操作、get操作、remove操作、equals操作,都使用了synchronized
关键字修饰。
HashMap线程不安全,HashTable虽然线程安全,但性能差,那怎么破?使用ConcurrentHashMap
类吧,既线程安全,还操作高效,谁用谁说好。莫急,下面章节会详细解释ConcurrentHashMap
类。
HashMap的线程不安全
数据覆盖问题
两个线程执行put()操作时,可能导致数据覆盖。JDK7版本和JDK8版本的都存在此问题,这里以JDK7为例。
扩容导致死循环
只有JDK7及以前的版本会存在死循环现象,在JDK8中,resize()方式已经做了调整,使用两队链表,且都是使用的尾插法,及时多线程下,也顶多是从头结点再做一次尾插法,不会造成死循环。而JDK7能造成死循环,就是因为resize()时使用了头插法(数据转移的时候会逆序),将原本的顺序做了反转,才留下了死循环的机会。
如何规避HashMap的线程不安全
- 使用包装类
- 使用
ConcurrentHashMap
Map<String, Integer> testMap = new HashMap<>();
// 内部实现新增对象锁
Map<String, Integer> map = Collections.synchronizedMap(testMap);
Map<String, Integer> concurrentHashMap = new ConcurrentHashMap<>();
IO
IO是指输入和输出,通常指数据在内部存储器或者外部存储器或其他周边设备之间的输入和输出,从硬盘中读写数据或者从网络中收发数据,都属于IO行为
在Linux内核中,IO操作通常包括两个阶段
- 内核等待数据准备好
- 从内核复制数据到进程中
根据在这两个阶段的不同处理,Linux提供了以下5种不同的IO模型
- 阻塞IO模型
- 非阻塞IO模型
- IO复用模型
- 信号驱动式IO模型
- 异步IO模型
linux socket 编程的recvfrom
函数作为系统调用来说明 I/O 模型。recvfrom 函数类似于标准的 read 函数,它的作用是从指定的套接字中读取数据报。recvfrom 会从应用进程空间运行切换到内核空间中运行,一段时间后会再切换回来。
阻塞IO模型:最常用的IO模型是阻塞IO模型,也是最简单的模型
非阻塞IO模型:把 socket 设置为非阻塞的话,在调用 recvfrom 时,如果数据没准备就绪,进程并不会阻塞,而是由内核直接返回一个错误。
在非阻塞IO模型下,进程要轮询数据是否准备就绪,而这个轮询操作会消耗大量的CPU时间
IO复用模型:Linux提供select,poll,进程可以通过文件来传递给select或poll系统调用,阻塞在select操作上,这样select,poll就可以监听多个文件是否处于就绪状态,并且监听的文件数量有限。
IO多路复用有两个特别的系统调用select、poll、epoll函数。select调用是内核级别的
select可以等待多个socket,能实现同时对多个IO端口进行监听,当其中任何一个socket的数据准备好了,就能返回进行可读了
select或者poll调用后,会阻塞进程,与blockingIO阻塞不同在于,此时的select不是等到socket数据全部到达再处理,而是有了一部分数据就会调用用户进程来处理
多路复用的特点是通过一种机制一个进程能同时等待IO文件描述符,IO多路复用是阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的IO系统调用如recvfrom之上
从整个IO过程来看,都是顺序执行的,都是进程主动等待且向内核检查状态
异步 I/O 模型:异步 I/O 的机制是告知内核启动某个操作,由内核来完成操作(包括把内核数据复制到进程缓冲区),应用进程只需要等待内核通知操作完成即可,示意图如下:
阻塞和非阻塞
- 阻塞:进程/线程在调用一个函数时,会阻塞函数直至处理完毕。
- 非阻塞:不需要阻塞函数至处理完毕即可往下执行
同步和异步
同步和异步关注的是通知机制,同步的话就是在发出调用后一直等待结果返回,而异步则是发出调用后等待被调用者通知。
用户空间与内核空间
寻址空间为4G,针对linux操作系统而言,将最高的1G字节,供内核使用,而将较低的3G节点,供各个进程使用,成为用户空间
缓冲IO
大多数文件系统的默认IO操作都是缓存IO,在Linux的缓存IO机制中,操作系统会将IO的数据缓存在文件系统的页缓存中,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间
LinuxIO模型
网络IO的本质是socket的读取,socket在linux系统被抽象为流
线程池应用
线程池的好处
- 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损失
- 提高响应的速度:任务到达时,无需等待线程创建即可立即执行
- 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控。
- 提高了更强大的功能,允许开发人员向其中增加更多的功能。