一、共享带来的问题

1.临界区

  • 当多个线程同时对共享资源进行读写操作时会发生指令交错,导致数据错误。
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

例如下面的例子:

  1. package panw.monitor;
  2. import lombok.extern.slf4j.Slf4j;
  3. @Slf4j
  4. public class Test1{
  5. public static int count;
  6. public static void main(String[] args) {
  7. Thread t1 = new Thread(() -> {
  8. for (int i = 0; i < 10000; i++) {
  9. count++;
  10. }
  11. });
  12. Thread t2 = new Thread(() -> {
  13. for (int i = 0; i < 10000; i++) {
  14. count--;
  15. }
  16. });
  17. t1.start();
  18. t2.start();
  19. try {
  20. t1.join();
  21. t2.join();
  22. } catch (InterruptedException e) {
  23. throw new RuntimeException(e);
  24. }
  25. log.debug("count={}",count);
  26. }
  27. }

最终的结果并不是0,而是不断变化的未知值。

2.竞态条件

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

二、synchronized 解决方案

1.解决方案

为了避免临界区竞态条件发生,有下面几种方法实现。

  • 阻塞:synchronized 、Lock锁
  • 非阻塞: cas、原子变量

本章课使用阻塞式的解决方案:**synchronized**,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一 时刻至多只有一个线程能持有【对象锁】,其他线程想要再获取该【对象锁】就会阻塞,保证了拥有锁的线程可以安全执行完临界区代码。

2.synchronized语法

例:

  1. package panw.monitor;
  2. import lombok.extern.slf4j.Slf4j;
  3. @Slf4j
  4. public class Test1 {
  5. public static int count;
  6. public static final Object lock = new Object();
  7. public static void main(String[] args) {
  8. Thread t1 = new Thread(() -> {
  9. for (int i = 0; i < 10000; i++) {
  10. synchronized (lock) {
  11. count++;
  12. }
  13. }
  14. });
  15. Thread t2 = new Thread(() -> {
  16. for (int i = 0; i < 10000; i++) {
  17. synchronized (lock) {
  18. count--;
  19. }
  20. }
  21. });
  22. t1.start();
  23. t2.start();
  24. try {
  25. t1.join();
  26. t2.join();
  27. } catch (InterruptedException e) {
  28. throw new RuntimeException(e);
  29. }
  30. log.debug("count={}", count);
  31. }
  32. }

3.synchronized加在方法上

加在普通方法上:

  1. package panw.monitor;
  2. public class SynchronizedTest {
  3. public synchronized void add() {
  4. System.out.println("add");
  5. }
  6. //等价于
  7. public void add() {
  8. synchronized (this) {
  9. System.out.println("add");
  10. }
  11. }
  12. }

加在静态方法上:

  1. package panw.monitor;
  2. public class SynchronizedTest {
  3. public synchronized static void add() {
  4. System.out.println("add");
  5. }
  6. //等价于
  7. public static void add() {
  8. synchronized (SynchronizedTest.class) {
  9. System.out.println("add");
  10. }
  11. }
  12. }

三、变量的线程安全分析

1.成员变量和静态变量是否线程安全?

  • 如果他们没有被多个线程共享,则线程安全
  • 如果他们被共享了,又分两种情况

    • 只有读操作,线程安全
    • 如果有读有写,则该代码时临界区,需要考虑线程安全问题

      2.局部变量是否线程安全?

  • 局部变量时线程安全的

  • 如果局部变量引用的对象则有可能又线程安全问题
  • 每个方法都会在各自栈的栈帧中创建局部变量表,不会被其他线程共享

    3.常见线程安全类

  • String

  • Integer
  • StringBuffer
  • Random
  • Vector (List的线程安全实现类)
  • Hashtable (Hash的线程安全实现类)
  • java.util.concurrent 包下的类

    4.不可变类线程安全性

    String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
    有同学或许有疑问,Stringreplacesubstring等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?
    这是因为这些方法的返回值都创建了一个新的对象,而不是直接改变StringInteger对象本身。

    四、Monitor

    原理

    共享模型之管程 - 图1

  • 当线程执行到临界区代码时,如果使用了synchronized,会先查询synchronized中绑定的对象是否绑定了一个Monitor

    • 如果没有绑定,则先去和Monitor进行绑定,并且将Owner设为当前线程
    • 如果已经绑定,则会去查询该Monitor是否已经又Owner
      • 如果没有,Owner与当前线程绑定
      • 如果有,则将该线程放进EntryList,进入阻塞状态(Blocked)
  • MonitorOwner将临界区中代码执行完毕后,Owner便会被清空,此时EntryList中处于阻塞状态的线程会被叫醒并竞争。
  • 注意

    • 对象在使用了synchronized后与Monitor绑定时,会将对象头中的**Mark Word**置为Monitor指针。
    • 每个对象都会绑定一个唯一的Monitor,如果synchronized中所指定的对象(obj)不同,则会绑定不同的Monitor

      五、Synchronized原理进阶

      每个对象都有一个对象头,就类似IP数据报格式,对象头包含Mark WordClass Pointer 分别占64个字节。
  • MarkWord 保存对象自身的运行时数据。

  • Class Pointer 存储对象的类型指针,该指针指向它的类元数据

    1.Mark Word格式

    这里我们主要来看一下Mark Word的格式:
    Mark Word.png
    解释一下上面的具体意思:

  • unused:代表还未使用

  • hashcode:该对象的hashcode
  • age:涉及JVM中的垃圾回收,指分代的年龄
  • biased_lock:偏向锁标记
  • thread:当前得到锁线程
  • epoch:偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
  • ptr_to_lock_record:轻量级锁中指向线程栈中Lock Record的指针
  • ptr_to_heavyweight_monitor:指向互斥锁(重量级锁)的指针

    2.轻量级锁

    使用场景:当一个对象被多个线程所访问,但访问的时间是错开的(不存在竞争),此时就可以使用轻量级锁来优化

  • 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录对象,内部可以存储锁定对象的Mark Word(不再一开始就使用Monitor)

共享模型之管程 - 图3

  • 让锁记录中的Object Reference指向锁对象(Object),并尝试用cas去替换Object中的Mark Word,将此Mark Word放入Lock Record中保存

注:cas(compare and swap)是一种原子操作,现在你只需要知道它可以将数据进行原子性数据交换即可。
共享模型之管程 - 图4

  • 如果cas替换成功,则将Object的对象头替换为锁记录的地址状态 00(轻量级锁状态),并由该线程给对象加锁

共享模型之管程 - 图5

  • 如果cas替换替换失败即对象头已经是00这时还有两种情况
    • 如果是其他线程已经持有了Object的轻量级锁,这表明当前又竞争,进入锁膨胀过程。
    • 如果是自己执行了synchronized锁重入,那么会在添加一条Lock Word作为冲入的计数,该Lock Work锁记录的地址null

共享模型之管程 - 图6

  • 当退出synchronized代码块(解锁时)如果又取值为null的锁记录,标识有重入,这时重置锁记录直接将该Lock Record清掉,重入计数减一
  • 当退出synchronized代码块(解锁时)锁记录不为null,这时使用cas将Mark Work恢复给对象头

    • 成功,则解锁成功
    • 失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

      3.锁膨胀

  • 如果一个线程在给一个对象加轻量级锁时,cas替换操作失败(因为此时其他线程已经给对象加了轻量级锁),此时该线程就会进入锁膨胀,将轻量级锁变成重量级锁

共享模型之管程 - 图7

  • 此时便会给对象加上重量级锁(使用Monitor)
    • 为Object对象申请Monitor锁,将对象头的Mark Word改为Monitor的地址,并且状态改为10(重量级锁)
    • 并且自己进入EntryList中,并进入阻塞状态(blocked)

共享模型之管程 - 图8

  • Thread-0退出代码块解锁时,先尝试使用cas将Mark Word的值恢复给对象头,失败,这时进入重量级锁解锁流程,即,通过Monitor地址找到Monitor,然后将Owner置为null,唤醒EntryList中Blocked的线程

    4.自旋锁

    重量级锁竞争时,为了提高cpu利用率避免,减少线程上下文切换,可以使用自旋锁来进行优化。
    自旋锁指 自己在获取锁失败时进行自旋,如果在自旋过程中使用锁线程退出同步块,释放了锁,就可以不用进入阻塞状态。

    5.偏向锁

    轻量级锁在没有竞争时,每次重入操作都需要进行cas操作,降低性能。
    所以引入了偏向锁对性能进行优化:在第一次cas时会将线程的ID写入对象的Mark Word中。此后发现这个线程ID就是自己的,就表示没有竞争,就不需要再次cas,以后只要不发生竞争,这个对象就归该线程所有。

  • 如果开启了偏向锁(默认开启),在创建对象时,对象的Mark Word后三位应该是101

  • 但是偏向锁默认是有延迟的,不会再程序一启动就生效,而是会在程序运行一段时间(几秒之后),才会对创建的对象设置为偏向状态
  • 如果没有开启偏向锁,对象的Mark Word后三位应该是001

    撤销偏向

    以下几种情况会使对象的偏向锁失效

  • 调用对象的hashCode方法

  • 多个线程使用该对象
  • 调用了wait/notify方法(调用wait方法会导致锁膨胀而使用重量级锁
  • JVM设置:-XX:- UseBiasedLocking=false

    六、Wait/Notify

    1.原理

    共享模型之管程 - 图9

  • 锁对象调用wait方法(obj.wait),就会使当前线程进入WaitSet中,变为WAITING状态。

  • 处于BLOCKED和WAITING状态的线程都为阻塞状态,CPU都不会分给他们时间片。但是有所区别:
    • BLOCKED状态的线程是在竞争对象时,发现MonitorOwner已经是别的线程了,此时就会进入EntryList中,并处于BLOCKED状态
    • WAITING状态的线程是获得了对象的锁,但是自身因为某些原因需要进入阻塞状态时,锁对象调用了wait方法而进入了WaitSet中,处于WAITING状态
  • BLOCKED状态的线程会在锁被释放的时候被唤醒,但是处于WAITING状态的线程只有被锁对象调用了notify方法(obj.notify/obj.notifyAll),才会被唤醒。

    2.Wait/Notify使用

    注:只有当对象被锁以后,才能调用wait和notify方法 ```java package panw.monitor;

public class WaitNotifyTest { final static Object lock = new Object(); static int count; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { while (true) { synchronized (lock) { try { count ++; System.out.println(“t1 count = “ + count); lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }); t1.start(); Thread.sleep(2000); synchronized (lock) {

  1. lock.notify();
  2. }
  3. }

}

  1. <a name="oew4k"></a>
  2. ### 3.Wait与Sleep的区别
  3. <a name="eoxGi"></a>
  4. #### 不同点
  5. - Sleep是Thread类的静态方法,Wait是Object的方法,Object又是所有类的父类,所以所有类都有Wait方法。
  6. - Sleep在阻塞的时候不会释放锁,而Wait在阻塞的时候会释放锁
  7. - Sleep不需要与synchronized一起使用,而Wait需要与synchronized一起使用(对象被锁以后才能使用)
  8. <a name="MFQIi"></a>
  9. #### 相同点
  10. - 阻塞状态都为**TIMED_WAITING**
  11. <a name="onSXk"></a>
  12. ### 4.虚假唤醒
  13. 当有**多个**线程在运行时,对象调用了wait方法,此时这些线程都会进入WaitSet中等待。如果这时使用了**notify**方法,可能会造成**虚假唤醒**(唤醒的不是满足条件的等待线程),这时就需要使用**notifyAll**方法,并进行虚假唤醒的处理。
  14. <a name="PIfw3"></a>
  15. ## 七、模式之保护性暂停
  16. <a name="WRgAL"></a>
  17. ### 1.定义
  18. 用在一个线程等待另一个线程执行结果。<br />如下图:<br />![](https://cdn.nlark.com/yuque/0/2022/jpeg/28810082/1653008406826-290714e8-3778-497b-99b3-e61cd9c89611.jpeg)
  19. <a name="pCoSE"></a>
  20. ### 2.具体代码实现
  21. ```java
  22. package panw.model;
  23. import lombok.extern.slf4j.Slf4j;
  24. import java.util.HashMap;
  25. @Slf4j
  26. public class Guarded {
  27. public static void main(String[] args) {
  28. HashMap<String, String> map = new HashMap<>();
  29. map.put("a", "b");
  30. GuardedObject guardedObject = new GuardedObject();
  31. Thread t1 = new Thread(() -> {
  32. log.debug("Thread 1 started");
  33. try {
  34. Thread.sleep(5000);
  35. } catch (InterruptedException e) {
  36. }
  37. log.debug("Thread 1 send message"+map);
  38. guardedObject.setResponse(map);
  39. });
  40. Thread t2 = new Thread(() -> {
  41. log.debug("Thread 2 started");
  42. Object response = guardedObject.getResponse();
  43. log.debug("Response: {}", response);
  44. });
  45. t1.start();
  46. t2.start();
  47. }
  48. }
  49. class GuardedObject {
  50. private Object response;
  51. public Object getResponse() {
  52. while (response == null) {
  53. synchronized (this) {
  54. try {
  55. this.wait();
  56. } catch (InterruptedException e) {
  57. e.printStackTrace();
  58. }
  59. }
  60. }
  61. return response;
  62. }
  63. public void setResponse(Object response) {
  64. synchronized (this) {
  65. this.response = response;
  66. this.notifyAll();
  67. }
  68. }
  69. @Override
  70. public String toString() {
  71. return "GuardedObject{" +
  72. "response=" + response +
  73. '}';
  74. }
  75. }

3.超时判断

但是政策情况下,如果一个线程没获得另一个线程结果不可能一直死等下去,因此需要进行超时判断,如下进行getResponse()的修改:

  1. public Object getResponse(long timeout) {
  2. long base = System.currentTimeMillis();
  3. long passTime = 0;
  4. while (response == null) {
  5. long waitTime = timeout - passTime;
  6. if (waitTime <=0) {
  7. break;
  8. }
  9. synchronized (this) {
  10. try {
  11. this.wait(waitTime);
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. }
  16. passTime = System.currentTimeMillis() - base;
  17. }
  18. return response;
  19. }

4.join源码

看了以上的分析,你应该对wait/notify有了更深的了解,下面我们就来看看Threadjoin方法的源码是如何实现的。

  1. public final void join() throws InterruptedException {
  2. join(0);
  3. }
  4. public final synchronized void join(long millis)
  5. throws InterruptedException {
  6. long base = System.currentTimeMillis();
  7. long now = 0;
  8. if (millis < 0) {
  9. throw new IllegalArgumentException("timeout value is negative");
  10. }
  11. if (millis == 0) {
  12. while (isAlive()) {
  13. wait(0);
  14. }
  15. } else {
  16. while (isAlive()) {
  17. long delay = millis - now;
  18. if (delay <= 0) {
  19. break;
  20. }
  21. wait(delay);
  22. now = System.currentTimeMillis() - base;
  23. }
  24. }
  25. }
  26. public final synchronized void join(long millis, int nanos)
  27. throws InterruptedException {
  28. if (millis < 0) {
  29. throw new IllegalArgumentException("timeout value is negative");
  30. }
  31. if (nanos < 0 || nanos > 999999) {
  32. throw new IllegalArgumentException(
  33. "nanosecond timeout value out of range");
  34. }
  35. if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
  36. millis++;
  37. }
  38. join(millis);
  39. }

一共重载了三个方法,本质上都是调用join(long millis),具体实现和上面大同小异。

八、park/unpark

parkunpark方法是工具类LockSupport中的API,它的作用很简单,就是挂起和继续执行线程。

  1. //暂停线程运行
  2. LockSupport.park;
  3. //恢复线程运行
  4. LockSupport.unpark(thread);

1.简单使用

  1. package panw.monitor;
  2. import lombok.extern.slf4j.Slf4j;
  3. import java.util.concurrent.locks.LockSupport;
  4. @Slf4j
  5. public class ParkTest {
  6. public static void main(String[] args) throws InterruptedException {
  7. Thread t1 = new Thread(() -> {
  8. log.debug("t1 park");
  9. LockSupport.park();
  10. log.debug("t1 unpark");
  11. });
  12. t1.start();
  13. Thread.sleep(5000);
  14. LockSupport.unpark(t1);
  15. }
  16. }

2.与wait/notify区别

  • waitnotifynotifyAll必须配合**Object **``**Monitor**一起使用,而parkunpark不必
  • park ,unpark 是以线程为单位阻塞唤醒线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么精确
  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify

这个下面讲原理时会进行详细分析

  • park不会释放锁,而wait会释放锁

    3.原理

    LockSupport中的parkunpark方法时调用Unsafe类中的本地方法,我们具体来分析一下park对象:
    每个线程都有一个自己的Park对象,并且该对象_counter, _cond,__mutex组成
    _counter相当于一个标记位,_cond相当于阻塞队列

  • 线程启动时,会将_counter置为0;

  • 重要调用park时,查看_counter是否为0,如果是,就将线程放进阻塞队列_cond中,并将_counter再次置为0,如果之前已经调用过unpark方法,则_counter为1,那就直接将_counter置为0,不阻塞继续运行。
  • 调用unpark方法后,会将_counter的值设置为1
  • 去唤醒阻塞队列_cond中的线程
  • 线程继续运行并将_counter的值设为0

正常先调用park在调用unpark过程如下面两图:
共享模型之管程 - 图10
共享模型之管程 - 图11
下面是先调用unpark后调用park的过程:
共享模型之管程 - 图12

九、线程中的状态转换

这里就直接贴上图片,简单了解一下:
image.png

情况一:NEW –> RUNNABLE

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

    情况二: RUNNABLE <–> WAITING

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

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

        情况三:RUNNABLE <–> WAITING

  • 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE –> WAITING

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

    情况四: RUNNABLE <–> WAITING

  • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE –> WAITING

  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING –> RUNNABLE

    情况五: RUNNABLE <–> TIMED_WAITING

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

  • 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE –> TIMED_WAITING

  • t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时

    • 竞争锁成功,t 线程从 TIMED_WAITING –> RUNNABLE
    • 竞争锁失败,t 线程从 TIMED_WAITING –> BLOCKED

      情况六:RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE –> TIMED_WAITING

    • 注意是当前线程在t 线程对象的监视器上等待
  • 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING –> RUNNABLE

    情况七:RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE –> TIMED_WAITING

  • 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING –> RUNNABLE

    情况八:RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线 程从 RUNNABLE –> TIMED_WAITING

  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING–> RUNNABLE

    情况九:RUNNABLE <–> BLOCKED

  • t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE –> BLOCKED

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

    情况十: RUNNABLE <–> TERMINATED

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

    十、活跃性

    因为某种原因,使得代码一直无法执行完毕,这样的现象叫做活跃性

    1.死锁

    有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
    如: ```java package panw.monitor;

public class DeadLock { private static final Object lock1 = new Object(); private static final Object lock2 = new Object();

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

}

  1. 这里就涉及到一个经典的**哲学家就餐问题,**它可以这样表述,假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌上每两位哲学家之间有一只餐叉,哲学家吃东西必须且只能使用左右手两边的两只餐叉。<br />假设某一时刻,所有的哲学家都想要吃东西,他们同时拿起了各自左手边的餐叉,然后当他们想要拿起右手边的餐叉时,发现已经被右边的哲学家拿起来了,如果每一个哲学家都不同意放弃自己已经拿到手的餐叉,则整个系统就无法运行下去了,出现了死锁。
  2. ```java
  3. package panw.monitor;
  4. import lombok.extern.slf4j.Slf4j;
  5. import java.util.Random;
  6. public class Test {
  7. public static void main(String[] args) {
  8. Fork a = new Fork("a");
  9. Fork b = new Fork("b");
  10. Fork c = new Fork("c");
  11. Fork d = new Fork("d");
  12. Fork e = new Fork("e");
  13. Philosopher aristotle = new Philosopher("Aristotle", a, b);
  14. Philosopher socrates = new Philosopher("Socrates", b, c);
  15. Philosopher plato = new Philosopher("Plato", c, d);
  16. Philosopher kant = new Philosopher("Kant", d, e);
  17. Philosopher sartre = new Philosopher("Sartre", e, a);
  18. new Thread(aristotle).start();
  19. new Thread(socrates).start();
  20. new Thread(plato).start();
  21. new Thread(kant).start();
  22. new Thread(sartre).start();
  23. }
  24. }
  25. @Slf4j
  26. class Philosopher implements Runnable {
  27. private final String name;
  28. private final Fork leftFork;
  29. private final Fork rightFork;
  30. public Philosopher(String name, Fork leftFork, Fork rightFork) {
  31. this.name = name;
  32. this.leftFork = leftFork;
  33. this.rightFork = rightFork;
  34. }
  35. //思考
  36. public void think() {
  37. try {
  38. // log.debug("{} is thinking {} s", name,2);
  39. Thread.sleep(500);
  40. this.eat();
  41. } catch (InterruptedException e) {
  42. e.printStackTrace();
  43. }
  44. }
  45. //吃
  46. public void eat() {
  47. synchronized (leftFork) {
  48. synchronized (rightFork) {
  49. try {
  50. log.debug("{} is eating {} s", name,1);
  51. Thread.sleep(1000);
  52. } catch (InterruptedException e) {
  53. }}
  54. }
  55. this.think();
  56. }
  57. @Override
  58. public void run() {
  59. int k= new Random().nextInt(10);
  60. if(k%2==0){
  61. this.eat();
  62. }else{
  63. this.think();
  64. }
  65. }
  66. }
  67. class Fork{
  68. String name;
  69. public Fork(String name) {
  70. this.name = name;
  71. }
  72. public String getName() {
  73. return name;
  74. }
  75. public void setName(String name) {
  76. this.name = name;
  77. }
  78. }

2.活锁

活锁出现在两个线程互相改变对方的结束条件,后谁也无法结束。
这里做一个简单的演示,但是下面的修改并不是线程安全的,因为这是非原子操作 :

  1. package panw.monitor;
  2. import lombok.extern.slf4j.Slf4j;
  3. @Slf4j
  4. public class LiveLock {
  5. private static final Object lock = new Object();
  6. private static volatile int count=10;
  7. public static void main(String[] args) {
  8. new Thread(()->{
  9. while (count>0){
  10. log.debug(Thread.currentThread().getName()+" count:"+count);
  11. try {
  12. Thread.sleep(1000);
  13. } catch (InterruptedException e) {
  14. throw new RuntimeException(e);
  15. }
  16. count--;
  17. }
  18. }).start();
  19. new Thread(()->{
  20. while (count<100){
  21. log.debug(Thread.currentThread().getName()+" count:"+count);
  22. try {
  23. Thread.sleep(1000);
  24. } catch (InterruptedException e) {
  25. throw new RuntimeException(e);
  26. }
  27. count++;
  28. }
  29. }).start();
  30. }
  31. }

十一、ReentrantLock

1.和synchronized相比具有的的特点

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁 (先到先得)
  • 支持多个条件变量( 具有多个waitset)

    2.基本用法

    1. //获取ReentrantLock对象
    2. private ReentrantLock lock = new ReentrantLock();
    3. //加锁
    4. lock.lock();
    5. try {
    6. //需要执行的代码
    7. }finally {
    8. //释放锁
    9. lock.unlock();
    10. }

    1)可重入

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

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

    2)可打断

    如果某个线程处于阻塞状态,可以调用其interrupt方法让其停止阻塞,获得锁失败 ```java package panw.monitor;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantLock; @Slf4j public class ReentrantLockTest {

  1. public static void main(String[] args) throws InterruptedException {
  2. ReentrantLock lock = new ReentrantLock();
  3. Thread t1 = new Thread(() -> {
  4. try {
  5. lock.lockInterruptibly();
  6. log.debug("t1 get lock");
  7. Thread.sleep(1000000);
  8. } catch (InterruptedException e) {
  9. log.debug("t1 interrupted");
  10. e.printStackTrace();
  11. } finally {
  12. lock.unlock();
  13. }
  14. });
  15. Thread t2 = new Thread(() -> {
  16. try {
  17. lock.lockInterruptibly();
  18. log.debug("t2 get lock");
  19. while (true) {
  20. log.debug("t2 running");
  21. }
  22. } catch (InterruptedException e) {
  23. log.debug("t2 interrupted");
  24. e.printStackTrace();
  25. return;
  26. } finally {
  27. lock.unlock();
  28. }
  29. });
  30. t1.start();
  31. t2.start();
  32. Thread.sleep(100);
  33. t2.interrupt();
  34. }

}

  1. <a name="m4sH1"></a>
  2. #### 3)锁超时
  3. 使用`**lock.tryLock**`方法会返回获取锁是否成功。如果成功则返回true,反之则返回false。<br />并且tryLock方法可以**指定等待时间**,参数为:`tryLock(long timeout, TimeUnit unit)`, 其中timeout为最长等待时间,TimeUnit为时间单位<br />**简而言之**就是:获取失败了、获取超时了或者被打断了,不再阻塞,直接停止运行
  4. ```java
  5. package panw.monitor;
  6. import lombok.extern.slf4j.Slf4j;
  7. import java.util.concurrent.locks.ReentrantLock;
  8. @Slf4j
  9. public class ReentrantLockTest {
  10. public static void main(String[] args) throws InterruptedException {
  11. ReentrantLock lock = new ReentrantLock();
  12. lock.lock();
  13. Thread t1 = new Thread(() -> {
  14. try {
  15. if(!lock.tryLock()){
  16. log.debug("t1 get lock failed");
  17. return;
  18. }
  19. log.debug("t1 get lock");
  20. } finally {
  21. lock.unlock();
  22. }
  23. });
  24. t1.start();
  25. Thread.sleep(1000);
  26. lock.unlock();
  27. }

4)公平锁

在构造函数设为true

  1. //默认是不公平锁,需要在创建时指定为公平锁
  2. ReentrantLock lock = new ReentrantLock(true);

5)重要条件变量

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比

  • synchronized 是那些不满足条件的线程都在一间休息室等消息
  • 而 ReentrantLock 支持多间休息室,有专门打游戏的休息室、专门吃东西的休息室、唤醒时也是按休息室来唤醒

使用要点:

  • await前需要获得锁
  • await执行后,会释放锁,进入 conditionObject 等待
  • 使用signalawait的线程唤醒(或打断、或超时)重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await后继续执行 ```java package panw.monitor;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; @Slf4j public class ReentrantLockTest {

  1. static volatile boolean flag = false;
  2. public static void main(String[] args) throws InterruptedException {
  3. ReentrantLock lock = new ReentrantLock();
  4. Condition c1 = lock.newCondition();
  5. new Thread(()->{
  6. lock.lock();
  7. try {
  8. while (!flag){
  9. log.debug("不满足条件flag={},等待...",flag);
  10. c1.await();
  11. log.debug("满足条件flag={},继续执行...",flag);
  12. }
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. } finally{
  16. lock.unlock();
  17. }
  18. }).start();
  19. Thread.sleep(1000);
  20. flag= true;
  21. lock.lock();
  22. try {
  23. c1.signal();
  24. }finally {
  25. lock.unlock();
  26. }
  27. }
  1. <a name="NCsxv"></a>
  2. ## 十二、同步模式之顺序控制
  3. 学到这了,应该对这章有了清醒的认识,那下面就具体来实践一下!
  4. <a name="WTyI2"></a>
  5. ### 首先是使用`wait&notify`来实现交替输出:
  6. ```java
  7. package panw.model;
  8. import lombok.extern.slf4j.Slf4j;
  9. public class SequentialControlWaitNotify {
  10. public static void main(String[] args) {
  11. Symbol symbol = new Symbol(3);
  12. new Thread(()->{
  13. symbol.run("a",1,2);
  14. }).start();
  15. new Thread(()->{
  16. symbol.run("b",2,3);
  17. }).start();
  18. new Thread(()->{
  19. symbol.run("c",3,1);
  20. }).start();
  21. }
  22. }
  23. @Slf4j
  24. class Symbol {
  25. private int flag = 1;
  26. private int loopNum;
  27. public Symbol(int loopNum) {
  28. this.loopNum = loopNum;
  29. }
  30. public synchronized void run(String str,int flag,int nextFlag){
  31. for (int i = 0; i < loopNum; i++) {
  32. while (flag!=this.flag){
  33. try {
  34. this.wait();
  35. } catch (InterruptedException e) {
  36. e.printStackTrace();
  37. }
  38. }
  39. System.out.print(str+"\t");
  40. this.flag = nextFlag;
  41. this.notifyAll();
  42. }
  43. }
  44. public int getFlag() {
  45. return flag;
  46. }
  47. public void setFlag(int flag) {
  48. this.flag = flag;
  49. }
  50. public int getLoopNum() {
  51. return loopNum;
  52. }
  53. public void setLoopNum(int loopNum) {
  54. this.loopNum = loopNum;
  55. }
  56. }

使用await&signal实现:

  1. package panw.model;
  2. import java.util.concurrent.locks.Condition;
  3. import java.util.concurrent.locks.ReentrantLock;
  4. public class SequentialControlAwaitSignal {
  5. static AwaitSignal awaitSignal = new AwaitSignal(3);
  6. static Condition c1 = awaitSignal.newCondition();
  7. static Condition c2 = awaitSignal.newCondition();
  8. static Condition c3 = awaitSignal.newCondition();
  9. public static void main(String[] args) {
  10. new Thread(()-> awaitSignal.run("a",c1,c2)).start();
  11. new Thread(()-> awaitSignal.run("b",c2,c3)).start();
  12. new Thread(()-> awaitSignal.run("c",c3,c1)).start();
  13. try {
  14. Thread.sleep(1000);
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. }
  18. awaitSignal.lock();
  19. try {
  20. //唤醒一个等待的线程
  21. c1.signal();
  22. }finally {
  23. awaitSignal.unlock();
  24. }
  25. }
  26. }
  27. class AwaitSignal extends ReentrantLock {
  28. private int loopNum;
  29. public AwaitSignal(int loopNum) {
  30. this.loopNum = loopNum;
  31. }
  32. public void run(String str, Condition condition, Condition nextCondition) {
  33. for (int i = 0; i < loopNum; i++) {
  34. lock();
  35. try {
  36. condition.await();
  37. System.out.print(str+"\t");
  38. nextCondition.signal();
  39. } catch (InterruptedException e) {
  40. throw new RuntimeException(e);
  41. } finally {
  42. unlock();
  43. }
  44. }
  45. }
  46. public int getLoopNum() {
  47. return loopNum;
  48. }
  49. public void setLoopNum(int loopNum) {
  50. this.loopNum = loopNum;
  51. }
  52. }

十三、ThreadLocal(待写)

1.定义

ThreadLocal并不是一个Thread,而是Thread的局部变量

2.作用

ThreadLocal是解决线程安全的另一种途径,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

3.源码分析

三个类关系

我们先看看Thread、ThreadLocal、ThreadLocalMap之间的关系:

Thread
  1. public
  2. class Thread implements Runnable {
  3. ThreadLocal.ThreadLocalMap threadLocals = null;
  4. ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
  5. }

ThreadLocal
  1. public class ThreadLocal<T> {
  2. static class ThreadLocalMap {
  3. static class Entry extends WeakReference<ThreadLocal<?>> {
  4. Object value;
  5. Entry(ThreadLocal<?> k, Object v) {
  6. super(k);
  7. value = v;
  8. }
  9. }
  10. }
  11. }

从上述源码进一步证实了每个线程都有一个成员变量,即ThreadLocalMap;


下面来看看ThreadLocal具体的方法:

get

  1. public T get() {
  2. Thread t = Thread.currentThread(); //得到当前线程
  3. ThreadLocalMap map = getMap(t); //获取当前线程的成员变量threadLocals
  4. if (map != null) {
  5. ThreadLocalMap.Entry e = map.getEntry(this); 、、通过当前对象获得值
  6. if (e != null) {
  7. @SuppressWarnings("unchecked")
  8. T result = (T)e.value;
  9. return result;
  10. }
  11. }
  12. return setInitialValue(); //设置初始值
  13. }
  14. //ThreadLocal中的getMap
  15. ThreadLocalMap getMap(Thread t) {
  16. return t.threadLocals;
  17. }
  18. //ThreadLocal中的setInitialValue
  19. private T setInitialValue() {
  20. T value = initialValue(); //获得初始值,如果没被重写则尾null
  21. Thread t = Thread.currentThread();
  22. ThreadLocalMap map = getMap(t);
  23. if (map != null) {
  24. map.set(this, value);
  25. } else {
  26. createMap(t, value); //创建ThreadLocalMap
  27. }
  28. if (this instanceof TerminatingThreadLocal) {
  29. TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
  30. }
  31. return value;
  32. }
  33. //ThreadLocal中的createMap
  34. void createMap(Thread t, T firstValue) {
  35. t.threadLocals = new ThreadLocalMap(this, firstValue); //将Thread类中的threadLocals进行初始化
  36. }

set

  1. public void set(T value) {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t); //获取ThreadLocalMap
  4. if (map != null) {
  5. map.set(this, value); //设置map的kv值
  6. } else {
  7. createMap(t, value);
  8. }
  9. }

4.内存泄漏

5.强弱引用