Skip to content

1. 变量与运算符

2. 流程控制符

3. 数据类型(集合+数组)

3.1 Array

3.2 Collection

1) List(动态数组)

ArrayList

特点:数据不唯一、有序; 优点:随机索引查询速度快 缺点:随机插入删除数据效率低

ArrayList有两个重要的属性:Object数组(elementData)、size;ArrayList实例化的时候如果没有设置初始大小的时候,Object数组会赋值一个空数组,只有执行put的时候才会初始化一个长度为10的数组;当size大于elementData的长度,List会进行扩容至原来的1.5倍。

底层实现

java
  private static final int DEFAULT_CAPACITY = 10;

    /**
  共享空数组实例,用于空实例。
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
    共享空数组实例,用于默认大小的空实例。我们将其与 EMPTY_ELEMENTDATA 区分开来,以便知道当  添加第一个元素时要膨胀多少。
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
存储 ArrayList 元素的数组缓冲区。ArrayList 的容量就是这个数组缓冲区的长度。任何元素数据 == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的空 ArrayList 都会在添加第一个元素时扩展为 DEFAULT_CAPACITY。
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size; // Array的长度

常用方法

增加:add(E e) addAll(Collection<? extends E> c)

删除:clear() remove(Object o) removeAll(Collection<?> c)

修改:stream() 返回以此集合为源的顺序

查看:iterator() 遍历 size()

判断:contains(Object o) equals(Object o) isEmpty()

遍历方式:增强for循环、迭代器

存储有序的,可重复的数据

image-20230930190308935

java
/*
        **Collection常用方法**

        增加:`add(E e)` `addAll(Collection<?  extends E> c)`
        删除:`clear()` `remove(Object o)` `removeAll(Collection<?> c)`
        修改:`stream()` 返回以此集合为源的顺序
        查看:`iterator()` 遍历 `size()`
        判断:`contains(Object o)` `equals(Object o)` `isEmpty()`
         */
        //1 创建对象:接口不能创建对象,利用实现类创建对象
        Collection col = new ArrayList();
        //2 调用方法:
        //集合只能存放引用数据类型的数据,不能放基本数据类型
        //基本数据类型自动装箱,对应包装,int-->Integer
        col.add(18);
        col.add(12);
        col.add(10);
        col.add(90);

        System.out.println(col.toString());

        List list = Arrays.asList(new Integer[]{1, 2, 3, 4, 5});
        col.addAll(list);//将另一个集合添加入 col 中
        System.out.println(col);

//        col.clear();//集合清空
        System.out.println(col);
        System.out.println("集合中元素的数量:"+col.size());
        System.out.println("集合是否为空:"+col.isEmpty());

        boolean isRemove = col.remove(1);
        System.out.println(col);
        System.out.println("集合中的数据是否删除成功:"+isRemove);


        Collection col2 = new ArrayList();
        col2.add(18);
        col2.add(12);
        col2.add(10);
        col2.add(90);

        Collection col3 = new ArrayList();
        col3.add(18);
        col3.add(12);
        col3.add(10);
        col3.add(90);

        System.out.println("两集合是否一样:"+col2.equals(col3));
        System.out.println(col2==col3);//false,地址不一样

        System.out.println("是否包含元素:"+col3.contains(12));

遍历代码

java
//遍历方法
Collection col = new ArrayList();
col.add(18);
col.add(12);
col.add(10);
col.add(90);
col.add("sad");
col.add(3.4);
//对集合遍历(对集合中元素进行查看)
//方式1:普通for循环,不能完成
/*for(int i=0; i<col.size(); i++){
    col
}*/

//方式2:增强for循环
for(Object o:col){
    System.out.println(o);
}
System.out.println("-----------------");
//方式3:iterator()方法
//Iterator:迭代器
/*
通过hasNext()来判断是否有下一个元素,如果有下一个元素,则返回true,否则返回false;
next()方法将元素获取到并且将“指针”下移
 */
Iterator it = col.iterator();
while(it.hasNext()){
    System.out.println(it.next());
}

LinkedList

优点:随机插入删除效率比数组高 缺点:随机索引查询效率比数组低

  • LinkedList是双向链表实现的List、非线程安全、其元素允许null及重复元素
  • LinkedList基于链表实现,所以不存在容量不足的情况,也就没有扩容的方法

底层实现

java
    transient int size = 0;

    /**
     * Pointer to first node.
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     */
    transient Node<E> last;
   public boolean add(E e) {
        linkLast(e);
        return true;
    }

   private void linkFirst(E e) {
        final Node<E> f = first;
        final Node<E> newNode = new Node<>(null, e, f);
        first = newNode;
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;
        size++;
        modCount++;
    }

    /**
     * Links e as last element.
     */
    void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

Node结构

java
    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;
        }
    }

Vetor

Stack

Stack只有入栈和出栈的操作:

  • 把元素压栈:push(E)
  • 把栈顶的元素“弹出”:pop()
  • 取栈顶元素但不弹出:peek()

在Java中,我们用Deque可以实现Stack的功能:

  • 把元素压栈:push(E)/addFirst(E)
  • 把栈顶的元素“弹出”:pop()/removeFirst()
  • 取栈顶元素但不弹出:peek()/peekFirst()

为什么Java的集合类没有单独的Stack接口呢?因为有个遗留类名字就叫Stack,出于兼容性考虑,所以没办法创建Stack接口,只能用Deque接口来“模拟”一个Stack了。

当我们把Deque作为Stack使用时,注意只调用push()/pop()/peek()方法,不要调用addFirst()/removeFirst()/peekFirst()方法,这样代码更加清晰。

java
package array;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Stack;

public class MyStack {

    public static void main(String[] args) {
        Stack<Integer> stack = new Stack<>();
//        stack.
        Deque<Integer> deque = new ArrayDeque<>();
        deque.addFirst(1);
        deque.addFirst(2);
        deque.addFirst(3);
        deque.addFirst(4);


        Integer k = deque.peekFirst();
        System.out.println("栈顶元素" + k);

        deque.removeFirst();
        System.out.println("删除后,栈顶元素"+deque.peekFirst());

    }
}

2) Set

存储无需的,不可重复的数据

HashSet

HashSet是由数组+链表构成(HashMap);数据唯一性判定先通过hashCode()判断,再通过equals校验;使用hashCode的原因是为了提高比较检索速度。

底层实现

java
    private transient HashMap<E,Object> map;

    // Dummy value to associate with an Object in the backing Map
// 与后备映射中的对象相关联的虚拟值
    private static final Object PRESENT = new Object();

    public HashSet() {
        map = new HashMap<>();
    }
// 天猫价元素
    public boolean add(E e) {
            return map.put(e, PRESENT)==null;
     }

使用代码

java
HashSet<Integer> hashSet=new HashSet<>();

hashSet.add(1);
hashSet.add(2);
hashSet.add(3);
hashSet.add(1);
System.out.println(hashSet.toString());
System.out.println(hashSet.size());

LinkedHashSet

元素具有唯一性、有序性(按插入顺序) LinkedHashSet是HashSet的子类,通过数组+链表的方式保证元素的唯一性,再次基础上加上一个双向链表来确保元素按照插入顺序进行排序(LinkedHashMap);因为有一个双端队列,所以在插入元素方面略低于HashSet,但是在迭代访问的时候不受影响。

源码

java
public class LinkedHashSet<E>
    extends HashSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
     public LinkedHashSet() {
    super(16, .75f, true);
}
}

使用

java
LinkedHashSet<Integer> linkedHashSet=new LinkedHashSet<>();
linkedHashSet.add(2);
linkedHashSet.add(1);
System.out.println(linkedHashSet.toString());

TreeSet

特点:唯一,无序(没有按照输入的顺序进行输出),有序(按照升序进行遍历)。

java
package array;

import java.util.Iterator;
import java.util.TreeSet;

public class MyTreeSet {

    public static void main(String[] args) {
        TreeSet<Integer> k = new TreeSet<>();


        k.add(12);
        k.add(15);
        k.add(34);
        k.add(154);
        k.add(1);
        k.add(12);
        k.add(32);

        System.out.println(k);
        Iterator it = k.iterator();
        
        while (it.hasNext()) {
            System.out.println(it.next());
        }
    }
}

换一种属性

java
TreeSet<String> treeSet=new TreeSet<>();

treeSet.add("asda");
treeSet.add("dfs");
treeSet.add("ret");
treeSet.add("be324");
treeSet.add("Asda");
treeSet.add("123");

结果

txt
[123, Asda, asda, be324, dfs, ret]
123
Asda
asda
be324
dfs
ret

还可以实现匿名内部类

java
package array;


import java.util.Collections;
import java.util.Comparator;
import java.util.Objects;
import java.util.TreeSet;

class Student {

    String name;

    Integer age;

    public Student(String name, Integer age) {
        this.age = age;
        this.name = name;
    }

//    @Override
//    public boolean equals(Object obj) {
////        return super.equals(obj);
//
//        Student others = (Student) obj;
//        return this.age == others.age && Objects.equals(this.name, others.name);
//
//    }

    @Override
    public String toString() {
//        return super.toString();
        return "名称为: " + this.name + " 年龄为: " + this.age;
    }
}

public class MyTreeSet {


    public static void main(String[] args) {


        TreeSet<Student> treeSet = new TreeSet<>(new Comparator<Student>() {

            @Override
            public int compare(Student o1, Student o2) {

                if (o1.age != o2.age) {
                    return o1.age - o2.age;
                } else {
                    return o1.name.compareTo(o2.name);
                }
            }
        });


        treeSet.add(new Student("小花", 12));
        treeSet.add(new Student("小花", 12));
        treeSet.add(new Student("abc", 12));
        treeSet.add(new Student("ABC", 12));
        treeSet.add(new Student("efg", 12));
        treeSet.add(new Student("大黑", 24));
        treeSet.add(new Student("白熊", 52));
        treeSet.add(new Student("小明", 24));

//        System.out.println();
        for (Student s : treeSet) {
            System.out.println(s);
        }

//        treeSet.


    }
}

3) Queue

Queue分为三大类:继承Queue接口的BlockingQueue(阻塞队列)、Deque(双端队列),抽象实现Queue的AbstractQueue(非阻塞队列) 特点:先进先出(FIFO)

在这里插入图片描述

图中我们能看到最上层是Collection 接口,Queue满足了集合类的所有类的方法,都是非阻塞式的

txt
add(E e):增加元素,容量受限队列没有空间使用add会抛出IllegalStateException异常
offer(E e):增加元素,容量受限队列没有空间使用offer不会抛出异常
remove(Object o):删除元素,如果队列为空,会引起异常
poll():检索并删除队列头,如果队列为空,则返回null
element():检索但不删除队列头,如果队列为空,会引起异常
peek():检索但不删除队列头,如果队列为空,则返回null
clear():清除集合中所有元素;
size():集合元素的大小;
isEmpty():集合是否没有元素;
contains(Object o):集合是否包含元素o。

阻塞队列

方法异常抛出方法返回特殊值阻塞超时退出
插入方法add(e),队列满,抛出IIIegaISlabEepeplian异常offer(e),队列满,返回falseput(e),队列满一直阻塞offer(e,time,unit)
移除方法remove(),队列空,抛出IIIegaISlabEepeplian异常poll(),队列空,返回falsetake(),队列空一直阻塞poll(time,unit)
检查方法element(),队列空,抛出IIIegaISlabEepeplian异常peek(),队列空,返回false

阻塞

  • 阻塞队列满时,如果生产者线程往队列中put元素,队列会一直阻塞生产者线程,直到队列有空位,让元素插入到队列中或者响应中断停止
  • 阻塞队列空时,如果消费者线程从队列中take元素,队列会阻塞线程,直到拿到队列中的元素 超时退出
  • 阻塞队列满时,offer(e,time,unit)插入元素,队列会阻塞生产者线程一段时间,超过设置的时间,生产线程就会退出
  • 阻塞队列空时,poll(time,unit)移除元素,队列会阻塞消费者线程一段时间,超过设置的时间,消费者线程就会退出

阻塞队列成员:

队列有界性数据结构
ArrayBlockingQueuebounded(有界)加锁数组
LinkedBlockingQueueoptionally-bounded(默认Integer.MAX_VALUE,最好重新设置值)加锁链表
PriorityBlockingQueueunbounded加锁
DelayQueueunbounded加锁
SynchronousQueueunbounded加锁
LinkedTransferQueueunbounded加锁
LinkedBlockingDequeunbounded无锁

非阻塞队列

非阻塞队列:PriorityQueueConcurrentLinkedQueue

  • PriorityQueue 类实质上维护了一个有序列表。加入到 Queue 中的元素根据它们的天然排序(通过其 java.util.Comparable 实现)或者根据传递给构造函数的 java.util.Comparator 实现来定位。
  • ConcurrentLinkedQueue 是基于链接节点的、线程安全的队列。并发访问不需要同步。因为它在队列的尾部添加元素并从头部删除它们,所以只要不需要知道队列的大 小,ConcurrentLinkedQueue 对公共集合的共享访问就可以工作得很好。收集关于队列大小的信息会很慢,需要遍历队列。入队和出队操作均利用CAS(compare and set)更新,这样允许多个线程并发执行,并且不会因为加锁而阻塞线程,使得并发性能更好。

优先队列

PriorityQueueQueue的区别在于,它的出队顺序与元素的优先级有关,对PriorityQueue调用remove()poll()方法,返回的总是优先级最高的元素。

要使用PriorityQueue,我们就必须给每个元素定义“优先级”。

放入PriorityQueue的元素,必须实现Comparable接口,PriorityQueue会根据元素的排序顺序决定出队的优先级。

java
package array;

import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;

public class MyPriorityQueue {
    public static void main(String[] args) {
        Queue<User> q = new PriorityQueue<>(new UserComparator());
        // 添加3个元素到队列:
        q.offer(new User("Bob", "A1"));
        q.offer(new User("Alice", "A3"));
        q.offer(new User("Alice", "A2"));
        q.offer(new User("Alice", "A112"));
        q.offer(new User("Boss", "V1"));
        q.offer(new User("Boss", "A10"));
        q.offer(new User("Boss", "A999"));
        q.offer(new User("Boss", "A88"));
        System.out.println(q.poll()); // Boss/V1
        System.out.println(q.poll()); // Bob/A1
        System.out.println(q.poll()); // Alice/A2
        System.out.println(q.poll()); // Alice/A2
        System.out.println(q.poll()); // Alice/A2
        System.out.println(q.poll()); // Alice/A2
        System.out.println(q.poll()); // Alice/A2
        System.out.println(q.poll()); // Alice/A2
        System.out.println(q.poll()); // null,因为队列为空
    }
}

//Boss/V1
//Bob/A1
//Alice/A2
//Alice/A3
//Boss/A10
//Boss/A88
//Alice/A112
//Boss/A999
//null


class UserComparator implements Comparator<User> {
    public int compare(User u1, User u2) {
        // 如果两人的号都是A开头或者都是V开头,比较号的大小:
        if (u1.number.charAt(0) == u2.number.charAt(0)) {
            // 相同的字母开头,截取后面的值
            int v1 = u1.number.length();
            int v2 = u2.number.length();
            String n1 = u1.number.substring(1, v1);
            String n2 = u2.number.substring(1, v2);
            // 负值,n1排前面
            return Integer.parseInt(n1) - Integer.parseInt(n2);
//            return n1.compareTo(n2);
        }
        if (u1.number.charAt(0) == 'V') {
            // u1的号码是V开头,优先级高:
            return -1;
        } else {
            return 1;
        }
    }
}

class User {
    public final String name;
    public final String number;

    public User(String name, String number) {
        this.name = name;
        this.number = number;
    }

    public String toString() {
        return name + "/" + number;
    }
}

双端队列

Java集合提供了接口Deque来实现一个双端队列,它的功能是:

  • 既可以添加到队尾,也可以添加到队首;
  • 既可以从队首获取,又可以从队尾获取。

我们来比较一下QueueDeque出队和入队的方法:

3.3 Map

HashMap

HashMap底层是数组+链表;

在这里插入图片描述

java
    /**
    * Constructs an empty <tt>HashMap</tt> with the specified initial
    * capacity and load factor.
    *
    * @param  initialCapacity the initial capacity
    * @param  loadFactor      the load factor
    * @throws IllegalArgumentException if the initial capacity is negative
    *         or the load factor is nonpositive
    */
   public HashMap(int initialCapacity, float loadFactor) {
       if (initialCapacity < 0)
           throw new IllegalArgumentException("Illegal initial capacity: " +
                                              initialCapacity);
       if (initialCapacity > MAXIMUM_CAPACITY)
           initialCapacity = MAXIMUM_CAPACITY;
       if (loadFactor <= 0 || Float.isNaN(loadFactor))
           throw new IllegalArgumentException("Illegal load factor: " +
                                              loadFactor);
       this.loadFactor = loadFactor;
       this.threshold = tableSizeFor(initialCapacity);
   }

   /**
    * Constructs an empty <tt>HashMap</tt> with the specified initial
    * capacity and the default load factor (0.75).
    *
    * @param  initialCapacity the initial capacity.
    * @throws IllegalArgumentException if the initial capacity is negative.
    */
   public HashMap(int initialCapacity) {
       this(initialCapacity, DEFAULT_LOAD_FACTOR);
   }
    /**
    * Constructs an empty <tt>HashMap</tt> with the default initial capacity
    * (16) and the default load factor (0.75).
    */
   public HashMap() {
       this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
   }

添加的源码

java
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    // hash 是根据key生成的value
        Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 如果 transient Node<K,V>[] table 数组为空,则进行初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
   
    
    // 如果要存储的位置为空,则进行可以进行填充 
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            // 如果不为空,需要进行链表操作
            Node<K,V> e; K k;
            // 如果二者的哈希是一致的,并且二者的key是一致的,就进行 重新赋值操作
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 如果是链表的话
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            // 如果不是链表的话
            else {
                // 进行链表的链接
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // 如果数目超过 static final int TREEIFY_THRESHOLD = 8;
                        // 则进行链表转红黑树。防止极端情况
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 如果链表上的key和目标的key一致,则进行添加
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
    
        ++modCount;
    // 如果数量多余 阈值,则进行扩容操作
    // 随着不断添加元素,在满足一下的条件的情况下,会考虑扩容
    // threshold为数组长度* 加载因子
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

HashMap扩容为什么总是2的次幂?

HashMap的扩容公式:initailCapacity * loadFactor = HashMap

其中initailCapacity是初始容量:默认值为16(懒加载机制,只有当第一次put的时候才创建)

java
/**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
 * The load factor used when none specified in constructor.
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

HashMap扩容主要是给数组扩容的,因为数组长度不可变,而链表是可变长度的。从HashMap的源码中可以看到HashMap在扩容时选择了位运算,向集合中添加元素时,会使用(n - 1) & hash的计算方法来得出该元素在集合中的位置。只有当对应位置的数据都为1时,运算结果也为1,当HashMap的容量是2的n次幂时,(n-1)的2进制也就是1111111***111这样形式的,这样与添加元素的hash值进行位运算时,能够充分的散列,使得添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞,下面举例进行说明。 在这里插入图片描述

当HashMap的容量是16时,它的二进制是10000,(n-1)的二进制是01111,与hash值得计算结果如下:

在这里插入图片描述 上面四种情况我们可以看出,不同的hash值,和(n-1)进行位运算后,能够得出不同的值,使得添加的元素能够均匀分布在集合中不同的位置上,避免hash碰撞。

下面就来看一下HashMap的容量不是2的n次幂的情况,当容量为10时,二进制为01010,(n-1)的二进制是01001,向里面添加同样的元素,结果为:

在这里插入图片描述

可以看出,有三个不同的元素经过&运算得出了同样的结果,严重的hash碰撞了。导致某一个链表的长度特别长,影响查询的效率。

最终答案

终上所述,HashMap计算添加元素的位置时,使用的位运算,这是特别高效的运算;另外,HashMap的初始容量是2的n次幂,扩容也是2倍的形式进行扩容,是因为容量是2的n次幂,可以使得添加的元素均匀分布在HashMap中的数组上,减少hash碰撞,避免形成链表的结构,使得查询效率降低

有个问题:为啥不使用取模呢?因为取模运算速度比较低。

LinkedHashMap

HashTable

它与HashMap非常相似,但Hashtable是线程安全的,而HashMap不是线程安全的。Hashtable也可以存储键值对,并可以通过键快速查找到对应的值,Hashtable的键和值也都可以是任何类型的对象。

因为Hashtable是线程安全的,因此适用于多线程环境。在多线程环境中,如果需要对Hashtable进行多个操作,需要使用synchronized关键字来保证线程安全。但需要我们注意的是,在多线程环境中使用Hashtable可能会影响性能,所以如果不需要保证线程安全,请尽量使用HashMap。

java
package collection;

import java.util.Hashtable;

public class MyHashTable {

    public static void main(String[] args) {

        Hashtable<String, Integer> hashtable = new Hashtable<>();

        hashtable.put("sd", 213);
        hashtable.put("abc", 333);

        for (String keys : hashtable.keySet()) {
            System.out.println(hashtable.get(keys));
        }
    }
}

ConCurrentHashMap

主要在多线程中用,解决HashMap可能会导致死锁问题,ConCurrentHashMap既可以保持同步也可以提高并发效率

ConCurrentHashMap关于扩容需要知道两个重要的参数:

  • MIN_TREEIFY_CAPACITY : 最小树形化容量阈值,默认为64
  • TREEIFY_THRESHOLD : 树化阈值,指定桶位链表长度达到8的话,就可能发生树化操作 当元素个数大于最小树形化容量阈值并且链表长度大于8,才会调用treeifyBin()把链表转换成红黑树,否则会进行扩容操作。

Hashtable 和 ConcurrentHashMap 都是 Java 中用于存储键值对的类。虽然它们都提供了线程安全的访问机制,但它们之间存在一些重要的区别:

  1. 线程安全性:Hashtable 的所有方法都是同步的,这意味着在多线程环境下,多个线程可以安全地访问 Hashtable 的同一实例。而 ConcurrentHashMap 采用了一种不同的机制,它将整个 Map 分成了若干小块,每个小块都由一个独立的锁来控制。这种机制使得多个线程可以同时访问 ConcurrentHashMap 的不同部分,从而提高了并发访问效率。
  2. 性能:由于 Hashtable 的所有方法都是同步的,因此在高并发环境下,访问 Hashtable 的性能会受到较大的影响。而 ConcurrentHashMap 采用了分块锁的机制,因此在高并发环境下,它的性能比 Hashtable 要好得多。
  3. 允许 null 值:Hashtable 不允许键或值为 null,否则会抛出 NullPointerException 异常。而 ConcurrentHashMap 允许键和值都为 null。
  4. 迭代器:Hashtable 的迭代器是 fail-fast的,也就是说,如果在迭代的过程中对 Hashtable 进行了修改,迭代器会立即抛出 ConcurrentModificationException 异常。而 ConcurrentHashMap 的迭代器是 weakly consistent 的,也就是说,它不保证在迭代过程中看到的所有元素都是最新的。

综上所述,虽然 Hashtable 和 ConcurrentHashMap 都提供了线程安全的访问机制,但 ConcurrentHashMap 在性能和功能上都比 Hashtable 要更优秀。因此,如果需要在高并发环境下使用 Map,建议使ConcurrentHashMap。

TreeMap

它在内部会对Key进行排序,这种Map就是SortedMap。注意到SortedMap是接口,它的实现类是TreeMap

img

SortedMap保证遍历时以Key的顺序来进行排序。例如,放入的Key是"apple""pear""orange",遍历的顺序一定是"apple""orange""pear"因为String默认按字母排序

使用TreeMap时,放入的Key必须实现Comparable接口。StringInteger这些类已经实现了Comparable接口,因此可以直接作为Key使用。作为Value的对象则没有任何要求。

如果作为Key的class没有实现Comparable接口,那么,必须在创建TreeMap时同时指定一个自定义排序算法:

java
public class Main {
    public static void main(String[] args) {
        Map<Person, Integer> map = new TreeMap<>(new Comparator<Person>() {
            public int compare(Person p1, Person p2) {
                return p1.name.compareTo(p2.name);
            }
        });
        map.put(new Person("Tom"), 1);
        map.put(new Person("Bob"), 2);
        map.put(new Person("Lily"), 3);
        for (Person key : map.keySet()) {
            System.out.println(key);
        }
        // {Person: Bob}, {Person: Lily}, {Person: Tom}
        System.out.println(map.get(new Person("Bob"))); // 2
    }
}

class Person {
    public String name;
    Person(String name) {
        this.name = name;
    }
    public String toString() {
        return "{Person: " + name + "}";
    }
}
  1. 填装因子,负载因子,加载因子 为什么是0.75?
  • 装填因子设置为1:空间利用率得到了很大的满足,很容易碰撞,产生链表->查询效率低
  • 装填因子设置为0.5:碰撞的概率低,扩容,产生链表的几率低,查询效率高,空间利用率太低
  • 0.5~1 取中间值:0.75
  1. 主数组的长度为什么必须为2^n?
  • 原因1:h & (length-1) 等效于 h%length 操作,等效的前提就是:length必须是2 的倍数!
  • 原因2:防止哈希冲突,位置冲突:

4. 面向对象编程-基础

4.1 封装

把对象的状态和行为看成一个统一的整体,将二者存放在一个独立的模块中,比如:类;

细节隐藏, 把不想对外公开的实现细节隐藏起来,使用private私有化使其私有化,向外暴露public方法,保证调用者安全访问,不会出现因为越界导致本不应该出现的问题出现;

好处:

  • 调用者能够正确、方便地使用系统功能,有效地防止调用者随意修改系统属性。
  • 把特定的功能封装起来,提高功能的重用性。
  • 降低功能组件之间的耦合性,即使一个模块的实现细节发生改变,只要保证对外暴露的接口或者方法保持不变,就不会影响到其他调用者。

private:属于类访问权限,表示私有的,只能在当前类中访问,使用private修饰的类、方法、字段,在’离开当前类的作用域之后,调用者就不能直接访问。

(缺省):其实就是什么都不写,其属于包访问权限,表示包私有的,调用者的包必须和当前类(使用缺省修饰)的包相同才能访问。

protected:属于子类访问权限,表示受保护的,使用private修饰的类、方法、字段**,**不仅同包中类可以访问,而且即使不同包,但是如果有继承关系,也可以访问。

public:表示全局的公共访问权限,使用private修饰的类、方法、字段,在当前项目中任何地方都可以访问。接口(interface)中的方法默认都是public的。

4.2 继承 extends

从面向对象的角度上说,继承是一种从一般到特殊的关系,是一种**“is a”**的关系,即子类是对父类的拓展,是一种特殊的父类,

好处

  • 继承的出现减少了代码的冗余,提高了代码的通用性
  • 继承的出现,更加有利于功能的扩展
  • 继承的出现让让类与类之间产生了‘is-a’的关系,为多态的使用听过了前提。继承描述事物之间的所属关系,这种关系是‘is-a'的关系。

子类可以继承父类的哪些成员(根据访问修饰符来判断):

父类中使用protected、public修饰的成员;父类和子类在同一个包中,父类中缺省修饰符的成员可以被子类继承;

如何理解父类

类java.lang.Object是类层次的根类,即所有其他类的父类,每个类都是有Object

4.3 多态

是指一个对象在在不同时刻表现出来的不同形态。

重写 override

重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!

重写是发生在类的继承关系,或者类的实现关系中的,重写后的方法和原方法需要保持完全相同的返回值类型、方法名、参数个数以及参数类型,简单来说,就是子类重写的方法必须和父类保持完全一致

方法重写的规则。一同两小一大

  • 一同:方法签名必须相同。 方法签名= 方法名 + 方法的参数列表,参数类型、参数个数、参数顺序都必须一致
  • 两小:返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类(java5 及更早版本返回类型要一样,java7 及更高版本可以不同)。
  • 两小:子类方法声明抛出的异常类型和父类方法声明抛出的异常类型相同或者是其子类,子类方法中声明抛出的异常小于或等于父类方法声明抛出异常类型;子类方法可以同时声明抛出多个属于父类方法声明抛出异常类的子类(RuntimeException类型除外,RuntimeException是个特例);
  • **一大:**访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为 public,那么在子类中重写该方法就不能声明为 protected。
  • 父类的成员方法只能被它的子类重写。
  • 声明为 final 的方法不能被重写。
  • 声明为 static 的方法不能被重写,但是能够被再次声明。
  • 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法。
  • 子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法。
  • 重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重 写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。
  • 构造方法不能被重写。
  • 如果不能继承一个类,则不能重写该类的方法。

重载 overload

  • 被重载的方法必须改变参数列表(参数个数或类型不一样);
  • 被重载的方法可以改变返回类型;
  • 被重载的方法可以改变访问修饰符;
  • 被重载的方法可以声明新的或更广的检查异常;
  • 方法能够在同一个类中或者在一个子类中被重载。
  • 无法以返回值类型作为重载函数的区分标准。

重载和重写的区别

  1. 定义不同---重载是定义相同的方法名,参数不同;重写是子类重写父类的方法。
  2. 范围不同---重载是在一个类中,重写是子类与父类之间的。
  3. 多态不同---重载是编译时的多态性,重写是运行时的多态性。
  4. 返回不同---重载对返回类型没有要求,而重写要求返回类型,有兼容的返回类型。
  5. 参数不同---重载的参数个数、参数类型、参数顺序可以不同,而重写父子方法参数必须相同。
  6. 修饰不同---重载对访问修饰没有特殊要求,重写访问修饰符的限制一定要大于被重写方法的访问修饰符。

5. 面向对象变成-进阶

关键字 static

Java中变量的初始化顺序

  1. 首先会初始化父类,因为没有父类子类也无从谈起。第一步初始化父类的 静态变量静态代码块
  2. 初始化子类的 静态变量 静态代码块
  3. 顺序初始化父类普通变量 或者 父类普通变量初始化块 ,然后是构造函数
  4. 顺序初始化子类普通变量 或者 子类普通变量初始化块 ,然后是构造函数
java
package list;


class Father {
    Person person = new Person("父类的成员变量");

    static Person person1 = new Person("父类的静态变量的");


    static {
        System.out.println("父类的静态方法");
    }

    public Father() {
        System.out.println("父类的构造器");
    }

}


class Person {


    static {
        System.out.println("成员变量的静态方法");
    }


    public Person(String s) {
        System.out.println("person---" + s);
    }

}

public class MyTest extends Father {

    static {
        System.out.println("子类静态方法 static");
    }

    static Person person1 = new Person("子类的静态变量的");

    Person person = new Person("成员变量 MyTest");


    public MyTest(){
        System.out.println("子类的构造器");
    }

    public static void main(String[] args) {


        MyTest myTest = new MyTest();

    }

}

执行顺序

txt
成员变量的静态方法
person---父类的静态变量的
父类的静态方法
子类静态方法 static
person---子类的静态变量的
person---父类的成员变量
父类的构造器
person---成员变量 MyTest
子类的构造器

抽象 asbstract

包含抽象方法的类必须是抽象方法

abstract方法不能与哪些关键字一起写

  • 私有方法不能重写
  • 静态关键字

6. 面向对象编程-高阶

6.1 注解

7. 异常处理

8. 多线程

8.1 线程创建方式(三种)

1) 继承Thread类

1.创建出MyThread实例,并不代表在系统真的创建一个线程,只有调用start方法时,才创建出一个新的线程,新线程会执行run里的逻辑,直到run里逻辑执行完,线程就结束了;

2.运行一次Java程序就启动了一个进程,一个进程里至少会有一个线程,这里JVM默认创建的线程就是main线程(主线程),main主线程和MyThread创建出来的新线程是“并发执行”的关系(并发+并行),也可以理解为同时执行,各执行各的;

3.直接调用run并没有创建线程,只是在原来的线程中执行代码;

java
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("继承Thread,重写run方法创建线程");
    }
}
 
public class Main {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

2) 实现Runable

通过自定义一个类(这里起名为:MyRunnable)实现Runnable接口,重写run方法,最后在main方法new出MyRunnable实例和Thread实例,最后通过start方法创建并启动线程。

java
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("实现Runnable接口,重写run方法");
    }
}
 
public class Main {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}

3) 使用匿名类创建线程

java
public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                System.out.println("使用匿名内部类创建 Thread 子类对象");
            }
        };
        thread.start();
    }
}

4) lambda表达式

lambda本质上就是一个“匿名函数”,()表示函数的形参,{}表示函数体,->特殊语法,表示它是lambda表达式(->是从C++那里抄来的)。

java
public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
           System.out.println("使用lambda表示创建线程");
        });
        thread.start();
    }
 
}

5) 实现Callable接口

通过自定义类(这里起名为:MyCallable),实现Callable接口,重写call方法(call方法可以理解为线程需要执行的任务),并且带有返回值,这个返回表示一个计算结果,如果无法计算结果,则引发Exception异常,如下源码英文解释:

java
package k1;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class MyCallableTest implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        Thread.sleep(1000);
        System.out.println("创建线程:" + Thread.currentThread().getName());
        return 2;
    }
}

public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> task = new FutureTask<>(new MyCallableTest());
        Thread thread = new Thread(task);
        thread.start();
        System.out.println("创建线程的返回结果为:" + task.get());
        System.out.println("运行结束了" );

    }

}

运行结果

txt
创建线程:Thread-0
创建线程的返回结果为:2
运行结束了

8.2 锁

原子性,可见性,有序性

从功能上讲,分为共享锁,排他锁

加锁会存在性能问题:

  • 锁粒度的优化
  • 无锁并发-乐观锁
  • 锁的升级
  • 锁膨胀和锁消除
  • 读写锁(针对读多写少的情况下)
  • 公平锁/非公平锁(减少了线程的阻塞唤醒)

从锁的特效

  • 重入锁
  • 分布式锁

什么是锁?

Synchronized

什么是死锁?

java
package list;

public class DeadLockTest {


    public static void main(String[] args) {
        StringBuilder s1=new StringBuilder();

        StringBuilder s2=new StringBuilder();

        new Thread(){
            @Override
            public void run() {
              synchronized (s1){
                  s1.append("asd");
                  s2.append("sd");

                  System.out.println(s1);
                  System.out.println(s2);

                  try {
                      Thread.sleep(100);
                  } catch (InterruptedException e) {
                      throw new RuntimeException(e);
                  }

                  synchronized (s2){
                      s1.append("asd");
                      s2.append("sd");
                  }
              }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                synchronized (s2){
                    s1.append("2222sd");
                    s2.append("222sd");


                    System.out.println(s1);
                    System.out.println(s2);

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }

                    synchronized (s1){
                        s1.append("asd");
                        s2.append("sd");
                    }
                }
            }
        }.start();

    }
}

wait()和sleep()的区别

声明的位置不一致:wait()声明在Object类中,sleep()声明在Thread类中

使用的场景不同:wait():只能使用在同步代码块或同步方法中。sleep()可以在任何需要使用的场景

使用在同步代码块或同步方法中:wait()一旦执行,会释放同步监视器。sleep():一旦执行,不会释放同步监视器

结束阻塞的方式不一样:wait():到达指定时间自动结束阻塞或通过被notify唤醒,结束阻塞。sleep:到达指定时间自动结束阻塞。

8.3 线程的通信

为什么要处理线程间通信?

当我们需要多个线程来完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些通信机制,可以协调他们的工作。以此实现多线程共同操作一份数据。

线程A用来生产包子的,线程B用来吃包子的,包子可以理解为统一资源,线程A与线程B处理的动作,一个是生产,一个是消费,此时B线程必须等到A线程完成后才能执行,那么线程A与线程B之间就需要线程通信。

等待唤醒机制

这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争,比如去争夺锁。但是线程间也会有协作机制

在一个线程满足某个条件时,就进入等待状态(wait())。等待其他线程执行完他们的指定代码后将会被唤醒(notify())。或者指定wait的时间,自动被唤醒;在多个线程进行等到时,如果需要可以使用notifyAll()来唤醒所有的等待线程。wait/notify就是线程间的一种协作机制。

8.4 线程池

1) 线程池基本概念

线程池的作用

  1. 重用线程池中的线程,减少因对象创建,销毁所带来的性能开销;
  2. 能有效的控制线程的最大并发数,提高系统资源利用率,同时避免过多的资源竞争,避免堵塞;
  3. 能够多线程进行简单的管理,使线程的使用简单、高效。

Java里面线程池的顶级接口是Executor,通过工具类java.util.concurrent.Executors的静态方法来创建。Executors此包中所定义的 Executor、ExecutorService、ScheduledExecutorService、ThreadFactory 和 Callable 类的工厂和实用方法。

Executors工具类创建线程池

方法名功能
newFixedThreadPool(int nThreads)创建固定大小的线程池
newSingleThreadExecutor()创建只有一个线程的线程池
newCachedThreadPool()创建一个不限线程数上限的线程池,任何提交的任务都将立即执行
  • newFixedThreadPool:

    使用的构造方式为

    java
    new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue())

    设置了corePoolSize=maxPoolSize,keepAliveTime=0(此时该参数没作用),无界队列,任务可以无限放入,当请求过多时(任务处理速度跟不上任务提交速度造成请求堆积)可能导致占用过多内存或直接导致OOM异常

  • newSingleThreadExector:

    ​ 使用的构造方式为

    java
    new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), var0)

    基本同newFixedThreadPool,但是将线程数设置为了1,单线程,弊端和newFixedThreadPool一致

  • newCachedThreadPool

    使用的构造方式为

    java
    new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue())

    corePoolSize=0,maxPoolSize为很大的数,同步移交队列,也就是说不维护常驻线程(核心线程),每次来请求直接创建新线程来处理任务,也不使用队列缓冲,会自动回收多余线程,由于将maxPoolSize设置成Integer.MAX_VALUE,当请求很多时就可能创建过多的线程,导致资源耗尽OOM

  • newScheduledThreadPool

    使用的构造方式为

    java
    new ThreadPoolExecutor(var1, 2147483647, 0L, TimeUnit.NANOSECONDS, new ScheduledThreadPoolExecutor.DelayedWorkQueue())

    支持定时周期性执行,注意一下使用的是延迟队列,弊端同newCachedThreadPool一致

注意事项:

所以根据上面分析我们可以看到,FixedThreadPool和SigleThreadExecutor中之所以用LinkedBlockingQueue无界队列,是因为设置了corePoolSize=maxPoolSize,线程数无法动态扩展,于是就设置了无界阻塞队列来应对不可知的任务量;

而CachedThreadPool则使用的是SynchronousQueue同步移交队列,为什么使用这个队列呢?因为CachedThreadPool设置了corePoolSize=0,maxPoolSize=Integer.MAX_VALUE,来一个任务就创建一个线程来执行任务,用不到队列来存储任务;

SchduledThreadPool用的是延迟队列DelayedWorkQueue。在实际项目开发中也是推荐使用手动创建线程池的方式,而不用默认方式,

关于这点在《阿里巴巴开发规范》中是这样描述的: image-20231007101432523

ThreadPoolExecutor创建线程池案例

java
public ThreadPoolExecutor
(int corePoolSize,//核心线程数:当一个任务提交到线程池中,如果当前运行的线程数量小于核心线程数,会新开一个线程来执行任务
int maximumPoolSize,//最大线程数:当大于核心线程数时,可以设置一个最大线程数
long keepAliveTime,//最大空闲时间(存活时间):当一个任务没有运行时候,task可以存活的时间
TimeUnit unit,//时间单位:枚举类型,可设置天、小时、时分秒等
BlockingQueue<Runnable> workQueue,//任务队列(临时缓冲区):当任务达到核心线程数量时,再有task提交到线程池需要在任务队列先排队,当任务队列满了之后会根据最大线程数创建新线程
ThreadFactory threadFactory,//线程工厂:创建线程的工厂
RejectedExecutionHandler handler) //饱和处理机制:当核心线程数、最大线程数贺任务队列都满了需要处理的事情

参数讲解

  • corePoolSize:核心线程数,也是线程池中常驻的线程数,线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执行任务
  • maximumPoolSize:最大线程数,在核心线程数的基础上可能会额外增加一些非核心线程,需要注意的是只有当workQueue队列填满时才会创建多于corePoolSize的线程(线程池总线程数不超过maxPoolSize)
  • keepAliveTime:非核心线程的空闲时间超过keepAliveTime就会被自动终止回收掉,注意当corePoolSize=maxPoolSize时,keepAliveTime参数也就不起作用了(因为不存在非核心线程);
  • unit:keepAliveTime的时间单位
  • workQueue:用于保存任务的队列,可以为无界、有界、同步移交三种队列类型之一,当池子里的工作线程数大于corePoolSize时,这时新进来的任务会被放到队列中
  • threadFactory:创建线程的工厂类,默认使用Executors.defaultThreadFactory(),也可以使用guava库的ThreadFactoryBuilder来创建
  • handler:线程池无法继续接收任务(队列已满且线程数达到maximunPoolSize)时的饱和策略,取值有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy

workQueue队列

  • 直接提交队列(SynchronousQueue):不存储任何提交的任务,而是直接将任务交给空闲的工作线程进行处理,如果没有可用的工作线程,则新建一个线程来处理任务。
  • 无界队列(LinkedBlockingQueue):可无限制地添加新的任务,直到系统内存耗尽为止。当线程池中的所有工作线程都在忙碌处理任务时,新的任务会被暂存到该队列中等待处理。
  • 有界队列(ArrayBlockingQueue):固定容量大小,只能存储指定数量的任务。当队列已满时,新的任务会被阻塞。
  • 优先级队列(PriorityBlockingQueue):该队列按照任务的优先级顺序执行任务,具有最高优先级的任务会先被处理

handler拒绝策略

  • AbortPolicy:中断抛出异常
  • DiscardPolicy:默默丢弃任务,不进行任何通知
  • DiscardOldestPolicy:丢弃掉在队列中存在时间最久的任务
  • CallerRunsPolicy:让提交任务的线程去执行任务(对比前三种比较友好)

img

线程池监控

线程池提供了以下几个方法可以监控线程池的使用情况:

方法含义
getActiveCount()线程池中正在执行任务的线程数量
getCompletedTaskCount()线程池已完成的任务数量,该值小于等于taskCount
getCorePoolSize()线程池的核心线程数量
getLargestPoolSize()线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过,也就是达到了maximumPoolSize
getMaximumPoolSize()线程池的最大线程数量
getPoolSize()线程池当前的线程数量
getTaskCount()线程池已经执行的和未执行的任务总数

代码案例

java
  /**
     * <p>
     * 实例2:打印线程池参数
     * </P>
     */
    @Test
    public void printThreadPoolParameters() {
 
        // 开始时间
        long start = System.currentTimeMillis();
        int pcount = Runtime.getRuntime().availableProcessors();
        // 创建一个线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 4, 60, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
 
        for (int i = 0; i < 5; i++) {
            executor.execute(() -> {
                printThreadPoolStatus(executor);
            });
        }
        executor.shutdown();
        System.out.println("执行任务消耗了 :" + (System.currentTimeMillis() - start) + "毫秒");
    }
 
 
 private static void printThreadPoolStatus(ThreadPoolExecutor executor) {
        BlockingQueue queue = executor.getQueue();
        System.out.println(Thread.currentThread().getName() + "," +
 
                "当前的线程数量:" + executor.getPoolSize() + "," +
                "核心线程数:" + executor.getCorePoolSize() + "," +
                "最大线程数:" + executor.getMaximumPoolSize() + "," +
                "活动线程数:" + executor.getActiveCount() + "," +
                "任务总数:" + executor.getTaskCount() + "," +
                "任务完成数:" + executor.getCompletedTaskCount() + "," +
                "线程空闲时间:" + executor.getKeepAliveTime(TimeUnit.SECONDS) + "秒," +
                "当前排队线程数:" + queue.size() + "," +
                "队列剩余大小:" + queue.remainingCapacity() + "," +
                "线程池是否关闭:" + executor.isShutdown() + ","
        );
    }

2) 线程池状态流转

操作系统线程状态

JAVA线程状态

在这里插入图片描述

二者之间的关系

在这里插入图片描述

线程池状态

java
/**
* The runState provides the main lifecycle control, taking on values:
 *
 *   RUNNING:  Accept new tasks and process queued tasks
 *   SHUTDOWN: Don't accept new tasks, but process queued tasks
 *   STOP:     Don't accept new tasks, don't process queued tasks,
 *             and interrupt in-progress tasks
 *   TIDYING:  All tasks have terminated, workerCount is zero,
 *             the thread transitioning to state TIDYING
 *             will run the terminated() hook method
 *   TERMINATED: terminated() has completed
     */
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

int的高三位表示 状态,后面的29个所有位数表示线程数。

  1. RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。线程池的初始化状态是RUNNING。线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0。

  2. SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。调用线程池的shutdown()方法时,线程池由RUNNING -> SHUTDOWN。

  3. STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。调用线程池的**shutdownNow()**方法时,线程池由(RUNNING or SHUTDOWN ) -> STOP

  4. TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。因为terminated()在ThreadPoolExecutor类中是空的,所以用户想在线程池变为TIDYING时进行相应的处理;可以通过重载terminated()函数来实现。

    当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。

    当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。

  5. TERMINATED:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。

在这里插入图片描述

使用代码

java
package mythread;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class MainTest {

    public static void main(String[] args) {

        ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(
                5,
                20,
                100,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(6)
        );

        for(int i=0;i<15;i++){
            threadPoolExecutor.execute(()->{
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("线程名称 "+Thread.currentThread().getName());
            });
        }

        System.out.println("---------------------------");

        System.out.println("CorePoolSize:"+ threadPoolExecutor.getCorePoolSize());
        System.out.println("Queue Size:"+ threadPoolExecutor.getQueue().size());
        System.out.println("getMaximumPoolSize:"+ threadPoolExecutor.getMaximumPoolSize());
        System.out.println("PoolSize:"+ threadPoolExecutor.getPoolSize());
        System.out.println("---------------------------");

        threadPoolExecutor.shutdown();
    }
}

线程池为啥一定是阻塞队列

我们知道队列是先进先出的。当放入一个元素的时候,会放在队列的末尾,取出元素的时候,会从队头取。那么,当队列为空或者队列满的时候怎么办呢。

这时,阻塞队列,会自动帮我们处理这种情况。当阻塞队列为空的时候,从队列中取元素的操作就会被阻塞。当阻塞队列满的时候,往队列中放入元素的操作就会被阻塞。而后,一旦空队列有数据了,或者满队列有空余位置时,被阻塞的线程就会被自动唤醒。

这就是阻塞队列的好处,你不需要关心线程何时被阻塞,也不需要关心线程何时被唤醒,一切都由阻塞队列自动帮我们完成。我们只需要关注具体的业务逻辑就可以了。

线程池中线程如何关闭

线程池提供了两个关闭方法,shutdownNow和shuwdown方法。

shutdownNow方法的解释是:线程池拒接收新提交的任务,同时立马关闭线程池,线程池里的任务不再执行,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。

shutdown源码

java
  public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock(); // 加锁
        try {
            checkShutdownAccess(); // 检查每个线程是否有权限可以关闭
            advanceRunState(SHUTDOWN);// 设置
            interruptIdleWorkers();
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }

checkShutdownAccess代码。先判断当前JVM是否定义了安全管理器,如果有,进行校验,确保安全管理器能有权限中断所有的worker线程;

java
    private void checkShutdownAccess() {
        // assert mainLock.isHeldByCurrentThread();
        @SuppressWarnings("removal")
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkPermission(shutdownPerm);
            for (Worker w : workers)
                security.checkAccess(w.thread);
        }
    }

执行advanceRunState方法,该方法会原子设置线程池的rs(runState)

设置的逻辑是,如果当前的线程池状态已经是要设置的状态,或者已经超过了要设置状态(runStateAtLeast方法返回值是true),就保持不做任何操作了,直接break。如果线程池当前状态比要设置的状态小,比如当前是RUNNING,要设置是的SHUTDOWN,那么runStateAtLeast方法返回false,继续走第二个判断,原子设置rs,如果失败的话继续这个流程。

java
    private void advanceRunState(int targetState) {
        // assert targetState == SHUTDOWN || targetState == STOP;
        for (;;) {
            // 获取int值
            int c = ctl.get();
            // 如果当前的目标确实 c<=targetState
            if (runStateAtLeast(c, targetState) ||
                // 调用cas的设置的方法。直接跳出
                ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))))
                break;
        }
    }

interruptIdleWorkers代码,该方法是直接将所有的工作线程中断;

过程中,有一个tryLock方法,一个获取锁的步骤,在线程还有任务执行的时候,tryLock获取锁失败,在线程执行完释放锁之后,tryLock就可以获取锁成功,从而达到,只有在线程执行完任务之后才关闭线程池的目的

总结:只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true

java
private void interruptIdleWorkers() {
        interruptIdleWorkers(false);
}

private void interruptIdleWorkers(boolean onlyOne) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (Worker w : workers) {
                Thread t = w.thread;
                if (!t.isInterrupted() && w.tryLock()) {
                    try {
                        t.interrupt();
                    } catch (SecurityException ignore) {
                    } finally {
                        w.unlock();
                    }
                }
                if (onlyOne)
                    break;
            }
        } finally {
            mainLock.unlock();
        }
}

8.5 面试例题

交替打印

两个线程交替共同打印1-100。线程1 和线程2交替打印。

java
package list;


class PrintNumber implements Runnable {
    static int number = 0;


    @Override
    public void run() {
        while (true) {


            synchronized (this) {
                if (number <= 100) {
                    notify();
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + "---" + number);
                    number++;
                    
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                } else {
                    break;
                }
            }
        }
    }
}

public class PrintNumberTest {


    public static void main(String[] args) {

        PrintNumber printNumber = new PrintNumber();
        Thread t1 = new Thread(printNumber, "线程1");
        Thread t2 = new Thread(printNumber, "线程2");
        t1.start();
        t2.start();


    }
}

手写生成者和消费者

经典面试题 -- 手写生产者消费者模式 - 简书 (jianshu.com)

9. 泛型

10. IO流

11. 网络编程

TCP的三次握手建立链接和四次挥手释放链接 - 简书 (jianshu.com)

阻塞式io与非阻塞式io的区别

对比项BIONIO
概念阻塞式(Blocking IO)非阻塞式(Non blocking IO)
特点当我们没有获取到数据的时候,整个应用程序会实现阻塞等待,不能实现做其他的事情。不管是否有获取到数据,都必须立马获取到结果,如果没有获取数据的情况下,就会不断的重试获取数据
性能面向流(Stream oriented),按字节流读取,效率较低面向缓冲区(Buffer oriented),每次读取一整块,效率较高
历史一开始就有Jdk1.4版本之后推出,支持了面向缓冲区、基于通道实现的io的方案
亮点选择器(Selectors)—多路复用机制
场景悲观锁Cas(自旋锁)和乐观锁

=

12. 反射

12.1 概念

什么是反射?

反射reflection被认为动态语言的关键,反射机制允许程序在运行期间借助Reflection API取得任何内部信息,并能直接操作任意对象的内部属性及方法。

为什么会出现反射?

java程序中,所有的对象都有两种类型,编译时类型和运行时类型,而很多对象的编译时类型和运行时类型不一致。

例如:某些变量或形参的声明类型是object类型,但是程序却需要调用该对象运行时类型的方法,该方法不是object中的方法,那么如何解决呢?

在哪里会出现?

因为反射体现了动态性(可以在运行时动态的获取对象所属的类,动态的调用相关的方法,所以在设计框架时会大量的使用反射,意味着,如果大家需要学习框架源码,那么就需要学习反射。

优缺点

优点:·

  • 提高了Java程序的灵活性和扩展性,降低了耦合性,提高自适应能力。
  • 允许程序创建和控制任何类的对象,无需提前硬编码目标类

缺点:

  • 反射的性能较低。 反射机制主要应用在对灵活性和扩展性要求很高的系统框架上
  • 反射会模糊程序内部逻辑,可读性较差。

代码演示

java
package collection;

public class Person {

    public String name; // 这个属性得是 public

    private Integer age;


    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
    public void show(){
        System.out.println("你是一个好人");
    }

    private void showPrivate(){
        System.out.println("私下里你是一个好人,这个是私有的方法");
    }
    public Person() {

    }

    private Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class MyReflection {

    public static void main(String[] args) throws InstantiationException, IllegalAccessException, NoSuchFieldException, NoSuchMethodException, InvocationTargetException {

        Person person = new Person();
        person.setAge(10);
        person.setName("小明");
        System.out.println(person);

        person.show();

//         使用反射
        Class zz = Person.class;

        Person p1 = (Person) zz.newInstance();

        p1.setName("反射1");
        p1.setAge(12);

        System.out.println(p1);

        Field field = zz.getField("name"); // 只能填充public属性的
        field.set(p1, "自己填充");

        System.out.println(p1);


        Method showMethod = zz.getMethod("show");
        showMethod.invoke(p1);


//        访问私有变量

        Class clazz = Person.class;

        Constructor cons = clazz.getDeclaredConstructor(String.class, Integer.class);
        cons.setAccessible(true);
        Person p2 = (Person) cons.newInstance("私有变量", 100);
        System.out.println(p2);
        

    }
}

12.2 获取Class实例方式(三种)

java
package collection;

public class Person {

    public String name; // 这个属性得是 public

    private Integer age;


    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
    public void show(){
        System.out.println("你是一个好人");
    }

    private void showPrivate(){
        System.out.println("私下里你是一个好人,这个是私有的方法");
    }
    public Person() {

    }

    private Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}


public class MyClass {

    public static void main(String[] args) throws ClassNotFoundException {

        // 1. 调用运行时类的静态属性
        Class clazz1 = Person.class;

        Person person = new Person();

        // 2. 调用运行时类的对象的 getClass()
        Class clazz2 = person.getClass();
        // 运行时类在内存中会缓存起来,只运行一次
        System.out.println(clazz1 == clazz2);

        // 3. 调用class的静态方法forName(String className)
//        需要加上包名,还有包的类名
        String className = "collection.Person";
        try {
            Class clazz3 = Class.forName(className);
            System.out.println(clazz1 == clazz3);

        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }

        // 4. 使用类的加载器的方式
        Class clazz4=ClassLoader.getSystemClassLoader().loadClass(className);

        System.out.println(clazz1 == clazz4);


    }
}

12.3 类加载过程+类加载器

类加载过程

  1. 装载

    将类的class文件读入内存,并为之创建一个java.lang.Class对象,此过程由类加载器完成。

  2. 链接

    验证(Verfity):确保加载的类信息符合JVM规范,例如,以cafebabe开头,没有安全方面的问题。

    准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区

    解析(Resolve):虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程

  3. 初始化

    执行类构造器<clinit>()方法的过程

类加载器

(1) 启动类加载器(BootstrapClassLoader)

  • 这个类加载使用C/C++语言实现们嵌套在JVM内部。获取它的对象时往往返回null
  • 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容)。用于提供JvVM自身需要的类。
  • 并不继承自java.lang.ClassLoader,没有父加载器。
  • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类加载扩展类和应用程序类加载器,并指定为他们的父类加载器。

(2)扩展类加载器(ExtensionClassLoader)

  • Java语言编写,由sun.misc.LauncherSExtClassLoader实现。
  • 继承于ClassLoader类
  • 父类加载器为启动加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
java
package collection;

public class MyClassLoader {

    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader=ClassLoader.getSystemClassLoader();
        System.out.println(classLoader);


        ClassLoader classLoader1= classLoader.getParent();
        System.out.println(classLoader1);

        ClassLoader classLoader2= classLoader1.getParent();
        System.out.println(classLoader2);


        Class clazz=Person.class;

        ClassLoader classLoader3=clazz.getClassLoader();

        System.out.println(classLoader3);



        Class clazz2=Class.forName("java.lang.String");

        System.out.println(clazz2.getClassLoader());


    }
}

12.5 创建运行时类对象

13. 设计模式