并发篇


线程池的核心参数

corePoolSize 核心线程数目

最多保留的线程数

maximumPoolSize最大线程数目

核心线程 + 救急线程

KeepAliveTime生存时间

针对救急线程

unit时间单位

针对救急线程

workQueue

阻塞队列

threadFactory线程工厂

可以为线程创建时起个好名字

handler拒绝策略

  1. AbortPolicy 抛异常策略
  2. CallerRunsPolicy 线程池已经满了,让调用者去执行
  3. DiscardPolicy 把任务丢弃
  4. DiscardOldestPolicy 丢弃最先进入队列的任务,新任务加入队列中

sleep vs wait

共同点

wait(),wait(long)和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态

方法归属不同

  1. sleep(long)是Thread静态方法
  2. 而wait(),wait(long)都是Object的成员方法,每个对象都有

醒来时机不同

  1. 执行sleep(long) 和 wait(long)的线程都会在等待相应毫秒醒来
  2. wait(long)和wait() 还可以被notify唤醒,wait()如果不唤醒就会一直等待下去
  3. 他们都可以被打断唤醒

锁特性不同

  1. wait方法的调用必须先获取wait对象的锁,而sleep则无需
  2. wait方法执行后会释放对象锁,允许其他线程获得该对象锁(我弃用,你们可以用)
  3. 而sleep如果在synchorized代码块中执行,并不会释放对象锁(我放弃,你们也不能用)

lock vs synchronized

语法层面

  1. synchronized 是关键字,源码在jvm中,用c++语言实现
  2. Lock是接口,源码由jdk提供,用java语言实现
  3. 使用synchronized是,退出同步代码块锁会自动释放,而使用Lock时,需要手动调用unlock方法释放锁

功能层面

  1. 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
  2. Lock提供了许多synchronized 不具备的功能,例如获得等待状态,公平锁,可打断,可超时,多条件变量
  3. Lock有适合不同场景的实现,如ReentrantLock,ReentrantReadWriteLock

性能层面

  1. 在没有竞争时,synchronized做了很多优化,如偏向锁,轻量级锁,性能不赖
  2. 在竞争激烈时,Lock的实现通常会提供更好的性能

volatile能否保证线程安全

线程安全要考虑三个方面:可见性,有序性,原子性

  1. 可见性指,一个线程对共享变量修改,另一个线程能看到最新的结果
  2. 有序性指,一个线程内代码按编写顺序执行
  3. 原子性指,一个线程内多行代码以一个整体运行,期间不能有其他线程的代码插队

volatile能够保证共享变量的可见性与有限性,但并不能保证原子性

java中的悲观锁与乐观锁

1.悲观锁的代表是synchronized和Lock锁

  1. 其核心思想是(线程只有占有了锁,才能去操作共享变量,每次只有一个线程占锁成功,获取失败锁的线程,都得停下来等待)
  2. 线程从运行到阻塞,再从阻塞到唤醒,涉及线程上下文切换,如果频繁发生,影响性能
  3. 实际上,线程在获取synchronized 和 Lock 锁时,如果锁已被占用,都会做几次重试操作,减少阻塞的机会

乐观锁的代表是AtomicInteger,使用cas来保证原子性

  1. 其核心思想是(无需加锁,每次只有一个线程能成功修改共享变量,其它失败的线程不需要停止,不断重试直到成功)
  2. 由于线程一直运行,不需要阻塞,因此不涉及线程上下文切换
  3. 他需要多核cpu支持,且线程数不应超过cpu核数

Hashtable vs ConcurrentHashMap

相同点:键值不能为空

  1. Hashtable 与 ConcurrentHashMap 都是线程安全的Map集合
  2. Hashtable并发度低,整个Hashtable对应一把锁,同一时刻,只能有一个线程操作它
  3. 1.8之前ConcurrentHashMap使用了Segment + 数组 + 链表的结构,每个Segment对应一把锁,如果多个线程访问不同的Segment,则不会冲突
  4. 1.8开始ConcurrentHashMap将数组的每个头节点作为锁,如果多个线程访问的头节点不同,则不会冲突

Hashtable的小细节

  1. 它的初始化容量是质数
  2. 它的扩容是原容量*2 + 1
  3. 它是不需要二次hash的,求索引是hash值%容量

###1.7ConcurrentHashMap

  1. 它有三个参数:capacity(容量),factor(扩容因子),clevel(并发度)
  2. 它的数组容量不是根据capacity来的,而是capacity 除以 clevel,其中的最小容量是2

计算Segment和元素放置的位置

  1. 计算hash值,然后二次hash,
  2. 再看的并发度是的2的几次方,
  3. 去二次hash的二进制高位n位,转换成十进制
  4. 就是这个值在Segment下了

元素放置的位置

  1. 看数组容量是2的几次方
  2. 然后取二次hash的二进制低位n
  3. 就是元素放置在哪个位置下

Segment[0],这是细节,注意看

  1. 首先他只会给Segment[0]初始化数组容量
  2. 然后给其他Segment的添值的时候,则按着Segment[0]的的大小来
  3. 就是妥妥的设计模式中的原型模式

1.7和1.8的ConcurrentHashMap

在数据结构上

  1. 1.7是Segment + 数组 + 链表
  2. 1.8是 数组 + 链表 | 红黑树

###在初始化时机上

  1. 1.7 是饿汉式
  2. 1.8 是懒汉式

在扩容时机上

  1. 1.7 大于阈值就扩容
  2. 1.8 是等于阈值就扩容

1.8ConcurrentHashMap中的capacity和factor

  1. capacity 是你要放多少元素
  2. 然后根据othercapacity * factor
  3. 是否大于capacity,大于就扩容othercapacity
  4. factor也就用于一次,之后判断就是othercapacity * 0.75

1.8的ConcurrentHashMap的扩容机制

  1. 先从后面开始扫,没有元素,则标记
  2. 有,则在新的数据计算索引,并迁移到新数组中,注意,这里的迁移是复制新的元素,与旧数组元素不一样.

谈一谈对ThreadLocal的理解

  1. ThreadLocal可以实现(资源对象)的线程隔离,让每个线程各用各的(资源对象),避免争用引发的线程安全问题
  2. ThreadLocal同时实现了线程内的资源共享
  3. 其原理是,每个线程内有一个ThreadLocalMap类型的成员变量,用来存储资源对象

    调用set方法,就是以ThreadLocal自己作为key,资源对象作为value,放入当前线程的ThreadLocalMap集合中
    调用get方法,就是以ThreadLocal自己作为key,到当前线程中查找关联的资源值
    调用remove方法,就是以ThreadLocal自己作为key,移除当前线程关联的资源值

ThreadLocal扩容机制

  1. 等于floor【容量* 2/3】
  2. 采用是开放寻址法

为什么ThreadLocalMap中的key(即ThreadLocal)要设计为弱引用?

  1. Thread可能需要长时间运行(如线程池中的线程),如果key不再使用,需要在内存不足(GC)时释放其占用的内存
  2. 但GC仅是让key的内存释放,后续还要根据key是否为null来进一步释放值得内存,释放时机有

    a) 获取key发现null key
    b) set key时,会使用启发式扫描,清除临近的nullkey,启发次数与元素个数,是否发现null key有关
    c) remove时(推荐),因为一般使用ThreadLocal时都把它作为静态变量,因此GC无法回收


文章作者: 小猩
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 小猩 !
  目录