本文为整合篇,用于复习用,如需详细的,可翻看以前文章。
java官网下载地址上找到对应的版本下载,本文以mac、jdk17
为例。
自行选择是Arm还是普通的芯片下载,下载完成后,dmg格式则直接安装,压缩包则直接解压。
添加到环境变量:
vi ~/.zshrc
#添加到path,我这是解压缩的目录
export JAVA_HOME="/Users/admin/jdk-17.0.6.jdk/Contents/Home"
export CLASSPATH=".:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar"
保存测试是否成功:
java version "17.0.6" 2023-01-17 LTS
Java(TM) SE Runtime Environment (build 17.0.6+9-LTS-190)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.6+9-LTS-190, mixed mode, sharing)
Java安装好后会包含JDK和JRE两个目录。
正常流程创建一个test.java文件,然后在里面输入包名、类名和main方法,然后再javac编译得到.class文件,最后java test即可执行main方法。
由于基本是项目式开发,这里直接上工具了(IDEA):
到这里可以说会写程序了!哈哈
关键字 | 含义 |
---|---|
abstract | 表明类或者成员方法具有抽象属性 |
assert | 断言,用来进行程序调试 |
boolean | 基本数据类型之一,声明布尔类型的关键字 |
break | 提前跳出一个块 |
byte | 基本数据类型之一,字节类型 |
case | 用在switch语句之中,表示其中的一个分支 |
catch | 用在异常处理中,用来捕捉异常 |
char | 基本数据类型之一,字符类型 |
class | 声明一个类 |
continue | 回到一个块的开始处 |
default | 默认,例如,用在switch语句中,表明一个默认的分支。Java8 中也作用于声明接口函数的默认实现 |
do | 用在do-while循环结构中 |
double | 基本数据类型之一,双精度浮点数类型 |
else | 用在条件语句中,表明当条件不成立时的分支 |
enum | 枚举 |
extends | 表明一个类型是另一个类型的子类型。对于类,可以是另一个类或者抽象类;对于接口,可以是另一个接口 |
final | 用来说明最终属性,表明一个类不能派生出子类,或者成员方法不能被覆盖,或者成员域的值不能被改变,用来定义常量 |
finally | 用于处理异常情况,用来声明一个基本肯定会被执行到的语句块 |
float | 基本数据类型之一,单精度浮点数类型 |
for | 一种循环结构的引导词 |
if | 条件语句的引导词 |
implements | 表明一个类实现了给定的接口 |
import | 表明要访问指定的类或包 |
instanceof | 用来测试一个对象是否是指定类型的实例对象 |
int | 基本数据类型之一,整数类型 |
interface | 接口 |
long | 基本数据类型之一,长整数类型 |
native | 用来声明一个方法是由与计算机相关的语言(如C/C++/FORTRAN语言)实现的 |
new | 用来创建新实例对象 |
package | 包路径 |
private | 一种访问控制方式:私用模式 |
protected | 一种访问控制方式:保护模式 |
public | 一种访问控制方式:共用模式 |
return | 从成员方法中返回数据 |
short | 基本数据类型之一,短整数类型 |
static | 表明具有静态属性 |
strictfp | 用来声明FP_strict(单精度或双精度浮点数)表达式遵循[IEEE 754](https://baike.baidu.com/item/IEEE 754)算术规范 |
super | 表明当前对象的父类型的引用或者父类型的构造方法 |
switch | 分支语句结构的引导词 |
synchronized | 表明一段代码需要同步执行 |
this | 指向当前实例对象的引用 |
throw | 抛出一个异常 |
throws | 声明在当前定义的成员方法中所有需要抛出的异常 |
transient | 声明不用序列化的成员域 |
try | 尝试一个可能抛出异常的程序块 |
void | 声明当前成员方法没有返回值 |
volatile | 表明两个或者多个变量必须同步地发生变化 |
while | 用在循环结构中 |
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
java数据自动类型转换顺序:byte -> short -> int -> long -> float ->double
整形:默认0
byte:1个字节8位
short:2个字节16位
int:4个字节32位
long:8个字节64位
浮点数:默认0.0
folat:4个字节32位
double:8个字节64位
布尔类型:
boolean:默认false
char类型:
char:2个字节16位
位(bit):计算机中数据存储的最小单位,例如10001100是一个位的二进制数
字节(byte):计算机数据处理的基本单位简称B
转换关系:1B=8bit 1024B=1KB 1024KB=1M 1024M=1G
二进制扩展:
按 128 64 32 16 8 4 2 1来解析计算机01编码
0 1 1 0 = 6
1 1 0 1 1 0 0 1 = 217
以此类推。
字符串
String类标识了final,说明不可被继承。
==是以基础类型时比较它们的值,在类或者对象时比较的是它们的内存地址。
equals方法,默认比较的是内存地址,该方法可以覆盖重新,自定义规则来比较两个对象是否相等
与list不同的是先定义、开辟空间、赋值在使用。java数组长度不可变
数组的定义:
1) String[] str0=null;//定义
2)String[] str1=new String[4];//定义并开辟空间
3)String[] str2={"A","B","C","D"};
4) String[] str3=new String[]{"A","B","C"};
数组的复制:
System.arraycopy(str1,0,str2,0,1);//从左到右参数意义:从哪个数组,从哪开始复制,复制到哪?开始位置,复制多长
System.out.println("copyOf:"+Arrays.toString(Arrays.copyOf(str1, 3)));
基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
底层数据结构:
//底层实现 在list中链表是一个值 这里是KV键值对 一个map由N多个Node组成
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //hash值
final K key; //key
V value; //value
Node<K,V> next; //hash碰撞时,存储在链表的下一个节点中
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
默认数量16,扩容因子0.75(扩容因子越高,碰撞概率越高,存储在桶里面的元素越来越多,每次put的代价就越大,会影响效率)。每次扩容是原来的2倍(保证元素要么在新表中保持不动要么满足新位置=原数组+原位置)
//新数组 Node实现了Map.Entry所以遍历用的是父类
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
扩容高低位:在JDK1.8前,多线程情况下使用HashMap put元素会造成死循环,这是多次put操作引发HashMap的扩容机制,扩容机制会采用头插法的方式移动元素,这样会造成闭环,形成死循环。在1.8使用了高低位进行均为扩容解决了死循环问题。
死循环模拟:jdk1.7的transfer是用头插法,新的链表和原来的是倒着的,所以这时候假如有两个线程,第一个线程只执行到Entry next = e.next;然后就第二个线程执行了,等到第二个线程执行完,其实这时候已经完成了扩容的任务,且链表里的顺序 已经倒置了,这时候第一个线程继续执行,这时候就把尾巴又指向头了,然后就造成了环。
当发生hash碰撞时储存桶的元素达到8个时(先添加子元素,再转换)立即转换为红黑树。红黑树是特殊的平衡二叉树,它的特性如下:
1.根节点必须是黑色
2.节点是黑色或者红色
3.如果一个节点是红色,那么它的子节点必须是黑色
4.每个为空的叶子节点都是黑色
5.从一个节点到该节点的叶子节点所有路径上都包含了相同数量的黑色节点
常见疑问:
1.为什么长度要是2的n次方?
为了存储高效,减少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现就在把数据存到哪个链表中的算法;
这个算法实际就是取模,hash%length,计算机中直接求余效率不如位移运算,源码中做了优化hash&(length-1),
hash%length==hash&(length-1)的前提是length是2的n次方;
为什么这样能均匀分布减少碰撞呢?2的n次方实际就是1后面n个0,2的n次方-1 实际就是n个1;
HashMap计算添加元素的位置时,使用的位运算,这是特别高效的运算;另外,HashMap的初始容量是2的n次幂,扩容也是2倍的形式进行扩容,是因为容量是2的n次幂,可以使得添加的元素均匀分布在HashMap中的数组上,减少hash碰撞,避免形成链表的结构,使得查询效率降低!
有序map,结构为一个特殊的平衡二叉树,简称红黑树,TreeMap底层维护这一个Entry
底层数据结构
K key;
V value;
Entry<K,V> left; //左节点
Entry<K,V> right; //右节点
Entry<K,V> parent; //父节点
boolean color = BLACK; //节点颜色
看下put一个值的部分源码
if (cmp < 0)
//新增节点的key小于当前节点的key,则添加到当前节点的左节点
t = t.left;
else if (cmp > 0)
//大于 就添加到当前节点的右节点
t = t.right;
else
//新旧key值相等 直接覆盖并返回新值
return t.setValue(value);
获取元素是从根节点开始遍历查找,时间复杂度是O(n)
底层和HashMap一样, 键值对不能为空,线程安全,效率低。
底层同时是node数组组成,不同的点是初始容量11(把容量选为质数,让所有的key和数组长度的公约数只有自己和1,后面扩容也是按照2N+1的方式,从而保证余数的均匀分配,降到冲突率)
为什么是线程安全的?
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
//这里的计算方式与HashMap不同,HashMap是位运算,这里是直接取模
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
//数组先取到链表, 再遍历链表
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
是因为方法上的synchronized关键字
List 接口的大小可变数组的实现。实现了所有可选列表操作,并允许包括 null 在内的所有元素。线程不安全
List list = new ArryList(); list.add(111); list.remove(下标|元素)
Arrays.asList(new Integer[]{1,2,3})
底层存储,默认是一个空数组:{}
//存储数据的桶 transient/标识不用序列号
transient Object[] elementData;
//构造方法初始化,如果传入的容量是0则为空数组,否则new一个指定容量的数组
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//private static final Object[] EMPTY_ELEMENTDATA = {};
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//在add时会检测数组size,如果是length为0那么再次赋值
//private static final int DEFAULT_CAPACITY = 10;
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
所以说创建ArrayList并知道容量时,预先传入容量能减少一次数组的创建。
扩容:
//旧数组的容量+本身右移1位 简单计算为右移的容量/2的一次方 即原始容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//最后Arrays.copyOf 截取原数组或用 null 填充以获得指定的长度 扩容结束,数组重新赋值
elementData = Arrays.copyOf(elementData, newCapacity);
List 接口的链接列表实现。实现所有可选的列表操作,并且允许所有元素(包括 null),线程不安全,使用是一样的,但是底层存储的数据结构不一致:
//最底层的静态内部类链表
private static class Node<E> {
E item; //元素
Node<E> next; //下一个元素链表
Node<E> prev; //前一个元素链表
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
链表:保存了前后指引的一条链。
如果是知道元素查前后时,LinkedList的查询时间复杂度就是O(2)
HashSet是一个不保证顺序,但是保证去重。
//new HashSet
public HashSet() {
map = new HashMap<>();
}
//add添加元素
public boolean add(E e) {
//private static final Object PRESENT = new Object(); value值就是一个空object对象
return map.put(e, PRESENT)==null;
}
//remove
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
实际上底层就是一个HashMap,value存储的就是一个静态的空对象,因为HashMap中没有索引的概念,如果想通过索引取值的话,可以转到ArrayList中操作。
Set<String> sets = new HashSet<>();
ArrayList<String> strings = new ArrayList<>(sets);
基于 TreeMap 的 NavigableSet 实现。使用元素的自然顺序对元素进行排序,或者根据创建 set 时提供的 Comparator 进行排序,具体取决于使用的构造方法。
底层数据结构
//贴下构造方法就懂了,其他的就不多说了
public TreeSet() {
this(new TreeMap<E,Object>());
}
private transient NavigableMap<E,Object> m;
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
继承:子类通过继承父类可以调用父类的方法,也可以覆盖重写父类的方法,但是不能继承父类的构造方法。
封装:通过private把对象的方法或者成员变量隐藏起来,提供一个公共的入口来进行访问。
多态:未知的具体类型,在运行阶段才知道的具体实例对象,多态代码灵活多变,需要满足继承、重写或者实现父类对象的方法。例如抽象类调用某个方法,由某个子类继承后重写实现调用。
类:一个类型的总称,里面可以定义成员变量和方法,可以继承其他类或者实现接口
接口:预先定义好方法的名称和形参,由实现类去实现方法。在1.8接口可以添加默认方法
抽象类:类和接口的结合体,可以单独的定义接口由子类继承实现。
一个服务一个进程,一个进程内可以有n个线程。
线程是操作系统能够进行调度的最小单位,硬件CPU核心数就对应可同时执行的线程数。
多线程的特点:异步,并行。
//继承Thread类
class Test0 extends Thread{
@Override
public void run() {
System.out.println("继承Thread");
}
}
//实现Runnable接口
class Test1 implements Runnable{
@Override
public void run() {
System.out.println("实现Runnable接口");
}
}
//实现callable 这种实现有返回值
class Test2 implements Callable {
@Override
public Object call() throws Exception {
System.out.println("实现Callable接口,并且获得返回值");
Thread.sleep(10000);
return "call线程 返回值";
}
}
线程运行原理概括:
start一个线程 ->jvm调用到不同的操作系统创建一个线程并开始线程 ->CPU调度算法执行(几核CPU就可同时执行几个线程) -> 执行线程调用回JVM run方法 -> JVM回调到我们自己实现的run方法中
1.执行完成即刻停止结束
2.友好的结束发送interrrupted中断指令
Thread thread = new Thread(() -> {
//系统只会发送中断标识,是否中断需要开发者判断
//在这里判断是否中断
while (!Thread.currentThread().isInterrupted()) {
System.out.println(11111);
}
}, "N1");
thread.start();
Thread.sleep(1000);
//通过当前线程的中断标识去停止
thread.interrupt();
System.out.println(22222);
}
3.暴力结束
上面趋向于手动发送结束标识,如果现在阻塞是怎么结束呢?
Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(3333);
}
System.out.println(11111);
}
}, "N1");
thread.start();
Thread.sleep(3000);
//发送中断指令,会往上抛InterruptedException
thread.interrupted();
System.out.println(22222);
}
但是在实际测试中上述代码并不会停止运行,这种情况就叫线程的中断复位。原理如下:
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//这里的选择权在开发者本人
//InterruptedException 异常抛出来后会还原中断状态 就是所说的复位
//然后这里你希望中断就用这个 Thread.currentThread().interrupt()
//然后在异常中断后做一些事情
System.out.println(3333);
}
当给出线程interrupted标识时,当前线程并不会马上结束,而是会抛出InterruptedException告诉开发者我现在无法自己处理中断,并且将中断标记复位,需要开发者自己中断线程。因为在某些由于异常原因或者阻塞或者死循环等的线程中断后可能需要做一些其他的记录。相应于给了外界一个改变线程状态的入口。
我们先来看一段代码,正常来说结果要是10000的
static int num = 0;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
num ++;
}).start();
}
Thread.sleep(5000);
System.out.println(num);
}
//然鹅,出来的结果
9999
这就是多个多线程并行去做同一件事,导致出来的结果会和预想中的有偏差,所以才引入了锁。锁是用来保证数据安全的。
锁的本质:读写锁、排它锁、互斥锁。
CAS机制:
在修改锁的标记,修改线程指针的指向操作时用到。
CompareAndSwap(old,expect,update)
old : ThreadA expect:TheadA update:TheadB
乐观锁的概念,例如:mysql操作update xxx set status = 1 where id = 1 and status = 0
一定会成功,不会存在安全问题
锁升级:
无锁
偏向锁,无锁情况下A线程进入过一次,下次则不需要再拿锁,当有其他线程竞争的时候才会升级轻量级锁
轻量级锁,自旋锁实现,基于CAS修改锁flag标记,达到一定的自旋次数后break,再升级重量级锁
重量级锁,对性能有影响,把最终还没抢到锁的线程加入到等待队列,objectMonitor去处理
自旋锁:
for(;;){
//condition 自旋到达一定适应的程度会锁升级重量级锁
//还没拿到锁的阻塞然后加入到等待队列 后续来的线程判断标记发现是重量级锁就不需要再走下面的自旋了
//会浪费性能
if(cas){//true
//只会有一个线程成功
break;
}
}
synchronized同步锁:方法级别,同一时间只能由一个线程获得锁。
执行原理:
当线程进入这个sysnchronized方法时,线程可以在lock中读到锁标识,然后确定是否有资格抢占锁,如果没有资格(锁被其他线程持有)再考虑阻塞。
线程A变量对线程B不可见,例如数据库脏读。
代码示列:
static boolean flag = false;
static int num = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
//里面无触发活性的东西 会导致活性失效
while (!flag){
num++;
}
}).start();
Thread.sleep(1000);
System.out.println(num);
flag = true;
}
//输出结果
1255362997
然后惊奇的发现,程序并没有停止呀,可见性问题就此展开
活性失效
简单来说,操作的值没有再触发操作,值还是以前的值。存在缓存
失活数据实则存在缓存,未进行CPU切换与数据重载。这就是缓存一致性问题。
由来:CPU运行速度会比磁盘读写快得多,那么在磁盘读写时,CPU将处于阻塞状态,大大的浪费了资源,于是便有了一系列的优化。
CPU增加3级高速缓存(L1 L2 L3)
操作系统中,增加进程、线程-> 通过CPU时间片切换,提升CPU利用率
编译器优化(JVM的深度优化)
读取数据的顺序:CPU -> L1(区分) -> L2(单个CPU共享) -> L3(整个共享) ->主存。
从上图可以看到,CPU1在修改完flag后同步到主存,但是当CPU2去取flag的时候,因为先从缓存取的缘故,可能还有旧的值,这就是缓存一致性问题。
缓存一致性协议,MESI,分别表示修改(modify)、独占(exclusive)、共享(share)、失效(invalid),那么看下状态会如何触发:
修改:当CPU修改变量的时候,会把状态修改为modify状态
独占:该变量只存在当前的CPU缓存行中
共享:读个CPU都加载了该变量
失效:当缓存行变量发生改变时,发现是共享状态,那么需要通知另一个CPU的该变量修改为失效状态
根据MESI协议,读取的时候只有MES走缓存,失效状态I要直接访问内存。
当修改i = 1时,整个流程如下
* CPU1修改变量i时,状态会修改为M(修改)状态,同时通知CPU2的变量i修改为I(无效)状态
* CPU1修改完成后同步到主存,并将变量i设置为E(独占)状态
* 当CPU2操作变量i时,发现是失效状态,会去内存中重新加载。最终通过总线探测得到CPU1也有加载变量i,就会将CPU1和CPU2中的变量i都会修改为S(共享)状态,否则就是独占状态
多线程情况下步骤讲解:
a = 1
,发现并没有加载a,a在共享状态下(CPU1和CPU2下共享),需要把其他CPU的缓存读取过来并置为失效状态,最终完成后也就到了第二步a=0/E
,此时a在其他CPU处于失效状态,所以在CPU1下是独占状态。b=a+1
了,此时b没有被加载,所以b=0/E
是独占状态,接下来第4步,此时a还在异步等待,b就变成b=0+1 -> b=1/M
修改状态a=1
,但此时b=a+1
已经执行完毕,所以就导致了指令重排序问题怎么解决指令重排序问题?
由于该问题已经无法再CPU层面解决,于是提供内存屏障指令,由开发者根据需求使用。
内存屏障
为什么CPU层面无法实现?因为CPU不知道什么时候允许优化,什么时候不允许优化。
JMM内存屏障模型
JAVA线程去访问内存的一个规范,它是一种抽象模型,解决有序性可见性问题(由关键字实现)
多线程操作同一个变量 -> 可见性问题(失活) -> 增加CPU高速缓存(MESI) -> 导致指令重排序(JMM内存屏障)
使用synchronized volatile finanl关键字加锁保证可见,以解决可见性、有序性问题。
可重入、互斥、用来解决死锁问题
static Lock lock = new ReentrantLock();
lock.lock(); //抢占锁 没有抢占的会阻塞
lock.unlock(); //释放锁
J.U.C Lock和Synchronized使用的区别就在于Lock的加锁和释放锁需要手动操作
AQS流程图说明:
重入:在抢到锁之后且没有其他线程竞争时,下次不需要再次获取锁。
JUC包中用来通过阻塞和唤醒来实现线程的通信,例如:
notify/wait
lock/unlock
signal/await
Lock lock = new ReentrantLock();
//与synchronized不同的是Condition里面可以用多个队列 放不同的线程
Condition addCondition = lock.newCondition();
Condition removeCondition = lock.newCondition();
//源码在前面文章
//阻塞生产者 释放锁 消费者消费时会唤醒生产者
addCondition.await();
//唤醒消费者
removeCondition.signal();
实际最常见的就是阻塞队列了。可以通过不同的需求来唤醒不同的消费者,例如阻塞队列ArrayBlockingQueue。
作用:线程隔离,每个线程都有一个自己的存储空间。
底层数据结构ThreadLocal.ThreadLocalMap
//WeakReference 弱引用 用于释放线程
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
//我们自定义的存储对象
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//核心结构 初始size16的数组 当一个线程不同的ThreadLocal时都存储在这个table里面
private Entry[] table;
寻址和Map中hash寻址一致,但是这里是散列并没有像Map一样有桶的概念,于是引入了线性探索的概念。
线性探索
线性探测是计算机程序解决散列表冲突时所采取的一种策略
为了搜索给定的键 x,散列表中由h(x)对应的单元开始的相邻单元h(x) + 1,h(x) + 2, …, 都将被检查,直到找到了内容为空的单元或是找到了存储给定键为x的单元。其中,h是散列函数。如果找到了存储给定键的单元,搜索将会返回单元中存储的键对应的值。
简单来说就是在插入数组下标X的时候,存在X位置的值和插入的值是一致的,则直接覆盖。当不一致时也就是说存在hash碰撞时,就从相邻的下标X+1、X+2 ...
都将会被检测,直到找到内容为空
或者key一致时
才会在当前下标的空间存入。
弱引用(WeakReference)
ThreadLocalMap里面的Entry对象,它的key指向的就是线程,每当线程执行完成,key就没有引用了,而 Object value = v是强引用
指向我们的存储对象实例,也就是没有remove时会有一种情况:
线程执行完毕了,线程声明周期结束,线程就会被GC回收,那么Entry中的key引用会为null,但是value此时还是存在的
按照代码分析,每次进行set、get、remove都会去做Entry脏数据的移除,那为什么不remove还是会出现内存泄漏问题?
经过多方资料查询,如果一个线程执行链路长且在最后没有终止的过程中,value也没有继续使用了,那么value 的强引用就一直还在,就会有可能导致内存泄漏,所以使用完最好都调用remove方法防止内存泄漏
池化技术
初始化时创建一系列的线程,让线程不结束,通过阻塞队列来实现,唤醒某个线程则执行run方法,同时支持消费者动态扩容,如果扩容也执行不过来,还具有拒绝策略:
参数分析
public ThreadPoolExecutor(int corePoolSize, //核心线程数
int maximumPoolSize, //最大线程数
long keepAliveTime, //工作线程(非核心线程)存活时间
TimeUnit unit, //单位
BlockingQueue<Runnable> workQueue, //存储任务的阻塞队列
ThreadFactory threadFactory, //初始化线程工厂
RejectedExecutionHandler handler) {} //拒绝策略
线程池类型:
核心线程数 = 最大线程数
阻塞队列用的是LinkedBlockingQueue
拒绝策略,使用默认的AbortPolicy报错
就一个线程
阻塞队列用的是LinkedBlockingQueue
拒绝策略,使用默认的AbortPolicy报错
来多少执行多少的线程池
SynchronousQueue队列,无结构不做存储的阻塞队列,没有消费者时生产者将阻塞
拒绝策略默认的AbortPolicy报错
由ScheduledThreadPoolExecutor实现,最大 Integer.MAX_VALUE
阻塞队列DelayedWorkQueue延时队列
原理总结
执行一个任务时,如果工作线程小于核心线程,那么直接初始化一个核心线程(Worker),存储在成员变量HashSet workers里面,如果添加成功则会启动Worker中的线程。
如果工作线程大于核心线程,判断线程池的状态后并使用offer()(添加成功返回true,反之false) 添加到阻塞队列中,并检查线程池运行状态,如果不在运行中则直接移除该任务并调用拒绝策略,如果正常但是工作线程为0时则新建一个无任务的工作线程。
如果**offer()**添加失败(阻塞队列已满)则直接添加工作线程,如果线程池有效线程数小于maximumPoolSize则直接新增工作线程执行,如果线程池已满则调用拒绝策略。
worker实现了线程Rannable同时继承了AQS,在run方法中循环去阻塞队列中通过getTask()获取所需要执行的任务,获取到任务后直接run方法执行,这里使用的是AQS中的互斥锁。
getTask()获取任务,是根据是否设置存活时间或者当前工作线程数量大于核心线程数来决定阻塞还是带超时时间的阻塞,如果是超时阻塞就说明阻塞队列为null了,此时该工作线程就可以销毁了。
如果设置了KeepAliveTime,核心线程也会被销毁(设置了allowCoreThreadTimeOut为true),但如果队列不为空但是线程数为0时,则重新在新建一个worker。
allowCoreThreadTimeOut
如果为false(默认值),核心线程即使在空闲时也会保持活动状态。如果为true,核心线程将使用keepAliveTime来超时等待工作。
代理模式是Java常用的设计模式,他的特征是代理类与委托类有同样的接口,其他类想访问这个类必须先通过代理类。常用的动态代理生成类有两种方式:
简单的用一个类加方法来生成动态代理类,通过CGLIB生成代理
//新建一个类 里面就一个方法
public class Person{
public void sayHello() {
System.out.println("hello 我是Girl");
}
}
//生成的动态代理类源码
//我们实际拿到的是这个代理对象
public final class $Proxy extends Proxy implements Person {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
//构造类 通过newInstance(h) 直接将InvocationHandler传参直接到父类
public $Proxy(InvocationHandler var1) throws UndeclaredThrowableException {
super(var1);
}
//重点看这里
public final void sayHello() {
try {
//调用父类的h 然后通过invoke到对应的方法
super.h.invoke(this, m3, (Object[])null);
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
......省略后续代码,源码在前面文章
应该可以说动态代理是后续很多框架起到至关重要的作用,一定要了解清楚。
接下来开始整合框架篇了。
上一篇:随手记录第七话 – Charles的使用大全(https、地址替换、重写参数、断点))
下一篇:随手记录第九话 – Java框架整合篇
读书破万卷,下笔如有神