Redis基础知识

一、Redis基础

1.Redis为什么快

  • 纯内存KV操作:减少了一些不必要的 I/O 操作。
  • 单线程操作:省去多线程时CPU上下文切换的时间,也不用去考虑各种锁的问题。(机器内存和网络带宽才是瓶颈)
  • 高效的数据结构:底层多种数据结构支持不同的数据类型,使得数据存储时间复杂度降到最低。
  • I/O 多路复用:多个客户端的I/O请求,当某个socket(客户端连接)的请求可读或者可写的时候,它会给一个通知,达到一个线程同时处理多个IO请求的目的。

注:Redis 单线程是说一个线程处理所有网络请求,接收到用户的请求后,全部推送到一个队列里,然后交给文件事件分派器。后面的依旧是多线程。

注2:I/O :网络 I/O,多路:多个 TCP 连接,复用:共用一个线程或进程。也就是多个客户端的I/O操作复用一个线程。

注3:IO多路复用是指内核一旦发现进程指定的一个或者多个文件描述符IO条件准备好以后就通知该进程。

注4:Redis6.0 之后引入了多线程,处理命令还是单线程,网络数据的读写这类耗时操作上是用多线程。

2.Redis与Memcached

2.1 概述:

Memcached :是高性能分布式内存缓存服务器,本质一个key-value 数据库,但不支持数据持久化,服务器关闭后全丢失。只支持key-value结构。

Redis:将大部分数据放在内存中,支持的类型有:字符串、hash、list、set、zset。

2.2 区别

两者都是内存数据库,数据都在内存。不过memcache还可用于缓存其他东西,例如图片、视频等等

1.Redis数据类型更丰富;

2.Redis支持持久化,挂掉后可以通过aof文件进行恢复,而memcache不行;

3.Redis还可以用来做消息队列、数据缓存等,而memcache主要用来做SQL语句缓存、用户临时数据缓存等;

4.Redis支持数据备份,即master-slave模式的主从数据备份;

5.Redis并不是所有数据all存在内存中,当物理内存用完时,可以将一些很久没用到的value 交换到磁盘。

3.数据结构

Redis 有8?种数据结构,主要由5种最基本的数据结构(String、List、Hash、Set、zset) 加 HyperLogLogs(基数/不重复元素 统计)、Bitmap (位存储)、Geospatial (地理位置)。

注:最新的可能不长这样,最新的有QuickList,结合了双端队列和压缩链表的优点,细节可以见 这里

粗略帮助记忆:

String:C的char*不高效,所以用了sds。

List:有序队列,所以用双端队列。

Hash:Hash和压缩链表。

Set:元素不重复,用hash结构(不重复)。

Zset:有序列表,所以用跳表。

3.1 String

动态字符串SDS(Simple Dynamic String)。String只用SDS数据结构。

简单说就是:1.用一个 len 字段记录当前字符串的长度,想要获取长度只需要获取 len 字段即可,而传统C语言需要遍历字符。2.每次空间预分配double空间 3.字符串缩短时不直接回收,记录到free里面,后续操作直接用free对应的空间。

注:Redis基于C语言开发,但是没用char*类型,引入了sds。因为c里面判断数组长度得遍历到 ‘\0’,sds可以直接知道长度。

  • 空间预分配对 SDS 修改或扩充时,会额外分配未使用的空间。len长度小于 1M,double。如果修改后长度大于 1M,那么将分配1M的使用空间。

  • 惰性空间释放SDS 缩短时,并不会回收多余的内存空间,而是使用 free 字段将多出来的空间记录下来。如果后续有变更操作,直接使用 free 中记录的空间,减少了内存的分配。

用途:平常的存储token等

3.2 List

List用双端列表和压缩列表两种数据结构。双端列表只有List用。

列表 List 更多是被当作队列或栈来使用。底层是用的双端队列:链表中每个节点有两个指针,prev 指向前节点,next 指向后节点。

1.头结点有len长度,能知道链表长度。2.头节点里有 head 和 tail 两个参数,分别指向头节点和尾节点,方便对两端操作。

用途:消息队列。

3.3 Hash

List、Hash、Zset都会压缩列表。

类似于双端链表,只不过双端列表需要存储前后指针等额外的数据。所以用压缩列表。

所有的操作通过指针与解码出来的偏移量进行的。并且压缩列表的内存是连续分配的,遍历的速度很快。

注:压缩列表可以理解成数组的升级版,只不过数组每个元素空间大小固定(取最大元素的长度作为每个元素空间大小,存储不同数据可能有空隙),压缩列表更紧凑。所以每个entity会存储上一元素和当前元素的大小(用于遍历的时候区分不同元素)。

  • 好处:极大的节省了内存空间
  • 缺点:不能存储太多的元素,否则遍历效率就会很低;其次,新增或者修改某个元素时,会重新分配内存,甚至可能会引起连锁更新(每个entity记录上一节点元素大小的字段是一样的,如果跨数量级了就会连锁更新)。

用途:存储对象。

3.4 Set

用hash数据结构,能够在 O(1) 时间复杂度内取出和插入关联的值。

用途:去重

3.5 Zset

用跳跃表和压缩列表两种数据结构。跳跃表只有Zset用。

跳跃表,其在链表的基础上增加了多级索引来提升查找效率。

简单的说就是多级链表,方便快速查找。查找时间复杂度 O(logN)。

用途:排行榜

4.分布式锁

利用setnx和getset命令。Getset 命令用于设置指定 key 的值,并返回 key 的旧值。

原来分布式锁实现会比较复杂:1.要考虑锁不释放,引入expire 2.setnx和expire非原子操作,引入自动检测是否过期,过期了则getset占用锁 3.需要考虑是否会删除别人的锁(超时挂起了被别的线程占用了,恢复后把锁删了)(解决:存储时增加标记,删除判断标记是否是自己的)

注:最新的setnx和expire官方提供了原子操作,所以不用考虑后续了。

5.过期数据删除策略

  • 惰性删除 :只会在取出 key 的时候才对数据进行过期检查。优点:对 CPU 友好 缺点:可能会造成太多过期 key 没有被删除。
  • 定期删除 : 每隔一段时间取一批过期key 执行删除操作。优点:对内存更友好。

注:Redis对应key的过期时间,维护在过期字典里(hash表),每个key对应一个过期时间,是一个long long类型的整数。

6.内存淘汰策略

Redis 提供 6 种数据淘汰策略:

  • volatile-lru(least recently used):从设置了过期时间的key中,选最近最少使用的数据淘汰
  • volatile-ttl:从设置了过期时间的key中,挑选将要过期的数据淘汰
  • volatile-random:从设置了过期时间的key中,任意选择数据淘汰
  • allkeys-lru(least recently used):在键空间中,移除最近最少使用的 key(这个是最常用的)
  • allkeys-random:从数据集中任意选择数据淘汰
  • no-eviction:禁止驱逐数据,报错。

简单说就是:当空间不足时,会报错,或者随机删key,或者从有过期时间的key里面随机删或者根据LRU删或者根据快要过期删

4.0 版本后增加以下两种:

  • volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
  • allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

二、数据持久化方式

rdb

aof

三、缓存一致性

1.更新缓存 VS 淘汰缓存

  • 更新缓存:数据不但写入数据库,还会写入缓存

优点:缓存不会增加一次miss,命中率高

缺点:加锁及各种并发情况可能会复杂

  • 淘汰缓存:数据只会写入数据库,不会写入缓存,只会把数据淘汰掉

优点:简单

缺点:会存在缓存miss

注:淘汰缓存后也可以通过加锁来保证只有一次命中DB,所以尽量是淘汰缓存。如果更新缓存的复杂度低那么可能才会考虑。

 

  • 先删缓存,再更新数据库  —> 进阶版:延迟双删(更新完了再删一遍)
  • 先更新数据库,再删缓存 —> 进阶版:监听binlog变更的mq,做删除

 

其他的更新缓存的不推荐:

  • 比如先更新缓存,再同步更新DB;或者先更新缓存,再异步批量更新DB。
  • 或者先更新DB再更新缓存

2.3种常用的缓存读写策略

  • Cache Aside Pattern(旁路缓存模式):先更新DB,然后删除缓存
  • Read/Write Through Pattern(读写穿透):先更新缓存再同步更新DB(缓存没有直接更新DB)
  • Write Behind Pattern(异步缓存写入):只更新缓存,异步批量更新 DB。

注:上面的读都一样,缓存有读缓存,缓存没有读DB写缓存。

注2:后两种并不常用。第三种可能点赞,或者维护浏览量这种场景可能用,吞吐量比较高。

 

3.几种缓存问题

  • 缓存穿透:请求不存在的key,每次都会落在DB上。

解决方法:布隆过滤器(一个白名单,判断请求值在不在集合中);缓存空对象(避免用一个不存在的key一直请求一直打到DB)

注:布隆过滤器可能会误判,因为采用的是hash,可能会hash冲突,那么会有小概率误判(部分不在白名单的误判成在了)。但是不影响,因为布隆过滤器说没在,那么就肯定没在。

  • 缓存击穿:缓存失效的时候,所有请求打到了DB。

解决方法:加锁

  • 缓存雪崩:同一时间不同key的缓存同时失效,请求都打到了DB。

解决方法:打散过期时间。

 

Java基础知识

一、JAVA面向对象的三大特征

封装、继承、多态

1.封装:

隐藏实现细节,提高代码的复用性,提高了安全性。

2.继承:

多个事物直接有共同的属性和行为放到父类中,特有的放在子类中,子类可以继承父类的属性和行为。通过extends关键字。

注:JAVA只支持单继承,不支持多继承。一个类只能有一个父类,不可以有多个父类。但可以多层继承。子类访问父类的成员变量通过super关键字。

  • 优点:可以继承父类的特征
  • 缺点:如果多层父类有相同名字的实例变量或者相同的方法,调用可能会产生歧义,不知道用的哪个父类的。

3.多态:

子类的对象放在父类的引用中,如 Animal a=new Cat(),子类的对象当父类对象来用。

优点:提高了程序的扩展性和复用性

缺点:通过父类引用操作子类对象时,只能用父类中有的方法,不能操作子类特有的方法。

注:多态的前提:1、必须有关系:继承、实现  2、通常都有重写的操作

3.1 向上转型:

当父类的引用指向子类的对象时,就发生了向上转型,即把子类类型转成了父类类型。

优点:隐藏了子类类型,提高了代码的扩展性

弊端:只能使用父类的内容,无法使用子类特有的功能

3.2 向下转型:

当要使用子类特有的功能时,就需要使用向下转型

优点:可以使用子类的 特有功能

弊端:需要面对具体的子类对象时,向下转型时容易发生ClassCastException类型转换异常。所以转型前必须要做判断。

3.3 对象的强制转换:

格式: 引用 instanceof 类型 判断当前对象是否是引用类

用法: Animal a1=new Dog();

if( !Cat instanceof a1){ //判断当前对象是不是Cat类型

}

Cat d=(Cat)a1;

二、java异常

1.异常

Java主要分为Error错误Exception异常两类。Error(错误)是程序无法处理的错误,表示运行应用程序中较严重问题。Exception(异常)是程序本身可以处理的异常。Exception(异常)又包含运行时异常非运行时异常(编译异常)

  • Error:OOM等
  • 运行时异常:ArrayIndexOutOfBoundException数组越界、NullPointerException空指针等。
  • 非运行时异常:如IOException IO异常。

2.可查的异常和不可查异常

Java的异常(Throwable)分为可查的异常(checked exceptions)不可查的异常(unchecked exceptions)

 

  • 可查异常必须要去处理,否则编译不通过,如IOException和ClassNotFoundException等。

处理异常方法:要么用try-catch语句捕获它,要么用throws子句声明抛出它。

  •  不可查异常(编译器不要求强制处置的异常)包括运行时异常和错误,不用捕获对应异常,应该找出错误程序进行修改。

三、基础知识点汇总

1.java中extends和implements

extends是继承类,implements是实现接口。 类只能继承一个,接口可以实现多个。
extends继承父类的时候可以重写父类的方法,implements实现接口,必须实现接口的所有方法。

abstract class A {
    abstract m(): void;
}


class B extends A{
}

class C implements A {
    m(): void { } //必须要实现定义在A中的所有方法
}

 

2.基本类型和包装类型

2.1 区别

  • 包装类型对应变量default值是 null ,而基本类型有默认值且不是 null
  • 包装类型可用于泛型,而基本类型不可以。
  • 基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 static 修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,几乎所有对象实例都存在于堆中。
  • 相比于对象类型, 基本数据类型占用的空间非常小。

2.2 装箱与拆箱

  • 装箱:将基本类型用它们对应的引用类型包装起来;
  • 拆箱:将包装类型转换为基本数据类型;

举例:

Integer i = 10;  //装箱
int n = i;   //拆箱

3.接口和抽象类

3.1 共同点 :

  • 都不能被实例化。
  • 都可以包含抽象方法。
  • 都可以有默认实现的方法(Java 8 可以用 default 关键字在接口中定义默认方法)。

3.2 区别 :

  • 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
  • 一个类只能继承一个类,但是可以实现多个接口。
  • 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。

4.深拷贝、浅拷贝、引用拷贝

浅拷贝、深拷贝、引用拷贝示意图

  • 引用拷贝:两个不同的引用指向同一个对象
  • 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),对象内部的属性是引用类型的话,指向的是同一地址。
  • 深拷贝 :深拷贝会完全复制整个对象。

5.== 和 equals()

  • 对于基本数据类型来说,== 比较的是值。
  • 对于引用数据类型来说,== 比较的是对象的内存地址。

 ==:比较的是值是否相等(基本类型比较值,引用类型(对象)比较的是内存地址)

equals():只能用来判断对象,不能用于基本数据类型。不重写等同于==,重写可以自定义。

6.String、StringBuffer、StringBuilder 的区别

  • String :里面的对象是不可变的,线程安全。适用于操作少量数据
  • StringBuffer :线程安全。适用于多线程操作大量数据。
  • StringBuilder:非线程安全的。适用于单线程操作大量数据。

四、反射

赋予了我们在运行时分析类以及执行类中方法的能力。通过反射可以获取任意一个类的所有属性和方法。

优点:代码更加灵活、为各种框架提供开箱即用的功能提供了便利。

缺点:不安全(比如无视泛型参数的安全检查),性能会变差。

应用场景:业务场景使用较少,框架使用较多。像 Spring/Spring Boot、MyBatis这些框架, Spring 里面的注解,都用了反射机制。里面也用了动态代理,动态代理的实现也依赖反射。

五、集合

1.java集合框架

两大接口派生而来:一个是 Collection接口,主要用于存放单一元素;另一个是 Map 接口,主要用于存放键值对。

2. List, Set, Queue, Map 四者的区别

List: 有序、可重复。
Set: 无序、不可重复。
Queue: 排队,有序、可重复。一般用于排队功能的叫号机。
Map: 使用键值对(key-value)存储。

List

  • ArrayList: Object[] 数组
  • VectorObject[] 数组
  • LinkedList: 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)

Set

  • HashSet(无序,唯一): 基于 HashMap 实现,底层采用 HashMap 来保存元素
  • LinkedHashSet: 通过 LinkedHashMap 实现。
  • TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树)

Queue

  • PriorityQueueObject[] 数组来实现二叉堆
  • ArrayQueueObject[] 数组 + 双指针

Map

  • HashMap: 数组+链表组成,链表长度大于阈值(默认为 8)会出现红黑树转换。
  • LinkedHashMap: 继承自 HashMap,增加了一条双向链表,可以实现顺序访问。
  • Hashtable: 数组+链表组成的,数组是 Hashtable 的主体,链表则是主要为了解决哈希冲突而存在的
  • TreeMap: 红黑树(自平衡的排序二叉树)

注:上图是LinkedHashMap的存储结构,在hashMap的基础上多了一个双向链表,可以实现顺序访问。

3.区别

3.1 ArrayList 和 Vector :

  • ArrayList 是 List 的主要实现类,底层使用 Object[ ]存储,线程不安全 ;
  • Vector 是 List 的古老实现类,底层使用Object[ ] 存储,线程安全的。

3.2 ArrayList 与 LinkedList:

  • 数据结构:一个数组一个链表数组。LinkedList 是 双向链表 (JDK1.6 之前为循环链表,JDK1.7 取消了循环)
  • 访问速度:数组可以快速随机访问,链表不行
  • 更新:链表对中间数据插入较友好好。
  • 线程安全:都不是线程安全的;
注:很少使用LinkedList

3.3 HashSet、LinkedHashSet 和 TreeSet

  • 都是 Set 接口的实现类,元素唯一,并且都不是线程安全的。
  • HashSet 用于不需要保证元素插入和取出顺序的场景,LinkedHashSet 用于 FIFO 的场景,TreeSet 用于元素需要自定义排序场景。

3.4 Queue 与 Deque 

  • Queue :单端队列,一端入,另一端出,一般遵循 先进先出(FIFO) 规则
  • Deque :双端队列,在队列的两端均可以插入或删除元素。

3.5HashMap 和 Hashtable

  • HashMap 非线程安全,Hashtable 是线程安全的(内部方法基本都经过synchronized 修饰)。
  • HashMap 要比 Hashtable 效率高一点。

注:基本不会用Hashtable,要线程安全就用ConcurrentHashMap

3.6ConcurrentHashMap在jdk1.7和1.8的区别

  • 数据结构:1.7是Segment 数组 + hashMap(HashEntry 数组 + 链表),1.8是 Node 数组 + 链表 / 红黑树。相当于原来是Segment和hashMap的数组是映射关系,现在合并了。
  • 锁1.7是Segment 分段锁,针对Segment(段/槽)加锁,1.8后锁的力度更细了,锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点。
  • 1.7的分段锁继承自 ReentrantLock。1.8 放弃了分段锁设计,采用 Node + CAS + synchronized 保证线程安全。
  • 并发度 :JDK 1.7 最大并发度是 Segment 的个数,默认是 16。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大

3.7 HashMap实现

默认大小16,负载因子0.75,超过对应阈值会扩容。

put过程:先计算key的hash值,然后低16位和高16位做异或(充分利用每一位),然后与数组长度-1做与操作确定数组的位置,然后在链表最后插入对应的节点(如果已经有相同的key会替换)。如果超过8则会有红黑树的转换。

六、并发编程

1.进程和线程

进程:是系统运行程序的基本单位。一个进程在其执行的过程中可以产生多个线程。

线程:是一个比进程更小的执行单位。同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈。多线程之间切换负担要比进程小得多。

2.并发与并行的区别

:两个及两个以上的作业在同一 时间段 内执行。
:两个及两个以上的作业在同一 时刻 执行。

3.同步和异步的区别

同步 : 发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
异步 :调用在发出之后,不用等待返回结果,该调用直接返回。

4.线程的生命周期和状态

5.sleep() 方法和 wait() 方法

  • 都可以暂停线程的执行。
  • sleep() 方法没有释放锁,而 wait() 方法释放了锁 。
  • sleep() 是 Thread 类的静态本地方法,wait() 则是 Object 类的本地方法(本质是每个对象都有对象锁,要释放当前线程占有的对象锁,所以操作的是对象而不是线程)。
  • wait() 需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法唤醒。sleep()方法执行完成后,线程会自动苏醒。

6.可以直接调用 Thread 类的 run 方法吗

可以但不会以多线程的方式执行,用当前线程执行的。

new 一个 Thread后,需要调用 start()方法,start()方法作用是启动一个线程处于就绪状态,当分配到时间片后就可以开始运行,这时候该线程会自动执行 run() 方法进行执行,这样就是多线程了。省略了start,那么就没有额外的线程,会用当前线程执行。

7.volatile

作用:保证变量的可见性,禁止指令重排。

7.1 如何保证变量可见性

volatile修饰的,不会有在工作内存有变量副本,线程直接从主内存取。

7.2 如何禁止指令重排

单例模式(线程安全) :

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public  static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

 uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. 将 uniqueInstance 指向分配的内存地址

JVM 的指令重排可能导致执行顺序不是1->2->3,而是 1->3->2。这样再多线程情况下,1->3的时候,另一个线程读到的就不是null了。

8.synchronized

8.1 作用域

修饰方法

修饰对象:根据修饰的对象不同可以分为全局锁(xxx.class)或者代码块.

8.2 底层原理

通过 JDK 自带的 javap 命令,可以查看java字节码.class信息,发现synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令;synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。不过两者的本质都是对对象监视器 monitor 的获取。

8.3 锁升级

偏向锁、轻量级锁、重量级锁

用synchronized修饰的会有锁升级过程,如果只有一个线程获取锁,那么这个锁会偏向这个线程,叫做偏向锁;如果这时候有其他线程也来获取这个锁,那么这个锁会变成轻量级锁,获取不到锁的线程会自旋,然后重新获取;如果还是获取不到,则会变为重量级锁,获取不到的线程会阻塞(锁池状态)。

8.4 synchronized 和 ReentrantLock 的区别

  • 两者都是可重入锁
  • synchronized非公平锁,ReentrantLock默认也是非公平锁,不过可以有参数可以配置变成公平锁
  • synchronized 依赖于 JVM , ReentrantLock 依赖于 API(JDK 层面实现),需要lock() 和 unlock() 方法配合 try/finally 语句块来完成
  • ReentrantLock功能更多,如可以变成公平锁

9.ThreadLocal

为了实现每个线程都有自己的专属本地变量。通过创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,从而避免了线程安全问题。

9.1 ThreadLocal 原理

// 源码:
public class Thread implements Runnable {
    //......
    //与此线程有关的ThreadLocal值。由ThreadLocal类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;

    //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    //......
}

ThreadLocal内部有一个ThreadLocalMap,每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。

10.线程池

10.1为什么使用线程池

  • 降低资源消耗:可以重复利用已创建的线程,降低线程创建和销毁造成的消耗。
  • 提高响应速度:当任务到达时,用已有线程可以直接执行。
  • 方便管理:统一创建,分配,调优和监控。

10.2 实现 Runnable 接口和 Callable 接口的区别

  • Runnable自 Java 1.0 以来一直存在,Callable在 Java 1.5 中引入
  • Runnable 接口不会返回结果或抛出检查异常,但是 Callable 接口 可以。所以如果任务不需要返回结果或抛出异常就使用 Runnable 接口 ,看起来更简洁。

10.3  execute()方法和 submit()方法的区别

  • execute()方法:没有返回值,无法判断任务是否被线程池执行成功;
  • submit()方法:线程池会返回一个 Future 类型的对象,可以判断是否执行成功。此外可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成。

10.4 四类线程池

CachedThreadPool: 可根据实际情况动态调整线程数量的线程池。

FixedThreadPool : 固定线程数量的线程池。

SingleThreadExecutor: 只有一个线程的线程池。

ScheduledThreadPool:调度类的线程池。

 

10.4.1 几个核心参数:

  • corePoolSize : 核心线程数。
  • maximumPoolSize : 最大线程数。
  • workQueue: 工作队列。
  • keepAliveTime:非核心线程(临时线程)存活时间;
  • handler :饱和策略。主要有报错、丢弃当前任务、丢弃最早未处理的任务、用提交任务的线程执行这四种。

10.4.2 大致流程

进来任务时,用核心线程做任务;如果任务超过核心线程数,则放入队列;如果任务还在增加,则创建临时线程执行,直到最大线程数;如果还在增加,则按照对应的饱和策略处理。

11.JUC 包中的4大Atomic原子类

基本类型:使用原子的方式更新基本类型

  • AtomicInteger:整型原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean:布尔型原子类

数组类型:使用原子的方式更新数组里的某个元素

  • AtomicIntegerArray:整型数组原子类
  • AtomicLongArray:长整型数组原子类
  • AtomicReferenceArray:引用类型数组原子类

引用类型:

  • AtomicReference:引用类型原子类
  • AtomicStampedReference:原子更新带有版本号的引用类型。
  • AtomicMarkableReference :原子更新带有标记位的引用类型

对象的属性修改类型

  • AtomicIntegerFieldUpdater:原子更新整型字段的更新器
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器
  • AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器

12.AQS

AQS 的全称为(AbstractQueuedSynchronizer),是一个用来构建锁和同步器的框架。如: ReentrantLock,Semaphore信号量等都是用的AQS框架。

12.1 AQS 原理

通过ReentrantLock加锁说明:简单的说就是,AQS对象内部有一个volatile的int变量,初始state的值是0,一个线程要来加锁时,通过CAS把0改为1,代表加锁成功,释放锁就是再减回去,减到0就代表锁释放了。

另外AQS内部还维护着一个先进先出的等待队列,如果其他线程也来加锁,但是因为锁已经被其他线程占用而加锁失败,就会存放这些加锁失败的线程。

 

注:CLH(Craig,Landin and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。

12.2 AQS 组件总结

  • ReentrantLock:一次只允许一个线程访问某个资源,可重入。
  • Semaphore(信号量)-允许多个线程同时访问问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
  • CountDownLatch (倒计时器): 可以让某一个线程等待直到倒计时结束,再开始执行。
  • CyclicBarrier(循环栅栏): 让一组线程到达栅栏时被阻塞,直到最后一个线程到达时,栅栏被推倒,所有线程开始执行。

七、反射

1.概念

可以在运行时分析类以及执行类中方法,通过反射可以获取和调用对应类的属性和方法。

 像Spring/Spring Boot、MyBatis 等框架中都大量使用了反射机制,动态代理也是基于反射。

2.优缺点

  • 优点 : 更加灵活、为各种框架实现对应功能提供了便利。
  • 缺点 :无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。性能也会稍差些。

3.获取 Class 对象的四种方式

3.1. 知道具体类的情况下可以使用:

Class alunbarClass = TargetObject.class;

3.2. 通过 Class.forName()传入类的全路径获取:

Class alunbarClass1 = Class.forName("cn.test.TargetObject");

3.3. 通过对象实例instance.getClass()获取:

TargetObject o = new TargetObject();
Class alunbarClass2 = o.getClass();

3.4. 通过类加载器xxxClassLoader.loadClass()传入类路径获取:

ClassLoader.getSystemClassLoader().loadClass("cn.test.TargetObject");

八、语法糖

1.概念:

计算机语言中的某种特殊语法,方便程序员使用。可以让程序更加简洁,有更高的可读性。

注:反编译,可以用java自带的javap,对.class文件进行反编译,可读性稍差。有一些网站可以把字节码反编译成对应的代码。所以查看语法糖依赖反编译。

2.常见语法糖

  • switch 支持 String 与枚举:java7开始支持switch string,正常switch对比的是基本数据类型的值或者ascii码。switch string,进行switch的实际是哈希值,然后通过使用equals方法比较进行安全检查。
  • 泛型:编译阶段通过类型擦除的方式进行解语法糖。
  • 自动装箱与拆箱:装箱通过调用包装器的 valueOf 方法实现,而拆箱通过调用包装器的 xxxValue 方法实现的。
  • 可变长参数:把可变长参数变成了一个数组,方法调用传的是数组。

注:在编译阶段,如果有一些代码根本不会走到(废代码),那么编译器会不进行编译。

 

九、jvm内存模型

十、垃圾回收

1.垃圾回收算法

1.1 标记-清除算法

该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。

  • 效率问题
  • 空间问题(标记清除后会产生大量不连续的碎片)

1.2 标记-复制算法

将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。

注:新生代采用标记-复制算法

1.3 标记-整理算法

先标记,然后让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

注:老年代有使用标记-整理算法

1.4 分代收集算法

根据对象存活周期的不同将内存分为几块。分为新生代和老年代。新生代朝生夕死的多,所以用标记-复制算法。老年代存活几率比较高,没有额外的空间来对它进行担保,所以用标记-清除或者标记-整理算法。

2.分代回收细节

分为新生代、老年代和永久代(1.8后改成meta space),新生代里面有一个Eden区,两个Survivor区,每次回收时,复制Eden区和Survivor区存活对象到另外一个Survivor区,如此反复。如果超过15次还存活的对象,则进入老年代。

注:大的对象直接进入老年代,如大的数组、字符串等。

注2:对象一般在Eden区分配,当 Eden 区没有足够空间进行分配时,将会进行 Minor GC

  • 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
  • 老年代收集(Major GC / Old GC):对老年代进行垃圾收集。
  • 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
  • 整堆收集 (Full GC):收集整个 Java 堆和方法区。

3.死亡对象判断

3.1 引用计数法

对象有一个引用计数器,每次被引用,计数器加 1,引用失效,计数器就减 1;为 0 的对象可以被回收。

优点:效率高

缺点:无法解决循环引用的问题

3.2 可达性分析算法

通过根节点 “GC Roots”向下搜索,当一个对象到 GC Roots 没有任何引用链相连的话,就可以被回收。

可以作为 GC Roots 的对象:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈(Native 方法)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁持有的对象

4. 引用类型

4.1.强引用(StrongReference):

我们平常使用的Object ob = new Object(),内存不足抛出 OOM,只有ob=null,才会回收。

4.2.软引用(SoftReference)

内存空间足够,就不会回收它,如果内存空间不足了,就会回收这些对象的内存。

4.3.弱引用(WeakReference)

垃圾收集器只要扫到,都会回收。

4.4.虚引用(PhantomReference)

被回收的时候会有一个通知,主要用来跟踪对象被垃圾回收的活动。

十一、高cpu占用优化

1.背景:

saas服务启动时,cpu占用太高。

2.过程:

  1. 首先确定CPU占比高的进程,线程ID。通过windows提供的工具Process Explorer可查到saas服务的进程ID(PID), 以及内部线程占用cpu情况。实时显示选中的进程内部线程ID,占用CPU情况。 操作saas,记录下占比CPU比较高的线程ID。
  2. 通过Jstack工具查看进程内部的线程执行情况: jstack -l PID > XXX.stack .
  3. 通过线程ID在jstack文件中找到消耗cpu比较大的线程为:C2 CompilerThread0 .在server模式下启动jar包后默认是开启tiered compiler对javac产生的字节码进行优化,这个线程消耗资源比较多。
  4.  用 -client 模式启动jar包后, 初步看内存占用从400+MB降到200+MB,CPU占比高的持续时间明显降低。

3.分析:

因为我们都知道JIT( just in time ), 也就是即时编译编译器。使用即时编译器技术,能够加速 Java 程序的执行速度。主要有Server 模式和 client 模式两种启动模式,主要的差别在于:-server 模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升。原因是:当虚拟机运行在-client 模式的时候,使用的是一个代号为 C1 的轻量级编译器,而-server 模式启动的虚拟机采用相对重量级代号为 C2 的编译器。C2 比 C1 编译器编译的相对彻底,服务起来之后,性能更高。

 

java-concurrent包(JUC)

https://blog.csdn.net/u013851082/article/details/68488640

https://blog.csdn.net/wbwjx/article/details/57856045

 

Demo:

package com.buaahy.juc;

import java.util.concurrent.*;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @author hy
 * @date 2018-08-06 16:51
 */
public class JUCDemo {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<String>(100);
        new Thread(() -> blockingQueue.add("阻塞队列中的一个元素")).start();
        new Thread(() -> {
            try {
                System.out.println(blockingQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        //闭锁测试
        CountDownLatch countDownLatch = new CountDownLatch(3);
        new Thread(() -> {
            try {
                System.out.println("线程开始等待,等待倒计时结束");
                countDownLatch.await();
                System.out.println("线程等待结束,开始运行");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(() -> {
            System.out.println("倒计时开始");
            while (countDownLatch.getCount() > 0) {
                System.out.println(countDownLatch.getCount());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                countDownLatch.countDown();
            }
            System.out.println("倒计时结束");
        }).start();


        //读写锁测试  两个线程加读锁  一个线程加写锁  发现读锁上还可以加读锁  而写锁必须等到加在上面的所有锁释放后才能获得锁
        ReadWriteLock lock = new ReentrantReadWriteLock();
        new Thread(() -> {
            System.out.println("开始加读锁1");
            lock.readLock().lock();
            System.out.println("读锁1加成功");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.readLock().unlock();
            System.out.println("读锁1释放");
        }).start();

        new Thread(() -> {
            System.out.println("开始加读锁2");
            lock.readLock().lock();
            System.out.println("读锁2加成功");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.readLock().unlock();
            System.out.println("读锁2释放");
        }).start();

        new Thread(() -> {
            System.out.println("开始加写锁");
            lock.writeLock().lock();
            System.out.println("写锁加成功");
            lock.writeLock().unlock();
            System.out.println("写锁释放成功");
        }).start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //信号量测试 只有一个许可(一个线程获得后,其他线程获取要等待) 非公平的
        Semaphore semaphore = new Semaphore(1, false);
        new Thread(() -> {
            try {
                System.out.println("线程1尝试获得许可");
                semaphore.acquire();
                System.out.println("线程1获得许可成功");
                Thread.sleep(2000);
                System.out.println("线程1开始释放许可");
                semaphore.release();
                System.out.println("线程1释放许可成功");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            try {
                System.out.println("线程2尝试获得信号量");
                semaphore.acquire();
                System.out.println("线程2获得信号量成功");
                semaphore.release();//使用完后释放
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        //栅栏测试
        CyclicBarrier cyclicBarrier = new CyclicBarrier(2, new Runnable() {
            @Override
            public void run() {
                System.out.println("栅栏被推翻了,开始执行。");
            }
        });

        cyclicBarrier.isBroken();
        new Thread(() -> {
            try {
                System.out.println("栅栏线程1开始等待");
                cyclicBarrier.await();
                System.out.println("栅栏线程1等待结束,继续执行");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(() -> {
            try {
                System.out.println("当前等待线程数:" + cyclicBarrier.getNumberWaiting());
                System.out.println("栅栏线程2开始等待");
                cyclicBarrier.await();
                System.out.println("栅栏线程2等待结束,继续执行");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }).start();

        System.out.println("Finished!");


    }
}