面试中可能会遇到这种问题:“一个类中有多个同步方法,多个线程能同时访问吗?”,如果对这块不熟悉的话,真的就是无从答起了,网上关于这种问题文章也很多,但是看起来比较乱,所以今天我实际上手测试了一下,也算是加深了印象。下边将实例代码和结论列出。
目录
- 一、结论
- 二、作用于非静态方法测试
- 三、作用于静态方法测试
一、结论
其实这个问题可以通过被加锁的对象或方法是静态的
还是非静态的
分为两大类。
先直接上结论:
一、作用于 非静态方法
锁住的是对象实例(this),每一个对象实例只有一个锁。
- 当多个线程同时访问
同一个实例
的多个同步方法时,因为只有一个锁,没抢到锁的线程需要等待持有锁的线程执行完成后才能获取锁然后执行。
但如果这时有个线程请求的是普通方法(非同步方法),因为不需要竞争锁,所以可以直接执行。 - 当多个线程同时访问
不同实例
的多个同步方法时,因为实例不同,所以相互不干扰,可以并发执行。
二、作用于 静态方法
锁住的是类的Class对象,因为Class的相关数据存储在永久代元空间,元空间是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁住所有调用该方法的线程(不同实例之间也会锁)。
- 当多个线程同时访问
同一个实例
的多个同步方法时,因为只有一个锁,没抢到锁的线程需要等待持有锁的线程执行完成后才能获取锁然后执行。
但如果有个线程请求的是普通方法(非同步方法),因为不需要竞争锁,所以可以直接执行。 - 当多个线程同时访问
不同实例
的多个同步方法时,因为是静态方法,所以锁住的是类的Class对象,不同实例之间也会被锁住,所以不可以并发执行(注意,这里和上边非静态方法的情况不同)。
三、其他
- 作用于
this
,锁住的是对象实例,每一个对象实例有一个锁。
synchronized (this) {
}
- 作用于
静态成员变量
,锁住的是该静态成员变量对象,由于是静态变量,因此全局只有一个。
public static User user = new User();
synchronized (user) {
}
二、作用于非静态方法测试
好了,现在上代码测试,看结果是否符合上边的结论。
-
TestSyn
类中有两个非静态方法method1
和method2
,分别被synchronized
修饰,还有一个不加锁的普通方法method3
,所有方法中都是打印一下文本然后退出。
public class TestSyn {
//同步方法1 非静态方法
public synchronized void method1() throws InterruptedException {
System.out.println();
System.out.println("进入--同步方法1(非静态方法)");
for (int i = 1; i < 4; i++){
System.out.println("同步方法1(非静态方法)--" + i);
Thread.sleep(1000);
}
System.out.println("退出--同步方法1(非静态方法)");
}
//同步方法2 非静态方法
public synchronized void method2() throws InterruptedException {
System.out.println();
System.out.println("进入--同步方法2(非静态方法)");
for (int i = 1; i < 4; i++){
System.out.println("同步方法2(非静态方法)--" + i);
Thread.sleep(1000);
}
System.out.println("退出--同步方法2(非静态方法)");
}
//普通方法3
public void method3() throws InterruptedException {
System.out.println();
System.out.println("进入--普通方法3(非静态方法)");
for (int i = 1; i < 4; i++){
System.out.println("普通方法3(非静态方法)--" + i);
Thread.sleep(1000);
}
System.out.println("退出--普通方法3(非静态方法)");
}
}
此时在main
方法中我们new两个实例s1
和s2
,一会用于访问不同实例,然后新建两个线程同时访问同一个实例的多个同步方法。
public static void main(String[] args) {
TestSyn s1 = new TestSyn();
TestSyn s2 = new TestSyn();
//非静态方法测试
Thread t1 = new Thread(() -> {
try {
s1.method1(); //s1实例
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t2 = new Thread(() -> {
try {
s1.method2(); //s1实例
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t3 = new Thread(() -> {
try {
s1.method3();//调用普通方法
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
// t3.start();
}
输出结果:
进入--同步方法1(非静态方法)
同步方法1(非静态方法)--1
同步方法1(非静态方法)--2
同步方法1(非静态方法)--3
退出--同步方法1(非静态方法)
进入--同步方法2(非静态方法)
同步方法2(非静态方法)--1
同步方法2(非静态方法)--2
同步方法2(非静态方法)--3
退出--同步方法2(非静态方法)
上边结果说明多个线程同时访问同一个实例的多个同步方法时,需要同步进行。然后我们把上边的t3.start();
注释放开,再执行一次,看同时调用普通方法是否受影响。
输出结果:
进入--同步方法1(非静态方法)
进入--普通方法3(非静态方法)
同步方法1(非静态方法)--1
普通方法3(非静态方法)--1
同步方法1(非静态方法)--2
普通方法3(非静态方法)--2
普通方法3(非静态方法)--3
同步方法1(非静态方法)--3
退出--普通方法3(非静态方法)
退出--同步方法1(非静态方法)
进入--同步方法2(非静态方法)
同步方法2(非静态方法)--1
同步方法2(非静态方法)--2
同步方法2(非静态方法)--3
退出--同步方法2(非静态方法)
可以看出,同步方法1和同步方法2还是需要同步进行,但是普通方法因为不需要获取锁,所以可以并发调用。结论一.1正确。
- 将上边t2线程的调用改为调用实例s2的方法。测试调用不同实例。
public static void main(String[] args) {
TestSyn s1 = new TestSyn();
TestSyn s2 = new TestSyn();
//非静态方法测试
Thread t1 = new Thread(() -> {
try {
s1.method1(); //s1实例
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t2 = new Thread(() -> {
try {
s2.method2(); //注意:s2实例
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t3 = new Thread(() -> {
try {
s1.method3();//调用普通方法
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
// t3.start();
}
输出结果:
进入--同步方法1(非静态方法)
同步方法1(非静态方法)--1
进入--同步方法2(非静态方法)
同步方法2(非静态方法)--1
同步方法1(非静态方法)--2
同步方法2(非静态方法)--2
同步方法2(非静态方法)--3
同步方法1(非静态方法)--3
退出--同步方法1(非静态方法)
退出--同步方法2(非静态方法)
从结果可以看出访问不同实例时同步方法1和方法2是并发执行。所以结论一.2正确。
三、作用于静态方法测试
我们将TestSyn
类中的方法换成静态方法。
//同步方法1 静态方法
public synchronized static void staMethod1() throws InterruptedException {
System.out.println();
System.out.println("进入--同步方法1(静态方法)");
for (int i = 1; i < 4; i++){
System.out.println("同步方法1(静态方法)--" + i);
Thread.sleep(1000);
}
System.out.println("退出--同步方法1(静态方法)");
}
//同步方法2 静态方法
public synchronized static void staMethod2() throws InterruptedException {
System.out.println();
System.out.println("进入--同步方法2(静态方法)");
for (int i = 1; i < 4; i++){
System.out.println("同步方法2(静态方法)--" + i);
Thread.sleep(1000);
}
System.out.println("退出--同步方法2(静态方法)");
}
- 测试访问同一个实例的多个同步方法。
//静态方法测试
Thread t1 = new Thread(() -> {
try {
s1.staMethod1();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t2 = new Thread(() -> {
try {
s1.staMethod2();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t3 = new Thread(() -> {
try {
s1.method3();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
// t3.start();
输出结果:
进入--同步方法1(静态方法)
同步方法1(静态方法)--1
同步方法1(静态方法)--2
同步方法1(静态方法)--3
退出--同步方法1(静态方法)
进入--同步方法2(静态方法)
同步方法2(静态方法)--1
同步方法2(静态方法)--2
同步方法2(静态方法)--3
退出--同步方法2(静态方法)
结果是同步执行,结论二.1正确。
- 测试不同实例,将t2线程的调用换成s2的调用。
Thread t1 = new Thread(() -> {
try {
s1.staMethod1(); //s1实例
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t2 = new Thread(() -> {
try {
s2.staMethod2(); //注意,s2实例
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t3 = new Thread(() -> {
try {
s1.method3();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
// t3.start();
输出结果:
进入--同步方法1(静态方法)
同步方法1(静态方法)--1
同步方法1(静态方法)--2
同步方法1(静态方法)--3
退出--同步方法1(静态方法)
进入--同步方法2(静态方法)
同步方法2(静态方法)--1
同步方法2(静态方法)--2
同步方法2(静态方法)--3
退出--同步方法2(静态方法)
注意,这里和非静态的情况不一样!因为静态方法锁住的是类,所以即使是不同实例之间也会被影响,无法并发执行,只能同步进行。所以结论二.2正确。