Java 同步块标识着一个方法或者一个代码块是同步处理的。Java同步块可以用来避免竞态条件。

Java 中的 synchronized 关键字

在Java中,通过synchronized关键字来标记同步块。Java中的同步块是基于某些对象上的同步。 所有在同一个对象上的同步块,意味着同一个时间点只能有一个线程可以进入。 而其他想要进入的线程会一直阻塞等待已经入同步块的线程退出。

synchronized关键字可以用来表示4中不同的同步块:

实例方法(this)

静态方法(Class)

实例方法中的代码块(Object)

静态方法中的代码块(Object)

这些同步块是在不同的对象上进行同步的。使用时需要结合具体的情况。

TIPS:(使用时,需要思考在什么对象上进行同步,如果在不同的对象上同步,则相互无法进行约束,也就达不到同步的效果)

实例中的同步方法

public synchronized void add(int value){
this.count += value;
}

注意方法签名中的synchronized关键字。这就意味着Java会对这个方法的调用进行同步处理。

实例中的同步方法,是在实例本身(this)上进行同步。因此,每个实例中的同步方法是在不同的对象上进行的同步。 只会有一个线程可以进入一个实例同步方法,而如果有多个实例,那么每个实例的同步方法都可以有一个线程进入。 也就是说,这种同步是每个实例一个线程

静态同步方法

静态方法上带上synchronized关键字,作用和实例方法类似。如:

public static synchronized void add(int value){
count += value;
}

静态方法的同步是在class对象上,即:静态方法的同步是基于所属的Class上。 JVM的机制可以保障同一个Class只会有一个,所以,静态同步方法,意味着同一时间只会有一个线程可以执行一个Class中的静态同步方法。

如果是不同Class中的静态同步方法,那一个线程就可以执行每个Class中的同步方法(同步对象是不同的Class)

这种同步就是每个Class一个线程

实例方法中的同步代码块

可以不对整个方法使用同步。有的时候,只对方法中的一部分逻辑进行同步会更好。

TIPS:(更小的锁粒度,有助于降低锁竞争,提高吞吐量)

那这种情况,就可以在方法中使用同步代码块。如:

public void add(int value){
synchronized(this){
this.count += value;
}
}

可以看到,这个方法签名上并没有sychronized关键字。 而是,在方法内,使用了Java同步代码块,这个代码块的作用和同步方法是一样的。

要注意,Java同步代码块在括号中会指定一个对象。上例中,使用this,这就意味着这个代码块使用了this,意味着是在实例本身上做同步,与同步方法是同一个同步对象。 而这个括号里的对象,被称之为:监控器对象(monitor)。 而实例同步方法,实际也是使用方法所在实例的监控器对象作为同步对象的。

由同一个监控器对象所保护的代码块(或者,方法),一次只能有一个线程可以进入。

来看看下面这个例子:

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

这个例子中的两个方法,实际是使用了同一个监控器对象来做同步的。因此,从同步的角度来说,这两个方法是等价的。

也正是因为两个方法是由同一个监控器对象来保护的,所以,同一时间点,只能有一个线程进入其中任意一个方法。

而如果第二个方法的同步块中不是使用this,而是使用其他对象来做监控器,那两个方法就可以同时被不同的线程访问。

静态方法中的同步代码块

再来看看静态方法中使用同步代码块的形式:

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

和上一节类似。只不过,静态同步方法,是使用方法所在的Class作为监控器对象的。 因此,这个例子中的两个方法,也是在同一个监控器对象的保护下。因此,同一时间点,只能有一个线程能够进入这两个方法。

Java 同步示例

下面展示了一个示例,启动两个线程调用同一个Counter实例中的add方法。 因为,是同步方法,所以,同一个时间,同一个实例中的add方法只能被一个线程调用。

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

创建两个线程。两个线程,实际是操作着同一个Counter实例。对于同步方法add,两个线程不能同时访问。 因此,同一时间,只能有一个线程执行add方法,另一个线程只能阻塞等待。这样Counter中的计数器是并发安全的。

而如果两个线程各自持有着一个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();
}
}

线程 counterA 和 counterB 调用的 add 方法,实际是由两个监控器同步的。因此, counterA 不会因为 counterB 调用add而阻塞。

Java并发工具集(JUC包)

synchronized是Java语言层面的一个最基本的同步机制。synchronized并没有什么高级的功能,而且性能代价比较大(尽管JDK5u6中,引入偏向锁 提高了synchronized 的性能)。所以,从Java 5开始就提供了一套并发工具集:java.util.concurrent(JUC包),通过这些工具可以实现更细粒度的并发控制。