Java 中 synchronized 和 Thread 的使用场合介绍
在 Java 编程中,synchronized
和 Thread
是处理并发与多线程编程的关键工具。多线程编程是为了在单一程序中并行执行多个任务,Java 提供了丰富的 API 和关键字以实现这一目标,而其中 synchronized
和 Thread
是非常基础和重要的部分。
synchronized
的用途和实现机制
synchronized
关键字用于在 Java 程序中实现线程同步。线程同步的主要目的是解决多线程访问共享资源时的竞争问题,确保数据的一致性。Java 提供了内置的同步机制来避免多个线程同时访问共享资源而引发的竞争条件。synchronized
可以用来修饰方法或代码块,确保在某一时刻只能有一个线程访问被修饰的代码片段。
synchronized
的三种使用方式
- 修饰实例方法:当一个方法被
synchronized
修饰后,线程在调用该方法时会先获得当前对象的锁。其他线程如果也想调用该方法或其他同样使用了synchronized
修饰的实例方法,则必须等待当前线程释放锁。典型的使用场景是多个线程同时操作同一对象时需要同步。
-
修饰静态方法:当一个静态方法被
synchronized
修饰后,线程在调用该方法时会获得该类对象的类级别锁。由于静态方法属于类而不是某个实例,因此此种情况下加锁的是类对象本身,而非具体的实例。 -
修饰代码块:代码块级别的同步通过
synchronized
锁定特定的对象,而非整个方法。这样可以灵活控制锁的粒度,减少对性能的影响。这种方式尤其适合只需要同步部分代码的场景,而非整个方法。
JVM 中的实现
synchronized
的底层实现依赖于 JVM 的对象监视器(Monitor)。在 JVM 中,每个对象都关联有一个监视器对象,当一个线程进入 synchronized
块时,它必须首先获得与该对象关联的监视器。当该线程获得监视器时,其他线程就无法再进入该对象的 synchronized
代码块,直到当前线程退出并释放监视器。
上图出处:https://medium.com/javarevisited/whats-a-monitor-in-java-8f0ebecaea2a
监视器的实现依赖于两条 JVM 指令:monitorenter
和 monitorexit
。在 Java 字节码层面,编译后的 synchronized
代码块会插入 monitorenter
和 monitorexit
指令,分别对应线程进入和退出同步代码块。JVM 在执行这两条指令时会检查当前线程是否拥有该监视器,确保锁的获取和释放是正确的。
具体来看,monitorenter
指令会在进入 synchronized
代码块时执行,它会尝试获取监视器对象的所有权。如果获取成功,线程可以进入临界区;如果获取失败,线程将被阻塞,直到监视器被释放。monitorexit
指令则在 synchronized
代码块执行完毕后释放监视器,使其他等待的线程有机会获得锁。
值得注意的是,JVM 优化了 synchronized
的性能,引入了偏向锁(biased locking)、轻量级锁和重量级锁等机制,以减少线程争抢锁的开销。在没有竞争的情况下,锁的获取和释放成本极低,提升了多线程环境下的性能。
真实世界的例子
假设一个在线银行系统中,多个用户可能同时对他们的账户进行操作,比如存款、取款和转账。为了避免多个线程同时操作同一个账户时引发的数据不一致问题,可以使用 synchronized
确保每次只能有一个线程执行修改操作。
public class BankAccount {private int balance;public BankAccount(int initialBalance) {this.balance = initialBalance;}public synchronized void deposit(int amount) {balance += amount;}public synchronized void withdraw(int amount) {if (balance >= amount) {balance -= amount;} else {System.out.println("Insufficient funds");}}public synchronized int getBalance() {return balance;}
}
在上面的代码中,deposit
、withdraw
和 getBalance
方法都被 synchronized
修饰,保证多个线程访问这些方法时不会发生竞态条件。
Thread
的用途和实现机制
在 Java 中,Thread
是创建并管理线程的基本类。每个 Thread
对象代表一个独立执行的线程。Java 提供了两种实现多线程的方式:通过继承 Thread
类或者实现 Runnable
接口。
Thread
的使用方式
- 继承
Thread
类:通过继承Thread
类并重写run()
方法来定义线程的任务。线程启动时调用start()
方法,系统会自动调用run()
方法执行线程中的任务。
class MyThread extends Thread {public void run() {System.out.println("Thread is running...");}
}public class Main {public static void main(String[] args) {MyThread thread = new MyThread();thread.start();}
}
- 实现
Runnable
接口:这是创建线程的另一种方式,通过实现Runnable
接口并重写run()
方法定义任务,然后将Runnable
对象传递给Thread
对象。
class MyRunnable implements Runnable {public void run() {System.out.println("Thread is running...");}
}public class Main {public static void main(String[] args) {Thread thread = new Thread(new MyRunnable());thread.start();}
}
两者的区别在于,继承 Thread
类的方式不能再继承其他类,而实现 Runnable
接口的方式可以实现多重继承。因此,推荐使用 Runnable
接口实现多线程。
JVM 中的线程模型
Java 的线程由 JVM 和底层操作系统共同管理。在 JVM 层面,每个 Thread
对象都与一个操作系统的原生线程关联。线程的调度由底层操作系统完成,JVM 通过操作系统的 API 创建、销毁和管理线程。
在现代操作系统中,线程调度通常基于时间片轮转(time slicing)或抢占式调度(preemptive scheduling)。操作系统会根据优先级、时间片等因素在不同线程之间切换,确保多线程程序的高效运行。JVM 的线程调度由操作系统提供支持,确保多线程程序能够充分利用系统资源。
Java 中的线程有五种基本状态:新建(New)、就绪(Runnable)、运行中(Running)、阻塞(Blocked)和终止(Terminated)。线程从创建到结束的整个生命周期都由 JVM 进行管理。JVM 通过线程调度器(Thread Scheduler)决定哪个线程在某一时刻应该被执行,线程的调度是不可预知的,具体的调度方式依赖于底层操作系统。
线程的同步与通信
在多线程编程中,线程之间的同步与通信是一个重要的话题。除了通过 synchronized
实现的线程同步外,Java 还提供了 wait()
、notify()
和 notifyAll()
等方法进行线程之间的通信。这些方法用于在多个线程之间协调工作,避免线程的忙等待(busy waiting)问题。
当一个线程执行 wait()
方法时,它会释放持有的锁并进入等待状态,直到其他线程调用 notify()
或 notifyAll()
方法唤醒它。这种机制通常用于生产者-消费者模型中,生产者线程负责生成数据,消费者线程负责处理数据,wait()
和 notify()
用于协调两者的工作。
线程池的使用场景
虽然 Thread
类是多线程编程的基础,但在实际应用中直接使用 Thread
并不是最优的选择,尤其是在需要频繁创建和销毁线程的场景下。线程的创建和销毁是昂贵的操作,会影响程序的性能。为了解决这个问题,Java 提供了线程池(Thread Pool)机制,允许重用现有的线程,而不是每次都创建新的线程。
Java 的 java.util.concurrent
包提供了多种线程池的实现,如 FixedThreadPool
、CachedThreadPool
和 ScheduledThreadPool
等。线程池通过事先创建一定数量的线程,并将任务提交给这些线程处理,从而避免频繁创建和销毁线程的开销。
真实世界的例子
假设一个 Web 服务器需要处理大量并发请求,每个请求可能涉及复杂的计算和 I/O 操作。为了提高服务器的性能,通常会使用线程池来处理这些请求,而不是为每个请求都创建一个新的线程。下面的例子展示了如何使用线程池处理并发任务:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class WebServer {private final ExecutorService threadPool = Executors.newFixedThreadPool(10);public void handleRequest(Runnable request) {threadPool.execute(request);}public void shutdown() {threadPool.shutdown();}public static void main(String[] args) {WebServer server = new WebServer();// 模拟多个并发请求for (int i = 0; i < 100; i++) {server.handleRequest(() -> System.out.println("Handling request in thread: " + Thread.currentThread().getName()));}server.shutdown();}
}
volatile
与 synchronized
的对比
在并发编程中,除了 synchronized
,Java 还提供了 volatile
关键字来控制变量的可见性。volatile
关键字用于修饰共享变量,保证变量的可见性,但不保证操作的原子性。相较于 synchronized
,volatile
更加轻量级,但适用的场景较为有限。
当一个变量被声明为 volatile
时,JVM 保证所有线程在访问该变量时都能读取到最新的值。JVM 通过禁用线程的本地缓存确保这一点。因此,volatile
适用于简单的标志位变量,而不适用于复杂的线程同步场景。
真实世界的例子
volatile
适用于某些无需复杂同步的场景,例如一个停止标志位:
public class VolatileExample {private volatile boolean running = true;public void start() {while (running) {System.out.println("Thread is running...");}}public void stop() {running = false;}public static void main(String[] args) {VolatileExample example = new VolatileExample();Thread thread = new Thread(example::start);thread.start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}example.stop();}
}
在这个例子中,running
变量被声明为 volatile
,确保当 stop()
方法被调用时,running
的修改能被其他线程立即看到,从而使线程正确退出。
结束语
Thread
和 synchronized
是 Java 多线程编程的核心概念。通过理解它们的用途和在 JVM 中的实现机制,开发者可以编写高效的并发程序。在实际应用中,合理地使用线程池、volatile
关键字以及高级并发工具,可以帮助开发者更好地管理并发操作,提升程序的性能和稳定性。