Runnable 和 Callable:

Runnable:只是一个接口,代表了一个任务,和线程没有任何关系

  1. public Thread(Runnable target) {
  2. init(null, target, "Thread-" + nextThreadNum(), 0);
  3. }

Runnable 的限制:

  • 不能返回值
  • 不能抛出 check exception

Callable 就没有上述限制,不过 Callable 不能通过 new Thread() 方式来运行,只能使用线程池 submit,并返回异步结果 Future。


Thread 中断:

TODO


局部变量中的引用

image.png
Stack 是线程私有的,所以任何基本数据类型局部变量都是线程私有的。
但对于引用类型局部变量,比如对象或数组,都是在 Heap 堆上分配的(栈上分配除外)。
由于堆是线程共享的,所以这些变量会存在线程安全问题。


竞争条件:

多个线程在访问共享变量的时候没有同步,导致一个线程在操作共享变量时,该变量被其他线程改变了。

例1:原子性

i++,并不是原子的,因为它是先从内存读取值,然后将值加一,然后再写回内存。
设 i = 0,两个线程同时执行 i++,预期最后 i 会等于 2,但是如果两个线程同时将值从内存读出,为 0,然后将值加成 1,再写回内存,最终 i 会等于 1,和预期不同。

例2:懒汉单例模式

image.png
先判断是否为空,是的话执行一次 new。
多线程下,多个线程都判断 INSTANCE 为空,然后都执行了 new 操作,返回的 INSTANCE 就不是同一个了。

例3:ConcurrentHashMap

image.png
即使使用了线程安全容器 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

  1. public final class Unsafe {
  2. // 为了方便查看,重命名了变量
  3. public final int getAndSetInt(Object obj, long field, int newV) {
  4. int oldV;
  5. do {
  6. oldV = this.getIntVolatile(obj, field);
  7. } while(!this.compareAndSwapInt(obj, field, oldV, newV));
  8. return oldV;
  9. }
  10. }

ABA问题:
在读取 oldV 和 compareAndSet 的过程中,变量的值被改了多次,最后被改回到 oldV,所以这次 CAS 会成功。

解决:加入版本号,AtomicStampedReference
// TODO 分离


死锁:

写一个死锁:

  1. static Object lock1 = new Object();
  2. static Object lock2 = new Object();
  3. public static void main(String[] args) throws InterruptedException {
  4. new Thread(() -> {
  5. synchronized (lock1) {
  6. try {
  7. Thread.sleep(500);
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. }
  11. synchronized (lock2) {
  12. System.out.println("t1");
  13. }
  14. }
  15. }).start();
  16. synchronized (lock2) {
  17. Thread.sleep(500);
  18. synchronized (lock1) {
  19. System.out.println("main");
  20. }
  21. }
  22. }

使用 jstack 查看栈信息:

  1. "Thread-0":
  2. at com.example.demo.DeathLock.lambda$main$0(DeathLock.java:17)
  3. - waiting to lock <0x000000076b77e6d0> (a java.lang.Object)
  4. - locked <0x000000076b77e6c0> (a java.lang.Object)
  5. at com.example.demo.DeathLock$$Lambda$1/1225358173.run(Unknown Source)
  6. at java.lang.Thread.run(Thread.java:748)
  7. "main":
  8. at com.example.demo.DeathLock.main(DeathLock.java:25)
  9. - waiting to lock <0x000000076b77e6c0> (a java.lang.Object)
  10. - locked <0x000000076b77e6d0> (a java.lang.Object)
  11. Found 1 deadlock.

死锁相关问题:哲学家就餐问题


JMM

image.png

Java 内存模型和线程规范:

  • 为了发挥多核 CPU 的潜能,每个线程都会持有主内存中变量的副本。即工作内存(CPU 高速缓存)与主内存之间。
  • 以至于,一个线程修改了一个共享变量的值,而其他线程看到的却是旧的值

解决变量的可见性:volatile

volatile

  • 可见性
  • 禁止指令重排序
  • 不保证原子性

可见性:

  • 写入 volatile 变量,会直接写入主内存,并让其他线程的工作内存中的变量副本失效
  • 如果线程工作内存中的变量副本失效后,就会重新从主内存读取

禁止指令重排序: