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

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

前言

这是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悬着的心也终于放了下来

![QQ截图20180130221943.png](http://upload-images.jianshu.io/upload_images/4909537-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同步方法(代码块)持有锁的对象不同,则多线程执行相应的同步代码时互不干扰;若相同,则获得该对象锁的线程先执行同步代码,其他访问同步代码的线程会被阻塞等待锁的释放

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容

  • 本文出自 Eddy Wiki ,转载请注明出处:http://eddy.wiki/interview-java.h...
    eddy_wiki阅读 1,898评论 0 14
  • Java多线程学习 [-] 一扩展javalangThread类 二实现javalangRunnable接口 三T...
    影驰阅读 2,921评论 1 18
  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 2,376评论 1 15
  • Hi,欢迎来到朱小峰的文字江山,我是他的第二分身朱小黑,专为践行得到APP的大神专栏而生。 今天我们继续学习刘润五...
    朱小峰阅读 248评论 0 2
  • 1、《十七岁单车》 推荐指数:✰✰✰✰ 电影彩蛋:两位女神年少时。 一句话影评:一部单车就是我们整个的青春。 观后...
    洞见人生百态阅读 198评论 0 1