八股打卡(四)

HashMap的put方法(回顾复习)

  1. 判断键值对数组是否为空或null,如果是则进行resize()扩容;
  2. 根据key计算哈希值得到table中对应的索引i。如果table[i]为null,说明数组的该位置没有值,新建一个节点插入,若超出容量则扩容;如果table[i]不为null,说明该位置油值,发生了哈希冲突;
  3. 判断数组该位置的首个元素的hashCode和equals是否与key相同。若相同,那么就覆盖对应的value;若不相同,那么需要在链表或者红黑树上查找;
  4. 判断table[i]是否为TreeNode。若是,则它是红黑树,插入键值对;若不是,则为链表;
  5. 先遍历链表的长度,若长度大于等于8,则转化为红黑树,插入键值对;若长度小于8,则在链表中插入。在遍历链表的过程中若找到key,则直接覆盖其value;
  6. 超出容量则扩容。

HashMap的扩容机制

JDK1.8之前

  1. 生成新数组;
  2. 遍历老数组每个位置的链表每个元素;
  3. 获取每个元素的key,基于新数组的长度,计算出元素在新数组中的下标位置;
  4. 将元素添加到新数组;
  5. 元素转移完毕,将新数组赋值给HashMap的table属性。

JDK1.8之后

  1. 生成新数组;
  2. 遍历老数组中的链表和红黑树;
  3. 如果是链表,计算出元素在新数组中的下标直接插入;
  4. 如果是红黑树,计算出元素在新数组中的下标,统计每个下标的元素个数,大于等于8那么生成新的红黑树,将根节点放在新数组对应位置;小于8则生成新的链表,将头节点放在新数组的对应位置
  5. 元素转移完毕,将新数组赋值给HashMap的table属性。

HashMap的线程安全性

HashMap是线程不安全的。
原因有:

  1. 并发修改:当多个线程对一个HashMap进行并发读写操作,读取的数据可能不一致,造成并发修改异常。
  2. 非原子性:HashMap的操作是非原子性的,例如检查某些键是否存在或者根据键获取值等,多线程环境下可能产生竞态条件。

如何实现线程安全:

  1. 使用Collections.synchronizedMap方法可以创建一个线程安全的HashMap;
  2. 使用ConcurrentHashMap:专门为多线程环境设计的,使用分段锁的机制,能够是得多个线程并发进行读操作,提高并发性能。
  3. 在自定义的HashMap中使用显示的锁(如ReentrantLock)。

ConcurrentHashMap的线程安全性

  1. JDK1.7中HashMap使用数组和链表,常用Segment和ReentrantLock;
  2. JDK1.8中HashMap使用数组链表红黑树,通过CAS操作和synchronized保证线程安全。

HashSet vs. HashMap

  1. 使用
    HashMap用于存储键值对,每个键唯一,每个键对应一个值;
    HashSet用于存储唯一的元素,不可重复。
  2. 内部结构
    HashMap通过键值对存储数据,由哈希表来实现;
    HashSet基于HashMap实现的,但是它只使用键的部分,将值设置为一个常量。
  3. 元素类型
    HashMap存储键值对,通过键可以获取值;
    Hash Set存储单个元素,只能通过元素本身进行操作。
  4. 是否允许null
    HashMap允许键和值为null;
    HashSet只允许一个null元素。
  5. 迭代方式
    两者都是通过迭代器或增强型for循环遍历。
  6. 关联关系
    HashMap的键值对一一对应;
    HashSet没有对应关系。
  7. 性能
    HashMap收到键的哈希分布和哈希冲突的影响;
    HashSet也受到元素的哈希分布和哈希冲突的影响,但是它只涉及键的部分,所以性能比HashMap稍好。

HashMap vs. HashTable

  1. 同步
    HashTable是同步的,通过在每个方法上加synchronized关键字保证多线程环境的安全;
    HashMap是不同步的,多线程环境下不能保证安全性。
  2. 性能
    在单线程环境中,HashTable的性能要差于HashMap,因为它存在同步开销。
  3. 是否允许null
    HashTable的key 和value都不允许为null;
    HashMap的key和value都可以为null。
  4. 继承方式
    HashTable是Dictionary类的子类;
    HashMap是AbstractMap类的子类,实现了Map接口。
  5. 迭代器
    HashTable的迭代器是通过Enumerator实现的;
    HashMap的迭代器是通过Iterator实现的。
  6. 初始容量和负载因子
    HashTable的初始容量和负载因子是固定的;
    HashMap的这两者可以通过构造方法来设定。

HashMap vs. ConcurrentHashMap

  1. 线程安全性
    HashMap是线程不安全的;
    ConcurrentHashMap是线程安全的,通过分段锁的机制,将数据结构或分为不同的段,每个段都有自己的锁,允许多个线程同时对不同的段进行并发访问。
  2. 同步机制
    HashMap没有明确的同步机制,可以通过外部同步,如Collections.synchronizedMap建立同步HashMap;
    ConcurrentHashMap采用一种更细粒度的锁提高并发性能。
  3. 迭代
    HashMap在迭代时被其他线程修改可能会抛出并发修改异常;
    ConcurrentHashMap在迭代时允许其他线程并发插入删除,不会抛出异常,但是不保证迭代器的顺序,因为不同线程可以会以不同顺序进行操作。
  4. 初始化容量和负载因子
    HashMap通过构造方法设定初始容量和负载因子;
    ConcurrentHashMap通过构造方法设定初始容量和负载因子以及并发级别。
  5. 性能
    低并发下,HashMap的性能可能比ConcurrentHashMap要好,因为后者存在并发控制开销;
    高并发下,ConcurrentHashMap更好。

Java创建线程的方式

  1. 通过继承Thread类
    创建一个继承Thread类的子类,重写其中的run方法。
  2. 实现Runnable接口
    创建一个实现Runnable接口的类,重写其中的run方法,将其实例化作为参数传递给Thread对象。
  3. 实现Callable接口
    创建一个实现Callable接口的类,重写其中的call方法,该方法能够返回结果和抛出异常。使用ExecutorService管理线程池,通过Callable任务获取Future对象,以便在未来某个时刻能够获取Callable任务的计算结果。
  4. 使用线程池
    通过Executor建立线程池,并通过线程池管理线程的创建与复用。

线程run和start的区别

  1. run方法是线程的执行体,里面包含线程执行代码。调用run方法会在当前线程的上下文中执行,不会创建新的线程。
  2. start方法用于启动一个新的线程。调用run方法系统会分配资源给新线程,使其处于就绪状态,当调度器选择该线程时执行run中的代码。

Java中的锁

公平锁/非公平锁

公平锁:按照申请锁的顺序来获取锁。
非公平锁:不按照顺序,可插队。
例如ReentrantLock和synchronized都是非公平锁。

可重入锁

一个线程在外部方法获取了锁,进入内部方法会自动获取锁。
例如ReenTrantLock和synchronized都是可重入锁。

共享锁/独占锁

共享锁:可以被多个线程持有。
独占锁:只能被一个线程持有。
例如: ReentrantLock和synchronized都是独占锁,ReadWriteLock中Read是共享锁,Write是独占锁。

乐观锁/悲观锁

乐观锁:对同一数据的并发操作不会执行,更新数据时采用尝试更新方式。
悲观锁:对同一数据的并发操作一定执行,加锁。
悲观锁用于写操作多的场景,乐观锁用于读操作多的场景。
悲观锁是加锁编程,乐观锁是无锁编程,常采用CAS操作通过自旋方式实现原子更新。

分段锁

ConcurrentHashMap所采用。

偏向锁/轻量级锁/重量级锁

当一段同步代码被一个线程一直访问,那么该线程获得了偏向锁;此时其他线程访问该段代码,通过自旋方式尝试获取锁,不会阻塞,锁升级成为轻量级锁;当自旋达到一定次数还没获得锁,那么锁升级为重量级锁,将所有试图访问的线程阻塞。

自旋锁

获得自旋锁的线程不会阻塞,它循环去获取锁,好处是减少线程上下文切换,坏处循环消耗cpu。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值