概述:
(1)两个线程访问不同实例对象的synchronized修饰的同步方法时,并不影响另一个线程的访问
(2)同一个实例对象下被synchronized修饰的方法会互相影响,也就是说当访问同一个实例对象中被synchronized修饰的a方法时,其他线程无法访问这个实例对象下被Synchronized修饰的b方法
(3)t.join()方法只会使主线程(或者说调用t.join()的线程)进入等待池并等待t线程执行完毕后才会被唤醒,不影响同一时刻处在运行状态的其他线程。本文借此做了相关验证
在下列这个代码中,运行发现会出现死锁问题,原因是多线程同步方法时出现问题,解决方法是删掉任意一个synchronized,即可正常输出。
但实际上我并没有很理解为什么会造成死锁,为什么删掉任意一个synchronized即可,以及尝试过程中对synchronized同步方法产生了新的困惑,下面是一些粗浅的思考。
public class Deadlock implements Runnable {
public static void main(String arg[]) {
Deadlock dlk1=new Deadlock();
Deadlock dlk2=new Deadlock();
Thread t1=new Thread(dlk1);
Thread t2=new Thread(dlk2);
dlk1.grabIt=dlk2;
dlk2.grabIt=dlk1;
t1.start();
t2.start();
System.out.println("Started");
try {
t1.join();
t2.join();
}
catch(InterruptedException e) {
System.out.println("error occurred");}
System.exit(0);
}
Deadlock grabIt;
public synchronized void run() {
try{
Thread.sleep(1000);
}
catch(InterruptedException e){
System.out.println("error occurred");
}
grabIt.syncIt();//死锁发生在这一行第二次执行时候
}
public synchronized void syncIt() {
try {
Thread.sleep(1000);
System.out.println("Sync");
}
catch(InterruptedException e){
System.out.println("error occurred");}
System.out.println("In the synchIt() method");
}
}
一、代码细节思考
在源代码上做了一些修改和阐释,具体可以看注释
public class Deadlock implements Runnable {
public static void main(String arg[]) {
Deadlock dlk1=new Deadlock();
Deadlock dlk2=new Deadlock();
/*Thread类有构造函数public Thread(Runnable target, String name),可以把实现Runnable接口的类包装成Thread类,后续使用Thread类方法
同时加上名字,方便后续追踪线程的执行顺序*/
Thread t1=new Thread(dlk1,"线程1");
Thread t2=new Thread(dlk2,"线程2");
dlk1.grabIt=dlk2;
dlk2.grabIt=dlk1;
t1.start();
t2.start();
System.out.println("Started");
try {
t1.join();
t2.join();
}
catch(InterruptedException e) {
System.out.println("error occurred");}
System.exit(0);
}
Deadlock grabIt;
//方便后续追踪线程的执行顺序,创建静态变量m/n
static int m=0;
static int n=0;
public void run() {
try{
//可以观察两个线程的执行顺序
for (int i=0;i<5;i++){System.out.println(Thread.currentThread().getName() +" "+m++);}
Thread.sleep(1000);
}
catch(InterruptedException e){
System.out.println("error occurred");
}
grabIt.syncIt();
}
public void syncIt() {
//是否有输出反映程序是否产生死锁,和原代码起相同作用,除此外观察两个线程执行顺序
for (int i=0;i<5;i++){System.out.println(Thread.currentThread().getName() +" * "+n++);}}}
其中grabIt.syncIt()是造成死锁的语句,但我会疑惑线程1执行run()语句时,grabIt指向dlk2,那这一语句是由线程1完成还是线程2完成呢?在删掉synchronized后,我们可以加上if判断解决这个问题。
if (Thread.currentThread().getName().equals("线程1")){
System.out.println("ok");
grabIt.syncIt();
}

if语句使得只有线程1才执行grabIt.syncIt()语句,从结果看虽然grabIt指向dlk2,但还是由线程1完成。Deadlock grabIt语句是给类创建一个属性,类型是Deadlock,dlk1.grabIt=dlk2语句是指对象dlk1的这一属性是dlk2。
利用dlk1创建Thread对象线程1,线程1运行run()方法时指向dlk1,grabIt.syncIt()先找到dlk1的grabIt属性(dlk2),再调用dlk2的synclt()方法(因为grabIt也是Deadlock类),和线程2并无关系。
二、synchronized方法同步
大家可能疑惑上面的输出结果,从run()方法的输出结果可以看出,线程1和线程2在交替运行,那synchronized起到了什么作用呢?而且令人抓瞎的是,删掉任意一个synchronized,或两个都删掉,运行结果上两个线程都是交替运行的,似乎synchronized根本没锁住方法,但偏偏两个都有的时候会死锁,那synchronized到底锁住了什么?
图为保留synchronized修饰syncIt方法的输出结果(线程1的输出不一定先于线程2)。

这是因为synchronized修饰一个方法时,其作用的范围是整个方法,作用的对象是调用这个方法的对象,也就是说会给实例化对象的某个方法上锁,而不是给这个类上锁(这个方法由static修饰会有不同)。而例子中线程1指向dlk1对象,线程2指向dlk2对象,synchronized给dlk1的run方法上锁时,和dlk2完全没关系。
可以通过下面的例子验证,当线程1、线程2都用dlk1创建时,使用synchronized修饰run方法,并注释掉synclt()方法,输出结果表明,当线程1完成run方法所有语句后线程2才执行run方法。

同理,当线程1、线程2都用dlk1创建时,使用synchronized修饰syncIy方法,运行结果中run方法是两个线程交替进行,syncIy方法是线程1执行完线程2再执行,也验证了同步方法的作用范围。只是更加令人困惑,两个synchronized为什么会产生死锁?

三、 同步方法的特点和死锁
这个问题在一篇博客上找到了一个可能的解释,同步方法因为是对象锁有以下特点:
(1)同一个实例对象下被Synchronized修饰的方法会互相影响,也就是说当访问同一个实例对象中被Synchronized修饰的a方法时,其他线程无法访问这个实例对象下被Synchronized修饰的b方法。
(2)但是如果是同一线程访问a方法后和b方法,不会被阻塞,因为Synchronized支持重入。
(3)被Synchronized修饰的方法与未被Synchronized修饰的方法互不影响
第一点解释了死锁的原因,实例对象dlk1的run方法和syncIt都由Synchronized修饰时,线程1访问其run方法时,线程2的grabIt属性指向dlk1,试图访问其syncIt方法,但由于同一个实例对象下被Synchronized修饰的方法会互相影响不能成功;同理,线程1也无法访问dlk2的syncIt方法,使得程序无法执行产生死锁。
如果把grabIt.syncIt()改为this.syncIt(),线程1的run方法指向dlk1,并会指向dlk1调用syncIt方法,由于是同一个线程访问同一对象多个被synchronized修饰的方法,能正常输出,这可以由第二点解释。
删去任意一个Synchronized时,能正常输出,可以由第三点解释。也就是说a/b是同一个实例对象的方法,a上锁b不上锁,a被线程1访问时并不影响b被线程2访问。
除此外,我们可以通过join方法做一个巧妙的试验,验证这一猜想。t.join()方法只会使主线程(或者说调用t.join()的线程)进入等待池并等待t线程执行完毕后才会被唤醒,不影响同一时刻处在运行状态的其他线程。
public class Deadlock implements Runnable {
public static void main(String arg[]) {
Deadlock dlk1=new Deadlock();
Deadlock dlk2=new Deadlock();
Thread t1=new Thread(dlk2,"线程1");
Thread t2=new Thread(dlk1,"线程2");
dlk1.grabIt=dlk2;
dlk2.grabIt=dlk1;
System.out.println(Thread.currentThread().getName());
t1.start();
System.out.println(Thread.currentThread().getName());
try {
t1.join();
}
catch(InterruptedException e) {
System.out.println("error occurred");}
System.out.println(Thread.currentThread().getName());
t2.start();
System.out.println(Thread.currentThread().getName());
try {
t2.join();
}
catch(InterruptedException e) {
System.out.println("error occurred");}
System.out.println("Started");
System.exit(0);
}
Deadlock grabIt;
static int m=0;
static int n=0;
public synchronized void run() {
try{
for (int i=0;i<5;i++){System.out.println(Thread.currentThread().getName() +" "+m++);}
Thread.sleep(1000);
}
catch(InterruptedException e){
System.out.println("error occurred");
}
grabIt.syncIt();
}
public synchronized void syncIt() {
for (int i=0;i<5;i++){System.out.println(Thread.currentThread().getName() +" * "+n++);}}}
此代码两个线程指向不同对象,且synchronized修饰两个方法,但join语句分开写。使得先执行main方法里的主线程,t1.start()语句执行后线程1进入准备状态,t1.join()执行后主线程需要等待线程1结束后再准备运行,而t2.start()语句还没有执行。所以线程1运行到死亡的全过程线程2都是notRunnable,线程1访问dlk2的syncIt方法时,dlk2的run方法没有被锁住,能正常运行。同理,线程2访问dlk1的syncIt方法时,线程1已死亡,dlk1的run方法没有被锁住,能正常运行。
输出结果如下,主线程输出main的顺序可以帮助大家理解join()语句,而线程2能正常输出且在线程1死亡后输出,可以验证死锁产生的原因。
顺带一句,多线程的优先级设置后结果还是比较随机,这种join语句看起来比优先级好使,不知道有没有实际用处。

最后,附上参考的博客与资料。
Java多线程——synchronized使用详解_synchronized用法-CSDN博客 (关于同步方法指向同一对象/不同对象)
Synchronized锁详解(全网最细)_java synchronized 方法 锁对象-CSDN博客 (第三部分解释原因来源)
JAVA多线程中join()方法的详细分析_多线程join-CSDN博客(join方法与验证猜测)

1629

被折叠的 条评论
为什么被折叠?



