Runnable 和 Callable:
Runnable:只是一个接口,代表了一个任务,和线程没有任何关系
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
Runnable 的限制:
- 不能返回值
- 不能抛出 check exception
Callable 就没有上述限制,不过 Callable 不能通过 new Thread() 方式来运行,只能使用线程池 submit,并返回异步结果 Future。
Thread 中断:
TODO
局部变量中的引用
Stack 是线程私有的,所以任何基本数据类型的局部变量都是线程私有的。
但对于引用类型的局部变量,比如对象或数组,都是在 Heap 堆上分配的(栈上分配除外)。
由于堆是线程共享的,所以这些变量会存在线程安全问题。
竞争条件:
多个线程在访问共享变量的时候没有同步,导致一个线程在操作共享变量时,该变量被其他线程改变了。
例1:原子性
i++,并不是原子的,因为它是先从内存读取值,然后将值加一,然后再写回内存。
设 i = 0,两个线程同时执行 i++,预期最后 i 会等于 2,但是如果两个线程同时将值从内存读出,为 0,然后将值加成 1,再写回内存,最终 i 会等于 1,和预期不同。
例2:懒汉单例模式
先判断是否为空,是的话执行一次 new。
多线程下,多个线程都判断 INSTANCE 为空,然后都执行了 new 操作,返回的 INSTANCE 就不是同一个了。
例3:ConcurrentHashMap
即使使用了线程安全容器 ConcurrentHashMap,但如果多个操作之间没有同步,那也不是线程安全的。
解决:
使用不可变对象:
- String、Integer、BigDecimal
反例:java.util.Date 是可变对象
使用锁:
- synchronized
- Lock
使用 JUC:
- 底层:CAS
- int/long:AtomicInteger/Long
- int[]/long[]:AtomicIntegerArray/AtomicLongArray
- Object:AtomicReference
- HashMap:ConcurrentHashMap
- ArrayList:CopyOnWriteArrayList
- TreeMap:ConcurrentSkipListMap
CAS:
Compare And Set/Swap:比较并交换
从对象中获取属性值 oldV,
然后调用 compareAndSwapInt,对比当前对象中该属性的值是否与上一次读取的值 oldV 一样,是的话就把新的值 set 进去,这里使用的是一个底层汇编提供原子操作,cmpxchg
public final class Unsafe {
// 为了方便查看,重命名了变量
public final int getAndSetInt(Object obj, long field, int newV) {
int oldV;
do {
oldV = this.getIntVolatile(obj, field);
} while(!this.compareAndSwapInt(obj, field, oldV, newV));
return oldV;
}
}
ABA问题:
在读取 oldV 和 compareAndSet 的过程中,变量的值被改了多次,最后被改回到 oldV,所以这次 CAS 会成功。
解决:加入版本号,AtomicStampedReference
// TODO 分离
死锁:
写一个死锁:
static Object lock1 = new Object();
static Object lock2 = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (lock1) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("t1");
}
}
}).start();
synchronized (lock2) {
Thread.sleep(500);
synchronized (lock1) {
System.out.println("main");
}
}
}
使用 jstack 查看栈信息:
"Thread-0":
at com.example.demo.DeathLock.lambda$main$0(DeathLock.java:17)
- waiting to lock <0x000000076b77e6d0> (a java.lang.Object)
- locked <0x000000076b77e6c0> (a java.lang.Object)
at com.example.demo.DeathLock$$Lambda$1/1225358173.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"main":
at com.example.demo.DeathLock.main(DeathLock.java:25)
- waiting to lock <0x000000076b77e6c0> (a java.lang.Object)
- locked <0x000000076b77e6d0> (a java.lang.Object)
Found 1 deadlock.
死锁相关问题:哲学家就餐问题
JMM
Java 内存模型和线程规范:
- 为了发挥多核 CPU 的潜能,每个线程都会持有主内存中变量的副本。即工作内存(CPU 高速缓存)与主内存之间。
- 以至于,一个线程修改了一个共享变量的值,而其他线程看到的却是旧的值
解决变量的可见性:volatile
volatile
- 可见性
- 禁止指令重排序
- 不保证原子性
可见性:
- 写入 volatile 变量,会直接写入主内存,并让其他线程的工作内存中的变量副本失效
- 如果线程工作内存中的变量副本失效后,就会重新从主内存读取
禁止指令重排序: