共享带来的问题

两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

static int counter = 0;
public static void main(String[] args) throws InterruptedException {
 
Thread t1
= new Thread(() -> {   for (int i = 0; i < 5000; i++) {     counter++;    } }, "t1");
Thread t2
= new Thread(() -> {   for (int i = 0; i < 5000; i++) {     counter--; } }, "t2");
t1.start(); t2.start(); t1.join(); t2.join(); log.debug(
"{}",counter); }

以上的结果可能是正数、负数、零。为什么呢?因为 Java 中 对静态变量的 自增,自减并不是原子操作

例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i

而对应 i-- 也是类似:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i

出现负数的情况:

 出现正数的情况:

临界区 Critical Section

  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源
    • 多个线程读共享资源其实也没有问题
    • 多个线程对共享资源读写操作时发生指令交错,就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

竞态条件 Race Condition

多个线程临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

 

synchronized

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量

注意

虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

解决

注意为了保护同一个资源,应该对同一个对象加锁

static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1
= new Thread(() -> {   for (int i = 0; i < 5000; i++) {     synchronized (room) {     counter++;   } } }, "t1");
Thread t2
= new Thread(() -> {   for (int i = 0; i < 5000; i++) {     synchronized (room) {     counter--;   } } }, "t2");
t1.start(); t2.start(); t1.join(); t2.join(); log.debug(
"{}",counter); }

优化面向对象改进

把需要保护的共享变量放入一个类

class Room {
 int value = 0;
 public void increment() {
   synchronized (this) {
     value++;
   }
 }
 public void decrement() {
   synchronized (this) {
     value--;
   }
 }
 public int get() {
   synchronized (this) {
     return value;
   }
 }
}
@Slf4j
public class Test1 { public static void main(String[] args) throws InterruptedException {   Room room = new Room();
   Thread t1
= new Thread(() -> {     for (int j = 0; j < 5000; j++) {       room.increment();     }   }, "t1");
  Thread t2
= new Thread(() -> {     for (int j = 0; j < 5000; j++) {      room.decrement();   }   }, "t2");   t1.start();   t2.start();   t1.join();   t2.join();   log.debug("count: {}" , room.get()); } }

一、方法上的 synchronized

sychronized 只能锁对象。加再方法上,等于锁住了 this 对象

class Test{
 public synchronized void test() {
 
 }
}
等价于
class Test{
 public void test() {
   synchronized(this) {
 
   }
 }
}

二、加在静态方法上的 synchronized

sychronized 只能锁对象。加在静态方法上,等于锁住了类对象(类对象只有一个)

  • 静态方法:是使用static关键字修饰的方法,又叫类方法.属于类的,不属于对象, 在实例化对象之前就可以通过类名.方法名调用静态方法。 (静态属性,静态方法都是属于类的,可以直接通过类名调用)。
  • 非静态方法:是不含有static关键字修饰的普通方法,又称为实例方法,成员方法。属于对象的,不属于类的。(成员属性,成员方法是属于对象的,必须通过new关键字创建对象后,再通过对象调用)。
class Test{
 public synchronized static void test() {
 }
}
等价于
class Test{
 public static void test() {
   synchronized(Test.class) {
 
   }
 }
}

 例子: n1 和 n2 虽然是两个不同的对象,但是 a 和 b 都是静态方法被修饰。所以 n1.a() 和 n2.b() 锁住的都是同一个类对象 Number.class , 有互斥

class Number{
   public static synchronized void a() {
     sleep(1);
     log.debug("1");
   }
   public static synchronized void b() {
     log.debug("2");
   }
}
public static void main(String[] args) {   Number n1 = new Number();   Number n2 = new Number();   new Thread(()->{ n1.a(); }).start();   new Thread(()->{ n2.b(); }).start(); }

 

 

变量的线程安全分析

一、成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

二、局部变量是否线程安全?

  • 局部变量是线程安全的
  • 但局部变量引用的对象则未必
    • 如果该对象没有逃离方法的作用访问,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全

三、局部变量线程安全分析

相比静态变量一般都是安全的,因为是每个线程创建一个新对象

图一:List 作为 static 全局变量,对其进行读写操作(add remove)

图一:List 作为方法内的局部变量,对其进行读写操作(add remove)

 局部变量不安全的情况

新起了线程对局部变量进行操作,这样这个局部变量对于 原线程 和 新线程 来说是共享资源,就带来了线程安全的问题

(将局部变量暴露了出去?)

从这个例子可以看出 private 或 final 提供【安全】的意义所在,请体会开闭原则中的【闭】

class ThreadSafe {
   public final void method1(int loopNumber) {
     ArrayList<String> list = new ArrayList<>();
     for (int i = 0; i < loopNumber; i++) {
       method2(list);
       method3(list);
     }
   }
   private void method2(ArrayList<String> list) {
     list.add("1");
   }
   public void method3(ArrayList<String> list) {
     list.remove(0);
   }
}
class ThreadSafeSubClass extends ThreadSafe{
   @Override
   public void method3(ArrayList<String> list) {
     new Thread(() -> {
       list.remove(0);
     }).start();
   }
}

 

三 常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类
  • (Date SimpleDateFormat 不是线程安全的)

这里说它们是线程安全的是指,多个线程调用它们同一个实例的同个方法时,是线程安全的。也可以理解为

它们的每个方法是原子的

Hashtable table = new Hashtable();
new Thread(()->{
   table.put("key", "value1");
}).start();
new Thread(()->{   table.put("key", "value2"); }).start();

线程安全类方法的组合

它们多个方法的组合不是原子的

Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
 table.put("key", value);
}

不可变类线程安全性

String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的

如 String 的 substring replace 方法都是重新返回一个新创建的 String 对象

public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }
public String(char value[], int offset, int count) {
    if (offset < 0) {
        throw new StringIndexOutOfBoundsException(offset);
    }
    if (count <= 0) {
        if (count < 0) {
            throw new StringIndexOutOfBoundsException(count);
        }
        if (offset <= value.length) {
            this.value = "".value;
            return;
        }
    }
    // Note: offset or count might be near -1>>>1.
    if (offset > value.length - count) {
        throw new StringIndexOutOfBoundsException(offset + count);
    }
    this.value = Arrays.copyOfRange(value, offset, offset+count);
}

无状态(没有成员变量)的类一般是线程安全的

 

sychronized 原理、锁升级

见《sychronized 原理,锁升级优化

 

wait/notify

一、wait / notify 原理

  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
  • BLOCKED 是线程在等待获取锁,而 WAITING 是获取锁后又调用 wait() 方法放弃了锁(只有获取了 sychronized 锁的对象,也就是说在同步代码块内,才可以调用 wait(),否则会报错
  • BLOCKED 线程会在 Owner 线程释放锁时唤醒
  • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争

二、API 介绍

  • obj.wait() 让进入 object Monitor 的线程(Monitor 的 owner)释放持有的锁(不再是 owner),到 waitSet 进行无时限的等待
  • obj.wait() 有时限的等待
  • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒,唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争
  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒,唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争

它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须先获得此对象的锁(在 sychronized 同步代码块内),才能调用这几个方法,否则运行时会报错

new Thread(() -> {
   synchronized (room) {
     log.debug("有烟没?[{}]", hasCigarette);
     if (!hasCigarette) {
       log.debug("没烟,先歇会!");
       try {
         room.wait();
       } catch (InterruptedException e) {
         e.printStackTrace();
       }
     }
     log.debug("有烟没?[{}]", hasCigarette);
     if (hasCigarette) {
       log.debug("可以开始干活了");
     } else {
      log.debug("没干成活...");
     }
   }
  }, "小南").start();


new Thread(() -> {   synchronized (room) {     Thread thread = Thread.currentThread();     log.debug("外卖送到没?[{}]", hasTakeout);     if (!hasTakeout) {       log.debug("没外卖,先歇会!");       try {         room.wait();       } catch (InterruptedException e) {         e.printStackTrace();       }     }     log.debug("外卖送到没?[{}]", hasTakeout);     if (hasTakeout) {       log.debug("可以开始干活了");     } else {       log.debug("没干成活...");     }   } }, "小女").start(); sleep(1); new Thread(() -> {   synchronized (room) {     hasTakeout = true;     log.debug("外卖到了噢!");     room.notifyAll();    } }, "送外卖的").start();

 

三、sleep(long n) 和 wait(long n) 的区别

  • sleep 是 Thread 方法,而 wait 是 Object 的方法
  • sleep 不需要强制和 synchronized 配合使用,但 wait 需要 和 synchronized 一起用
  • sleep 在睡眠的同时,不会释放对象锁的,wait 在等待的时候会释放对象锁

共同点:

进入 sleep 或 wait 后状态都是 TIMED_WAITING

wait / notify 的正确姿势

step1:用 sleep 等待烟(有烟才能干活)

  • 其它干活的线程,都要一直阻塞,效率太低
  • 小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来
  • 加了 synchronized (room) 后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main 没加 synchronized 就好像 main 线程是翻窗户进来的
  • 解决方法,使用 wait - notify 机制

step2:用 wait - notify 机制(烟到了去notify)

  • 解决了其它干活的线程阻塞的问题
  • 但如果有其它线程也在等待条件呢?

step3: 有多个等待的线程(有个在等烟有个在等外卖),用 wait - notify (外卖到了去notify)

  • notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线 程,称之为【虚假唤醒】
  • 解决方法,改为 notifyAll

step4: 有多个等待的线程(有个在等烟有个在等外卖),用 wait - notifyAll (外卖到了去notify)

  • 用 notifyAll 仅解决多个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新【继续等待 & 直到被正确条件唤醒】的机会了
  • 解决方法,用 while + wait,当条件不成立,再次 wait

step5:将 if 改为 while

if (!hasCigarette) {
   log.debug("没烟,先歇会!");
   try {
     room.wait();
   } catch (InterruptedException e) {
     e.printStackTrace();
   }
}

改为

while (!hasCigarette) {
   log.debug("没烟,先歇会!");
   try {
     room.wait();
   } catch (InterruptedException e) {
     e.printStackTrace();
   }
}

模式

synchronized(lock) {
   while(条件不成立) {
     lock.wait();
   }
   // 干活
}


//另一个线程 synchronized(lock) {   lock.notifyAll(); }

四、join() 原理

回忆用法:可以在 t2 里面通过调用 t1.join() 来等待 t1 运行完毕

就是用了 wait()/notify() 的保护性暂停原理

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

五、阻塞队列原理

可以基于 wait/notify 来实现

class Message {
   private int id;
   private Object message;

   public Message(int id, Object message) {
     this.id = id;
     this.message = message;
   }
 
  public int getId() {
     return id;
  }

   public Object getMessage() {
     return message;
    }
}

class MessageQueue {
   private LinkedList<Message> queue;
   private int capacity;
 
  public MessageQueue(int capacity) {
     this.capacity = capacity;
     queue = new LinkedList<>();
   }

   public Message take() {
     synchronized (queue) {
      //队列空,取出操作阻塞
      while (queue.isEmpty()) {
         log.debug("没货了, wait");
         try {
           queue.wait();
         } catch (InterruptedException e) {
           e.printStackTrace();
         }
      }
      //队列不空了,可以取出了
       Message message = queue.removeFirst();
      //通知等待的所有线程(因为刚取出,所以因为队列满而put等待的线程会被真正唤醒)
      queue.notifyAll();
      return message;
    }
 }

 public void put(Message message) {
   synchronized (queue) {
      //队列满,放入操作阻塞
     while (queue.size() == capacity) {
       log.debug("库存已达上限, wait");
       try {
         queue.wait();
       } catch (InterruptedException e) {
         e.printStackTrace();
       }
     }
    //队列不空了,可以取出了
     queue.addLast(message);
    //通知等待的所有线程(因为刚取出,所以因为队列空而take等待的线程会被真正唤醒)
    queue.notifyAll();
   }
 }
}

 

park/unpark

一、基本使用

它们是 LockSupport 类中的方法

// 暂停当前线程
LockSupport.park(); 
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
  • 先 park 再被 unpark:park() 时阻塞,当别的线程调用了 unpark(t1) 后会将 t1 唤醒
  • 先被 unpark 再 park:park() 时不会阻塞
Thread t1 = new Thread(() -> {
 log.debug("start...");
 sleep(1);
 log.debug("park...");
 LockSupport.park();
 log.debug("resume...");
},"t1");
t1.start();
sleep(3);
log.debug("unpark...");
LockSupport.unpark(t1);

输出

18:42:52.585 c.TestParkUnpark [t1] - start... 
18:42:53.589 c.TestParkUnpark [t1] - park... 
18:42:54.583 c.TestParkUnpark [main] - unpark... 
18:42:54.583 c.TestParkUnpark [t1] - resume... 

特点:

与 Object 的 wait & notify 相比

  • wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必 
  • unpark 是以线程为单位来【唤醒】线程,notify 只能随机唤醒一个等待线程,notifyAll唤醒所有等待线程,就不那么【精确】
  • park & unpark 可以先 unpark(后面park不会阻塞),而 wait & notify 不能先 notify

二、原理

1.先 park 再被 unpark(t1)

  1. 当前线程调用 Unsafe.park() 方法
  2. 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁
  3. 线程进入 _cond 条件变量阻塞 
  4. 设置 _counter = 0

  1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1(counter 最多到1)
  2. 唤醒 _cond 条件变量中的 Thread_0
  3. Thread_0 恢复运行
  4. 设置 _counter 为 0

2.先被 unpark(t1) 再 park

  1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
  2. 当前线程调用 Unsafe.park() 方法
  3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
  4. 设置 _counter 为 0

 

 

线程状态转换

假设有线程 Thread t

情况 1 NEW --> RUNNABLE

当调用 t.start() 方法时,由 NEW --> RUNNABLE

情况 2 RUNNABLE <--> WAITING

t 线程用 synchronized(obj) 获取了对象锁后

  • 调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING
  • 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
    • 竞争锁成功,t 线程从 WAITING --> RUNNABLE
    • 竞争锁失败,t 线程从 WAITING --> BLOCKED

情况 3 RUNNABLE <--> WAITING

  • 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING
    • 注意是当前线程在t 线程对象的监视器上等待
  • t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE

情况 4 RUNNABLE <--> WAITING

  • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE

情况 5 RUNNABLE <--> TIMED_WAITING

obj.wait(long n) ,情况2的有时限版,只是退出的条件多了一个等待时间超过了 n 毫秒

情况 6 RUNNABLE <--> TIMED_WAITING

 t.join(long n) ,情况3的有时限版,只是退出的条件多了一个等待时间超过了 n 毫秒

情况 7 RUNNABLE <--> TIMED_WAITING

  • 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
  • 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE

情况 8 RUNNABLE <--> TIMED_WAITING

LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) ,情况4的有时限版,只是退出的条件多了一个等待时间超过了 n 毫秒

情况 9 RUNNABLE <--> BLOCKED

  • t 线程用 synchronized(obj) 获取对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
  • 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED

情况 10 RUNNABLE <--> TERMINATED

当前线程所有代码运行完毕,进入 TERMINATE

 

多把锁与活跃性

1.死锁问题

为了并发度更高,将锁的粒度设置得更小后,如果一个线程需要同时获取多把锁时,就会容易产生死锁

互相持有并等待

避免死锁要注意加锁顺序

另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下

可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查

定位死锁

检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:

cmd > jstack 33200
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
2018-12-29 05:51:40
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.91-b14 mixed mode):
"DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x0000000003525000 nid=0x2f60 waiting on condition 
[0x0000000000000000]
 java.lang.Thread.State: RUNNABLE
"Thread-1" #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting for monitor entry 
[0x000000001f54f000]
 java.lang.Thread.State: BLOCKED (on object monitor)
 at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)
 - waiting to lock <0x000000076b5bf1c0> (a java.lang.Object)
 - locked <0x000000076b5bf1d0> (a java.lang.Object)
 at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source)
 at java.lang.Thread.run(Thread.java:745)
"Thread-0" #11 prio=5 os_prio=0 tid=0x000000001eb68800 nid=0x1b28 waiting for monitor entry 
[0x000000001f44f000]
 java.lang.Thread.State: BLOCKED (on object monitor)
 at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)
 - waiting to lock <0x000000076b5bf1d0> (a java.lang.Object)
- locked <0x000000076b5bf1c0> (a java.lang.Object)
 at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source)
 at java.lang.Thread.run(Thread.java:745)
 
// 略去部分输出
Found one Java-level deadlock:
=============================
"Thread-1":
 waiting to lock monitor 0x000000000361d378 (object 0x000000076b5bf1c0, a java.lang.Object),
 which is held by "Thread-0"
"Thread-0":
 waiting to lock monitor 0x000000000361e768 (object 0x000000076b5bf1d0, a java.lang.Object),
 which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
 at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)
 - waiting to lock <0x000000076b5bf1c0> (a java.lang.Object)
 - locked <0x000000076b5bf1d0> (a java.lang.Object)
 at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source)
 at java.lang.Thread.run(Thread.java:745)
"Thread-0":
 at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)
 - waiting to lock <0x000000076b5bf1d0> (a java.lang.Object)
 - locked <0x000000076b5bf1c0> (a java.lang.Object)
 at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source)
 at java.lang.Thread.run(Thread.java:745)
Found 1 deadlock.

哲学家就餐问题

如果每个哲学家都同时拿起左手的筷子,那么就形成了环形依赖,在这种特殊的情况下,每个人都拿着左手的筷子,都缺少右手的筷子,那么就没有人可以开始吃饭了,自然也就没有人会放下手中的筷子。这就陷入了死锁,形成了一个相互等待的情况。

多种解决方案
(1)服务员检查
第一个解决方案就是引入服务员检查机制。比如我们引入一个服务员,当每次哲学家要吃饭时,他需要先询问服务员:我现在能否去拿筷子吃饭?此时,服务员预先判断他拿筷子有没有发生死锁的可能,假如有的话,服务员会说:现在不允许你吃饭。这是一种解决方案

(2)领导调节
死锁检测和恢复策略,可以引入一个领导,这个领导进行定期巡视。如果他发现已经发生死锁了,就会剥夺某一个哲学家的筷子,让他放下。这样一来,由于这个人的牺牲,其他的哲学家就都可以吃饭了。这也是一种解决方案

(3)改变一个哲学家拿筷子的顺序
我们还可以利用死锁避免策略,那就是从逻辑上去避免死锁的发生,比如改变其中一个哲学家拿筷子的顺序。我们可以让 4 个哲学家都先拿左边的筷子再拿右边的筷子,但是有一名哲学家与他们相反,他是先拿右边的再拿左边的,这样一来就不会出现循环等待同一边筷子的情况,也就不会发生死锁了

ABCDE 五个哲学家,他们中间12345无双筷子,都是先拿左手边再拿右手边,那么顺序是:

A 1 2  --- B 2 3 --- C 3 4 --- D 4 5 --- E 5 1

改变 E 的拿筷子顺序,由 5 1 变为 1 5

A 1 2  --- B 2 3 --- C 3 4 --- D 4 5 --- E 1 5

(4)锁超时

获取不到锁就直接放弃,或者等待一段时间后没获得再放弃。如用 ReentranedLock 的 tryLock() 方法

@Override
 public void run() {
   while (true) {
     // 尝试获得左手筷子
     if (left.tryLock()) {
       try {
         // 尝试获得右手筷子
         if (right.tryLock()) {
           try {
             eat();
           } finally {
             right.unlock();
           }
         }
       } finally {
         left.unlock();
       }
     }
   }
 }

2.活锁问题

活锁出现在两个线程互相改变对方的结束条件最后谁也无法结束,例如

解决:

引入随机性

public class TestLiveLock {
  
static volatile int count = 10;
  
static final Object lock = new Object();
  
public static void main(String[] args) {     new Thread(() -> {       // 期望减到 0 退出循环       while (count > 0) {         sleep(0.2);         count--;         log.debug("count: {}", count);       }     }, "t1").start();
    new Thread(() -> {     // 期望超过 20 退出循环     while (count < 20) {       sleep(0.2);       count++;       log.debug("count: {}", count);     }    }, "t2").start();    } }

3.饥饿问题

很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不 易演示,讲读写锁时会涉及饥饿问题。

在实际情况中,如果不是顺序加锁,容易死锁,如线程1先对A加锁再对B加锁,线程2先对B加锁再对A加锁,那么这种情况就容易发生死锁

而如果改为顺序加锁,都是先对A加锁再对B加锁,则容易出现饥饿问题:看看他举的哲学家就餐例子

ABCDE 五个哲学家,他们中间12345无双筷子,都是先拿左手边再拿右手边,那么顺序是:

A 1 2  --- B 2 3 --- C 3 4 --- D 4 5 --- E 5 1

改变 E 的拿筷子顺序,由 5 1 变为 1 5

A 1 2  --- B 2 3 --- C 3 4 --- D 4 5 --- E 1 5

 

 

ReentrantLock

相对于 synchronized 它具备如下特点

  • 可打断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量

与 synchronized 一样,都支持可重入

基本用法

// 获取锁
reentrantLock.lock();
try {
 // 临界区
} finally {
 // 释放锁
 reentrantLock.unlock();
}

可重入

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁

如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

可打断

我在进入 Blocked list 等待锁的过程中,别的线程可以用 interupt() 方法打断我的等待(sychronized不可打断,reentrantLock.lock() 不可打断,reentrantLock.lockInterruptibly() 才可以打断)

被打断后会抛出 InteruptException 异常

可以说是一种 被动地避免死等的手段

锁超时

lock.tryLock() 没有参数的话,如果没获得锁,立刻返回false

有超时参数的话,等待一段时间后还没获得锁,再返回false

ReentrantLock lock = new ReentrantLock();
  Thread t1 = new Thread(() -> {
   log.debug("启动...");
   try {
     if (!lock.tryLock(1, TimeUnit.SECONDS)) {
       log.debug("获取等待 1s 后失败,返回");
       return;
     }
     } catch (InterruptedException e) {
       e.printStackTrace();
     }
     try {
       log.debug("获得了锁");
     } finally {
       lock.unlock();
     }
  }, "t1");

  lock.lock();   log.debug(
"获得了锁");   t1.start();   try {   sleep(2);   } finally {   lock.unlock(); }

用 tryLock 解决哲学家就餐问题

 @Override
 public void run() {
   while (true) {
     // 尝试获得左手筷子
     if (left.tryLock()) {
       try {
         // 尝试获得右手筷子
         if (right.tryLock()) {
           try {
             eat();
           } finally {
             right.unlock();
           }
         }
       } finally {
         left.unlock();
       }
     }
   }
 }

公平锁

非公平锁:之前持有锁的线程释放了锁,那么所有在 entryList 的线程一拥而上,谁先抢到了谁就是主人,而不是谁先到谁就先得

公平锁:阻塞队列里的线程抢锁时,是按进入阻塞的时间先入先得的顺序获得锁

本意是为了减少饥饿问题,但是 tryLock 可能更好。一般不会使用公平锁,因为会降低并发度(后面原理会讲?)

ReentrantLock 默认是非公平锁,如果要是公平锁要显式设置

ReentrantLock lock = new ReentrantLock(true);

条件变量

synchronized 中也有条件变量,就是获取了 sychronized 获取锁后可以调用 .wait() ,当条件不满足时进入 waitSet 等待

ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比 synchronized 是那些不满足条件的线程都在一间休息室等消息

而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

与 sychronized 配合的 wait() 类似,使用要点:

  • await 前需要获得 ReentrantLock 锁
  • await 执行后,会释放锁,进入 conditionObject 等待
  • await 的线程被 .signal() .signalAll() 唤醒(或打断、或超时)时重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行

用法:

lock 有一点就是一定要在 finally 里释放,lock 和 unlock 成对出现

static ReentrantLock roomlock = new ReentrantLock();
static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitbreakfastQueue = lock.newCondition();
static volatile boolean hasCigrette = false;
static volatile boolean hasBreakfast = false;

public static void main(String[] args) {   new Thread(() -> {     try {       roomlock.lock();       while (!hasCigrette) {         try {          waitCigaretteQueue.await();        } catch (InterruptedException e) {          e.printStackTrace();        }       }       log.debug("等到了它的烟");     } finally {       lock.unlock();     }   }).start();
  new Thread(() -> {     try {       roomlock.lock();       while (!hasBreakfast) {         try {           waitbreakfastQueue.await();         } catch (InterruptedException e) {           e.printStackTrace();         }       }       log.debug("等到了它的早餐");    } finally {       lock.unlock();     }   }).start();
  sleep(
1);   sendBreakfast();   sleep(1);   sendCigarette(); }
private static void sendCigarette() {   roomlock.lock();   try {     log.debug("送烟来了");     hasCigrette = true;     waitCigaretteQueue.signal();   } finally {     lock.unlock();   } }
private static void sendBreakfast() {   roomlock.lock();   try {     log.debug("送早餐来了");     hasBreakfast = true;     waitbreakfastQueue.signal();   } finally {     lock.unlock();   } }

 

本章小结

Monitor 有的也被翻译为管程。sychronized 是 jvm 用 c++ 使用的 monitor ,ReentrandLock 是用 java 语言使用的 Monitor

本章我们需要重点掌握的是

  • 分析多线程访问共享资源时,哪些代码片段属于临界区
    • 使用 synchronized 互斥解决临界区的线程安全问题
    • 掌握 synchronized 锁对象语法
    • 掌握 synchronzied 加在成员方法和静态方法语法(成员方法锁 this 对象,静态方法锁 class 对象)
  • 掌握 wait/notify 同步方法
    • 使用 lock 互斥解决临界区的线程安全问题
    • 掌握 lock 的使用细节:可打断(.lockInterruptibly())、锁超时(.tryLock())、公平锁、条件变量(reentrandLock 比 sychronized 的强处)
  • 学会分析变量的线程安全性、掌握常见线程安全类的使用
  • 了解线程活跃性问题:死锁、活锁、饥饿
  • 应用方面
    • 互斥:使用 synchronized 或 Lock 达到共享资源互斥效果
    • 同步:使用 wait/notify (sychronized住的对象.wait() sychronized住的对象.notify())或 Lock 的条件变量(condition = reentrandLock.newCondition() condition.await() condition.signal())来达到线程间通信效果
  • 原理方面
    • monitor、synchronized 、wait/notify 原理
    • synchronized 进阶原理
    • park & unpark 原理
  • 模式方面
    • 同步模式之保护性暂停
    • 异步模式之生产者消费者
    • 同步模式之顺序控制