synchronized关键字简单地讲,可以使用这一简单的关键字来保证线程安全。但是我们首先得知道synchronized到底锁住的是什么?锁住的范围又是什么?
这里测试实验仅用两个类:
Test.java
类,里面只有一个函数,在线程中调用的函数。
Main.java
类,整个实验的测试入口
Tips
:
代码有试探性实验的意味
,所以正确与否需要查看对应的代码分析
这里使用最简单的synchronized 锁住整个函数,也就是类似如下的使用方式:
public synchronized void test(){
// 代码段
}
下面是Test.java
public class Test {
public synchronized void test(int i) {
System.out.println("第"+i+"个函数块开始运行...");
//System.out.println("第"+i+"try-catch代码块开始..");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//System.out.println("第"+i+"try-catch代码块结束..");
System.out.println("第"+i+"函数块结束运行...");
}
}
Main.java:
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
final int k = i;
new Thread(()->{
new Test().test(k+1);
}).start();
}
}
}
先看我电脑运行后的结果(各函数的顺序不一定是一样的,但是两个部分是一致的:三个开始,三个结束
):
第1个函数块开始运行...
第2个函数块开始运行...
第3个函数块开始运行...
第2函数块结束运行...
第3函数块结束运行...
第1函数块结束运行...
Main.java中,我们知道,接连创建三个线程,三个线程中运行三个不同Test对象的函数
。通过结果,看到,由于线程的Thread.sleep(3000)
睡眠3秒。CPU调度到其它线程运行了,所以才出现三个函数同时运行。
我们想要的效果是:一个线程进入synchronized函数,另一个线程想进入,必须等到正在占有锁的线程运行完。
那么这里的synchronized显然不是符合我们想要的效果。
我们先不做解释,再看下面的代码修改。下文会进行解释。
// Test类,修改成synchronized(this)锁代码块
public class Test {
public void test(int i) {
synchronized(this) {
System.out.println("第" + i + "个函数块开始运行...");
//System.out.println("第"+i+"try-catch代码块开始..");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//System.out.println("第"+i+"try-catch代码块结束..");
System.out.println("第" + i + "函数块结束运行...");
}
}
}
// Main.java不做任何修改
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
final int k = i;
new Thread(()->{
new Test().test(k+1);
}).start();
}
}
}
测试运行,发现与版本1实现的效果没有什么差别:
第2个函数块开始运行...
第3个函数块开始运行...
第1个函数块开始运行...
第3函数块结束运行...
第1函数块结束运行...
第2函数块结束运行...
接着看下面,如何解决。
我们发现,对Test类的修改,没有任何效果。
如下进行改进:
重点查看Main.java的改变
// 版本1的Test类,synchronized锁住函数
public class Test {
public synchronized void test(int i) {
System.out.println("第" + i + "个函数块开始运行...");
//System.out.println("第"+i+"try-catch代码块开始..");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//System.out.println("第"+i+"try-catch代码块结束..");
System.out.println("第" + i + "函数块结束运行...");
}
}
// Main.java
public class Main {
public static void main(String[] args) {
final Test testObject = new Test(); // 重点在这里哈
for (int i = 0; i < 3; i++) {
final int k = i;
new Thread(()->{
testObject.test(k+1);
}).start();
}
}
}
我们看到,这一次我们在三个线程中执行的是同一个对象的带锁方法。
我的电脑得到的结果如下(顺序不一定一定是如下所示的,但是结果总是会有如下的结构:同一个线程函数运行的开始与结束成对
):
第1个函数块开始运行...
第1函数块结束运行...
第3个函数块开始运行...
第3函数块结束运行...
第2个函数块开始运行...
第2函数块结束运行...
我们看到如上的结果,都是必须在一个函数运行结束之后才能开始新线程函数的运行。
从上面我们可以看到,带有synchronized锁的函数
,它锁的是某个对象的特定函数
。
也就是说,synchronized函数不会对一个类的不同对象起作用。
通过下面的例子我们知道,它确实是锁的特定对象
的特定函数
。
// Test.java
public class Test {
public synchronized void test(int i) {
System.out.println("第" + i + "个函数块开始运行...");
// System.out.println("第"+i+"try-catch代码块开始..");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// System.out.println("第"+i+"try-catch代码块结束..");
System.out.println("第" + i + "函数块结束运行...");
}
public void test1(){
System.out.println("Test1");
}
}
// Main.java
public class Main {
public static void main(String[] args) {
final Test testObject = new Test();
for (int i = 0; i < 3; i++) {
final int k = i;
new Thread(()->{
if((k+1)%2 == 0){
testObject.test(k+1);
} else {
testObject.test1();
}
//
}).start();
}
}
}
我电脑的运行结果如下,据此我们知道synchronized函数锁,它锁的并不是整个对象,而是此对象的此synchronized函数
Test1
第2个函数块开始运行...
Test1
第2函数块结束运行...
下面的试验也很重要:
先上代码:
// Test, 注意:这里有两个synchronized方法
public class Test {
public synchronized void test(int i) {
System.out.println("第" + i + "个函数块开始运行...");
// System.out.println("第"+i+"try-catch代码块开始..");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//System.out.println("第"+i+"try-catch代码块结束..");
System.out.println("第" + i + "函数块结束运行...");
}
public synchronized void test1(int i){
System.out.println("第" + i + "个test1开始...");
// ...
System.out.println("第" + i + "个test1开始...");
}
}
// Main,使用同一个对象,但是线程执行的函数有不同的地方
// 这里for循环开10个线程
public class Main {
public static void main(String[] args) {
final Test testObj = new Test();
for (int i = 0; i < 10; i++) {
final int k = i;
new Thread(()->{
if(((k+1)%2)==0) {
testObj.test(k + 1);
} else {
testObj.test1(k+1);
}
}).start();
}
}
}
先看结果:
第1个test1开始...
第1个test1开始...
第3个test1开始...
第3个test1开始...
第2个函数块开始运行...
第2函数块结束运行...
第6个函数块开始运行...
第6函数块结束运行...
第10个函数块开始运行...
第10函数块结束运行...
第9个test1开始...
第9个test1开始...
第8个函数块开始运行...
第8函数块结束运行...
第7个test1开始...
第7个test1开始...
第5个test1开始...
第5个test1开始...
第4个函数块开始运行...
第4函数块结束运行...
开十个线程,奇数号线程执行test1,偶数号线程执行test
。通过结果我们能得出如下结论:多个线程执行同一对象的synchronized方法;无论这些方法是否是同一个,都只能有一个线程能执行该对象的同步方法。
通过对并发的理解,我们知道,平常我们定义一个如下所示的最简单的类:一个成员变量,加上与之配套的getter,setter。
注意,这个类并不是线程安全的,
因为 过期数据问题:如果一个线程正在调用setter, 而另一个线程此时正在调用getter,它可能就看不到更新的数据了。
// NotThreadSafe
public class MutableInteger {
private int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
通过上面的分析,我们知道经过如下的修改,可以得到一个线程安全的类:
// ThreadSafe
public class MutableInteger {
private int value;
// 注意,添加关键字synchronized
public synchronized int getValue() {
return value;
}
// 注意,添加关键字synchronized
public synchronized void setValue(int value) {
this.value = value;
}
}
再回到下面的代码:
// Test.java
public class Test {
public void test(int i) {
synchronized(this) {
System.out.println("第" + i + "个函数块开始运行...");
//System.out.println("第"+i+"try-catch代码块开始..");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//System.out.println("第"+i+"try-catch代码块结束..");
System.out.println("第" + i + "函数块结束运行...");
}
}
}
// Main.java
public class Main {
public static void main(String[] args) {
final Test testObject = new Test();
for (int i = 0; i < 3; i++) {
final int k = i;
new Thread(()->{
testObject.test(k+1);
}).start();
}
}
}
通过之前的研究,我们很容易知道上面的结果符合我们的预期。
我们将Test.java改为如下所示:
,看结果又什么不同(注意代码注释的变化
):
// Test.java 修改
public class Test {
public void test(int i) {
System.out.println("第" + i + "个函数块开始运行...");
synchronized(this) {
System.out.println("第"+i+"try-catch代码块开始..");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第"+i+"try-catch代码块结束..");
}
System.out.println("第" + i + "函数块结束运行...");
}
}
这里特意让synchronized(this) {}代码块括起来的为函数中间的一部分。
我得到的结果如下:
第1个函数块开始运行...
第1try-catch代码块开始..
第2个函数块开始运行...
第3个函数块开始运行...
第1try-catch代码块结束..
第1函数块结束运行...
第3try-catch代码块开始..
第3try-catch代码块结束..
第3函数块结束运行...
第2try-catch代码块开始..
第2try-catch代码块结束..
第2函数块结束运行...
得到的结果很乱,但是我们要从中得到的信息就是:synchronized(this)锁住的为对象的某个代码块
。也就是说,不在代码块内的代码,对象在多线程环境下可以任意运行,但只能有一个线程能拥有执行synchronized(this){}代码块的锁。
通过前面的例子,我们知道synchronized(this){}锁住的为特定对象的特定代码块。
现在我们介绍下面的方法,锁住特定类的特定代码段。
注意Test类的注释//, Main.java的改动
Test类
public class Test {
public void test(int i) {
//System.out.println("第" + i + "个函数块开始运行...");
synchronized(Test.class) {
System.out.println("第"+i+"try-catch代码块开始..");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第"+i+"try-catch代码块结束..");
}
//System.out.println("第" + i + "函数块结束运行...");
}
}
Main类:
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
final int k = i;
new Thread(()->{
new Test().test(k+1);
}).start();
}
}
}
运行的结果可能如下:
第1try-catch代码块开始..
第1try-catch代码块结束..
第3try-catch代码块开始..
第3try-catch代码块结束..
第2try-catch代码块开始..
第2try-catch代码块结束..
分析:
看到Main.java的一段核心代码:
new Thread(()->{
new Test().test(k+1);
}).start();
可以看到,每个线程运行的都是不同的Test对象。但是却能够达到我们的对锁的预期。
看到Test类的核心操作:
synchronized(Test.class) {
// 代码段,省略
}
synchronized(Test.class)
锁的是 整个Test类
的 代码段
。两个关键词:
整个Test类
,代码段
。
也就是说,线程中只要是Test类的对象,只能有一个线程的Test对象能够访问synchronized(Test.class){}代码段
。
我们去掉Test类的注释代码段:
public class Test {
public void test(int i) {
System.out.println("第" + i + "个函数块开始运行...");
synchronized(Test.class) {
System.out.println("第"+i+"try-catch代码块开始..");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第"+i+"try-catch代码块结束..");
}
System.out.println("第" + i + "函数块结束运行...");
}
}
运行后得到的结果可能如下:
第1个函数块开始运行...
第1try-catch代码块开始..
第2个函数块开始运行...
第3个函数块开始运行...
第1try-catch代码块结束..
第3try-catch代码块开始..
第1函数块结束运行...
第3try-catch代码块结束..
第3函数块结束运行...
第2try-catch代码块开始..
第2try-catch代码块结束..
第2函数块结束运行...
运行结果很乱,但是我们能够提取到关键的信息:
synchronized(Test.class) {}代码块之外的代码,不同线程的Test对象可以任意执行;但是synchronized(Test.class) {}代码块之内的代码一次只能由一个线程进入并执行完全。
我们过滤一下信息,可以看到如下的效果:
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX第1个函数块开始运行...
第1try-catch代码块开始..
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX第2个函数块开始运行...
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX第3个函数块开始运行...
第1try-catch代码块结束..
第3try-catch代码块开始..
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX第1函数块结束运行...
第3try-catch代码块结束..
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX第3函数块结束运行...
第2try-catch代码块开始..
第2try-catch代码块结束..
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX第2函数块结束运行...
现在结果是否很清晰了呢?
synchronized锁住的是代码还是对象