大话Android多线程(二) synchronized使用解析

5,155 阅读13分钟

版权声明:本文为博主原创文章,未经博主允许不得转载
源码:github.com/AnliaLee
大家要是看到有错误的地方或者有啥好的建议,欢迎留言评论

前言

这是Android多线程篇的第二章,在上章我们比较了ThreadRunnable创建线程的异同,也简单地模拟了多线程执行任务的场景。但实际上,这样执行多线程任务是不安全的,这章我们将分析为何会出现线程不安全的情况以及如何使用synchronized解决这样的问题

往期回顾
大话Android多线程(一) Thread和Runnable的联系和区别


synchronized使用解析

同步方法(非静态)

上回说到小R(Runnable)因为诚信经营,生意越来越好了,于是小R便多招了一个业务员。某日,在售出10张门票后不久,小R就收到了顾客的投诉,说他们买到了假票。小R怀疑是自己的手下动了手脚,便展开了调查:

当时的业务流程如下

private class SellTask {
	private int ticket = 10;
	public void sellTicket(){
		if (ticket > 0) {
			try{
				Thread.sleep(500);
				Log.e("R公司",Thread.currentThread().getName() + "卖了一张票,编号为r" + (ticket--));
			}catch (Exception e){
				e.printStackTrace();
			}
		}
	}
}

public class TicketRunnable implements Runnable {
	SellTask sellTask;
	public TicketRunnable(SellTask sellTask){
		this.sellTask = sellTask;
	}

	public void run() {
		for (int i = 0; i < 10; i++) {
			sellTask.sellTicket();
		}
	}
}

票交由3个业务员去卖

SellTask sellTask = new SellTask();
TicketRunnable runnable = new TicketRunnable(sellTask);
Thread r1 = new Thread(runnable, "1号业务员");
Thread r2 = new Thread(runnable, "2号业务员");
Thread r3 = new Thread(runnable, "3号业务员");

r1.start();
r2.start();
r3.start();

调查发现,出现了多个业务员售出编号相同的票的情况

进一步调查后得知,出现这样的情况是因为业务员答应了卖给顾客某编号的票,并收取了订金,回头拿票时才发现哥几个卖得是同一张票(多个线程先后操作共享数据造成数据错误),没办法只能自己复制一张给顾客企图蒙混过关。小R不知道该怎么约束自己的手下,遂公开招聘能解决问题的人

这天,一位自称synchronized的男人前来应聘。小R问道:“s先生有何高见啊?”s先生淡定地喝了口茶,答道:

“你现在的业务流程不太可靠(线程不安全),让我来统一管理整个售票业务,每一张票的出售都需经过我的审批,一张票卖完后业务员才能来我这再次申请拿票出售,这样每张票都只能由一个业务员进行出售,问题也自然解决了(在Java中每一个对象都有一个内部锁,当使用synchronized关键字声明某个方法时,该方法将受到对象锁的保护,这样一次就只能有一个线程可以进入该方法获得该对象的锁,其他线程要想调用该方法,只能排队等待。当获得锁的线程执行完该方法并释放对象锁后,别的线程才可拿到锁进入该方法)。”

小R听后觉得这方法不错,便让s先生来试试。这次依然是要出售10张票,业务流程经过s先生改进后如下

private class SellTask {
	private int ticket = 10;
	public synchronized void sellTicket(){//使用synchronized声明sellTicket方法
		if (ticket > 0) {
			try{
				Thread.sleep(500);
				Log.e("R公司",Thread.currentThread().getName() + "卖了一张票,编号为r" + (ticket--));
			}catch (Exception e){
				e.printStackTrace();
			}
		}
	}
}

问题果然解决了,小R悬着的心也终于放了下来


同一个对象内多个同步方法

某日,小R又开始向s先生抱怨起来:“我那帮二愣子手下啊,每次进我办公室汇报工作都是乱糟糟的,让他们按顺序一个个来就是不听,s先生觉得该如何管管他们啊?”s先生依然淡定地抿了口茶,说道:“不急,容我先看看他们是怎么汇报的。”小R便依着s先生的意思安排了两个手下过来汇报工作

private class ReportTask {
	public void report1(){
		Log.e("R公司","1号业务员" + "进办公室");
		try{
			Log.e("R公司","1号业务员" + "开始汇报");
			Thread.sleep(1000);

		}catch (Exception e){
			e.printStackTrace();
		}
		Log.e("R公司","1号业务员" + "汇报完毕");
		Log.e("R公司","1号业务员" + "出办公室");
	}

	public void report2(){
		Log.e("R公司","2号业务员" + "进办公室");
		try{
			Log.e("R公司","2号业务员" + "开始汇报");
			Thread.sleep(1000);

		}catch (Exception e){
			e.printStackTrace();
		}
		Log.e("R公司","2号业务员" + "汇报完毕");
		Log.e("R公司","2号业务员" + "出办公室");
	}
}

public class ReportRunnable1 implements Runnable {
	ReportTask task;
	public ReportRunnable1(ReportTask task){
		this.task = task;
	}

	public void run() {
		task.report1();
	}
}

public class ReportRunnable2 implements Runnable {
	ReportTask task;
	public ReportRunnable2(ReportTask task){
		this.task = task;
	}

	public void run() {
		task.report2();
	}
}

不一会儿,两个手下前后脚进了办公室

ReportTask reportTask = new ReportTask();
ReportRunnable1 runnable1 = new ReportRunnable1(reportTask);
ReportRunnable2 runnable2 = new ReportRunnable2(reportTask);

Thread r1 = new Thread(runnable1);
Thread r2 = new Thread(runnable2);
r1.start();
r2.start();

小R揉了揉脑袋,叹气道:“唉,他们就是这样汇报的,每次他们一起讲的时候我都不知该听谁的。”s先生哈哈一笑,道:

“这个不难解决,下次他们再来汇报,进来第一个人我就把门锁了,让下一个在门外等,等第一个讲完了我再放第二个进来就行了(当一个线程访问对象的某个synchronized同步方法时,其他线程对对象中所有其它synchronized同步方法的访问将被阻塞)”

小R听后深以为然,便又安排刚刚那两个业务员过来重新汇报一次,这次由s先生亲自守门(别问我为啥他们傻傻的,计算机就是这么工作的 ╮(╯▽╰)╭)

private class ReportTask {
	public synchronized void report1(){
		Log.e("R公司","1号业务员" + "进办公室");
		try{
			Log.e("R公司","1号业务员" + "开始汇报");
			Thread.sleep(1000);

		}catch (Exception e){
			e.printStackTrace();
		}
		Log.e("R公司","1号业务员" + "汇报完毕");
		Log.e("R公司","1号业务员" + "出办公室");
	}

	public synchronized void report2(){
		Log.e("R公司","2号业务员" + "进办公室");
		try{
			Log.e("R公司","2号业务员" + "开始汇报");
			Thread.sleep(1000);

		}catch (Exception e){
			e.printStackTrace();
		}
		Log.e("R公司","2号业务员" + "汇报完毕");
		Log.e("R公司","2号业务员" + "出办公室");
	}
}

不一会儿,两个业务员又来了,这次的结果令小R非常满意

看着小R这么开心,s先生不禁泼起了冷水:

“你别高兴得太早,你窗户可没锁呢,说不定你那帮二愣子手下进不了门就从窗户爬进来了(当一个线程访问对象的某个synchronized同步方法时,另一个线程仍然可以访问该对象中的非synchronized同步方法)。”

果然,之后的某次工作汇报中,这样的事就发生了

private class ReportTask {
	public synchronized void report1(){
		Log.e("R公司","1号业务员" + "进办公室");
		try{
			Log.e("R公司","1号业务员" + "开始汇报");
			Thread.sleep(1000);

		}catch (Exception e){
			e.printStackTrace();
		}
		Log.e("R公司","1号业务员" + "汇报完毕");
		Log.e("R公司","1号业务员" + "出办公室");
	}

	public synchronized void report2(){
		Log.e("R公司","2号业务员" + "进办公室");
		try{
			Log.e("R公司","2号业务员" + "开始汇报");
			Thread.sleep(1000);

		}catch (Exception e){
			e.printStackTrace();
		}
		Log.e("R公司","2号业务员" + "汇报完毕");
		Log.e("R公司","2号业务员" + "出办公室");
	}

	public void report3(){
		Log.e("R公司","3号业务员" + "进办公室");
		try{
			Log.e("R公司","3号业务员" + "开始汇报");
			Thread.sleep(1000);

		}catch (Exception e){
			e.printStackTrace();
		}
		Log.e("R公司","3号业务员" + "汇报完毕");
		Log.e("R公司","3号业务员" + "出办公室");
	}
}

//线程启动代码略...

小R:


同步代码块

是日,好友小T前来拜访小R,却看见小R的办公室门窗紧闭,几个业务员在门外排着队。小T十分疑惑,遂敲门招呼小R让他开门,然而却没有得到任何回应。没办法,小T只能跟着业务员在办公室外面等了

private class ReportTask {
	public void report1(){
		synchronized(this){
			Log.e("R公司","1号业务员" + "进办公室");
			try{
				Log.e("R公司","1号业务员" + "开始汇报");
				Thread.sleep(1000);

			}catch (Exception e){
				e.printStackTrace();
			}
			Log.e("R公司","1号业务员" + "汇报完毕");
			Log.e("R公司","1号业务员" + "出办公室");
		}
	}

	public void report2(){
		synchronized(this){
			Log.e("R公司","2号业务员" + "进办公室");
			try{
				Log.e("R公司","2号业务员" + "开始汇报");
				Thread.sleep(1000);

			}catch (Exception e){
				e.printStackTrace();
			}
			Log.e("R公司","2号业务员" + "汇报完毕");
			Log.e("R公司","2号业务员" + "出办公室");
		}
	}

	public void report3(){
		synchronized(this){
			Log.e("R公司","3号业务员" + "进办公室");
			try{
				Log.e("R公司","3号业务员" + "开始汇报");
				Thread.sleep(1000);

			}catch (Exception e){
				e.printStackTrace();
			}
			Log.e("R公司","3号业务员" + "汇报完毕");
			Log.e("R公司","3号业务员" + "出办公室");
		}
	}

	public void report4(){
		synchronized (this){
			Log.e("R公司","小T" + "进办公室");
		}

	}
}
ReportTask reportTask = new ReportTask();
ReportRunnable1 runnable1 = new ReportRunnable1(reportTask);
ReportRunnable2 runnable2 = new ReportRunnable2(reportTask);
ReportRunnable3 runnable3 = new ReportRunnable3(reportTask);
ReportRunnable4 runnable4 = new ReportRunnable4(reportTask);

Thread s1 = new Thread(runnable1);
Thread s2 = new Thread(runnable2);
Thread s3 = new Thread(runnable3);
Thread s4 = new Thread(runnable4);
s1.start();
s2.start();
s3.start();
s4.start();

好不容易等到门开了,一个业务员走了出来,小T便一闪身溜了进去,门立刻被小R锁上了。“你干啥呢,差点就夹到我了!”小T抱怨道。小R不好意思笑笑,说道:“原来是小T啊,你坐你坐,待会再向你解释,我先放下个业务员进来...”

好不容易应付完所有业务员,小R向小T解释了来龙去脉,并拿出了一把钥匙交给小T

private class ReportTask {
	public void report1(){
		synchronized(this){
		//省略部分代码...
		}
	}

	public void report2(){
		synchronized(this){
		//省略部分代码...
		}
	}

	public void report3(){
		synchronized(this){
		//省略部分代码...
		}
	}

	private String window = "window";
	public void report4(){
		synchronized (window){
			Log.e("R公司","小T" + "进办公室");
		}
	}
}

"s先生自然有应对这种情况的妙计,这把钥匙可以打开窗户(持有window对象的内置锁),你以后可以从窗户直接爬进来,不用在门外排队(synchronized (obj){}同步代码块用synchronized声明方法的作用基本一致,都是对synchronized作用范围内的代码进行加锁保护,其区别在于synchronized同步代码块使用更加灵活、轻巧synchronized (obj){}括号内的对象参数即为该代码块持有锁的对象。例如上述例子中,前面三个report方法中的同步代码块持有锁的对象为ReportTask的实例对象,而report4方法中的同步代码块持有锁的对象则为window。因为对象都有自己的对象锁,只能保护属于自己的同步代码块或同步方法,所以即使其他线程进入前三个方法的同步代码块中并获得相应对象的锁,也不会阻塞进入report4方法的线程执行其中的同步代码)。"

拿到钥匙后,小T再也不用和业务员一起在门外排队了


静态同步方法

了解了上述知识后,我们回过头再来理解同步方法和静态同步方法的区别。从持有锁的对象的不同我们可以将synchronized同步代码的方式分为两大派系:

  • synchronized声明非静态方法同步代码块的synchronized (this){}和synchronized (非this对象){}这三者持有锁的对象实例对象(类的实例对象可以有很多个),线程想要执行该synchronized作用范围内的同步代码,需获得对象锁
public class SynchronizedTest {
    public synchronized void test1(){
        //持有锁的对象为SynchronizedTest的实例对象
    }

    public void test2(){
        synchronized (this){
            //持有锁的对象为SynchronizedTest的实例对象
        }
    }

    private String obj = "obj";
    public void test3(){
        synchronized (obj){
            //持有锁的对象为obj
        }
    }
}
  • synchronized声明静态方法以及同步代码块的synchronized (类.class){}这两者持有锁的对象Class对象(每个类只有一个Class对象,而Class对象是Java类编译后生成的.class文件,它包含了与类有关的信息),线程想要执行该synchronized作用范围内的同步代码,需获得类锁
public class SynchronizedTest {
    public static synchronized void test4(){
        //持有锁的对象为SynchronizedTest的Class对象(SynchronizedTest.class)
    }

    public void test5(){
        synchronized (SynchronizedTest.class){
            //持有锁的对象为SynchronizedTest的Class对象(SynchronizedTest.class)
        }
    }
}

有关实例对象Class对象的详细知识大家可以继续在网上查找资料进行深挖,这里就不赘述了,总之我们要记住的一点是

synchronized同步方法(代码块)持有锁的对象不同,则多线程执行相应的同步代码时互不干扰;若相同,则获得该对象锁的线程先执行同步代码,其他访问同步代码的线程会被阻塞等待锁的释放


更新

为了更好地理解静态和非静态同步方法的区别,我这里补充一下示例吧

在某个平行宇宙中,小R穿越去了火影的世界,为了在村子里谋个一官半职,遂决定请村长鸣人(本体,即Class对象)和他的影分身(实例对象)吃拉面(影分身也可以理解为克隆体,现实中没找到啥好的例子可以代入,大家将就吧哈哈)。为了找到鸣人分布在各个地方的影分身,小R也使出了影分身

public class SynchronizedTest {
    public synchronized void test1(){
        try{
            System.out.println("请鸣人影分身 "+Thread.currentThread().getName()+" 吃拉面");
            Thread.sleep(1000);

        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("鸣人影分身 "+Thread.currentThread().getName()+" 吃完拉面了");
    }

    public static void main(String args[]) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                new SynchronizedTest().test1();
            }
        }, "1号");
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                new SynchronizedTest().test1();
            }
        }, "2号");
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                new SynchronizedTest().test1();
            }
        }, "3号");
        t1.start();
        t2.start();
        t3.start();
    }
}

鸣人的影分身各吃各的,并没有出现争抢的现象(上述代码中非静态同步方法使用new SynchronizedTest().xxx的方式进行调用,所以它们持有锁的对象不同,因此相互之间没有影响

至于鸣人本体嘛,肯定要重点“贿赂”的,所以小R派了3个影分身去请他吃拉面

public class SynchronizedTest {
    public static synchronized void test2(){
        try{
            System.out.println("请鸣人 本体 吃拉面:" + Thread.currentThread().getName());
            Thread.sleep(1000);

        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("鸣人 本体 吃完拉面了:" + Thread.currentThread().getName());
    }

    public static void main(String args[]) {
        Thread t4 = new Thread(new Runnable() {
            @Override
            public void run() {
//                test2();//test2方法是静态方法,可以直接调用
                new SynchronizedTest().test2();//这里的调用方式仅为了作对照
            }
        },"t4");
        Thread t5 = new Thread(new Runnable() {
            @Override
            public void run() {
                new SynchronizedTest().test2();
            }
        },"t5");
        Thread t6 = new Thread(new Runnable() {
            @Override
            public void run() {
                new SynchronizedTest().test2();
            }
        },"t6");

        t4.start();
        t5.start();
        t6.start();
    }
}

鸣人本体只有一个,即时3个人一起请面也是要一碗一碗吃的(静态同步方法持有锁的对象都是Class对象,持有锁的线程正常执行任务,其他未持有锁的线程则阻塞等待

至于同步代码块synchronized (this){}、synchronized (xxx.class){}的区别和上面一致,我就不赘述了,总之理解静态和非静态同步方法的区别最重要的是理清Class对象实例对象的关系,这里推荐一篇博客给大家:JAVA中类、实例与Class对象。如果大家还有什么疑问,欢迎留言评论,我有(shang)空(ban)会(tou)来(lan)一一答复的