简述
计算机单线程在执行任务时,是严格按照程序的代码逻辑,按照顺序执行的。因此单位时间内能执行的任务数量有限。为了能在相同的时间内能执行更多的任务,就必须采用多线程的方式来执行(注意:多线程模式无法减少单次任务的执行时间 )。但是引入了多线程之后,又带来了线程安全的问题。而为了解决线程安全的问题,又引入了锁的概念。java 中常用的锁有synchronized 和lock两种,本文我们来分析 synchronized 的具体用法和使用注意事项。
基本使用
同步代码块
/**
* 同步代码块
* @throws Exception
*/
public void synchronizedCode() {
try {synchronized (this) {System.out.println(getCurrentTime() + ":I am synchronized Code");
Thread.sleep(5000);// 延时 5 秒,方便后面测试
}
} catch (Exception e) {e.printStackTrace();
}
}
作用代码块时,synchronized 方法中的 this,是指调用该方法的对象。需要主要的是,synchronized 作用代码块时,只会锁住这一小块代码。代码块的上下部分的其他代码在所有的线程仍然是能同时访问的。同时需要注意的是每个对象有用不同的锁。即不会阻塞不同对象的调用。
同步方法
/**
* 同步方法
*/
public synchronized void synchronizedMethod() {
try {System.out.println(getCurrentTime() + ":I am synchronized method");
Thread.sleep(5000);// 延时 5 秒,方便后面测试
} catch (Exception e) {e.printStackTrace();
}
}
synchronized 作用在方法上,其实是缺省了 this 关键字,实际上是 synchronized(this)。this 是指调用该方法的对象。此锁也不会阻塞不同对象之间的调用。
同步静态方法
/**
* 同步静态方法
*/
public synchronized static void synchronizedStaticMethod() {
try {System.out.println(getCurrentTime() + ":I am synchronized static method");
Thread.sleep(5000);// 延时 5 秒,方便后面测试
} catch (Exception e) {e.printStackTrace();
}
}
使用方式和作用普通方式相同,唯一需要注意的地方是此锁所有对象共用,即不同对象之间会阻塞调用。
测试准备
以下所有的测试,都是基于下面这个类。
简单说明一下:有一个线程池,在执行多任务时使用。每个同步方法或者代码块中都有一个休眠 5 秒的动作,利用打印时间加休眠来看线程之间是否有阻塞效果。然后有一个 1 秒打印一次时间的方法。
public class Synchronized {
// 打印时间时格式化
public static final String timeFormat = "HH:mm:ss";
// 执行多任务的线程池
public static final ExecutorService executor = Executors.newFixedThreadPool(4);
/**
* 同步代码块
* @throws Exception
*/
public void synchronizedCode() {
try {synchronized (this) {System.out.println(getCurrentTime() + ":I am synchronized Code");
Thread.sleep(5000);// 延时 5 秒,方便后面测试
}
} catch (Exception e) {e.printStackTrace();
}
}
/**
* 同步方法
*/
public synchronized void synchronizedMethod() {
try {System.out.println(getCurrentTime() + ":I am synchronized method");
Thread.sleep(5000);// 延时 5 秒,方便后面测试
} catch (Exception e) {e.printStackTrace();
}
}
/**
* 同步静态方法
*/
public synchronized static void synchronizedStaticMethod() {
try {System.out.println(getCurrentTime() + ":I am synchronized static method");
Thread.sleep(5000);// 延时 5 秒,方便后面测试
} catch (Exception e) {e.printStackTrace();
}
}
/**
* 循环打印时间
*/
public static void printNumber() {executor.execute(new Runnable() {
@Override
public void run() {while (true) {
try {printOnceASecond();
} catch (Exception e) {e.printStackTrace();
}
}
}
});
}
/**
* 一秒打印一次时间
*
* @throws Exception
*/
public static void printOnceASecond() throws Exception {System.out.println(getCurrentTime());
Thread.sleep(1000);
}
/**
* 获取当前时间
*
* @return
*/
public static String getCurrentTime() {return LocalDateTime.now().format(DateTimeFormatter.ofPattern(timeFormat));
}
}
OK,接下来我们就来测试下锁的互斥性以及使用注意事项(都是 多线程 的情况下)。
开始测试
同一个对象同步代码块
public static void main(String[] args) throws Exception {printNumber();// 控制台循环打印时间
Synchronized es = new Synchronized();
executor.execute(() -> es.synchronizedCode());
executor.execute(() -> es.synchronizedCode());
}
execute
20:34:41:I am synchronized Code
20:34:41
20:34:42
20:34:43
20:34:44
20:34:45
20:34:46:I am synchronized Code
同步代码块中休眠 5 秒,导致另外一个线程阻塞 5 秒后再执行。说明代同步码块会阻塞同一个对象的不同线程之间的调用(同步方法和同步静态方法也会阻塞同一个对象的不同线程之间的调用,此处省略测试代码)
不同对象同步代码块
public static void main(String[] args) throws Exception {printNumber();// 控制台循环打印时间
Synchronized es = new Synchronized();
Synchronized es1 = new Synchronized();
executor.execute(() -> es.synchronizedCode());
executor.execute(() -> es1.synchronizedCode());
}
execute
20:44:34:I am synchronized Code
20:44:34:I am synchronized Code
由结果可以看出,不同对象之间代码块锁互不影响(多线程也一样)。原因是因为代码块中synchronized (this)
锁的是当前调用对象,不同对象之间不是同一把锁,因此互不影响(同步方法原理也是如此,省略测试代码)。
同一对象同步代码块和方法
public static void main(String[] args) throws Exception {printNumber();// 控制台循环打印时间
Synchronized es = new Synchronized();
executor.execute(() -> es.synchronizedCode());
executor.execute(() -> es.synchronizedMethod());
}
execute
20:51:27:I am synchronized method
20:51:27
20:51:28
20:51:29
20:51:30
20:51:31
20:51:32:I am synchronized Code
因为同步代码块和同步方法,都是锁当前调用对象,因此执行后打印上述结果应该在意料之中。基于这样的特性,实际开发在使用 spring 的时候就需要注意了,我们的 bean 交给 spring 容器管理之后,默认都是单例的。那么这个时候使用 synchronized 关键字就需要注意了(推荐使用同步代码块,同步的代码块中传入外部定义的一个变量)。
不同对象静态同步方法
public static void main(String[] args) throws Exception {printNumber();// 控制台循环打印时间
Synchronized es = new Synchronized();
Synchronized es1 = new Synchronized();
executor.execute(() -> es.synchronizedStaticMethod());
executor.execute(() -> es1.synchronizedStaticMethod());
}
execute
21:05:39:I am synchronized static method
21:05:40
21:05:41
21:05:42
21:05:43
21:05:44:I am synchronized static method
由上述结果可以看出来,静态同步方法会阻塞所有的对象。原因是所有的静态同步方法都是占用的同一把锁。
相同对象同步方法和静态同步方法
public static void main(String[] args) throws Exception {printNumber();// 控制台循环打印时间
Synchronized es = new Synchronized();
executor.execute(() -> es.synchronizedMethod());
executor.execute(() -> es.synchronizedStaticMethod());
}
execute
21:11:03:I am synchronized static method
21:11:03:I am synchronized method
由此结果可以看出,同步方法和静态同步方法之间不会造成阻塞的现象。因为他们锁的对象不一样。同步方法占用的锁是调用对象,静态同步方法锁的是编译后的 class 对象。
总结
同一个对象,同步方法、同步代码块之间互斥,同时和自己也互斥。静态同步方法只和自己互斥。
不同对象之间,同步方法、同步代码块不会互斥。静态同步方法会互斥