26届JAVA 学习日记——Day5

2024.11.06 周三
上午没有课在炒股,持仓的科创芯片etf和标普500etf大涨,但是踏空了没有高位卖出,下午上课(有课堂小测必须去),后面一会儿学习一会儿玩,晚上跑了会步(不锻炼感觉身体要废了)

总结一下今天学习的内容:
List的数据结构、线程安全相关知识、回溯算法复习、堆排序复习(优先队列的学习)
明天目标:Java并行开发相关知识、锁相关知识、项目进展、算法刷几道简单题熟悉Java语法和数据结构相关api

八股

List

List

Arraylist和LinkedList的区别,哪个集合是线程安全的?

List是有序的Collection,使用此接口能够精确的控制每个元素的插入位置,用户能根据索引访问List中的元素,常用的实现List的类有LinkedList,ArrayList,Vector,Stack。

  • 底层数据结构不同:ArrayList使用数组实现,通过索引进行快速访问元素。LinkedList使用链表实现,通过节点之间的指针进行元素的访问和操作。
  • 插入和删除操作的效率不同:ArrayList在尾部的插入和删除操作效率较高,但在中间或开头的插入和删除操作效率较低,需要移动元素。LinkedList在任意位置的插入和删除操作效率都比较高,因为只需要调整节点之间的指针,但是LinkedList是不支持随机访问的,所以除了头结点外插入和删除的时间复杂度都是0(n),效率也不是很高所以LinkedList基本没人用。
  • 随机访问的效率不同:ArrayList支持通过索引进行快速随机访问,时间复杂度为O(1)。LinkedList需要从头或尾开始遍历链表,时间复杂度为O(n)。
  • 空间占用:ArrayList在创建时需要分配一段连续的内存空间,因此会占用较大的空间。LinkedList每个节点只需要存储元素和指针,因此相对较小。
  • 使用场景:ArrayList适用于频繁随机访问和尾部的插入删除操作,而LinkedList适用于频繁的中间插入删除操作和不需要随机访问的场景。
  • 线程安全:这两个集合都不是线程安全的,Vector是线程安全的
ArrayList的扩容机制说一下

ArrayList在添加元素时,如果当前元素个数已经达到了内部数组的容量上限,就会触发扩容操作。ArrayList的扩容操作主要包括以下几个步骤:

  • 计算新的容量:一般情况下,新的容量会扩大为原容量的1.5倍(在JDK 10之后,扩容策略做了调整),然后检查是否超过了最大容量限制。
  • 创建新的数组:根据计算得到的新容量,创建一个新的更大的数组。
  • 将元素复制:将原来数组中的元素逐个复制到新数组中。
  • 更新引用:将ArrayList内部指向原数组的引用指向新数组。
  • 完成扩容:扩容完成后,可以继续添加新元素。

之所以扩容是 1.5 倍,是因为 1.5 可以充分利用移位操作,减少浮点数或者运算时间和运算次数。

// 新容量计算
// >>1 属于位运算,指右移一位,相当于将 OldCapacity 除以2
// 110 >> 1 = 011 [ 2^n -> 2^(n-1) ]
int newCapacity = oldCapacity + (oldCapacity >> 1);
为什么ArrayList不是线程安全的,具体来说是哪里不安全?

在高并发添加数据下,ArrayList会暴露三个问题;

  • 部分值为null(我们并没有add null进去)
  • 索引越界异常
  • size与我们add的数量不符

ArrayList中add增加元素的代码如下:

/**
E e在这里表示一个通用的泛型参数,没有指定任何边界
若public <E extends Number> boolean add(E e)
则表示这是一个有界泛型参数,E 必须是 Number 类或其子类的实例。
*/
public boolean add(E e) {
        ensureCapacityInternal(size + 1); // 判断长度->扩容
        elementData[size++] = e;
        return true;
    }
  • 部分值为null:当线程1走到了扩容那里发现当前size是9,而数组容量是10,所以不用扩容,这时候cpu让出执行权,线程2也进来了,发现size是9,而数组容量是10,所以不用扩容,这时候线程1继续执行,将数组下标索引为9的位置set值了,还没有来得及执行size++,这时候线程2也来执行了,又把数组下标索引为9的位置set了一遍,这时候两个先后进行size++,导致下标索引10的地方就为null了。
    总结:即多线程读取到同一个size,不用扩容时,在同一个size位置set值,又将size++执行两次,此时原来size+1的位置没有set值就变成null。
  • 索引越界异常:线程1走到扩容那里发现当前size是9,数组容量是10不用扩容,cpu让出执行权,线程2也发现不用扩容,这时候数组的容量就是10,而线程1 set完之后size++,这时候线程2再进来size就是10,数组的大小只有10,而你要设置下标索引为10的就会越界(数组的下标索引从0开始)。
    总结:线程1和线程2同时执行判断是否要扩容,当线程1 set值时size++,线程2再set值的时候发现size已超过边界。
  • size与我们add的数量不符:这个基本上每次都会发生,这个理解起来也很简单,因为size++本身就不是原子操作,可以分为三步:获取size的值,将size的值加1,将新的size值覆盖掉原来的,线程1和线程2拿到一样的size值加完了同时覆盖,就会导致一次没有加上,所以肯定不会与我们add的数量保持一致的;
size++不属于原子操作:
1.获取size的值;
2.将size的值+1;
3.将新的size值覆盖掉原来的
因此若多个线程拿到一样的size值同时覆盖就会导致一次没+上,与add的数量就不一致
把ArrayList变成线程安全有哪些方法?
  • 使用Collections类的synchronizedList方法将ArrayList包装成线程安全的List:
List<String> synchronizedList = Collections.synchronizedList(arrayList);
  • 使用CopyOnWriteArrayList类代替ArrayList,它是一个线程安全的List实现:
CopyOnWriteArrayList<String> copyOnWriteArrayList = new CopyOnWriteArrayList<>(arrayList)
  • 使用Vector类代替ArrayList,Vector是线程安全的List实现:
Vector<String> vector = new Vector<>(arrayList);
CopyonWriteArraylist是如何实现线程安全的?

CopyOnWriteArrayList底层也是通过一个数组保存数据,使用volatile关键字修饰数组,保证当前线程对数组对象重新赋值后,其他线程可以及时感知到。

/**
transient表示array是一个瞬态变量,不会被序列化。
volatile表示array是一个易失变量,在多线程环境中,
该字段确保对变量的读写操作直接在主内存中进行,而不是在本地线程缓存中。
*/
private transient volatile Object[] array;

// 在写入操作时,加了一把互斥锁ReentrantLock以保证线程安全。
public boolean add(E e) {
    //获取锁
    final ReentrantLock lock = this.lock;
    //加锁
    lock.lock();
    try {
        //获取到当前List集合保存数据的数组
        Object[] elements = getArray();
        //获取该数组的长度(这是一个伏笔,同时len也是新数组的最后一个元素的索引值)
        int len = elements.length;
        //将当前数组拷贝一份的同时,让其长度加1
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //将加入的元素放在新数组最后一位,len不是旧数组长度吗,为什么现在用它当成新数组的最后一个元素的下标?
        //因为旧数组长度在新数组中表示索引位置时是最后一个元素(空)的索引值
        newElements[len] = e;
        //替换引用,将数组的引用指向给新数组的地址
        setArray(newElements);
        return true;
    } finally {
        //释放锁
        lock.unlock();
    }
}

// 读是没有加锁的,所以读是一直都能读
public E get(int index) {
    return get(getArray(), index);
}

在执行替换地址操作之前,读取的是老数组的数据,数据是有效数据;
执行替换地址操作之后,读取的是新数组的数据,同样也是有效数据,
而且使用该方式能比读写都加锁要更加的效率。

算法

347.前k个高频元素(堆排序-优先队列)
78.子集(回溯算法)
136.只出现一次的数字(异或运算)

        1. 任何数和 0 做异或运算,结果仍然是原来的数,即 a⊕0=a
        2. 任何数和其自身做异或运算,结果是 0,即 a⊕a=0
        3. 异或运算满足交换律和结合律,即 a⊕b⊕a=b⊕a⊕a=b⊕(a⊕a)=b⊕0=b

优先队列(PriorityQueue)

在Java中,PriorityQueue类就是基于优先级堆实现的。当向PriorityQueue中添加元素时,元素会被插入到合适的位置以保持堆性质。当从PriorityQueue中移除元素时,总是移除优先级最高的元素(对于最小堆)或优先级最低的元素(对于最大堆),并重新调整堆以保持堆性质。

  • 基于优先级堆:PriorityQueue使用一个优先级堆来存储元素,这使得它能够高效地添加和移除元素
  • 无界队列:PriorityQueue是一个无界队列,这意味着它没有固定的容量限制。但是,如果队列为空,PriorityQueue容量将动态增长
  • 常用方法:
    • add(E e):将元素e添加到队列中。
    • offer(E e):将元素e添加到队列中,与add方法相同。
    • poll():移除并返回队列中优先级最高的元素,如果队列为空,则返回null。
    • peek():返回队列中优先级最高的元素,但不移除它,如果队列为空,则返回null。
    • size():返回队列中元素的数量。
        // 创建一个PriorityQueue,用于存储整数数组,并按照数组中第二个元素的大小进行排序
        PriorityQueue<int[]> queue = new PriorityQueue<int[]>(new Comparator<int[]>(){
            public int compare(int[] m, int[] n){
                // 比较两个数组中第二个元素的大小,并返回它们的差值
                return m[1] - n[1];
            }
        } );

回溯算法(78.子集)

  • 非常标准的回溯算法
class Solution {
    List<List<Integer>> res = new ArrayList<List<Integer>>();
    int n;

    public List<List<Integer>> subsets(int[] nums) {
        n = nums.length;
        for (int k = 0; k <= n; k++){
            backtrace(0, k, new ArrayList<Integer>(), nums);
        }
        return res;
    }
    /**
        start是子集第一个数在nums中最早可以出现的位置
        k是当前需要构造子集的长度
        cur储存当前正在构造的子集
        backtrack运行一次能够构造所有长度为k的子集
     */
    public void backtrace(int start, int k, ArrayList<Integer> cur, int[] nums){
    	// 回溯的终止条件
        if (k == 0){
            res.add(new ArrayList<Integer>(cur));
            return;
        }
        for (int i = start; i < n; i++){
            cur.add(nums[i]); // 当前步骤要做的
            backtrace(i + 1, k - 1, cur, nums); // 开始下一步
            cur.remove(cur.size() - 1); // 回溯到上一步
        }
    }
}

项目

项目今天没有进展,有点累,先歇会,明天主要搞项目。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值