怎样停止一个线程?
官方给出了两个api:
- stop
- interrupt
但是stop是已经被废弃了。废弃原因是什么呢?
- 线程共享的东西有三种:cpu,共享内存,文件
- Thread1访问共享内存,并持有锁,线程2等待,此时阻塞
- Thread1释放锁,被销毁,此时Thread2立即持有共享内存的锁,如果Thread1有脏数据没有及时清理,这时候,Thread2运行时发现内存状态异常,可能会crash.
那么,怎样才是良好的中断线程的方式呢?
首先:Thread提供了两个方法:
- isInterrupted: 非静态方法,获取当前方法的中断状态,不清空
- 调用的想蹭对象对应的线程
- 可重复调用,中断清空钱一直返回true
- interrupted:静态方法,加锁 获取当前线程的终端状态,并清空
- 当前运行的线程
- 中断状态调用后清空,重复调用后续返回false.
二者都是jni方法,所以在性能上可能不占用优势
另外一种方式是使用boolean标志位
public class InterruptedThread extends Thread {
volatile boolean isStopped = false; // 保证可见性
@Override
public void run() {
super.run();
for (int i = 0; i < 10000; i++) {
if (isStopped) {
return;
}
}
}
}
public static void main(String args[]) {
InterruptedThread thread;
thread = new InterruptedThread();
thread.start();
thread.isStopped = true;
}
在使用boolean标志位的方式上,一定要注意volatile关键字的使用,保证可见性。
这两种方式有什么差异呢?
内容 | interrupt | boolean |
---|---|---|
系统方法 | 是 | 否 |
使用JNI | 是 | 否 |
加锁 | 是 | 否 |
触发方式 | 抛异常 | 布尔值判断,也可以抛异常 |
- 需要支持系统方法时(如thread.sleep)使用中断
- 其他情况boolean标志位
如何写出线程安全的程序?
1. 知识点
- 是否对线程安全有初步了解
- 是否对线程安全的产生原因有思考
- 是否知道final,volatile 关键字的作用
- 是否知道1.5之前java DCL 为什么有缺陷
- 是否清楚的知道如何编写线程安全的程序
- 是否对ThreadLocal的使用注意事项有认识
2. 剖析
什么才是线程安全的?
volatile 关键字
每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。
也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中
什么是线程安全–>可变资源(内存)线程间共享
1. 不共享资源
解决方法,纯函数,传一个参数进来,再返回一个参数出去,中间不涉及任何内存的访问。
2. 使用的内存变量与线程绑定
ThreadLoacal 就是典型的例子,ThreadLocal 中存储数据的类是ThreadLocalMap.Entry. 首先,通过线程对象获取对应的ThreadlocalMap,如果map不为null,再获取当点对象对应的数据。
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
对比 | THreadLocalMap | WeakHashMap |
---|---|---|
对象持有 | 弱引用 | 弱引用 |
对象GC | 不影响 | 不影响 |
应用清除 | 1. 主动移除 2. 线程退出移除 | 1. 主动移除 2. GC后移除(监听ReferenceQueue) |
Hash冲突 | 开放定址法 | 单链表法 |
hash计算 | 神奇数字的倍数 | 对象 hashCode 在散列 |
使用场景 | 对象较少 | 通用 |
使用ThreadLocal 注意事项
-
声明为全局的静态final成员,因为在存取对象的时候,是通过使用threadLoacal对象为key的,如果频繁更换threadlocal对象,可能会导致设置的对象找不回来。其次就是可见性的问题了。
-
避免存储大量的对象。
-
用完后及时移除对象。没有监听referenceQueue的机制,如果一个线程一直存在,就导致这个引用一直不会被移除。
-
共享不可变资源
- final 关键字 有禁止重排序的功能
- volatile 关键字(从1.5 之后,语义增强,增加禁止重排序功能)
-
共享可变资源
- 可见性
- 操作原子性
- 禁止重排序
保证可见性的方法
- 使用final 关键字
- 使用volatile 关键字
- 加锁,释放锁时,会将高速缓存中的数据强制刷新到主存中
在1.5之前
分配内存,调用构造方法,赋值 三步
concurrentHashMap如何支持并发访问
CHM的并发优化历程
版本 | 优化 |
---|---|
JDK1.5 | 分段锁,必要时加锁 |
JDK1.6 | 优化二次hash算法 |
JDK1.7 | 段懒加载,volatile&cas |
JDK1.8 | 摒弃段,基于hashMap原理实现并发 |
区别于hashTable :整个hash表锁
JDk1.5存在的问题
如果我的key是一个整数,hash值对应的高位,对于30000多一下对应的整数对应的hash值都是一样的,导致都存在了一个segment 中,使得整个chm退化成了hashTable
jdk1.6 分段锁的hash优化,保证了高位低位都均匀分布。
jdk1.7 分段锁懒加载
hashMap是一个散列结构先来看看几个重要的局部变量吧
在1.7中,hashmap的数据结构图
- 初始化桶大小,因为底层是数组,所以这是数组默认的大小。
- 桶最大值。
- 默认的负载因子(0.75)
- table 真正存放数据的数组。
- Map 存放数量的大小。
- 桶大小,可在初始化时显式指定。
- 负载因子,可在初始化时显式指定。
整体结构是数组+链表的形式,在解决hash冲突的问题上,选用了下拉连法。这个处理方式有个弊端:当数据足够多时,hash冲突的概率增加,这时候的查询复杂度可能退化成O(n).
hashEntry
使用了final关键字修饰了hash,kay,防止指令重排序
使用volatile修饰value,next
UnSafe.getObjectVolatile.
put和get操作实现
- put方法
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
首先是通过 key 定位到 Segment,之后在对应的 Segment 中进行具体的 put。
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
虽然 HashEntry 中的 value 是用 volatile 关键词修饰的,但是并不能保证并发的原子性,所以 put 操作时仍然需要加锁处理。
首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。
- 尝试自旋获取锁。
- 如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。
- 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。
- 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
- 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
- 最后会解除在 1 中所获取当前 Segment 的锁。
get方法
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。
由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。
ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。
- 1.7 以前,segment是直接初始化的。当构造完chm后,16个segment都被new 出来了,而在1.7时,是需要哪个用哪个。
- 此时,就涉及到一个可见性的点,
CHM 1.8 实现
1.7 已经解决了并发问题,但是在查询效率上,会出现hashMap在1.7中遇到的同样问题,可能会退化成O(n)
在1.8中直接摒弃了Segment分段锁,而采用了CAS+synchronized来保证并发安全性。
put 方法
- 根据 key 计算出 hashcode 。
- 判断是否需要进行初始化。
- f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
- 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
- 如果都不满足,则利用 synchronized 锁写入数据。
- 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。
get方法
- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
- 如果是红黑树那就按照树的方式获取值。
- 就不满足那就按照链表的方式遍历获取值。
CHM如何计数
- JDK5-7基于段元素个数求和,如果两次求和不同,二次不同就加锁。
- jdk1.8引入CounterCell,本质上也是分段加锁。
CHM是弱一致性
- 添加元素后不一定马上能读到
- 清空之后可能仍然会有元素
- 遍历之前的段元素的变化会读到
- 遍历之后的段元素变化读不到
- 遍历时元素发生变化不抛异常
hashtable的问题
- 大锁:对整个hashTable对象加锁
- 长锁:直接对方法加锁
- 读写锁共用:只有一把锁,从头到尾
CHM的解法
- 小锁: 分段锁(5-7),桶节点锁(8)
- 短锁:先尝试获取锁,失败在加锁
- 分离读写锁:失败在加锁(5-7),volatile读,cas写(7-8)
如何进行锁优化
- 长锁不如短锁:尽可能只锁必要的部分
- 大锁不如小锁: 尽可能对加锁的对象拆分
- 公锁不如私锁:尽可能将锁的逻辑放到私有代码中。
- 嵌套锁不如扁平锁
- 分离读写锁: 尽可能将读锁和写锁分离。
- 粗化高频锁: 尽可能合并处理频繁过短的锁
- 消除无用锁:尽可能不加锁,或用volatile代替。
AtomicReference和AtomicRefereFiledUpdater有何异同
涉及知识点
- 原子操作的概念
- 是否熟悉AR和ARFU这两个类的用法和原理
- java对象的内存占用的认识
android 中如何写出优雅的代码
考察点
- 是否熟练编写异步和同步的代码
- 是否书序回调地狱
- 是否熟练使用RxJava
- 是否对Kotlin协程有了解
- 是否具备编写良好的代码的意识和能力
异步的目的是什么
- 提高CPU利用率
- 提高GUI程序的响应速度
- 异步不一定快
为了解决异步回调地狱的问题:有Rxjava