手撕JUC并发编程1

简介: 本文系统介绍了进程与线程的基本概念及Java线程实现。首先阐述了进程与线程的区别:进程是资源分配单位,线程是CPU调度单位,线程共享进程资源但更轻量级。接着讲解了并行与并发的概念,以及Java线程的创建方式(Thread、Runnable、FutureTask)。重点分析了线程安全问题,包括临界区、竞态条件等概念,并详细讲解了synchronized同步机制、锁升级过程(偏向锁、轻量级锁、重量级锁)及其底层Monitor原理。此外还介绍了线程状态转换、死锁/活锁问题、AQS框架及ReentrantLock实

 1.进程与线程

1.1 基本概念

1.1.1 进程

进程可以理解为是程序的一个运行示例。一个程序由指令和数据构成,这些指令要运行,数据要读写,就必须将指令加载到CPU,数据加载到内存。在指令运行过程中需要用到磁盘,网络等设备,进程就是用来加载指令,管理内存,管理IO的。  当一个程序运行时,会被从磁盘加载到内存中。这时就开启了一个进程。大部分程序可以同时运行多个进程示例,也有程序只能启动一个进程示例。

1.1.2 线程

线程是操作系统最小的调度单位,一个进程之内可以分为一到多个线程。一个线程就是一个指令流,这个指令流中的一条条指令会以一定的顺序交给CPU执行。进程是操作系统作为资源分配的最小单位,进程是不活动的,只是作为线程的容器。

1.1.3 进程与线程对比

1.进程之间基本上是相互独立的,而线程存在于进程之内,是进程的一个子集

2.进程拥有共享的资源(如内存空间),共其内部的线程共享

3.进程间的通信较为复杂,同一台计算机之间的进程通信称为IPC,不同计算机之间的进程通信需要通过网络协议通信。

4.线程通信相对简单,线程之间共享进程内的内存(如多线程可以访问同一个共享变量)

5.线程更加轻量,线程的上下文切换成本一般比进程的上下文切换低。

1.2 并行与并发

在单核CPU下,线程实际还是串行执行的,而操作系统中有一个组件叫任务调度器,任务调度器将CPU时间片分给不同程序使用,由于CPU时间片非常短,CPU在线程间切换的速度非常快,此时我们感觉多线程之间是同时运行的。这种多线程轮流使用CPU的做法称为并发。

image.gif 编辑

image.gif 编辑

多核CPU下,每个核心都可以调度运行线程,这时候称之为线程是可以并行的。

image.gif 编辑 image.gif 编辑

引用Rob Pike的一段叙述:

并发是同一时间应对多件事的能力。并行事同一时间动手做多件事的能力。

1.3 应用

1.3.1 异步调用

从调用方式上来讲,需要等待结果返回才能继续运行的方式就是同步,不需要等待结果返回就能继续运行的方式就是异步。

多线程可以让方法变为异步的,比如读取磁盘文件时,假设读取花费了5秒钟,如果没有线程调度机制,这5秒CPU什么也做不了,后续的业务逻辑也无法执行。

再比如再项目中,视频文件需要进行转换格式等费时操作时,这时候开一个新线程处理视频转换,就能避免主线程阻塞。

再比如TomCat中的servlet也是如此,让用户线程来处理耗时的操作,避免阻塞tomcat工作线程。

1.3.2 多核CPU的多线程应用

单核CPU下,多线程并不能提高实际程序的运行效率,只是为了再不同任务之间切换,做到不同线程轮流使用CPU,不至于让一个线程一直占有CPU。多核CPU可以并行跑多个线程,但能否提高程序运行效率还是要看具体情况的。有些任务经过进行设计拆分可以提高运行效率,但并非所有任务都需要拆分,任务的目的不同,谈拆分和效率将毫无意义。IO操作不占用CPU,一般我们使用的是阻塞IO,此时线程虽然不用CPU,但需要一直等待IO结束,这个过程并没有重复利用线程。

2.Java线程

2.1 Java创建运行线程

2.1.1 直接使用Thread类

// 创建线程对象
Thread t = new Thread() {
   public void run() {
   // 要执行的任务
   }
};
// 启动线程
t.start();

image.gif

Thread t1 = new Thread("t1") {
    @Override
  //run 方法内实现了要执行的任务
    public void run() {
      log.debug("hello");
     }
};
t1.start();

image.gif

2.1.2 使用Runnable接口配合Thread类

Runnable runnable = new Runnable() {
   public void run(){
   // 要执行的任务
   }
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();

image.gif

// 创建任务对象
Runnable task2 = new Runnable() {
   @Override
   public void run() {
    log.debug("hello");
   }
};
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();

image.gif

lambda表达式精简代码:

// 创建任务对象
Runnable task2 = () -> log.debug("hello");
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();

image.gif

使用Runnable更容易与线程池等高级API配合,并且让任务脱离了Thread继承体系,更加灵活。

2.1.3 FutureTask配合Thread

FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况

// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
   log.debug("hello");
   return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);

image.gif

多线程之间交替执行,谁先谁后无法控制。

2.2 查看进程线程的方法

windows:

任务管理器可以查看进程和线程数,也可以用来杀死进程

tasklist 查看进程

taskkill 杀死进程

Linux:

ps -fe 查看所有进程

ps -fT -p <PID> 查看某个进程(PID)的所有线程

kill 杀死进程

top 按大写 H 切换是否显示线程

top -H -p <PID> 查看某个进程(PID)的所有线程

Java:

jps 命令查看所有 Java 进程

jstack <PID> 查看某个 Java 进程(PID)的所有线程状态

jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

2.3 线程运行

2.3.1 栈与栈帧

JVM由堆,栈,方法区构成,其中栈内存就是给线程使用的,每个线程启动后,虚拟机会为其分配一块栈内存。每个栈由多个栈帧构成,每个栈帧对应着每次方法调用时所占的内存。每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。

2.3.2 线程上下文切换

因为下面列举的一些原因导致CPU不再执行当前线程,转而执行另一个线程的代码:

线程的CPU时间片用完,垃圾回收,有更高优先级的线程需要运行,线程自己调用了 sleepyieldwaitjoinparksynchronizedlock 等方法。

当上下文切换发生时,需要操作系统保存当前线程的状态,并恢复另一个线程的状态,状态信息包括程序计数器,每个栈帧的信息,如局部变量,操作数帧,返回地址等。Java中对应的概念就是程序计数器,它的作用是记住下一条JVM指令的执行地址,是线程私有的。频繁的上下文切换会影响性能。

2.4 Java中线程操作常用方法

image.gif 编辑

image.gif 编辑

2.4.1 start与run

@Slf4j(topic = "c.TestStart")
public class TestStart {
    public static void main(String[] args) {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug(Thread.currentThread().getName());
                FileReader.read(Constants.MP4_FULL_PATH);
            }
        };
        t1.run();
        log.debug("do other things ...");
    }
}

image.gif

程序仍在 main 线程运行, FileReader.read() 方法调用还是同步的

@Slf4j(topic = "c.TestStart")
public class TestStart {
    public static void main(String[] args) {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug(Thread.currentThread().getName());
                FileReader.read(Constants.MP4_FULL_PATH);
            }
        };
        t1.start();
        log.debug("do other things ...");
    }
}

image.gif

程序在 t1 线程运行, FileReader.read() 方法调用是异步的

直接调用 run 是在主线程中执行了 run,没有启动新的线程 ,使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码。

2.4.2 sleep与yield

Sleep:

1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)

2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出

InterruptedException

3. 睡眠结束后的线程未必会立刻得到执行

4. 建议用 TimeUnit sleep 代替 Thread sleep 来获得更好的可读性

yield:

1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程

2. 具体的实现依赖于操作系统的任务调度器

线程优先级:

线程优先级会提示调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它,如果CPU比较忙,那么优先级高的线程会获取更多时间片,但CPU空闲时,优先级几乎没作用。

@Slf4j(topic = "c.TestYield")
public class TestYield {
    public static void main(String[] args) {
        Runnable task1 = () -> {
            int count = 0;
            for (;;) {
                System.out.println("---->1 " + count++);
            }
        };
        Runnable task2 = () -> {
            int count = 0;
            for (;;) {
//                Thread.yield();
                System.out.println("              ---->2 " + count++);
            }
        };
        Thread t1 = new Thread(task1, "t1");
        Thread t2 = new Thread(task2, "t2");
        t1.setPriority(Thread.MIN_PRIORITY);
        t2.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        t2.start();
    }
}

image.gif

2.4.3 join方法

让当前目标线程执行完业务逻辑后再执行其他线程的业务逻辑。

private static void test1() throws InterruptedException {
        log.debug("开始");
        Thread t1 = new Thread(() -> {
            log.debug("开始");
            sleep(1);
            log.debug("结束");
            r = 10;
        });
        t1.start();
        log.debug("结果为:{}", r);
        log.debug("结束");
    }

image.gif

上面代码执行,因为主线程和t1线程是并行执行的,t1线程需要1秒计算之后才能算出r=10,而主线程一开始就要打印r的结果,所以只能打印出r=0,此时我们可以在start方法之后引入join方法,等待线程t1运行结束。

t1.start();
        t1.join();
        log.debug("结果为:{}", r);
        log.debug("结束");

image.gif

问:下面代码cost大约需要多少秒:

private static void test2() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            sleep(1);
            r1 = 10;
        });
        Thread t2 = new Thread(() -> {
            sleep(2);
            r2 = 20;
        });
        t1.start();
        t2.start();
        long start = System.currentTimeMillis();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        long end = System.currentTimeMillis();
        log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
    }

image.gif

第一个join:等待t1时,t2并没有停止,而是正在运行

第二个join:1s后,执行到此,t2也运行了1s,因此也只需要再等待1s。

颠倒两个join最终的结果也是一样的

20:45:43.239 [main] c.TestJoin - r1: 10 r2: 20 cost: 2005

image.gif

image.gif 编辑

有时效的join

等够时间:

static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
     test3();
}
public static void test3() throws InterruptedException {
     Thread t1 = new Thread(() -> {
     sleep(1);
     r1 = 10;
});
  long start = System.currentTimeMillis();
  t1.start();
  // 线程执行结束会导致 join 结束
  t1.join(1500);
  long end = System.currentTimeMillis();
  log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}

image.gif

输出

20:48:01.320 [main] c.TestJoin - r1: 10 r2: 0 cost: 1010

image.gif

没等够时间:

static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
  test3();
}
public static void test3() throws InterruptedException {
   Thread t1 = new Thread(() -> {
   sleep(2);
   r1 = 10;
 });
   long start = System.currentTimeMillis();
   t1.start();
   // 线程执行结束会导致 join 结束
   t1.join(1500);
   long end = System.currentTimeMillis();
   log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}

image.gif

输出

20:52:15.623 [main] c.TestJoin - r1: 0 r2: 0 cost: 1502

image.gif

2.4.4 interrupt方法

打断 sleepwaitjoin 的线程

这几个方法都会让线程进入阻塞状态,打断 sleep 的线程, 会清空打断状态,以 sleep 为例

t1线程原本计划休眠0.5秒后继续执行,但被打断后会被强制唤醒并因捕捉异常而终止,不会正常运行后续逻辑

private static void test1() throws InterruptedException {
        Thread t1 = new Thread(()->{
            sleep(1);
        }, "t1");
        t1.start();
        sleep(0.5);
        t1.interrupt();
        log.debug(" 打断状态: {}", t1.isInterrupted());
    }

image.gif

21:18:10.374 [main] c.TestInterrupt - 打断状态: false

image.gif

打断正常运行的线程

打断正常运行的线程, 不会清空打断状态

private static void test2() throws InterruptedException {
        Thread t2 = new Thread(()->{
            while(true) {
                Thread current = Thread.currentThread();
                boolean interrupted = current.isInterrupted();
                if(interrupted) {
                    log.debug(" 打断状态: {}", interrupted);
                    break;
                }
            }
        }, "t2");
        t2.start();
        sleep(0.5);
        t2.interrupt();
    }

image.gif

20:57:37.964 [t2] c.TestInterrupt - 打断状态: true

image.gif

原理解释:Java线程中断不是强制杀死线程,而是通过设置中断标志给目标线程发送了一个中断信号。它的核心作用是传递 “你可以停止工作了” 的通知,最终线程是否停止、何时停止,完全由线程自身决定。

打断park线程

打断 park 线程, 不会清空打断状态

private static void test3() {
        Thread t1 = new Thread(() -> {
            log.debug("park...");
            LockSupport.park();
            log.debug("unpark...");
            log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
        }, "t1");
        t1.start();
        sleep(0.5);
        t1.interrupt();
    }

image.gif

输出

21:11:52.795 [t1] c.TestInterrupt - park... 
21:11:53.295 [t1] c.TestInterrupt - unpark... 
21:11:53.295 [t1] c.TestInterrupt - 打断状态:true

image.gif

如果打断标记已经是 true, park 会失效

private static void test4() {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                log.debug("park...");
                LockSupport.park();
                log.debug("打断状态:{}", Thread.interrupted());
            }
        });
        t1.start();
        sleep(1);
        t1.interrupt();
    }

image.gif

输出

21:13:48.783 [Thread-0] c.TestInterrupt - park... 
21:13:49.809 [Thread-0] c.TestInterrupt - 打断状态:true 
21:13:49.812 [Thread-0] c.TestInterrupt - park... 
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true 
21:13:49.813 [Thread-0] c.TestInterrupt - park... 
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true 
21:13:49.813 [Thread-0] c.TestInterrupt - park... 
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true 
21:13:49.813 [Thread-0] c.TestInterrupt - park... 
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true

image.gif

2.4.5 不推荐的方法

image.gif 编辑

2.5 主线程与守护线程

默认情况下,Java进程需要等待所有线程都运行结束才会结束,有一种特殊的线程叫做守护线程,只要其他非守护线程运行结束了,及时守护线程代码没执行完,也会强制结束。

@Slf4j(topic = "c.TestDaemon")
public class TestDaemon {
    public static void main(String[] args) {
        log.debug("开始运行...");
        Thread t1 = new Thread(() -> {
            log.debug("开始运行...");
            sleep(2);
            log.debug("运行结束...");
        }, "daemon");
        // 设置该线程为守护线程
        t1.setDaemon(true);
        t1.start();
        sleep(1);
        log.debug("运行结束...");
    }
}

image.gif

输出

08:26:38.123 [main] c.TestDaemon - 开始运行... 
08:26:38.213 [daemon] c.TestDaemon - 开始运行... 
08:26:39.215 [main] c.TestDaemon - 运行结束..

image.gif

垃圾回收器线程就是一种守护线程。TomCat中的Acceptor和Poller线程都是守护线程,所以TomCat收到shutdown命令后不会等待它们处理完当前请求。

2.6 线程五状态模型

从操作系统的层面来描述:

image.gif 编辑

初始状态:在语言层面创建了进程,还未与操作系统关联

就绪状态(可运行状态):该线程已经被创建(与操作系统关联),可以由CPU调度执行

运行状态:获取了CPU时间片中的状态,当CPU时间片用完时,会从运行状态转为就绪状态,会导致线程上下文的切换。

阻塞状态:如果调用了阻塞API,如BIO读写文件,这时线程不会用到CPU,会导致线程上下文切换,进入阻塞状态,等BIO等IO操作结束后,会由操作系统唤醒阻塞的线程,转为就绪状态。

终止状态:线程执行完毕,生命周期已经结束,不会转为其他状态。

2.7 Java层面的线程六状态

image.gif 编辑

New:线程刚被创建,还没有调用start方法

Runnable:调用了start方法之后,此阶段涵盖了操作系统层面的就绪状态,运行状态,阻塞状态

Blocked,Waiting,Timed_waiting:是Java API对阻塞状态的细分

Terminated:线程代码运行结束。

3.共享模型之管程

3.1问题引入

两个线程对初始值为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);
}

image.gif

上述结果可能为整数,负数,0,因为Java中对静态变量的自增自建并非原子操作,要彻底理

解,必须从字节码来进行分析

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

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

image.gif

而对应 i-- 也是类似:

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

image.gif

Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:

image.gif 编辑

临界区:

多个线程读共享资源是没问题的,但如果在多个线程对共享资源读写操作时发生指令交错,那就会出现问题。一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区,操作系统定义临界区为同一时间只能有一个线程访问的资源区域。

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

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

image.gif

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

3.2 synchronized

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

阻塞式解决方案:synchronized,Lock

非阻塞式方案:原子变量

synchronized,俗称对象锁,采用互斥的方式让同一时刻最多只有一个线程持有对象锁,其他线程再想获取这个对象锁就会被阻塞住,这样就可以保证拥有锁的线程可以安全执行临界区的代码,不用担心中途发生线程上下文的切换。

注意

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

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

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

3.2.1 基础语法

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

image.gif

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

image.gif

你可以做这样的类比:

synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人进行计算,线程 t1t2 想象成两个人,当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行count++ 代码,这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了。

这中间即使 t1 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦), 这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入 ,当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码。

synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。

面向对象改进:

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

image.gif

3.2.2 方法上的sychronied

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

image.gif

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

image.gif

不加 synchronized 的方法

不加 synchronzied 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)

3.2.3 线程8锁

考察先锁住哪个对象

情况112 21

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

image.gif

情况21s12,或 2 1s 1

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

image.gif

情况33 1s 12 23 1s 1 32 1s 1

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

image.gif

情况42 1s 1

@Slf4j(topic = "c.Number")
class Number{
 public synchronized void a() {
   sleep(1);
   log.debug("1");
 }
 public 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();
}

image.gif

情况52 1s 1

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

image.gif

情况61s 12, 或 2 1s 1

@Slf4j(topic = "c.Number")
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();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n1.b(); }).start();
}

image.gif

情况72 1s 1

@Slf4j(topic = "c.Number")
class Number{
 public static synchronized void a() {
   sleep(1);
   log.debug("1");
 }
 public 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();
}

image.gif

情况81s 12, 或 2 1s 1

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

image.gif

3.3 变量的线程安全分析

3.1.1 引入

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

如果他们没有共享那么就线程安全。如果他们被共享了,根据他们状态是否能够改变,又分为两种情况:如果只有读操作,那线程安全,如果有读写操作,则这段代码式临界区,需要考虑线程安全问题。

局部变量是否线程安全?

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

局部变量的线程安全性分析

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

image.gif

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

public static void test1();
 descriptor: ()
Vflags: ACC_PUBLIC, ACC_STATIC
 Code:
 stack=1, locals=1, args_size=0
 0: bipush 10
 2: istore_0
 3: iinc 0, 1
 6: return
 LineNumberTable:
 line 10: 0
 line 11: 3
 line 12: 6
 LocalVariableTable:
 Start Length Slot Name Signature
 3 4 0 i I

image.gif

image.gif 编辑

局部变量的引用稍有不同

一个成员变量的示例:

class ThreadUnsafe {
  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);
 }
}

image.gif

执行:

static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
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();
 }
}

image.gif

其中一种情况是,如果线程2 还未 add,线程1 remove 就会报错:

Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 
 at java.util.ArrayList.rangeCheck(ArrayList.java:657) 
 at java.util.ArrayList.remove(ArrayList.java:496) 
 at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35) 
 at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26) 
 at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14) 
 at java.lang.Thread.run(Thread.java:748)

image.gif

分析:

无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量 ,method3 与 method2 分析相同。

image.gif 编辑

list 修改为局部变量,那么就不会有上述问题了

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");
 }
 private void method3(ArrayList<String> list) {
  list.remove(0);
 }
}

image.gif

此时list是局部变量,每个线程调用时会创建其不同实例,没有共享,而method2的参数是从method1中传递过来的,与method1中引用同一个对象,method3的参数分析与method2相同。

image.gif 编辑

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

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

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

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

image.gif

  • 若将 method2method3 改为 public
  • 情况 1(外部线程调用):可能导致 list 被多线程共享,引入线程安全问题。
  • 情况 2(子类覆盖):子类可通过重写进一步破坏线程安全,风险更高。

原代码的线程安全依赖于 “list 仅被当前线程访问”,而 private 修饰符是保证这一前提的重要手段。扩大方法的访问权限(如改为 public)会破坏这种封装性,增加线程安全风险。

3.1.2 常见线程安全的类

String

Integer

StringBuffer

Random

Vector

Hashtable

java.util.concurrent 包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为它们的每个方法是原子的,注意它们多个方法的组合不是原子的。

线程安全类方法的组合

分析下面代码是否线程安全?

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

image.gif

image.gif 编辑

这段代码不是线程安全的,即使 Hashtable 本身的 get()put() 方法是线程安全的(加了 synchronized 锁),但组合起来的 “判断 - 修改” 操作是非原子的,可能导致线程安全问题。

不可变类线程安全性

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

,或许有疑问,String replacesubstring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?

public class Immutable{
 private int value = 0;
 public Immutable(int value){
     this.value = value;
 }
 public int getValue(){
     return this.value;
 }
}

image.gif

  • 不可变性valueprivate 修饰,且没有任何修改它的方法(无 setter),一旦创建,value 无法改变。
  • 线程安全:多线程调用 getValue() 时,读取的都是初始值,不存在冲突,因此线程安全。

如果想增加一个增加的方法呢?

public class Immutable{
 private int value = 0;
 public Immutable(int value){
 this.value = value;
 }
 public int getValue(){
 return this.value;
 }
 
 public Immutable add(int v){
 return new Immutable(this.value + v);
 } 
}

image.gif

  • 不可变性add 方法不会修改当前对象的 value,而是返回一个新的 Immutable 对象(包含计算后的值)。原对象始终保持初始状态。
  • 线程安全
  • 多线程调用 add 时,各自基于原对象的 value 创建新对象,互不干扰;
  • 即使多个线程持有同一个 Immutable 对象,也只能读取 value,无法修改,因此安全。

总结:

  1. 不可变类的线程安全核心:对象状态创建后不可修改,多线程访问时只有读操作,无写操作,因此无冲突。
  2. “修改” 不可变对象的本质:如 String.replace 或自定义的 add 方法,都是通过创建新对象实现 “逻辑上的修改”,原对象不变,因此仍安全。
  3. 复合操作的线程安全:需通过加锁等机制保证原子性,与不可变类的安全机制不同(不可变类无需处理修改,因此天然安全)。

3.1.3 线程安全性实例分析:

1

public class MyServlet extends HttpServlet {
 // 是否安全?
 Map<String,Object> map = new HashMap<>();
 // 是否安全?
 String S1 = "...";
 // 是否安全?
 final String S2 = "...";
 // 是否安全?
 Date D1 = new Date();
 // 是否安全?
 final Date D2 = new Date();
 
 public void doGet(HttpServletRequest request, HttpServletResponse response) {
// 使用上述变量
 }
}

image.gif

Servlet 是单实例多线程的 ——Web 容器会创建一个 MyServlet 实例,所有请求都由这个实例的 doGet 方法处理(即多线程共享同一个实例变量)。线程安全与否,取决于 “变量是否被多线程共享” 且 “是否存在修改操作”。

变量 线程安全? 核心原因
map ❌ 不安全 共享可变对象 HashMap本身是线程不安全的(无同步机制),并发修改会出错。
S1 ✅ 安全 共享不可变类 String,仅读无改。
S2 ✅ 安全 final 限制变量不换指向,String 不可变,双重保障。
D1 ❌ 不安全 共享可变对象 Date,可修改内部时间,并发访问会出错。
D2 ❌ 不安全 final 仅限制变量不换指向,但 Date 内部可修改,共享时仍有风险。

例2:

public class MyServlet extends HttpServlet {
 // 是否安全?
 private UserService userService = new UserServiceImpl();
 
 public void doGet(HttpServletRequest request, HttpServletResponse response) {
 userService.update(...);
 }
}
public class UserServiceImpl implements UserService {
 // 记录调用次数
 private int count = 0;
 
 public void update() {
 // ...
 count++;
 }
}

image.gif

这个案例中,MyServletUserServiceImpl 的组合是线程不安全的,核心问题出在 UserServiceImpl 中的实例变量 count 被多线程共享且存在修改操作。count 是共享的可变状态,无同步机制保护共享修改。

例3:

@Aspect
@Component
public class MyAspect {
 // 是否安全?
 private long start = 0L;
 
 @Before("execution(* *(..))")
 public void before() {
 start = System.nanoTime();
 }
 
 @After("execution(* *(..))")
 public void after() {
 long end = System.nanoTime();
 System.out.println("cost time:" + (end-start));
 }
}

image.gif

这个 MyAspect 切面类在多线程环境下是线程不安全的,核心问题出在实例变量 start 被多线程共享且存在读写冲突。

例4:

public class MyServlet extends HttpServlet {
 // 是否安全
 private UserService userService = new UserServiceImpl();
 
 public void doGet(HttpServletRequest request, HttpServletResponse response) {
 userService.update(...);
 }
}
public class UserServiceImpl implements UserService {
 // 是否安全
 private UserDao userDao = new UserDaoImpl();
 
 public void update() {
 userDao.update();
 }
}
public class UserDaoImpl implements UserDao { 
 public void update() {
 String sql = "update user set password = ? where username = ?";
 // 是否安全
 try (Connection conn = DriverManager.getConnection("","","")){
 // ...
 } catch (Exception e) {
 // ...
 }
 }
}

image.gif

线程安全的,因为:不存在多线程共享的可变状态,每个线程的操作都是独立的

  1. 所有实例变量都是无状态的(无修改操作),或引用不可变。
  2. 方法内部仅使用局部变量(线程私有),不存在多线程共享的可变状态。

这种 “无状态组件”(如 UserServiceImplUserDaoImpl 无实例变量修改)是多线程环境下的理想设计,天然具备线程安全性。

例5:

public class MyServlet extends HttpServlet {
 // 是否安全
 private UserService userService = new UserServiceImpl();
 
 public void doGet(HttpServletRequest request, HttpServletResponse response) {
 userService.update(...);
 }
}
public class UserServiceImpl implements UserService {
 // 是否安全
 private UserDao userDao = new UserDaoImpl();
 
 public void update() {
 userDao.update();
 }
}
public class UserDaoImpl implements UserDao {
 // 是否安全
 private Connection conn = null;
 public void update() throws SQLException {
 String sql = "update user set password = ? where username = ?";
 conn = DriverManager.getConnection("","","");
 // ...
 conn.close();
 }
}

image.gif

UserDaoImpl 中的 conn 实例变量被多线程共享且存在修改操作,会导致数据库连接被错误覆盖、重复关闭或状态混乱,因此整个调用链线程不安全。每个线程的数据库连接应该是线程私有的(局部变量),但此处被所有线程共享,且存在并发赋值和状态修改,必然导致资源竞争和操作混乱。

如果要变为线程安全,需要将 conn 改为局部变量,确保每个线程操作自己的连接,互不干扰。

例6:

public class MyServlet extends HttpServlet {
 // 是否安全
 private UserService userService = new UserServiceImpl();
 
 public void doGet(HttpServletRequest request, HttpServletResponse response) {
 userService.update(...);
 }
}
public class UserServiceImpl implements UserService { 
 public void update() {
 UserDao userDao = new UserDaoImpl();
 userDao.update();
 }
}
public class UserDaoImpl implements UserDao {
 // 是否安全
 private Connection = null;
 public void update() throws SQLException {
 String sql = "update user set password = ? where username = ?";
 conn = DriverManager.getConnection("","","");
 // ...
 conn.close();
 }
}

image.gif

UserServiceImpl 不再将 userDao 作为实例变量,而是在 update() 方法中每次创建新的 UserDaoImpl 实例,这意味着:每个线程调用 userService.update() 时,都会得到一个独立的 UserDaoImpl 对象(线程私有),避免了多个线程共享同一个 UserDaoImpl 实例。虽然 UserDaoImpl 变为线程私有,避免了多线程共享 conn 的问题,但 conn 作为实例变量仍可能导致。

例7:

public abstract class Test {
 
 public void bar() {
 // 是否安全
 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 foo(sdf);
}
 
 public abstract foo(SimpleDateFormat sdf);
 
 
 public static void main(String[] args) {
 new Test().bar();
 }
}

image.gif

其中 foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法

public void foo(SimpleDateFormat sdf) {
 String dateStr = "1999-10-11 00:00:00";
 for (int i = 0; i < 20; i++) {
 new Thread(() -> {
 try {
 sdf.parse(dateStr);
 } catch (ParseException e) {
 e.printStackTrace();
 }
 }).start();
 }
}

image.gif

  • sdf 原本是 bar 方法的局部变量(每个 bar 调用创建一个独立实例),但 foo 方法启动了 20 个线程并发调用 sdf.parse(),导致 sdf 被多线程共享。
  • 由于 SimpleDateFormat 内部有可变状态(如 Calendar 实例),多线程并发 parse 会导致状态混乱,出现 ParseException 或解析出错误的日期(例如年份变成负数、月份错乱)。

例8:

private static Integer i = 0;
public static void main(String[] args) throws InterruptedException {
 List<Thread> list = new ArrayList<>();
 for (int j = 0; j < 2; j++) {
 Thread thread = new Thread(() -> {
 for (int k = 0; k < 5000; k++) {
     synchronized (i) {
     i++;
 }
 }
 }, "" + j);
     list.add(thread);
 }
 list.stream().forEach(t -> t.start());
 list.stream().forEach(t -> {
 try {
 t.join();
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 });
log.debug("{}", i);
}

image.gif

synchronized 的锁对象必须是引用不变的对象(如 final 修饰的对象)。若锁对象的引用会变化(如本例中的 i),则会导致锁失效,无法保证同步效果。这是使用 synchronized 时的常见陷阱,尤其需要注意 “锁对象是否可变”。

3.4 两个线程安全性实例

3.4.1 卖票练习

import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;
@Slf4j(topic = "c.ExerciseSell")
public class ExerciseSell {
    public static void main(String[] args) throws InterruptedException {
        // 模拟多人买票
        TicketWindow window = new TicketWindow(1000);
        // 所有线程的集合
        List<Thread> threadList = new ArrayList<>();
        // 卖出的票数统计
        List<Integer> amountList = new Vector<>();
        for (int i = 0; i < 2000; i++) {
            Thread thread = new Thread(() -> {
                // 买票
                int amount = window.sell(random(5));
                // 统计买票数
                amountList.add(amount);
            });
            threadList.add(thread);
            thread.start();
        }
        for (Thread thread : threadList) {
            thread.join();
        }
        // 统计卖出的票数和剩余票数
        log.debug("余票:{}",window.getCount());
        log.debug("卖出的票数:{}", amountList.stream().mapToInt(i-> i).sum());
    }
    // Random 为线程安全
    static Random random = new Random();
    // 随机 1~5
    public static int random(int amount) {
        return random.nextInt(amount) + 1;
    }
}
// 售票窗口
class TicketWindow {
    private int count;
    public TicketWindow(int count) {
        this.count = count;
    }
    // 获取余票数量
    public int getCount() {
        return count;
    }
    // 售票
    public synchronized int sell(int amount) {
        if (this.count >= amount) {
            this.count -= amount;
            return amount;
        } else {
            return 0;
        }
    }
}

image.gif

3.4.2 转账练习

import lombok.extern.slf4j.Slf4j;
import java.util.Random;
@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
    public static void main(String[] args) throws InterruptedException {
        Account a = new Account(1000);
        Account b = new Account(1000);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.transfer(b, randomAmount());
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                b.transfer(a, randomAmount());
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        // 查看转账2000次后的总金额
        log.debug("total:{}", (a.getMoney() + b.getMoney()));
    }
    // Random 为线程安全
    static Random random = new Random();
    // 随机 1~100
    public static int randomAmount() {
        return random.nextInt(100) + 1;
    }
}
// 账户
class Account {
    private int money;
    public Account(int money) {
        this.money = money;
    }
    public int getMoney() {
        return money;
    }
    public void setMoney(int money) {
        this.money = money;
    }
    // 转账
    public void transfer(Account target, int amount) {
        synchronized(Account.class) {
            if (this.money >= amount) {
                this.setMoney(this.getMoney() - amount);
                target.setMoney(target.getMoney() + amount);
            }
        }
    }
}

image.gif

3.5 Monitor与Sychronized原理

3.5.1 Java对象头

32 位虚拟机为例

普通对象

|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|

image.gif

数组对象

|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|

image.gif

其中 Mark Word 结构为

|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4 | biased_lock:0 | 01 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | 00 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|-------------------------------------------------------|--------------------|

image.gif

64 位虚拟机 Mark Word

|--------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|--------------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
|--------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
|--------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | 00 | Lightweight Locked |
|--------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
|--------------------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|--------------------------------------------------------------------|--------------------|

image.gif

3.5.2 Monitor原理

Monitor称为监视器或管程

每个Java对象都可以关联一个Monitor对象,线程获取synchronized锁需要使用锁对象关联monitor,该对象头的Mark Word中就被设置指向Monitor对象的指针 image.gif 编辑

monitor有三个属性,owner,entrylist,waitset,其中owner关联获取锁的线程,entrylist关联处于阻塞状态的线程,waitset关联处于waiting状态的线程。

线程获取锁对象后会判断owner是否为空,如果为空,则和owner关联,否则,线程进入entrylist进入阻塞状态,调用wait方法的线程进入waitset。

刚开始Monitor中的Owner为空。当Thread2执行syschronized(obj)时,就会将Monitor所有者Owner置为Thread2,Monitor中只能有一个Owner。在Thread2上锁的过程中,如果其他线程也来执行syschronized(obj),就会进入EntryList BLOCKED。在Thread2执行完同步代码块的内容,然后唤醒EntryList中阻塞的线程。

3.5.3 synchronized原理

Monitor实现的锁属于重量级锁,加锁操作依赖于操作系统级别的mutex指令,涉及到用户态和内核态之间的切换,成本开销较大。在JDK1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决没有多线程竞争场景下传统锁机制带来的性能开销问题。偏向锁对应只有一个线程持有锁的情况,轻量级锁对应不同线程交替持有锁的情况,重量级锁对应多线程竞争锁的情况。锁对象对象头的MarkWord中的lock字段表示表明了是哪种类型的锁。

每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁后(重量级锁),该对象头的MarkWord中就被设置了指向Monitor对象的指针。

很多时候,在Java程序运行时,同步块中的代码都是不存在竞争的,不同线程交替指向同步块中的代码,这时候,重量级锁是没有必要的,因此引入了JVM轻量级锁的概念。

轻量级锁加锁解锁流程:

同步代码块中的代码不存在竞争且不同线程交替指向同步块中的代码时,JVM引入轻量级锁的概念。加锁过程如下:

每个线程获取锁时,都会在自己的线程栈中创建一个锁记录(LockRecord)并将其obj字段指向锁对象。然后通过CAS操作将对象头中的MarkWord替换为指向LockRecord地址的指针,同时把原来锁对象MarkWord保存到LockRecord中,完成数据交换。如果CAS成功完成交换数据,则表明此时线程持有了这个对象锁,如果CAS失败,有两种情况:如果此时有多线程竞争,则直接升级为重量级锁,如果当前还是同一个线程持有锁,发生了锁重入,则在当前线程栈中再添加一条LockRecord,

此时线程栈中LockRecord的数量就是锁的重入次数,每新加入一个锁记录,也都要执行一个obj指向锁对象和CAS的两步操作。有重入的锁记录其所锁记录地址为null。

解锁:遍历线程栈,找到所有obj字段等于锁对象的LockRecord,如果锁记录的MarkWord为null,则这个是重入锁的所记录,则清除这个锁记录,如果锁记录的MarkWord不为null,则用CAS指令将对象头中的MarkWord恢复为无锁状态(重写交换一次数据),如果失效则膨胀为重量级锁。

image.gif 编辑

偏向锁加锁解锁流程:

没有锁竞争,只有一个线程自己持有锁时采用偏向锁。加锁流程如下:

首先线程获取锁时会在自己的线程栈中创建一个LockRecord记录,并将其obj字段指向锁对象。然后通过CAS指令将LockRecord的线程id存到锁对象对象头的MarkWord中,同时设置偏向锁的标识为001,如果成功完成CAS则获取锁成功。如果出现锁重入,则再次创建一个WordRecord,同样将obj字段指向对象锁,然后此时不再执行CAS操作了,直接判断对象头中的线程id是否是自己的即可。一旦锁发生了竞争,就会升级为重量级锁。

3.5.4 wait/notify

obj.wait() 让进入 object 监视器的线程到 waitSet 等待

obj.notify() object 上正在 waitSet 等待的线程中挑一个唤醒

obj.notifyAll() object 上正在 waitSet 等待的线程全部唤醒

image.gif 编辑

3.5.5 Park/Unpark

它们是 LockSupport 类中的方法

// 暂停当前线程
LockSupport.park(); 
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)

image.gif

Object wait & notify 相比:

waitnotify notifyAll 必须配合 Object Monitor 一起使用,而 parkunpark 不必

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

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

3.6 重新理解线程状态转化

1.new->runnable:

线程t调用t.start()方法时。

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运行结束或者调用了当前线程的interrupt()时,线程从Waiting->Runnable。

4.Runnable->Waiting:

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

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

RUNNABLE。

5.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。

6.Runnable->Timed_Waiting:

当前线程调用 t.join(long n) 方法时,当前线程RUNNABLE --> TIMED_WAITING,注意是当前线程t 线程对象的监视器上等待。当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程interrupt() 时,当前线程TIMED_WAITING --> RUNNABLE。

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) 时,当前线RUNNABLE --> TIMED_WAITING。调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING--> RUNNABLE。

9.Runnable->Blocked

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

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

10.Runnable->Terminated

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

3.7 死锁

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

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

这个时候t1线程和t2线程都在互相等待对方的锁,就产生了死锁。

package com.itheima.basic;
import static java.lang.Thread.sleep;
public class Deadlock {
    public static void main(String[] args) {
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                System.out.println("lock A");
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (B) {
                    System.out.println("lock B");
                    System.out.println("操作...");
                }
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            synchronized (B) {
                System.out.println("lock B");
                try {
                    sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (A) {
                    System.out.println("lock A");
                    System.out.println("操作...");
                }
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}

image.gif

定位死锁:监测死锁用jps定位进程id,再用jstack定位死锁。也可以使用jconsole工具或VisualVM等故障处理工具。

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

3.8 活锁

两个线程互相改变对方的结束条件,最后谁也无法结束。

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

image.gif

3.9 饥饿

一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不

易演示,讲读写锁时会涉及饥饿问题。

下面我讲一下我遇到的一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题

image.gif 编辑

image.gif 编辑

3.10 AQS

全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,它是构建锁或者其他同步组件的基础框架。

AQS常见的实现类

  • ReentrantLock 阻塞式锁
  • Semaphore 信号量
  • CountDownLatch 倒计时锁

在AQS中维护了一个使用了volatile修饰的state属性来表示资源的状态,0表示无锁,1表示有锁。

线程0来了后,去尝试修改state属性,如果state属性为0,就修改其为1,标识线程0抢锁成功,线程1和2也会尝试修改state属性,发现此时state=1,有其他线程持有锁了,此时线程1和2会进入FIFO队列中等待,FIFO是一个双向队列,同时维护了标识头节点和尾节点的两个指针。在去修改state状态的时候,使用的cas自旋锁来保证原子性,确保只能有一个线程修改成功,修改失败的线程将会进入FIFO队列中等待

image.gif 编辑

公平锁与非公平锁:

新的线程与队列的等待线程共同抢资源,是非公平锁。新的线程到队列中等待,只让队列中的head线程获取锁,满足先来先得,是公平锁。

比较典型的AQS实现类ReentrantLock,它默认就是非公平锁,新的线程与队列中的线程共同来抢资源。

3.11 ReentrantLock

ReentrantLock翻译过来是可重入锁,相对于synchronized它具备以下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置公平锁
  • 支持多个条件变量
  • 与synchronized一样,都支持重入
// 获取锁
reentrantLock.lock();
try {
 // 临界区
} finally {
 // 释放锁
 reentrantLock.unlock();
}

image.gif

ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似

构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。查看ReentrantLock源码中的构造方法:

image.gif 编辑

提供了两个构造方法,不带参数的默认为非公平

如果使用带参数的构造函数,并且传的值为true,则是公平锁

其中NonfairSync和FairSync这两个类父类都是Sync

而Sync的父类是AQS,所以可以得出ReentrantLock底层主要实现就是基于AQS来实现的。

实现思路:

image.gif 编辑

  • 线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功
  • 假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部
  • 当exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程
  • 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁

3.12 synchronized和Lock有什么区别 ?

第一,语法层面

  • synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现,退出同步代码块锁会自动释放
  • Lock 是接口,源码由 jdk 提供,用 java 语言实现,需要手动调用 unlock 方法释放锁

第二,功能层面

  • 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
  • Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量,同时Lock 可以实现不同的场景,如 ReentrantLock, ReentrantReadWriteLock

第三,性能层面

  • 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
  • 在竞争激烈时,Lock 的实现通常会提供更好的性能

统合来看,需要根据不同的场景来选择不同的锁的使用。


相关文章
|
6天前
|
人工智能 JSON 机器人
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
本文带你零成本玩转OpenClaw:学生认证白嫖6个月阿里云服务器,手把手配置飞书机器人、接入免费/高性价比AI模型(NVIDIA/通义),并打造微信公众号“全自动分身”——实时抓热榜、AI选题拆解、一键发布草稿,5分钟完成热点→文章全流程!
10897 80
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
|
6天前
|
人工智能 IDE API
2026年国内 Codex 安装教程和使用教程:GPT-5.4 完整指南
Codex已进化为AI编程智能体,不仅能补全代码,更能理解项目、自动重构、执行任务。本文详解国内安装、GPT-5.4接入、cc-switch中转配置及实战开发流程,助你从零掌握“描述需求→AI实现”的新一代工程范式。(239字)
3970 129
|
2天前
|
人工智能 Kubernetes 供应链
深度解析:LiteLLM 供应链投毒事件——TeamPCP 三阶段后门全链路分析
阿里云云安全中心和云防火墙已在第一时间上线相关检测与拦截策略!
1366 5
|
3天前
|
人工智能 自然语言处理 供应链
【最新】阿里云ClawHub Skill扫描:3万个AI Agent技能中的安全度量
阿里云扫描3万+AI Skill,发现AI检测引擎可识别80%+威胁,远高于传统引擎。
1271 3
|
12天前
|
人工智能 JavaScript API
解放双手!OpenClaw Agent Browser全攻略(阿里云+本地部署+免费API+网页自动化场景落地)
“让AI聊聊天、写代码不难,难的是让它自己打开网页、填表单、查数据”——2026年,无数OpenClaw用户被这个痛点困扰。参考文章直击核心:当AI只能“纸上谈兵”,无法实际操控浏览器,就永远成不了真正的“数字员工”。而Agent Browser技能的出现,彻底打破了这一壁垒——它给OpenClaw装上“上网的手和眼睛”,让AI能像真人一样打开网页、点击按钮、填写表单、提取数据,24小时不间断完成网页自动化任务。
2697 6
|
5天前
|
人工智能 机器人 API
从零搭建OpenClaw多智能体系统:部署、API配置+飞书多机器人管理手册
在团队协作场景中,单一AI智能体往往难以满足多部门、多场景的差异化需求——研发团队需要代码专家,运营团队需要内容策划助手,客服团队需要高效问答机器人,若所有需求都由同一个智能体承接,不仅会导致响应质量下降,还可能出现记忆混乱、权限失控等问题。2026年,OpenClaw(曾用名Clawdbot)的多Agent架构完美解决了这一痛点,通过“多飞书机器人账号+多独立Agent+路由绑定”的配置,可实现不同机器人对应专属AI大脑,各司其职、精准响应。
1307 1