Java学习笔记

集合和多线程

集合

HashMap

默认大小为16,新创建的HashMap中,table为空,size为0,在放入第一个元素后,才会将table初始化(懒加载,节省内存)
阈值默认0.75,如果元素数量超过阈值时,以2的指数倍进行扩容

数据插入过程

  • 写入数据时(put),先计算元素hash,确定插入位置(hash%16)
    • 当该位置没有元素时,直接放入该位置
    • 当该位置有元素时
      • 如果hash相同,则替换
      • 不同,则挂载到该元素后端
        • 如果该位置是链表,则使用尾插法插入,如果链表长度大于8且数组长度大于64,则转换为红黑树
        • 如果是红黑树,则插入树中

为什么是2的指数倍扩容

HashMap和HashTable的区别是什么

  1. HashTable线程同步,HasMap不同步
  2. HashTable的<k,v>不允许空值,HashMap可以
  3. HashTable使用Enumeration,HashMap使用Iterator
  4. HashTable的hash数组默认·大小是11,增加方式为old*2+1,HashMap的hash数组默认大小为16,增加方式是2的指数倍
  5. HashTable继承Dictionary类,HashTable继承AbstractMap类

HashSet

内部使用HashMap进行数据存储,将数据存放在HashMap的key中,HashMap不允许有重复的键,因此HashSet也不会有重复的元素。

HashSet与HashMap的区别

问出这个问题的怕不是有十年脑血栓(

  1. HashSet实现了Set接口,HashMap实现Map接口
  2. HashSet使用成员对象计算hashcode,HashMap使用key计算hashcode
  3. HashSet存储对象,HashMap存储键值对
  4. HashSet使用add()添加元素,HashMap使用put()添加键值对

ArrayList

默认大小为10,初始化时,数组大小为0,扩容方式为原始大小的1.5倍(能充分利用已释放的空间)

  • 插入数据时先判断数组大小,需要则进行扩容(创建一个新数组,并复制原数组)

    LinkedList

    默认大小为0,初始化时,头节点和尾节点为空

ArrayList与LinkedList的区别

  1. ArrayList基于数组实现,LinkedList基于链表实现
  2. ArrayList基于索引读取数据速度快,时间复杂度为O(1),但是每次删除数据开销很大,需要重排数组的所有数据,在插入数据时还要更新索引(除了在末尾插入)
  3. LinkedList删除和插入更快,时间复杂度为O(1),
  4. LinkedList需要更多的内存,因为每个节点除了数据还存储了前一个节点和后一个节点的位置
  • 什么时候用ArrayList或LinkedList
    应用更多需要随机读取数据的时候用ArrayList,更多地增删数据的时候用LinkedList

多线程

什么是多线程

  1. 线程是操作系统调度的最小单元,可以使一个进程并发的处理多个任务
  2. 一个进程可以有多个线程,每个线程都有自己的计数器,堆栈和局部变量,并且能共享进程内的资源
  3. 使用多线程可以显著的提高程序的执行效率,减少程序的响应时间

怎么保证线程安全

原子类,volatile,锁

并发的三大特性

  • 原子性:不可分割的操作,多个步骤要保证同时成功或同时失败
  • 可见性:一个线程对共享变量的访问,另一个线程要立马看到
  • 有序性:线程的执行顺序要和代码的顺序一致

volatile

用于修饰属性,能保证可见性和有序性,使用了volatile修饰的属性,在修改属性时会直接将缓存中的数据写回主存中,在读取时直接从主存读取,保证了可见性
底层通过操作系统的内存屏障实现,由于使用了内存屏障,所以会禁止指令重排,保证了有序性
但是volatile无法保证原子性,因为写入时并不能保证变量未被其他线程修改,通常用于一个线程写,多个线程读的场景。

synchronized

java的一个关键字,主要是用于加锁,可用于变量,方法以及代码块

实现原理

synchronized采用Java对象头来存储锁信息,锁信息包括锁的标志和锁状态,存放在对象头的MarkWork部分。
JDK 1.6后,为减少性能消耗,引入了偏向锁和轻量级锁。
通过ACC_SYNCHRONIZED关键字隐式地加锁,当被标注ACC_SYNCHRONIZED时,需要先获得锁才能执行
同步代码块通过monitorenter和monitorexit执行来进行加锁和释放锁,当执行到monitorenter时需要先获得锁才能继续执行后面的代码,当执行monitorexit时则释放锁。

锁的升级过程

new - 偏向锁 - 轻量级锁(无锁,自旋锁)- 重量级锁

ReentrantLock

jdk1.5后提供的API层面的互斥锁,需配合lock()和unlock()方法来完成,相比synchronized提供了一些高级功能

  1. 可等待中断:持有锁的线程在长期不释放的时,正在等待的线程可以选择放弃等待,可以避免死锁的情况。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    ReentrantLock lock = new ReentrantLock();

    Thread t = new Thread(()->{

    try{
    //尝试获得锁
    lock.lockInterruptibly();
    } catch(InterruptedException e){
    //被中断,没有获取到锁
    e.printStackTrace();
    return;
    } finally {
    lock.unlock();
    }
    });

    t.interrupt();
  2. 公平锁:多个线程申请锁时,必须按照申请的时间顺序获得锁,synchronized为非公平锁,ReentrantLock可以在声明时设置为公平锁(默认是不公平锁),但公平锁性能不佳。
    1
    2
    3
    4
    //非公平锁
    ReentrantLock lock = new ReentrantLock();
    //公平锁
    ReentrantLock lock = new ReentrantLock(true);

和synchronized的区别

  1. synchronized是关键字,是原生语法层面的互斥锁,ReentrantLock是java.util.concurrent包下的类,是API层面的互斥锁。
  2. synchronized由编译器保证加锁和释放锁,ReentrantLock需要手动声明加锁和释放锁。
  3. 相比synchronized多了几个高级功能。

CAS

乐观锁

最终实现

1
lock cmpxchg

。。。。。。。