参考:
http://tutorials.jenkov.com/java-concurrency/synchronized.html
Java synchronized关键字
在Java中,同步代码块被synchronized关键字标记。Java中的同步块是在某个对象上同步。同一时间所有的在同一个对象上同步的同步块只能被一个线程进入执行。所有其他尝试进入同步块的线程都将被阻塞直到在同步块内执行的线程退出这个同步块。
synchronized关键字可以用来对以下四种代码块进行标记:
- 实例方法
- 静态方法
- 实例方法中的代码块
- 静态方法中的代码块
这些代码块在不同的对象上同步。我们需要按照需求情况来选择使用何种同步方式。以下是几种同步方式的具体举例。
同步实例方法
这是一个同步实例方法
public class MyCounter {
private int count = 0;
public synchronized void add(int value){
this.count += value;
}
}
注意add()方法声明中使用synchronized关键字告诉 Java 该方法是同步的。
Java 中的同步实例方法在拥有该方法的实例(对象)上进行同步。因此,每个实例对象的实例方法将在不同对象(拥有实例方法的实例对象本身)上进行同步。
每个实例只有一个线程可以在同步实例方法中执行。如果存在多个线程,则每一个实例对象,同一时刻只有一个线程可以进入其同步实例方法中执行。即每个实例一个线程。
对于同一对象(实例)的所有同步实例方法都是如此。因此,在下面的示例中,只有一个线程可以在两个同步方法中的任何一个中执行。
每个实例总共一个线程:
public class MyCounter {
private int count = 0;
public synchronized void add(int value){
this.count += value;
}
public synchronized void subtract(int value){
this.count -= value;
}
}
同步静态方法
在静态方法上使用synchronized关键字与在实例方法相同。下面是一个例子。
public static MyStaticCounter{
private static int count = 0;
public static synchronized void add(int value){
count += value;
}
}
同样,add()方法声明中使用synchronized关键字告诉 Java 该方法是同步的。
同步静态方法是在这个同步静态方法所属类的Class对象上进行同步。因为在JVM中每一个类只有一个Class对象存在,所以只有对于同一个类只有一个线程可以进入它的同步静态方法中执行。
如果一个类包含同步多个静态方法,同一个时刻只能有一个线程可以在这些方法中执行。如下例子:
public static MyStaticCounter{
private static int count = 0;
public static synchronized void add(int value){
count += value;
}
public static synchronized void subtract(int value){
count -= value;
}
}
任何时刻,只有一个线程可以在 add()和subtract() 这两个方法中的任意一个中执行。如果线程A在执行add()方法,则直到线程A退出add()方法之前,线程B不能进入add()和substract()中任何一个。
在实例方法中的代码块上同步
无需同步整个方法。有时候,同步方法中的一部分会更合适。在Java方法中同步代码块使之成为可能。下例是一个非同步实例方法中的同步代码块。
public void add(int value){
synchronized(this){
this.count += value;
}
}
例子中使用了Java同步块构造来标记一块代码是同步的。这个代码会像同步方法一阳执行。
注意Java同步块构造时如何使用括号中的对象。在例子中使用了this,这是调用add()方法的实例对象。同步块构造使用的这个在括号中的对象被称为监视对象。这个代码被称为在这个监视对象上同步。同步实例方法使用其所属的实例对象作为监视对象。
在同一个监视器对象上同步的 Java 代码块内只能执行一个线程。
下面两个例子都在其被调用的实例上进行同步,理论上对于同步两种方式是等价的:
public class MyClass {
public synchronized void log1(String msg1, String msg2){
log.writeln(msg1);
log.writeln(msg2);
}
public void log2(String msg1, String msg2){
synchronized(this){
log.writeln(msg1);
log.writeln(msg2);
}
}
}
因此,在此示例中,只有一个线程可以在两个同步块中的任何一个内执行。
如果第二个同步块在不同的实例对象上进行同步,则同一时刻,每个同步方法内都可以有一个线程在执行。
加静态方法中的同步块
同步块也可以在静态方法内使用。以下是上一节中与静态方法相同的两个示例。这些方法在方法所属的类的类对象上同步:
public class MyClass {
public static synchronized void log1(String msg1, String msg2){
log.writeln(msg1);
log.writeln(msg2);
}
public static void log2(String msg1, String msg2){
synchronized(MyClass.class){
log.writeln(msg1);
log.writeln(msg2);
}
}
}
同一时刻,一共只有一个线程可以在两个方法中的任意一个执行。
如果第二个方法在与MyClass.class对象不同的对象上进行同步,则同一时刻每个方法内都可以有一个线程进行执行。
Lambda表达式中的同步块
甚至可以在Lambda表达式或者匿名内部类中使用同步块。
下面是在Java lambda表达式中时候用同步块。注意同步块是在拥有lambda表达式的类对象上进行同步。如果有别的需求场景,同样可以在其他对象上进行同步。
import java.util.function.Consumer;
public class SynchronizedExample {
public static void main(String[] args) {
Consumer<String> func = (String param) -> {
synchronized(SynchronizedExample.class) {
System.out.println(
Thread.currentThread().getName() +
" step 1: " + param);
try {
Thread.sleep( (long) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(
Thread.currentThread().getName() +
" step 2: " + param);
}
};
Thread thread1 = new Thread(() -> {
func.accept("Parameter");
}, "Thread 1");
Thread thread2 = new Thread(() -> {
func.accept("Parameter");
}, "Thread 2");
thread1.start();
thread2.start();
}
}
Java 同步实例
这是一个例子,两个线程通过同一个实例对象调用方法。同一时刻只有一个线程可以通过这个相同的实例对象调用方法,因为这个方法是同步实例方法。
public class Example {
public static void main(String[] args){
Counter counter = new Counter();
Thread threadA = new CounterThread(counter);
Thread threadB = new CounterThread(counter);
threadA.start();
threadB.start();
}
}
这是上面例子中用到的两个类
public class Counter{
long count = 0;
public synchronized void add(long value){
this.count += value;
}
}
public class CounterThread extends Thread{
protected Counter counter = null;
public CounterThread(Counter counter){
this.counter = counter;
}
public void run() {
for(int i=0; i<10; i++){
counter.add(i);
}
}
}
创建两个线程,使用了同一个Counter实例对象传入线程的构造方法中。Counter的add()方法是在实例对象上进行同步,因为这个add方法是一个实例方法,而且被synchronized关键字标记。理论上同一时刻只有一个线程可以调用这个add()方法。另一个线程会等待,直到第一个线程离开add()方法,第二个方法可以执行这个方法,
如果两个线程分别指向了两个Counter实例对象,同时调用add()方法将不再会出现问题。这两个调用将会指向不同的对象,因此这两个被调用的方法将会在不同的对象(对应归属的实例对象)上进行同步。理论上不会产生阻塞,以下是例子:
public class Example {
public static void main(String[] args){
Counter counterA = new Counter();
Counter counterB = new Counter();
Thread threadA = new CounterThread(counterA);
Thread threadB = new CounterThread(counterB);
threadA.start();
threadB.start();
}
}
同步和数据可视性
没有使用synchronized关键字或者Java volatile关键字的时候,当一个线程修改共享给其他线程的变量的值的时候,不保证其他线程能够看到这个修改后的值。不保证当一个线程保存在CPU寄存器中变量的值什么时候提交至主内存,这一点和volatile的工作原理类似。
同步和指令重排
Java编译器和Java虚拟机允许重排指令顺序以提高你带吗的执行速度,通常是调整代码的顺序以使CPU能够并行执行。
指令重排可能会导致由多个线程同时执行的代码出现问题。例如,如果在同步块内部发生的对变量的写入被重新排序为在同步块外部发生。
为了解决这个问题,Javad的synchronized关键字对同步块之前、内部和之后的指令重新排序设置了一些限制。这类似于volatile 关键字的限制 。
最终结果是,可以确保代码正常工作:没有发生指令重新排序,最终使代码的行为与编写的代码的预期行为相同。
在什么对象上进行同步
同步块必须在某个对象上同步。实际上我们可以选择要同步的任何对象,但建议不要同步 String 对象或任何原始类型包装器对象,因为编译器可能会优化这些对象,以至于我们本意使用不同对象的情况下使用了相同对象。举例如下:
synchronized("Hey") {
//do something in here.
}
如果我们在多个地方使用了这个String值"Hey"来进行同步,虽然看起来是不同的对象,但因为String的机制,最终结果是这些地方使用了同一个对象进行同步,这将会导致与预期不同的行为。
同样的对于包装类型也是一样的,举例如下:
synchronized(Integer.valueOf(1)) {
//do something in here.
}
尽管我们多次调用Integer.valueOf()方法,当输入参数相同时我们很可能会获取到同一个包装对象实例。这意味着,我们使用了同一个对象在不同的地方作为同步块的监视对象。
因此为了安全起见,同步this- 或new Object(). 这些不会被 Java 编译器、Java VM 或 Java 库在内部缓存或重用。
同步块限制和替代方案
在Java中synchronized关键字存在一些限制。例如,Java中,同一时刻一个同步块只能被一个线程进入,等等。Java中提供了一些synchronized关键字的替代方案
- Read / Write Lock
同时允许可以使多个线程访问进行读取,或者只允许一个线程访问进行修改 - Semaphore
信号量类似于操作系统中的信号量,能够允许多个线程进入同一个代码块 - 公平锁
保证每个线程按照请求所的顺序获得锁 - volatile
提供了一写多读的解决方案
同步的性能损耗
在 Java 中进入和退出同步块会产生很小的性能开销。随着 Java 的发展,这种性能开销不断下降,但仍然需要付出很小的代价。
如果在一个紧凑的循环中多次进入和退出同步块,那么需要注意频繁多次进入和退出同步块的性能开销。
此外,尽量不要使用比必要更大的同步块。换句话说,只同步真正需要同步的操作——避免阻塞其他线程执行不需要同步的操作。只有在同步块中绝对必要的指令。这应该会增加代码的并行性。
同步块的重入
当一个线程进入一个同步代码块时,这个线程就被称为持有这个同步的监视对象的锁。如果这个线程在同步块中调用了其他方法,并在其中回调了第一个包含这个同步块的方法,那么持有这个锁的线程可以重新进入同步代码块。这不会产生阻塞,因为这个线程已经持有勒索,只有不同线程持有锁的时候才会产生阻塞。举例如下:
public class MyClass {
List<String> elements = new ArrayList<String>();
public void count() {
if(elements.size() == 0) {
return 0;
}
synchronized(this) {
elements.remove();
return 1 + count();
}
}
}
在这个count方法中,递归调用了其本身。调用此方法的线程将多次进入synchronized同步代码块,这是可能的,也是被允许的。
但是请注意,如果不仔细设计代码,线程进程多个同步块将会产生嵌套监视器锁定。
集群中的同步块
一个同步块只会阻塞同一个JVM中的将要进入同步块的线程。如果在多个JVM中运行相同的Java程序,同一时刻每个JVM中都会有一个线程进入了同一个同步块。
如果需要在集群中的所有JVM之间使用同步,需要使用其他同步机制而不是仅仅一个同步块。