随手记录第八话 -- Java基础整合篇

本文为整合篇,用于复习用,如需详细的,可翻看以前文章。

1.安装

1.1下载安装

java官网下载地址上找到对应的版本下载,本文以mac、jdk17为例。
随手记录第八话 -- Java基础整合篇_第1张图片
自行选择是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)

1.2 目录结构介绍

Java安装好后会包含JDK和JRE两个目录。

  • JDK(Java Development Kit Java开发工具包),JDK是提供给java开发人员使用的,其中包含了java的开发工具,也包括了jre,其中的开发工具:编译工具(javac.exe) 打包工具(java.exe)等等
  • jre(Java Runtime Environment Java运行环境),包括Java虚拟机(JVM Java Virtual Machine)和Java程序所需要的核心库类等,如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。JVM(Java Virtual Machine) 是Java虚拟机,Java程序需要运行在虚拟机上,不同的平台有自己的虚拟机,所以Java语言是跨平台语言。

1.3 第一个java程序

正常流程创建一个test.java文件,然后在里面输入包名、类名和main方法,然后再javac编译得到.class文件,最后java test即可执行main方法。

由于基本是项目式开发,这里直接上工具了(IDEA):
随手记录第八话 -- Java基础整合篇_第2张图片
到这里可以说会写程序了!哈哈

2.Java关键字介绍

关键字 含义
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是Java提供的一种轻量级的同步机制。同synchronized相比(synchronized通常称为重量级锁),volatile更轻量级。
    • 一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

    • 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

    • 2)禁止进行指令重排序。

  • Atomic原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

3.Java数据类型

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方法,默认比较的是内存地址,该方法可以覆盖重新,自定义规则来比较两个对象是否相等

4.Java数组

与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)));

5.Java集合–Map接口

Map:一套存储key/value数据的结构。
随手记录第八话 -- Java基础整合篇_第3张图片

5.1 HashMap(最常用)

基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
随手记录第八话 -- Java基础整合篇_第4张图片
底层数据结构:

//底层实现 在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];

hash的计算方式
随手记录第八话 -- Java基础整合篇_第5张图片

扩容高低位:在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碰撞,避免形成链表的结构,使得查询效率降低!

5.2 TreeMap

有序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)

5.3 HashTable

底层和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关键字

6.Java集合–Collection接口

  • Collection:java提供了一组对数组、链表数据结构操作的API,这组API即集合;存在于java.util
  • Collection接口的依赖图
    随手记录第八话 -- Java基础整合篇_第6张图片

6.1 ArrayList

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);

6.2 LinkedList

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)

6.3 HashSet

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);

6.4 TreeSet

基于 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;
    }

7.Java面向对象

7.1 Java的三大特性

继承:子类通过继承父类可以调用父类的方法,也可以覆盖重写父类的方法,但是不能继承父类的构造方法。
封装:通过private把对象的方法或者成员变量隐藏起来,提供一个公共的入口来进行访问。
多态:未知的具体类型,在运行阶段才知道的具体实例对象,多态代码灵活多变,需要满足继承、重写或者实现父类对象的方法。例如抽象类调用某个方法,由某个子类继承后重写实现调用。

7.2 类、接口、抽象类

类:一个类型的总称,里面可以定义成员变量和方法,可以继承其他类或者实现接口
接口:预先定义好方法的名称和形参,由实现类去实现方法。在1.8接口可以添加默认方法
抽象类:类和接口的结合体,可以单独的定义接口由子类继承实现。

8.Java线程

一个服务一个进程,一个进程内可以有n个线程。
线程是操作系统能够进行调度的最小单位,硬件CPU核心数就对应可同时执行的线程数。
多线程的特点:异步,并行。

8.1 线程的三种使用方法

//继承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方法中

8.2 线程停止或者中断

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告诉开发者我现在无法自己处理中断,并且将中断标记复位,需要开发者自己中断线程。因为在某些由于异常原因或者阻塞或者死循环等的线程中断后可能需要做一些其他的记录。相应于给了外界一个改变线程状态的入口。

9 Java并发锁

我们先来看一段代码,正常来说结果要是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

这就是多个多线程并行去做同一件事,导致出来的结果会和预想中的有偏差,所以才引入了锁。锁是用来保证数据安全的。

9.1 锁认识

锁的本质:读写锁、排它锁、互斥锁。
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去处理

锁升级原理图
随手记录第八话 -- Java基础整合篇_第7张图片

自旋锁:

for(;;){
    //condition 自旋到达一定适应的程度会锁升级重量级锁 
    //还没拿到锁的阻塞然后加入到等待队列 后续来的线程判断标记发现是重量级锁就不需要再走下面的自旋了
    //会浪费性能
    if(cas){//true
        //只会有一个线程成功
        break;
    }
}

9.2 同步锁 – synchronized

synchronized同步锁:方法级别,同一时间只能由一个线程获得锁。

执行原理:
随手记录第八话 -- Java基础整合篇_第8张图片
当线程进入这个sysnchronized方法时,线程可以在lock中读到锁标识,然后确定是否有资格抢占锁,如果没有资格(锁被其他线程持有)再考虑阻塞。

9.3 线程可见性问题

线程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

然后惊奇的发现,程序并没有停止呀,可见性问题就此展开

活性失效
简单来说,操作的值没有再触发操作,值还是以前的值。存在缓存

9.4 缓存一致性问题

失活数据实则存在缓存,未进行CPU切换与数据重载。这就是缓存一致性问题。
由来:CPU运行速度会比磁盘读写快得多,那么在磁盘读写时,CPU将处于阻塞状态,大大的浪费了资源,于是便有了一系列的优化。

  • CPU资源利用问题

CPU增加3级高速缓存(L1 L2 L3)
操作系统中,增加进程、线程-> 通过CPU时间片切换,提升CPU利用率
编译器优化(JVM的深度优化)

读取数据的顺序:CPU -> L1(区分) -> L2(单个CPU共享) -> L3(整个共享) ->主存。
随手记录第八话 -- Java基础整合篇_第9张图片
从上图可以看到,CPU1在修改完flag后同步到主存,但是当CPU2去取flag的时候,因为先从缓存取的缘故,可能还有旧的值,这就是缓存一致性问题。

9.5 缓存一致性问题解决方案

缓存一致性协议,MESI,分别表示修改(modify)、独占(exclusive)、共享(share)、失效(invalid),那么看下状态会如何触发:

修改:当CPU修改变量的时候,会把状态修改为modify状态
独占:该变量只存在当前的CPU缓存行中
共享:读个CPU都加载了该变量
失效:当缓存行变量发生改变时,发现是共享状态,那么需要通知另一个CPU的该变量修改为失效状态

根据MESI协议,读取的时候只有MES走缓存,失效状态I要直接访问内存。

随手记录第八话 -- Java基础整合篇_第10张图片
当修改i = 1时,整个流程如下
* CPU1修改变量i时,状态会修改为M(修改)状态,同时通知CPU2的变量i修改为I(无效)状态
* CPU1修改完成后同步到主存,并将变量i设置为E(独占)状态
* 当CPU2操作变量i时,发现是失效状态,会去内存中重新加载。最终通过总线探测得到CPU1也有加载变量i,就会将CPU1和CPU2中的变量i都会修改为S(共享)状态,否则就是独占状态

9.6 线程有序性和内存屏障

1.CPU指令重排序,代码在前面文章有。
随手记录第八话 -- Java基础整合篇_第11张图片

多线程情况下步骤讲解:

  1. CPU0执行a = 1,发现并没有加载a,a在共享状态下(CPU1和CPU2下共享),需要把其他CPU的缓存读取过来并置为失效状态,最终完成后也就到了第二步a=0/E,此时a在其他CPU处于失效状态,所以在CPU1下是独占状态。
  2. 由于是store buffer同步到cache是必须要等待到其他CPU都同步完成才会继续,可能存在的情况是先执行到b=a+1了,此时b没有被加载,所以b=0/E是独占状态,接下来第4步,此时a还在异步等待,b就变成b=0+1 -> b=1/M修改状态
  3. 最后第5步a终于执行完毕再设置a=1,但此时b=a+1已经执行完毕,所以就导致了指令重排序问题

怎么解决指令重排序问题?
由于该问题已经无法再CPU层面解决,于是提供内存屏障指令,由开发者根据需求使用。

内存屏障
为什么CPU层面无法实现?因为CPU不知道什么时候允许优化,什么时候不允许优化。

  • 读屏障(lfence) load 读操作必须在写操作之前完成
  • 写屏障(sfence)
  • 全屏障(mfence)

JMM内存屏障模型
JAVA线程去访问内存的一个规范,它是一种抽象模型,解决有序性可见性问题(由关键字实现)
多线程操作同一个变量 -> 可见性问题(失活) -> 增加CPU高速缓存(MESI) -> 导致指令重排序(JMM内存屏障)
使用synchronized volatile finanl关键字加锁保证可见,以解决可见性、有序性问题。

9.7 JUC下ReentrantLock可重入锁

可重入、互斥、用来解决死锁问题

static Lock  lock = new ReentrantLock();
lock.lock();	//抢占锁 没有抢占的会阻塞
lock.unlock();	//释放锁

J.U.C Lock和Synchronized使用的区别就在于Lock的加锁和释放锁需要手动操作

AQS流程图说明:
随手记录第八话 -- Java基础整合篇_第12张图片
重入:在抢到锁之后且没有其他线程竞争时,下次不需要再次获取锁。

9.8 线程通信之Condition

JUC包中用来通过阻塞和唤醒来实现线程的通信,例如:

  • synchronized的notify/wait
  • ReentrantLock的lock/unlock
  • Condition的signal/await
Lock lock = new ReentrantLock();
//与synchronized不同的是Condition里面可以用多个队列 放不同的线程
Condition addCondition = lock.newCondition();
Condition removeCondition = lock.newCondition();

//源码在前面文章
//阻塞生产者 释放锁 消费者消费时会唤醒生产者
                addCondition.await();
//唤醒消费者
            removeCondition.signal();

实际最常见的就是阻塞队列了。可以通过不同的需求来唤醒不同的消费者,例如阻塞队列ArrayBlockingQueue。

10 Java线程

10.1 本地线程ThreadLocal

作用:线程隔离,每个线程都有一个自己的存储空间。

底层数据结构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方法防止内存泄漏

10.2 线程池原理

池化技术
初始化时创建一系列的线程,让线程不结束,通过阻塞队列来实现,唤醒某个线程则执行run方法,同时支持消费者动态扩容,如果扩容也执行不过来,还具有拒绝策略:

  • 报错,直接往上抛出异常。AbortPolicy:丢弃任务并抛出RejectedExecutionException,这是线程池的默认拒绝策略
  • 直接丢弃 DiscardPolicy
  • 直接调用run方法(newCachedThreadPool用) CallerRunsPolicy
  • 移除头部任务(等待最久的任务),然后把新的任务加入 DiscardOldestPolicy
  • 存起来不被回收,需要自定义实现

参数分析

public ThreadPoolExecutor(int corePoolSize,		//核心线程数
                          int maximumPoolSize,	//最大线程数
                          long keepAliveTime,	//工作线程(非核心线程)存活时间
                          TimeUnit unit,		//单位
                          BlockingQueue<Runnable> workQueue,	//存储任务的阻塞队列
                          ThreadFactory threadFactory,		//初始化线程工厂
                          RejectedExecutionHandler handler) {}	//拒绝策略
  • 线程存活时间概定,run方法执行完毕即结束。工作线程回收方式,从队列中阻塞取任务超时后即结束,可以被回收

线程池类型:

  • 固定线程池newFixedThreadPool

核心线程数 = 最大线程数
阻塞队列用的是LinkedBlockingQueue
拒绝策略,使用默认的AbortPolicy报错

  • 单个线程池newSingleThreadExecutor

就一个线程
阻塞队列用的是LinkedBlockingQueue
拒绝策略,使用默认的AbortPolicy报错

  • 缓存线程池 newCachedThreadPool

来多少执行多少的线程池
SynchronousQueue队列,无结构不做存储的阻塞队列,没有消费者时生产者将阻塞
拒绝策略默认的AbortPolicy报错

  • 定时线程池 newScheduledThreadPool

由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来超时等待工作。

11 Java动态代理

代理模式是Java常用的设计模式,他的特征是代理类与委托类有同样的接口,其他类想访问这个类必须先通过代理类。常用的动态代理生成类有两种方式:

  • JDK代理是通过实现接口创建代理类,在创建的同时保存了原始类,在通过代理类调用同样的方法时,先经过代理对象invoke逻辑,然后在代理对象里面通过原始类调用对应的方法
  • CGLIB代理时通过字节码重置,经过它生成的对象,内部的代码已经发生改变

简单的用一个类加方法来生成动态代理类,通过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框架整合篇

读书破万卷,下笔如有神

你可能感兴趣的:(java,jvm,开发语言)