Java中的synchronized关键字原理详解
在Java多线程编程中,保证线程安全是十分重要的问题。而synchronized关键字,是Java中最常用的同步机制之一。本文将深入学习synchronized关键字的原理,包括为什么能够保证线程安全、如何保证或实现原子性、可见性和有序性等内容。
什么是synchronized?
synchronized是Java中最基本的同步机制之一,用来保护共享资源的访问。它可以修饰方法或代码块,被修饰的方法或代码块被称为同步方法或同步块。当一个线程想要执行同步方法或同步块时,它必须先获取该方法或块的锁,如果其他线程已经持有该锁,那么当前线程就会进入阻塞状态,直到其他线程释放了它所持有的锁。
synchronized关键字保证线程安全的原理
在Java中,线程安全是通过避免数据竞争(data race)来保证的。当多个线程并发地访问共享变量时,如果没有使用同步机制来保护这些变量的访问,就会出现数据竞争。也就是说,数据竞争的发生是因为多个线程在没有协调的情况下同时修改了同一个共享变量,从而导致该变量的值不确定或出现错误。synchronized关键字通过锁机制来保护共享对象的访问,从而避免数据竞争。
synchronized关键字的本质是对一个对象的监视器(monitor)进行访问控制。每个Java对象都有一个与之关联的监视器,当且仅当一个线程持有该对象的锁时,它才能进入这个对象的监视器中。在同步块中,首先需要获取锁,然后才能执行其中的代码,执行完毕后才会释放锁。在这个过程中,如果其他线程想要进入同步块,则必须等待当前线程执行完毕并释放锁之后才能继续执行。
synchronized保证原子性的实现
原子性是指一个操作或一组操作是不可分割的单位,要么全部完成,要么全部不完成。在多线程并发执行的情况下,如果没有采取特殊的措施,就会出现多个线程同时修改同一个变量,导致最终结果与预期不符。synchronized关键字可以用来保证特定代码块或方法的原子性。
假设有一个计数器,多个线程并发地对其进行自增操作:
public class Counter {
private int count = 0;
public void increment() {
count++;
}
}
该代码不是线程安全的,因为多个线程可能同时对count进行自增操作,从而导致计数器的值错误。可以使用synchronized关键字将increment()方法变成同步方法,从而保证其原子性:
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
以上代码中,increment()方法前加了synchronized关键字,表示该方法是同步方法,同一时刻只有一个线程能够调用该方法。这样就能够保证自增操作是原子性的,避免了数据竞争。
synchronized保证可见性的实现
可见性是指当一个线程修改了共享变量的值后,其他线程能够立即看到修改之后的值。如果共享变量没有被同步保护,那么其他线程就可能无法及时地看到共享变量的最新值,从而导致程序出错。synchronized关键字可以用来保证共享变量的可见性。
假设有两个线程A和B并发地对一个共享变量flag进行读写操作:
public class VisibilityDemo {
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (!flag) {
// do nothing
}
System.out.println("Thread A is finished.");
}).start();
Thread.sleep(1000); // 等待1秒钟,确保Thread A已经启动
new Thread(() -> {
flag = true;
}).start();
}
}
以上代码中,线程A不断地读取flag变量的值,直到其变为true才会结束。而在主线程中,等待了1秒钟之后就启动了另一个线程B,该线程将flag变量的值修改为true。根据线程安全的要求,应该能够看到线程A输出"Thread A is finished.",但实际上程序很可能陷入死循环,因为线程A无法及时地看到flag变量的最新值。
为了解决这个问题,可以使用synchronized关键字来保证变量的可见性。将flag变量声明为volatile类型即可保证共享变量的可见性:
public class VisibilityDemo {
private static volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (!flag) {
// do nothing
}
System.out.println("Thread A is finished.");
}).start();
Thread.sleep(1000); // 等待1秒钟,确保Thread A已经启动
new Thread(() -> {
flag = true;
}).start();
}
}
以上代码中,flag变量前加上了volatile关键字,表示该变量是一个共享变量,需要保证其可见性。这样一来,对flag变量的修改操作就会立即被其他线程看到,从而避免了可见性问题。
synchronized保证有序性的实现
有序性是指程序执行的顺序按照代码的先后顺序执行,在多线程并发执行的情况下需要特殊注意。在执行语句的过程中,可能会因为编译器或者硬件优化的原因导致指令执行顺序的改变,从而导致程序的执行结果出现错误。synchronized关键字可以用来保证程序的有序性。
假设有一个简单的例子,其中一个线程负责向共享的队列中添加元素,另外一个线程负责读取队列中的元素:
public class OrderDemo {
private List<String> queue = new ArrayList<>();
public synchronized void add(String element) {
queue.add(element);
}
public synchronized String remove() {
if (queue.isEmpty()) {
return null;
}
return queue.remove(0);
}
}
以上代码中,add()方法和remove()方法都被声明为同步方法,这样就能够保证它们的访问顺序是按照代码的先后顺序执行。
总结
本文主要介绍了Java中synchronized关键字的工作原理,包括如何保证线程安全、如何保证或实现原子性、可见性和有序性等内容。在多线程编程中,使用synchronized关键字是十分常见的方式,它能够保证程序的正确性以及保护共享资源的访问。需要注意的是,虽然synchronized关键字是保证线程安全的一种方式,但是它会降低程序的执行效率,因此在实际开发中应该根据实际情况进行选择。