本篇参考书籍为JaveEE零基础入门/史胜辉,王春明,沈学华编著.—北京:清华大学出版社,2021.1(2022.8重印) ISBN 978-7-302-56938-1
前言
大家好!今天让我们一起揭开Java多线程同步中的神秘面纱,特别是那个至关重要的关键字——synchronized。我们将从多线程的基础知识开始,逐步深入理解synchronized的工作原理,并通过一些生活化的例子帮助大家更好地消化这些概念。
一、多线程的生命周期与基本概念
首先,我们先回顾一下多线程的基本概念。在Java程序中,多线程是指在一个进程中可以同时执行多个不同任务的能力。每个任务作为一个独立的执行流(线程)存在,它们共享进程的内存空间,但各自拥有独立的执行上下文和堆栈。具体可以回顾一下我之前写的一篇,链接: 多线程的概念以及如何创建线程.
在Java程序中,每个线程都有一段生命周期,包括新建(new)、就绪(runnable)、运行(running)、阻塞(blocked)、等待(waiting)、超时等待(timed waiting)和终止(terminated)状态。
1、新建(New):当使用new关键字创建一个线程对象时,线程处于新建状态。此时,线程对象还未被启动,start()方法尚未被执行。
2、就绪(Runnable):当调用线程对象的start()方法后,线程进入就绪状态。此时线程已经准备好运行,但还没有分配到CPU时间片。线程位于操作系统的就绪队列中,等待被操作系统调度执行。
3、运行(Running):当线程获得CPU时间片后,开始执行run()方法,此时线程处于运行状态。需要注意的是,即使线程在运行,也可能因为线程调度的原因暂时让渡CPU给其他线程,随后再次回到运行状态。
4、阻塞(Blocked/Waiting):当线程执行过程中遇到以下情况时,会从运行状态转换为阻塞状态:
线程调用了sleep()、wait()、join()等方法;
线程尝试获取一个被其他线程持有的锁(即进入synchronized代码块或方法);
线程在等待某个I/O操作完成;
线程在等待某个条件满足(如Condition.await()方法)。
5、超时等待(Timed Waiting):类似于阻塞状态,但线程在等待一定时间后会自动恢复到就绪状态,例如调用了sleep(long millis)、wait(long timeout)、Future.get(long timeout, TimeUnit unit)等带超时参数的方法。
6、终止(Terminated):线程执行结束(run()方法执行完毕),或者主线程调用了Thread.stop()(已废弃,不推荐使用)、Thread.interrupt()(中断线程)等方法,线程会进入终止状态。终止状态的线程无法再次被启动。
当多个线程同时存在于一个程序中时,它们会在CPU调度下交替执行,共同完成一项或多项任务。
举个栗子:想象一下,一个厨房里有几位厨师(线程)同时做菜,他们分别有自己的案板和厨具(线程的运行资源)。如果多位厨师都需要同一只汤锅(共享资源),如果不加以协调,可能会出现两位厨师同时往锅里加料,导致混乱不堪的局面。在Java多线程中,这种资源共享的问题就需要借助synchronized来解决。
二、竞态条件与线程安全
竞态条件就像是上面提到的厨师争夺汤锅的例子。在多线程环境下,如果多个线程同时读取或修改共享数据,没有适当的保护措施,就可能出现不可预测的结果,这就是竞态条件。
解决这个问题的关键就是保证在同一时刻只有一个线程访问特定的资源,这就是“线程同步”的核心所在。而在Java中,synchronized关键字正是用于实现这一目标的关键工具。
三、synchronized关键字的使用
synchronized关键字有两种基本使用方式:同步方法和同步代码块。
1、同步方法:
public class CounterKitchen {
private int spoonCount = 0;
// 把整个"烹饪"过程(方法)上锁
public synchronized void useSpoon() {
spoonCount++;
System.out.println("正在使用的勺子数量:" + spoonCount);
// 做菜的过程...
spoonCount--;
System.out.println("归还勺子,现在勺子数量:" + spoonCount);
}
}
在这个例子中,useSpoon()方法被 synchronized修饰,意味着任何时刻只能有一个线程能执行这个方法,就像只能有一位厨师在使用汤锅一样。
2、同步代码块:
尽管可以在创建类时,把访问共享资源的方法定义为同步方法,实现线程对共享资源同步,但是这种方法并不是一直有效。例如:程序中调用了一个第三方类库中某个类的方法,无法获得该类库的源代码,这样,无法在相关方法前添加synchronized关键字。那怎么解决这个问题呢?通过使用同步代码块可以解决这个问题。
public class CounterKitchen {
private int spoonCount = 0;