《Java 并发编程》共享模型之管程

简介: 《Java 并发编程》共享模型之管程

🚀1. 共享带来的问题

🚁1.1 临界区

一个程序运行多个线程本身没有问题

问题出在多个线程访问共享资源

(1)多个线程读共享资源其实也没有问题

(2)在多个线程对共享资源读写操作时发生指令交错,就会出现问题

一段代码内如果存在对共享资源的多线程读写操作,称这块代码块为临界区

例如,下面代码中的临界区

static int counter = 0
static void increment() {
  // 临界区
  counter++;
}
static void decrement() {
  // 临界区
  counter--;
}

🚁1.2 竞态条件

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

🚀2. synchronized 解决方案

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


阻塞式的解决方案:synchronized、lock

非阻塞式的解决方案:原子变量

这里使用阻塞式的解决方案:synchronized 来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻最多只有一个线程能持有【对象锁】,其他线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心上下文切换。


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


互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码

同步是由于线程执行的先后、顺序不同,需要一个线程等待其他线程运行到某个点

🚁2.1 synchronized 语法

synchronized(对象) {   //线程1,线程2(blocked)
  临界区
}

案例代码

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); 
}

可以做这样的类比:


synchronized 中的对象,可以想象为一个房间,有唯一入口房间只能一次进入一个人进行计算,线程 t1 和 t2 想象成两个人

当线程 t1 执行到 synchronized 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行 counter++

这时如果 t2 也运行到了 synchronized 时,它发现门锁住了,只能在门外等待,发生了线程上下文切换,阻塞住了

这中间即使 t1 的 CPU 时间片不幸用完,被踢出了门外,这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片才能开门进入

当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 counter-- 代码

🚁2.2 synchronized 加在方法上

  1. 加在成员方法上
public class Test {
  //在方法上加上synchronized关键字
  public synchronized void test() {
  }
  //等价于
  public void test() {
    synchronized(this) {
    }
  }
}

加在静态方法上

public class Test {
  //在静态方法上加上synchronized关键字
  public synchronized static void test() {
  }
  //等价于
  public void test() {
    synchronized(Test.class) {
    }
  }
}

🚀3. 变量的线程安全分析

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


如果它们没有共享,则线程安全

如果它们被共享了,根据它们的状态是否能够改变,又分为两种情况

(1)如果只有读操作,则线程安全

(2)如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否安全?


局部变量是线程安全的

但局部变量引用的对象则未必线程安全

(1) 如果该对象没有逃离方法的作用访问,它是线程安全的

(2) 如果该对象逃离方法的作用范围,需要考虑线程安全

局部变量线程安全性分析

public static void test1() {
  int i = 10;
  i++;
}

每个线程调用 test1() 方法时,局部变量 i 会在每个线程的栈帧内存中被创建多份,因此不存在共享,是线程安全的。

2e718f6de6d94987ae2f2182c528d9c8.png

然而,局部变量的引用却有所不同,先看一个成员变量的例子

public class ThreadUnsafe {
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;
    ArrayList<String> list = new ArrayList<>();
    public void method1(int loopNumber) {
        for (int i = 0; i < loopNumber; i++) {
            //{临界区,会产生竞态条件
            method2();
            method3();
            //}
        }
    }
    private void method2() {
        list.add("1");
    }
    private void method3() {
        list.remove(0);
    }
    public static void main(String[] args) {
        ThreadUnsafe test = new ThreadUnsafe();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            },"Thread" + i).start();
        }
    }
}

运行之后,可能有一种情况,method2 还未 add,method3 便开始 remove 就会报错:

Exception in thread "Thread0" Exception in thread "Thread1" java.lang.ArrayIndexOutOfBoundsException: -1
  at java.util.ArrayList.remove(ArrayList.java:507)
  at com.czh.concurrent.ThreadUnsafe.method3(ThreadUnsafe.java:26)
  at com.czh.concurrent.ThreadUnsafe.method1(ThreadUnsafe.java:18)
  at com.czh.concurrent.ThreadUnsafe.lambda$main$0(ThreadUnsafe.java:33)
  at java.lang.Thread.run(Thread.java:748)
java.lang.ArrayIndexOutOfBoundsException: -1
  at java.util.ArrayList.add(ArrayList.java:465)
  at com.czh.concurrent.ThreadUnsafe.method2(ThreadUnsafe.java:23)
  at com.czh.concurrent.ThreadUnsafe.method1(ThreadUnsafe.java:17)
  at com.czh.concurrent.ThreadUnsafe.lambda$main$0(ThreadUnsafe.java:33)
  at java.lang.Thread.run(Thread.java:748)

分析:


无论哪个线程中的 method2,引用的都是同一个对象中的 list 成员变量

method3 与 method2 分析相同

2e718f6de6d94987ae2f2182c528d9c8.png

将 list 修改为局部变量

public class ThreadUnsafe {
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;
    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");
    }
    private void method3(ArrayList<String> list) {
        list.remove(0);
    }
    public static void main(String[] args) {
        ThreadUnsafe test = new ThreadUnsafe();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            },"Thread" + i).start();
        }
    }
}

分析:


list 是局部变量,每个线程调用时会创建其不同实例,没有共享

而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象

method3 的参数分析与 method2 相同

2e718f6de6d94987ae2f2182c528d9c8.png

方法修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会带来线程安全?


情况1:有其他线程调用 method2 和 method3

情况2:在情况1的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即

public class ThreadUnsafe {
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;
    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);
    }
    public static void main(String[] args) {
        ThreadUnsafe test = new ThreadUnsafe();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            },"Thread" + i).start();
        }
    }
}
class ThreadSafeSubClass extends ThreadUnsafe {
    @Override
    public void method3(ArrayList<String> list) {
        new Thread(()->{
            list.remove(0);
        });
    }
}

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


常见线程安全类


String

Integer

StringBuffer

Random

Vector (List的线程安全实现类)

Hashtable(Hash的线程安全实现类)

java.util.concurrent 包下的类

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

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);
}

2e718f6de6d94987ae2f2182c528d9c8.png


不可变类线程安全性


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

String 有 replace,substring 等方法【可以】改变值,那么这些方法又是如何保证线程安全的呢?

这是因为这些方法的返回值都创建了一个新的对象,而不是直接改变 String、Integer 对象本身

2e718f6de6d94987ae2f2182c528d9c8.png

🚀4. Monitor 概念

2e718f6de6d94987ae2f2182c528d9c8.png

当线程执行到临界区代码时,如果使用了 synchronized,会先查询 synchronized 中所指定的对象 (obj) 是否绑定了 Monitor.


如果没有绑定,则会先去与 Monitor 绑定,并且将 Owner 设为当前线程

如果已经绑定,则会去查询该 Monitor 是否已经有了 Owner

(1) 如果没有,则将 Owner 与将当前线程绑定

(2) 如果有,则放入 EntryList,进入阻塞状态(blocked)

当 Monitor 的 Owner 将临界区中代码执行完毕后,Owner 便会被清空,此时 EntryList 中处于阻塞状态的线程会被叫醒并竞争,此时的竞争是非公平的

注意:


对象在使用了 synchronized 后与 Monitor 绑定时,会将对象头中的 Monitor Word 置为 Monitor 指针

每个对象都会绑定一个唯一的 Monitor,如果 synchronized 中所指定的对象 (obj) 不同,则会绑定不同的 Monitor

🚀5. synchronized 原理进阶

Java 对象头格式

64 位虚拟机 Mark Word 结构如下:

2e718f6de6d94987ae2f2182c528d9c8.png

🚁5.1 轻量级锁(用于优化 Monitor 这类的重量级锁)

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


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

2e718f6de6d94987ae2f2182c528d9c8.png

让锁记录中的 Object Reference 指向锁对象(Object),并尝试用 CAS 去替换 Object 中的mark word,将此 mark word 放入 lock record 中保存

2e718f6de6d94987ae2f2182c528d9c8.png

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

2e718f6de6d94987ae2f2182c528d9c8.png

🚁5.2 锁膨胀

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

2e718f6de6d94987ae2f2182c528d9c8.png

此时便会给对象加上重量级锁(使用 Monitor)

将对象头的 Mark Word 改为 Monitor 的地址,并且状态改为 01 (重量级锁)

并且该线程放入 EntryList 中,并进入阻塞状态 (blocked)

2e718f6de6d94987ae2f2182c528d9c8.png

🚁5.3 自旋优化

重量级锁竞争时,还可以使用自旋来优化,如果当前线程在自旋成功(使用锁的线程退出了同步块,释放了锁),这时就可以避免线程进入阻塞状态。


第一种情况

2e718f6de6d94987ae2f2182c528d9c8.png

第二种情况

2e718f6de6d94987ae2f2182c528d9c8.png

🚁5.4 偏向锁(用于优化轻量级锁重入)

轻量级锁在没有竞争时,每次重入(该线程执行的方法中再次锁住该对象)操作仍需要 CAS 替换操作,这样会导致性能降低。


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

2e718f6de6d94987ae2f2182c528d9c8.png

偏向状态


Normal:一般状态,没有加任何锁,前面 62 位保存的是对象的信息,最后 2 位为状态(01),倒数第 3 位表示是否使用偏向锁(未使用:0)

Biased:偏向状态,使用偏向锁,前面 54 位保存的当前线程的 ID,最后 2 位为状态(01),倒数第 3 位表示是否使用偏向锁(使用:1)

Lightweight:使用轻量级锁,前 62 位保存的是锁记录的指针,最后两位为状态(00)

Heavyweight:使用重量级锁,前 62 位保存的是 Monitor 的地址指针,后两位为状态(10)

2e718f6de6d94987ae2f2182c528d9c8.png

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

但是偏向锁默认是有延迟的,不会在程序一启动就生效,而是会在程序运行一段时间(几秒之后),才会对创建的对象设置为偏向状态

如果没有开启偏向锁,对象的 Mark Word 后三位应该是 001

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


调用对象的 hashCode 方法

多个线程使用该对象

调用了 wait/notify 方法(调用 wait 方法会导致锁膨胀而使用重量级锁)

🚁5.5 批量重偏向

如果对象虽然被多个线程访问,但是线程间不存在竞争,这时偏向 T1 的对象仍有机会重新偏向 T2,重偏向会重置Thread ID

当撤销超过 20 次后(超过阈值),JVM 会觉得是不是偏向错了,这时会在给对象加锁时,重新偏向至加锁线程

🚁5.6 批量撤销

当撤销偏向锁的阈值超过 40 以后,就会将整个类的对象都改为不可偏向的

🚀6. wait/notify

🚁6.1 原理

2e718f6de6d94987ae2f2182c528d9c8.png

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

处于 BLOCKED 和 WAITING 状态的线程都为阻塞状态,CPU 都不会分给他们时间片。但是有所区别:

BLOCKED 状态的线程是在竞争对象时,发现 Monitor 的 Owner 已经是别的线程了,此时就会进入 EntryList 中,并处于BLOCKED状态

然而,WAITING 状态的线程是获得了对象的锁,但是自身因为某些原因需要进入阻塞状态时,锁对象调用了 wait 方法而进入了 WaitSet 中,处于 WAITING 状态

BLOCKED 状态的线程会在锁被释放的时候被唤醒,但是处于 WAITING 状态的线程只有被锁对象调用了 notify 方法 (obj.notify/obj.notifyAll),才会被唤醒

wait 和 notify 都是线程之间进行协作的手段,都属于 Object 对象的方法,必须获得此对象的锁,才能调用这几个方法,示例代码如下:

public class Test {
    final static Object obj = new Object();
    public static void main(String[] args) {
        new Thread(()->{
            synchronized (obj) {
                System.out.println("执行...");
                try {
                    obj.wait(); //让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("其他代码...");
        }).start();
        new Thread(()->{
            synchronized (obj) {
                System.out.println("执行...");
                try {
                    obj.wait(); //让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("其他代码...");
            }
        }).start();
        //主线程两秒后执行
        sleep(2);
        System.out.println("唤醒 obj 上其他线程");
        synchronized (obj) {
            obj.notify();  //唤醒obj上一个线程
            //obj.notifyAll();  //唤醒obj上所有等待线程
        }
    }
}

🚁6.2 使用 wait/notify 的正确姿势

wait 和 sleep 的区别:

sleep 是 Thread 类的静态方法,wait 是 Object 的方法,Object 又是所有类的父类,所以所有类都有 wait 方法

sleep 在阻塞(睡眠)的时候不会释放锁,而 wait 在阻塞的时候会释放锁

sleep 不需要与 synchronized 一起使用,而 wait 需要与 synchronized 一起使用(对象被锁以后才能使用)

wait 与 sleep 的相同点:


阻塞状态都为 TIMED_WAITING

什么时候适合使用 wait


当线程不满足某些条件,需要暂停运行时,可以使用 wait,这样会将对象的锁释放,让其他线程能够继续运行。如果此时使用 sleep,会导致所有线程都进入阻塞,导致所有线程都没法运行,直到当前线程 sleep 结束后,运行完毕,才能得到执行

使用 wait/notify 的注意点


当有多个线程在运行时,对象调用了 wait 方法,此时这些线程都会进入 WaitSet 中等待。如果这时使用了 notify 方法,可能会造成虚假唤醒(唤醒的不是满足条件的等待线程),这时就需要使用 notifyAll 方法

synchronized (LOCK) {
  while(//不满足条件,一直等待,避免虚假唤醒) {
    LOCK.wait();
  }
  //满足条件后再运行
}
synchronized (LOCK) {
  //唤醒所有等待线程
  LOCK.notifyAll();
}

🚀7. 模式之保护性暂停

定义


保护性暂停(Guarded Suspension)用在一个线程等待另一个线程的执行结果。

要点:


有一个结果需要从一个线程传递到另一个线程,让它们关联同一个 Guarded Object

如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者-消费者)

JDK 中,join 的实现、Future 的实现,采用的就是 Guarded Suspension 模式

因为要等待另一方的结果,因此归类到同步模式

2e718f6de6d94987ae2f2182c528d9c8.png

案例代码如下

public class Test {
  public static void main(String[] args) {
    String hello = "hello thread!";
    Guarded guarded = new Guarded();
    new Thread(()->{
      System.out.println("想要得到结果");
      synchronized (guarded) {
        System.out.println("结果是:"+guarded.getResponse());
      }
      System.out.println("得到结果");
    }).start();
    new Thread(()->{
      System.out.println("设置结果");
      synchronized (guarded) {
        guarded.setResponse(hello);
      }
    }).start();
  }
}
class Guarded {
  /**
   * 要返回的结果
   */
  private Object response;
    //优雅地使用 wait/notify
  public Object getResponse() {
    //如果返回结果为空就一直等待,避免虚假唤醒
    while(response == null) {
      synchronized (this) {
        try {
          this.wait();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }
    return response;
  }
  public void setResponse(Object response) {
    this.response = response;
    synchronized (this) {
      //唤醒休眠的线程
      this.notifyAll();
    }
  }
  @Override
  public String toString() {
    return "Guarded{" +
        "response=" + response +
        '}';
  }
}

带超时判断的暂停

public Object getResponse(long time) {
  synchronized (this) {
    //获取开始时间
    long currentTime = System.currentTimeMillis();
    //用于保存已经等待了的时间
    long passedTime = 0;
    while(response == null) {
      //看经过的时间-开始时间是否超过了指定时间
      long waitTime = time - passedTime;
      if(waitTime <= 0) {
        break;
      }
      try {
                    //等待剩余时间
        this.wait(waitTime);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      //获取当前时间
      passedTime = System.currentTimeMillis()-currentTime   
           }
  }
  return response;
}

join 源码——使用保护性暂停模式

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;
            }
        }
    }

🚀8. park & unpark

🚁8.1 基本使用

//暂停线程运行
LockSupport.park;
//恢复线程运行
LockSupport.unpark(thread);
public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(()-> {
      System.out.println("park");
            //暂停线程运行
      LockSupport.park();
      System.out.println("resume");
    }, "t1");
    thread.start();
    Thread.sleep(1000);
    System.out.println("unpark");
      //恢复线程运行
    LockSupport.unpark(thread);
}

🚁8.2 特点

与 Object 的 wait/notify 相比

wait/notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park&unpark 不必

park&unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】

park&unpark 可以先 unpark,而 wait & notify 不能先 notify

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

🚁8.3 原理

每个线程都有一个自己的 park 对象,并且该对象由 _counter, _cond,__mutex 组成


情况 1:先调用 park,再调用 unpark


先调用 park


线程运行时,会将 park 对象中的 _counter 的值设为 0

调用 park 时,会先查看 counter 的值是否为 0,如果为 0,则将线程放入阻塞队列 cond 中

放入阻塞队列后,会再次将 counter 设置为 0

2e718f6de6d94987ae2f2182c528d9c8.png

然后再调用 unpark


调用 unpark 方法后,会将 counter 的值设置为 1

去唤醒阻塞队列 cond 中的线程

线程继续运行并将 counter 的值设为 0

2e718f6de6d94987ae2f2182c528d9c8.png

情况 2:先调用 unpark,再调用 park


先调用 unpark


会将 counter 设置为 1(运行时0)

再调用 park


查看 counter 是否为 0

因为 unpark 已经把 counter 设置为 1,所以此时将 counter 设置为 0,但不放入阻塞队列 cond 中

2e718f6de6d94987ae2f2182c528d9c8.png

🚀9. 线程状态转换

2e718f6de6d94987ae2f2182c528d9c8.png

假设有线程 Thread t


情况一:NEW --> RUNNABLE


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

情况二:RUNNABLE <–> WAITING


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

(1)调用 obj.wait() 方法时,t 线程从 RUNNABLE –> WAITING

(2)调用 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() 时

(1)竞争锁成功,t 线程从 TIMED_WAITING –> RUNNABLE

(2)竞争锁失败,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

🚀10. 多把锁

将锁的粒度细分


优点,可以增强并发度

缺点,如果一个线程需要同时获得多把锁,就容易发生死锁

🚀11. 活跃性

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

🚁11.1 死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁。


t1 线程获得 A 对象锁,接下来想获取 B 对象的锁, t2 线程获得 B 对象锁,接下来想获取 A 对象 的锁, 例:

public static void main(String[] args) {
  final Object A = new Object();
  final Object B = new Object();
  new Thread(()->{
    synchronized (A) {
      try {
        Thread.sleep(2000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      synchronized (B) {
      }
    }
  }).start();
  new Thread(()->{
    synchronized (B) {
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      synchronized (A) {
      }
    }
  }).start();
}

发生死锁的四个必要条件


互斥条件:在一段时间内,一种资源只能被一个进程所使用。

请求和保持条件:进程已经拥有了至少一种资源,同时又去申请其他资源。因为其他资源被别的进程所使用,该进程进入阻塞状态,并且不释放自己已有的资源。

不可抢占条件:进程对已获得的资源在未使用完成前不能被强占,只能在进程使用完后自己释放。

循环等待条件:发生死锁时,必然存在一个进程——资源的循环链。

定位死锁的方法:


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

2e718f6de6d94987ae2f2182c528d9c8.png

2e718f6de6d94987ae2f2182c528d9c8.png

省略中间的一些信息,找到最后一段信息,末尾处出现 Found 1 deadlock

2e718f6de6d94987ae2f2182c528d9c8.png

注意点:避免死锁要注意加锁顺序;另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 Linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 top -Hp 进程 id 来定位是哪个线程,最后再用 jstack 排查。


哲学家就餐问题


有 5 位哲学家,围坐在圆桌旁。他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。

如果筷子被身边的人拿着,自己就得等待。


筷子类

class Chopstick {
    String name;
    public Chopstick(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "筷子{"+name+"}";
    }
}

哲学家类

class Philosopher extends Thread {
    Chopstick left;
    Chopstick right;
    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }
    private void eat() {
        System.out.println("eating...");
        this.right = right;
    }
    @Override
    public void run() {
        while (true) {
            //获得左手筷子
            synchronized (left) {
                //获得右手筷子
                synchronized (right) {
                    //吃饭
                    eat();
                }
                //放下右手筷子
            }
            //放下左手筷子
        }
    }
}

避免死锁的方法


在线程使用锁对象时,顺序加锁即可避免死锁

2e718f6de6d94987ae2f2182c528d9c8.png

🚁11.2 活锁

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


避免活锁的方法:在线程执行时,中途给予不同的间隔时间即可。


死锁与活锁的区别


死锁是因为线程互相持有对象想要的锁,并且都不释放,最后到时线程阻塞,停止运行的现象。

活锁是因为线程间修改了对方的结束条件,而导致代码一直在运行,却一直运行不完的现象。

🚁11.3 饥饿

某些线程因为优先级太低,导致一直无法获得资源的现象,在使用顺序加锁时,可能会出现饥饿现象。

🚀12. 可重入锁

和 synchronized 相比具有的的特点


可中断

可以设置超时时间

可以设置为公平锁 (先到先得)

支持多个条件变量( 具有多个 waitset)

基本语法

//获取ReentrantLock对象
private ReentrantLock lock = new ReentrantLock();
//加锁
lock.lock();
try {
  //需要执行的代码
}finally {
  //释放锁
  lock.unlock();
}

可重入


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

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

可打断

public static void main(String[] args) {
  ReentrantLock lock = new ReentrantLock();
  Thread t1 = new Thread(()-> {
    try {
      //加锁,可打断锁
      lock.lockInterruptibly();
    } catch (InterruptedException e) {
      e.printStackTrace();
               //被打断,返回,不再向下执行
      return;
    }finally {
      //释放锁
      lock.unlock();
    }
  });
  lock.lock();
  try {
    t1.start();
    Thread.sleep(1000);
    //打断
    t1.interrupt();
  } catch (InterruptedException e) {
    e.printStackTrace();
  } finally {
    lock.unlock();
  }
}

如果某个线程处于阻塞状态,可以调用其 interrupt 方法让其停止阻塞,获得锁失败。简而言之就是:处于阻塞状态的线程,被打断了就不用阻塞了,直接停止运行。

public static void main(String[] args) {
  ReentrantLock lock = new ReentrantLock();
  Thread t1 = new Thread(()-> {
           //未设置等待时间,一旦获取失败,直接返回false
    if(!lock.tryLock()) {
      System.out.println("获取失败");
               //获取失败,不再向下执行,返回
      return;
    }
    System.out.println("得到了锁");
    lock.unlock();
  });
  lock.lock();
  try{
    t1.start();
    Thread.sleep(3000);
  } catch (InterruptedException e) {
    e.printStackTrace();
  } finally {
    lock.unlock();
  }
}

锁超时


使用 lock.tryLock 方法会返回获取锁是否成功。如果成功则返回 true,反之则返回 false

并且 tryLock 方法可以指定等待时间,参数为:tryLock(long timeout, TimeUnit unit),其中timeout 为最长等待时间,TimeUnit 为时间单位

归纳就是,获取失败了、获取超时了或者被打断了,不再阻塞,直接停止运行

不设置等待时间,立刻失败

public static void main(String[] args) {
  ReentrantLock lock = new ReentrantLock();
  Thread t1 = new Thread(()-> {
           //未设置等待时间,一旦获取失败,直接返回false
    if(!lock.tryLock()) {
      System.out.println("获取失败");
               //获取失败,不再向下执行,返回
      return;
    }
    System.out.println("得到了锁");
    lock.unlock();
  });
  lock.lock();
  try{
    t1.start();
    Thread.sleep(3000);
  } catch (InterruptedException e) {
    e.printStackTrace();
  } finally {
    lock.unlock();
  }
}

设置等待时间

public static void main(String[] args) {
  ReentrantLock lock = new ReentrantLock();
  Thread t1 = new Thread(()-> {
    try {
      //判断获取锁是否成功,最多等待1秒
      if(!lock.tryLock(1, TimeUnit.SECONDS)) {
        System.out.println("获取失败");
        //获取失败,不再向下执行,直接返回
        return;
      }
    } catch (InterruptedException e) {
      e.printStackTrace();
      //被打断,不再向下执行,直接返回
      return;
    }
    System.out.println("得到了锁");
    //释放锁
    lock.unlock();
  });
  lock.lock();
  try{
    t1.start();
    //打断等待
    t1.interrupt();
    Thread.sleep(3000);
  } catch (InterruptedException e) {
    e.printStackTrace();
  } finally {
    lock.unlock();
  }
}

公平锁


在线程获取锁失败,进入阻塞队列时,先进入的会在锁被释放后先获得锁。这样的获取方式就是公平的

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

条件变量


synchronized 中也有条件变量,就是 waitSet 等待队列 ,当条件不满足时进入waitSet 等待

ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量,这就好比,synchronized 是那些不满足条件的线程都在一间休息室等消息,而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

使用要点:


await 前需要获得锁

await 执行后,会释放锁,进入 conditionObject 等待

await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁

竞争 lock 锁成功后,从 await 后继续执行

static Boolean judge = false;
public static void main(String[] args) throws InterruptedException {
  ReentrantLock lock = new ReentrantLock();
  //获得条件变量
  Condition condition = lock.newCondition();
  new Thread(()->{
    lock.lock();
    try{
      while(!judge) {
        System.out.println("不满足条件,等待...");
        //等待
        condition.await();
      }
    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      System.out.println("执行完毕!");
      lock.unlock();
    }
  }).start();
  new Thread(()->{
    lock.lock();
    try {
      Thread.sleep(1);
      judge = true;
      //释放
      condition.signal();
    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      lock.unlock();
    }
  }).start();
}

🚀13. 同步模式之顺序控制

static final Object LOCK = new Object();
//判断先执行的内容是否执行完毕
static Boolean judge = false;
public static void main(String[] args) {
  new Thread(()->{
    synchronized (LOCK) {
      while (!judge) {
        try {
          LOCK.wait();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
      System.out.println("2");
    }
  }).start();
  new Thread(()->{
    synchronized (LOCK) {
      System.out.println("1");
      judge = true;
               //执行完毕,唤醒所有等待线程
      LOCK.notifyAll();
    }
  }).start();
}

交替输出(wait/notify 版本)

public class Test {
  static Symbol symbol = new Symbol();
  public static void main(String[] args) {
    new Thread(()->{
      symbol.run("a", 1, 2);
    }).start();
    new Thread(()->{
      symbol.run("b", 2, 3);
    }).start();
    symbol.run("c", 3, 1);
    new Thread(()->{
    }).start();
  }
}
class Symbol {
  public synchronized void run(String str, int flag, int nextFlag) {
    for(int i=0; i<loopNumber; i++) {
      while(flag != this.flag) {
        try {
          this.wait();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
      System.out.println(str);
      //设置下一个运行的线程标记
      this.flag = nextFlag;
      //唤醒所有线程
      this.notifyAll();
    }
  }
  /**
   * 线程的执行标记, 1->a 2->b 3->c
   */
  private int flag = 1;
  private int loopNumber = 5;
  public int getFlag() {
    return flag;
  }
  public void setFlag(int flag) {
    this.flag = flag;
  }
  public int getLoopNumber() {
    return loopNumber;
  }
  public void setLoopNumber(int loopNumber) {
    this.loopNumber = loopNumber;
  }
}

🚀14. ThreadLocal

ThreadLocal 是 JDK 包提供的,它提供线程本地变量,也就是如果创建了一个ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。


使用

public class ThreadLocalTest {
   public static void main(String[] args) {
      // 创建ThreadLocal变量
      ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
      ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
      // 创建两个线程,分别使用上面的两个ThreadLocal变量
      Thread thread1 = new Thread(()->{
         // stringThreadLocal第一次赋值
         stringThreadLocal.set("thread1 stringThreadLocal first");
         // stringThreadLocal第二次赋值
         stringThreadLocal.set("thread1 stringThreadLocal second");
         // userThreadLocal赋值
         userThreadLocal.set(new User("Cristiano", 37));
         // 取值
         System.out.println(stringThreadLocal.get());
         System.out.println(userThreadLocal.get());
          // 移除
     userThreadLocal.remove();
     System.out.println(userThreadLocal.get());
      });
      Thread thread2 = new Thread(()->{
         // stringThreadLocal第一次赋值
         stringThreadLocal.set("thread2 stringThreadLocal first");
         // stringThreadLocal第二次赋值
         stringThreadLocal.set("thread2 stringThreadLocal second");
         // userThreadLocal赋值
         userThreadLocal.set(new User("Lionel", 34));
         // 取值
         System.out.println(stringThreadLocal.get());
         System.out.println(userThreadLocal.get());
      });
      // 启动线程
      thread1.start();
      thread2.start();
   }
}
class User {
   String name;
   int age;
   public User(String name, int age) {
      this.name = name;
      this.age = age;
   }
   @Override
   public String toString() {
      return "User{" +
            "name='" + name + '\'' +
            ", age=" + age +
            '}';
   }
}
thread1 stringThreadLocal second
thread2 stringThreadLocal second
User{name='Cristiano', age=37}
User{name='Lionel', age=34}
null

从运行结果可以看出


每个线程中的 ThreadLocal 变量是每个线程私有的,而不是共享的

ThreadLocal 其实就相当于其泛型类型的一个变量,只不过是每个线程私有的,stringThreadLocal被赋值了两次,保存的是最后一次赋值的结果

ThreadLocal可以进行以下几个操作:

set 设置值

get 取出值

remove 移除值

原理

public class Thread implements Runnable {
  ...
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    ...
}
static class ThreadLocalMap {
   static class Entry extends WeakReference<ThreadLocal<?>> {
       /** The value associated with this ThreadLocal. */
       Object value;
       Entry(ThreadLocal<?> k, Object v) {
           super(k);
           value = v;
       }
   }
}

Thread 类中有一个 threadLocals 和一个 inheritableThreadLocals,它们都是 ThreadLocalMap 类型的变量,而 ThreadLocalMap 是一个定制化的 Hashmap。在默认情况下,每个线程中的这两个变量都为 null.


ThreadLocal 中的方法

public void set(T value) {
  //获取当前线程
    Thread t = Thread.currentThread();
    //获得ThreadLocalMap对象, 返回Thread类中的threadLocals
    ThreadLocalMap map = getMap(t);
    if (map != null)
      //ThreadLocal自生的引用作为key,传入的值作为value
        map.set(this, value);
    else
        createMap(t, value);
}
void createMap(Thread t, T firstValue) {
    // 创建的同时设置想放入的值
    // threadLocal自生的引用作为key,传入的值作为value
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
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();
}
private T setInitialValue() {
     T value = initialValue();
     Thread t = Thread.currentThread();
     ThreadLocalMap map = getMap(t);
     if (map != null)
         map.set(this, value);
     else
         createMap(t, value);
     return value;
}
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
}

在每个线程内部都有一个名为 threadLocals 的成员变量,该变量的类型为 HashMap,其中 key 为我们定义的 ThreadLocal 变量的 this 引用,value 则为我们使用 set 方法设置的值。每个线程的本地变量存放在线程自己的内存变量 threadLocals 中

只有当前线程第一次调用 ThreadLocal 的 set 或者 get 方法时才会创建 threadLocals(inheritableThreadLocals 也是一样)。其实每个线程的本地变量不是存放在 ThreadLocal 实例里面,而是存放在调用线程的 threadLocals 变量里面

从 ThreadLocal 的源码可以看出,无论是 set、get、还是 remove,都是相对于当前线程操作

Thread t = Thread.currentThread();

因此 ThreadLocal 无法从父线程传向子线程,所以 InheritableThreadLocal 出现了,它能够让父线程中 ThreadLocal 的值传给子线程。


也就是从 main 所在的线程,传给 thread1 或 thread2

public class Test {
   public static void main(String[] args) {
      ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
      InheritableThreadLocal<String> stringInheritable = new InheritableThreadLocal<>();
      // 主线程赋对上面两个变量进行赋值
      stringThreadLocal.set("this is threadLocal");
      stringInheritable.set("this is inheritableThreadLocal");
      // 创建线程
      Thread thread1 = new Thread(()->{
         // 获得ThreadLocal中存放的值
         System.out.println(stringThreadLocal.get());
         // 获得InheritableThreadLocal存放的值
         System.out.println(stringInheritable.get());
      });
      thread1.start();
   }
}

运行结果

null
this is inheritableThreadLocal

InheritableThreadLocal 的值成功从主线程传入了子线程,而 ThreadLocal 没有。


原理

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    // 传入父线程中的一个值,然后直接返回
    protected T childValue(T parentValue) {
        return parentValue;
    }
    // 返回传入线程的inheritableThreadLocals
    // Thread中有一个inheritableThreadLocals变量
    // ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
  // 创建一个inheritableThreadLocals
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

InheritableThreadLocal 继承了 ThreadLocal,并重写了三个方法。InheritableThreadLocal 重写了createMap 方法,那么现在当第一次调用set方法时,创建的是当前线程的inheritableThreadLocals 变量的实例而不再是 threadLocals。当调用 getMap 方法获取当前线程内部的 map 变量时,获取的是 inheritableThreadLocals 而不再是 threadLocals

当父线程创建子线程时,构造函数会把父线程中 inheritableThreadLocals 变量里面的本地变量复制一份保存到子线程的 inheritableThreadLocals 变量里面


相关文章
|
2月前
|
Java 编译器 开发者
深入理解Java内存模型(JMM)及其对并发编程的影响
【9月更文挑战第37天】在Java的世界里,内存模型是隐藏在代码背后的守护者,它默默地协调着多线程环境下的数据一致性和可见性问题。本文将揭开Java内存模型的神秘面纱,带领读者探索其对并发编程实践的深远影响。通过深入浅出的方式,我们将了解内存模型的基本概念、工作原理以及如何在实际开发中正确应用这些知识,确保程序的正确性和高效性。
|
20天前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
21 0
|
25天前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
103 6
|
1月前
|
存储 缓存 安全
Java内存模型(JMM):深入理解并发编程的基石####
【10月更文挑战第29天】 本文作为一篇技术性文章,旨在深入探讨Java内存模型(JMM)的核心概念、工作原理及其在并发编程中的应用。我们将从JMM的基本定义出发,逐步剖析其如何通过happens-before原则、volatile关键字、synchronized关键字等机制,解决多线程环境下的数据可见性、原子性和有序性问题。不同于常规摘要的简述方式,本摘要将直接概述文章的核心内容,为读者提供一个清晰的学习路径。 ####
45 2
|
26天前
|
设计模式 安全 Java
Java 多线程并发编程
Java多线程并发编程是指在Java程序中使用多个线程同时执行,以提高程序的运行效率和响应速度。通过合理管理和调度线程,可以充分利用多核处理器资源,实现高效的任务处理。本内容将介绍Java多线程的基础概念、实现方式及常见问题解决方法。
56 0
|
2月前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
25 1
|
3月前
|
Java 开发者
深入探索Java中的并发编程
本文将带你领略Java并发编程的奥秘,揭示其背后的原理与实践。通过深入浅出的解释和实例,我们将探讨Java内存模型、线程间通信以及常见并发工具的使用方法。无论是初学者还是有一定经验的开发者,都能从中获得启发和实用的技巧。让我们一起开启这场并发编程的奇妙之旅吧!
33 5
|
3月前
|
算法 安全 Java
Java中的并发编程是如何实现的?
Java中的并发编程是通过多线程机制实现的。Java提供了多种工具和框架来支持并发编程。
21 1
|
3月前
|
缓存 监控 Java
Java中的并发编程:理解并应用线程池
在Java的并发编程中,线程池是提高应用程序性能的关键工具。本文将深入探讨如何有效利用线程池来管理资源、提升效率和简化代码结构。我们将从基础概念出发,逐步介绍线程池的配置、使用场景以及最佳实践,帮助开发者更好地掌握并发编程的核心技巧。
|
3月前
|
安全 Java 测试技术
掌握Java的并发编程:解锁高效代码的秘密
在Java的世界里,并发编程就像是一场精妙的舞蹈,需要精准的步伐和和谐的节奏。本文将带你走进Java并发的世界,从基础概念到高级技巧,一步步揭示如何编写高效、稳定的并发代码。让我们一起探索线程池的奥秘、同步机制的智慧,以及避免常见陷阱的策略。