版权声明:本文为博主原创文章,未经博主允许不得转载
Github:github.com/AnliaLee
首发地址:Anlia_掘金
大家要是看到有错误的地方或者有啥好的建议,欢迎留言评论
前言
这是Android多线程篇的第二章,在上章我们比较了Thread和Runnable创建线程的异同,也简单地模拟了多线程执行任务的场景。但实际上,这样执行多线程任务是不安全的,这章我们将分析为何会出现线程不安全的情况以及如何使用synchronized解决这样的问题
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悬着的心也终于放了下来
![QQ截图20180130221943.png](https://img.haomeiwen.com/i4909537/a3ba6c6a8b9497af.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)同一个对象内多个同步方法
某日,小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同步方法(代码块)持有锁的对象不同,则多线程执行相应的同步代码时互不干扰;若相同,则获得该对象锁的线程先执行同步代码,其他访问同步代码的线程会被阻塞并等待锁的释放
网友评论