同步方法与死锁问题

概述:

(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方法与验证猜测)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值