Synchronized 同步块可以是一个方法或一段synchronized修饰的代码。Synchronized同步块同一时间只能有一个线程访问,这也是其能避免争用条件的原因。下面详细介绍下synchronized关键字工作原理。
synchronized关键字
同步块是由关键字synchronized关键字修饰的。java同步块同步某些对象,使其在同一时候只能有提个线程能访问。其他线程尝试进入同步块的时候讲被阻塞,直至当前线程退出同步块。
synchronized关键字可以用在以下几种地方
- 成员方法
- 静态方法
- 成员方法里面的代码块
- 静态方法里面的代码块
成员方法
public class MyCounter {
private int count = 0;
public synchronized void add(int value){
this.count += value;
}
}
成员方法是对象的方法,同步成员方法也是不同的对象所拥有的(自己的的实例对象)
同一实例对象同一时刻只能有一个线程访问实例的同步成员方法。如果多个实例存在,那么每个实例同一时刻可以有一个线程。一个线程对应一个实例。
对于实例对象中有多个同步方法的也是一样的。所以下面的例子一个线程同一时刻只能执行两个同步方法中的其中一个。也就是一个线程一个实例对象
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;
}
}
静态同步方法
public static MyStaticCounter{
private static int count = 0;
public static synchronized void add(int value){
count += value;
}
}
就像普通成员成员方法前面加synchronized,静态方法前面加synchronized就是静态同步方法了。
静态同步方法是作用于方法持有的类的。因为java虚拟机中一个一个类只有一个类对象,所以同一个类同一时刻只有一个线程能够访问同步静态方法。
如果有不知一个静态同步方法,那么同一时刻只有一个线程能访问其中一个方法。来看个例子:
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线程B不能执行add或subtract除非线程A退出add方法。
如果两个静态同步方法是在不同的类中的,那么一个线程同一时刻可以访问其中一个类中的方法。一个线程对应一个类。
普通成员方法中的同步块
如果你不想将整个方法都变为同步那么你可以在方法内用同步块,请看例子:
public void add(int value){
synchronized(this){
this.count += value;
}
}
注意同步块括号中的对象,在这个例子中是this,就是add方法所属的对象。synchronized同步块括号中的对象成为监视器对象。同步块中的代码被监视器对象同步。同步实例方法将其所属的对象用作监视对象。
同一个监视器对象同一时刻只能有一个线程
下面两个例子都是在调用他们的实例对象上同步,他们的同步的效果是相同的。
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换为其他对象,那么同一时刻每个方法都可以被一个线程访问。
静态方法中的同步块
同步块在静态方法中的应用。下面的例子两个方法在类的class对象上是同步的。
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表达式或者匿名类中也可以用同步块。
下面的的lambda表达式中有一个同步块。注意同步块的监视器对象是持有该同步块的类。当然这边是可以换一个对象的如果场景需要。下面的例子用class对象更加合理。
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();
}
}
Example
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对象,那么两个线程都不会阻塞。
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关键字,当一个线程改变了变量的指将无法保证其他线程会看到变更。无法保证线程何时将cpu寄存器中的变量提交到主内存。无法保证其他线程何时会将cpu寄存器中的指刷到主内存中。
synchronized:当一个线程进入同步块所有的变量都会刷新使其对线程可见。当一个线程退出同步块的时候所有的变更将提交到主内存。这跟volatile关键字有些像。
同步和指令重排序
Java编译器和java虚拟机是允许指令重排序的,这样可以提高执行效率,尤其是CPU并行执行。
指令重排序在多线程执行的时候可能会引起一些问题。比如讲synchronized块中的写变量的操作重排序之后写操作在synchronized块之外了。
为了解决这个问题synchronized关键字之前,内部,之后 对于重排序设置了一些约束。这也跟volatile关键字有些像。这样可以确保你的代码正确运行–代码将按照你的期望运行,不会被指令重排序干扰。
同步哪些对象
之前多次提到一个同步块必须在某一个对象上同步。你可以选任何一个对象,除了String对象和其他原始类型的包装类,因为编译器会优化他们,使你可以在多个地方使用它们,看起来是在用不同的对象–实际上是同一个对象。来看下下面例子
synchronized("Hey") {
//do something in here.
}
如果你有多个同步块在“Hey”上同步,实际上编译器底层使用的是同一个对象。导致多个同步块同步在同一个对象上。这可能不是你所预期的。
下面是Integer也是一样
synchronized(Integer.valueOf(1)) {
//do something in here.
}
多次调用Integer.valueOf(1)将返回同一个对象。
同步块的缺点和其替代方法
即使两个线程是读取一个共享变量,同一时刻也只能有一个线程读取。所以java中有什么办法解这个问题?办法是有的你可以用Read/Write Lock
如果你想让N个线程进入到同步块而不是一个咋办?那么信号量Semaphore可以解决这个问题。
Synchronized块是公平锁,它不保证等待线程的优先级,是随机的。假设你想让线程按照一定顺序进入到同步块这就得实现一个非公平锁。
如果只有一个写线程,其余的都是读线程,那么你可能只需要用volatile变量就ok了。
这些优化的锁将会在之后JUC中讲到。
同步块的性能开销
Synchronized关键字是有些许性能开销的。虽然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,它是一个递归方法。线程调用count会进入同步块多次,这是允许的。
需要注意的是,多同步块设计的时候要特别小心放置嵌套监视器锁定
集群中的同步块
记住同步块只能阻塞同一个虚拟机中的其他线程,如果多个虚拟机,那么每个虚拟机同一时刻可以有一个线程进图同步块。
如果你想跨虚拟机同步,那么可能需要同步其他一些手段,比如分布式锁。