android 多线程 — synchronized

通过上篇博文(android 多线程 — java 内存模型)我们知道了多个线程同时多同一个对象读写可能会造成数据混乱,结果错误。

同步干啥了


那么 java 如果解决的这个问题呢,就是同步机制 — synchronized。什么是同步呢,就是让Object 象同一时间只能被一个 Thread 读写。那么又是如何让 Object 同一时间只能被一个 Thread 读写呢,是给每个 Object 里面加一把锁,哪个 Thread 在使用这个 Object 就把这个对象上的锁给谁,直到这个 Thread 执行完对这个 Object 的操作,把 Object 上的锁还给这个 Object ,然后下一个 Thread 才能对这个 Object 进行操作

synchronized 干的事就是这样,管理对象上锁,只给一个线程对象,保证同一时刻只有一个线程能操作这个对象

多余我们来说茶不必直接操作对象上的锁,我们只要把对象传给 synchronized 就行,至于是哪个对象,根据实际来选择。

synchronized


先不忙来看看其他人的描述,这个最好:

Synchronized,它就是一个:非公平,悲观,独享,互斥,可重入锁。
优化以后,是基于JVM内部实现,可以根据竞争激烈程度,从偏向锁-->轻量级锁-->重量级锁升级

synchronized 本身是一个关键字,用来修饰普通方法,静态方法和代码块

修饰方法

synchronized public static void staticMethod()

修饰代码块

synchronized (SynchronizedObject.class) {
    xxxxxxxxxx
}

synchronized 作为关键字,使用是很方便的,看这2个代码片段就能体会到,方法,代码块加了 synchronized 就能在多线程中保持内存同步,同一时间内只能有一个 Thread 进来操作 synchronized 标记的方法和代码块。

synchronized 用的谁身上的锁

然后我们进一步思考,synchronized 需要一把对象锁,在 synchronized 修饰方法时,不论是静态方法还是普通方法,都是在方法前面加上 synchronized 就行了,那么和这个 synchronized 对应的锁是用的谁的

  • synchronized 修饰普通方法
    用的是这个方法所在对象的锁

  • synchronized 修饰静态方法
    用的是这个方法所在对象的类的锁

同步代码块在具体书写时,我们会碰到下面几种使用锁的方式

// 使用 .class 锁
synchronized (SynchronizedObject.class) {
    xxxxxxxxxx
}

// 使用 Object 对象锁
Object c = new  Object ();
synchronized (c) {
    xxxxxxxxxx
}

// 使用当前对象的锁
synchronized (this) {
    xxxxxxxxxx
}

kotlin 上的写法,使用的是 @Synchronized

    @Synchronized
    fun test(){}

this 就是对象的锁一种写法,本质和普通同步方法相同,效果也相同

多线程并发环境下会造成阻塞,会影响执行效率,所以对象锁的阻塞范围要有清晰的了解,这是 synchronized 的特征,因为 synchronized 本身不带锁,要用别人的。

不同锁对应的阻塞范围

synchronized 的锁本质上2种,写法3种:

  • Object.class 类.class锁
  • this 当前对象锁,等同于同步方法思路,这个对象就是容器对象。
  • object 成员变量锁

我翻了好多书和资料,没看到有人仔细说不同的对象锁对应的阻塞范围,那咱们就自己东西来试试。

其实我们要搞清楚的就是下面几个问题没,前提是多线程环境下对个线程同时操作同一个对象的方法和成员变量:

  • 调用同一个同步方法会不会阻塞
  • 在别的线程调用同步方法时,非同步方法能不能同时调用,成员变量等同于方法
  • 在别的线程调用同步方法时,该对象的成员变量中的同步方法能不能同时调用
  • Object.class 和 对象锁一样不一样

测试用例:

  • 我们设计一个对象 Animal, 提供同步和非同步打印方法,连续打印5次,每次间隔1秒。
  • 这个对象有个成员变量 Book,Book 也能提供非同和非同步的打印方法
  • 我们在 UI 线程启动2个 thread 出来,分别调用 animal 对象的方法和 animal 的成员变量 Book 的方法

Animal 对象

    public class Animal {

        public String name;
        public Book book;

        public Animal(String name) {
            this.name = name;
            book = new Book("《Android 开发艺术探索》");
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public void speak() {
            for (int i = 0; i <= 5; i++) {
                Log.d("AAA", name + "第 " + i + " 次" + "非同步叫唤," + "Thread: " + Thread.currentThread().getName() + " / Time: " + System.currentTimeMillis());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        synchronized public void speakSynchronized() {
            for (int i = 0; i <= 5; i++) {
                Log.d("AAA", name + "第 " + i + " 次" + "同步叫唤," + "Thread: " + Thread.currentThread().getName() + " / Time: " + System.currentTimeMillis());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        synchronized public void OhterSynchronized() {
            for (int i = 0; i <= 5; i++) {
                Log.d("AAA", name + "其他同步方法," + " 第 " + i + " 次" + ", Thread: " + Thread.currentThread().getName() + " / Time: " + System.currentTimeMillis());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

Book 对象

    public class Book {

        public String name;

        public Book(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public void speak() {
            for (int i = 0; i <= 5; i++) {
                Log.d("AAA", name + "第 " + i + " 次" + "非同步阅读," + "Thread: " + Thread.currentThread().getName() + " / Time: " + System.currentTimeMillis());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        synchronized public void speakSynchronized() {
            for (int i = 0; i <= 5; i++) {
                Log.d("AAA", name + "第 " + i + " 次" + "同步阅读," + "Thread: " + Thread.currentThread().getName() + " / Time: " + System.currentTimeMillis());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }

测试1

2个线程同时调用 animal 的同步方法

测试代码

        Animal dog = new Animal("汪酱");

        Thread t1 = new Thread() {
            @Override
            public void run() {
                super.run();
                Log.d("AAA", Thread.currentThread().getName() + "开始执行......" + " / Time: " + System.currentTimeMillis());
                dog.speakSynchronized();
            }
        };

        Thread t2 = new Thread() {
            @Override
            public void run() {
                super.run();
                Log.d("AAA", Thread.currentThread().getName() + "开始执行......" + " / Time: " + System.currentTimeMillis());
                dog.speakSynchronized();
            }
        };

        t1.start();
        t2.start();
Snip20180602_9.png

不出所料,同一个对象里同一个同步方法只能有一个 Thread 调用,其他想调用改方法的 Thread 都得在后面排队,也就是阻塞。这是 synchronized 最常见的使用,也是 synchronized 的初衷。

测试2

1个线程调用 animal 对象同步方法的同时,另一个线程调用 animal 对象的非同步的普通方法

测试代码

        Animal dog = new Animal("汪酱");

        Thread t1 = new Thread() {
            @Override
            public void run() {
                super.run();
                Log.d("AAA", Thread.currentThread().getName() + "开始执行......" + " / Time: " + System.currentTimeMillis());
                dog.speakSynchronized();
            }
        };

        Thread t2 = new Thread() {
            @Override
            public void run() {
                super.run();
                Log.d("AAA", Thread.currentThread().getName() + "开始执行......" + " / Time: " + System.currentTimeMillis());
                dog.speak();
            }
        };

        t1.start();
        t2.start();
Snip20180602_10.png

可以看到,同步和非同步方法一起执行。着说明对象锁的阻塞范围不包括非同步方法。这下我们心里有跟了,没有同步标记的方法多线程中没有使用限制

测试3

1个线程调用 animal 对象同步方法的同时,另一个线程调用 animal 对象的另一个同步方法

测试代码

        Animal dog = new Animal("汪酱");

        Thread t1 = new Thread() {
            @Override
            public void run() {
                super.run();
                Log.d("AAA", Thread.currentThread().getName() + "开始执行......" + " / Time: " + System.currentTimeMillis());
                dog.speakSynchronized();
            }
        };

        Thread t2 = new Thread() {
            @Override
            public void run() {
                super.run();
                Log.d("AAA", Thread.currentThread().getName() + "开始执行......" + " / Time: " + System.currentTimeMillis());
                dog.OtherSynchronized();
            }
        };

        t1.start();
        t2.start();
Snip20180602_11.png

可以看到,2个不同的同步方法还是阻塞执行的,同一时间只有一个同步方法能跑。说明对象锁的阻塞范围是这个对象内的所有同步方法的。

测试4

1个线程调用 animal 对象同步方法的同时,另一个线程调用 animal 对象成员变量 book 的同步方法

测试代码

        Animal dog = new Animal("汪酱");

        Thread t1 = new Thread() {
            @Override
            public void run() {
                super.run();
                Log.d("AAA", Thread.currentThread().getName() + "开始执行......" + " / Time: " + System.currentTimeMillis());
                dog.speakSynchronized();
            }
        };

        Thread t2 = new Thread() {
            @Override
            public void run() {
                super.run();
                Log.d("AAA", Thread.currentThread().getName() + "开始执行......" + " / Time: " + System.currentTimeMillis());
                dog.book.speakSynchronized();
            }
        };

        t1.start();
        t2.start();
Snip20180602_12.png

结果可能出乎我们意料,但是想想又是非常合理的,2个同步方法同时执行饿了。着说明对象锁的阻塞范围仅限于自身直接的方法,而对于自身成员变量的同步方法是阻塞不了的,大家想想啊,我的成员变量是个对象,那么这个对象有自己的锁,肯定页不应该受外部容器对象锁的影响

测试5

对象锁我们基本摸清规律了,剩下的场景我们也能根据上面的阻塞规则分析出来了,现在我们还要解决 Object.class 的问题,和对象锁一样吗

我们先来测下静态同步方法,静态方法是属于类的,而不是对象的,这个好理解我们先来测

给 Animal 对象添加一个静态同步方法,我就不再上 Animal 的代码了,大家想象下,同时 new 2个 animal 对象出来,调用同一个静态同步方法

测试代码

        Animal dog = new Animal("汪酱");
        Animal dog2 = new Animal("papi酱");

        Thread t1 = new Thread() {
            @Override
            public void run() {
                super.run();
                Log.d("AAA", Thread.currentThread().getName() + "开始执行......" + " / Time: " + System.currentTimeMillis());
                dog.staticSynchronized();
            }
        };

        Thread t2 = new Thread() {
            @Override
            public void run() {
                super.run();
                Log.d("AAA", Thread.currentThread().getName() + "开始执行......" + " / Time: " + System.currentTimeMillis());
                dog2.staticSynchronized();
            }
        };

        t1.start();
        t2.start();
Snip20180602_13.png

不愧为 static 属于类本身一说啊,不管 new 几个同一类型的对象出来,类本身的 static 的静态同步方法同一时刻只能有一个线程调用。

测试6

我们来试试在代码块内使用 Object.class ,在 Animal 中添加一个普通方法内有同步代码块,使用 Object.class 的类锁。new 2个 Animal 对象,2个线程同时调用 这个方法

测试代码

        Animal dog = new Animal("汪酱");
        Animal dog2 = new Animal("papi酱");

        Thread t1 = new Thread() {
            @Override
            public void run() {
                super.run();
                Log.d("AAA", Thread.currentThread().getName() + "开始执行......" + " / Time: " + System.currentTimeMillis());
                dog.codeBlock();
            }
        };

        Thread t2 = new Thread() {
            @Override
            public void run() {
                super.run();
                Log.d("AAA", Thread.currentThread().getName() + "开始执行......" + " / Time: " + System.currentTimeMillis());
                dog2.codeBlock();
            }
        };

        t1.start();
        t2.start();
Snip20180602_14.png

恩,可以看到,2个 Animal 对象的 Object.class 同步代码块方法同时只能有一个线程跑。这说明在代码块中使用 Object.class 相当于把这个方法标记为静态同步的。

总结下对象锁的阻塞范围:

  • 对象锁的阻塞先于自身的同步方法,同步方法没有数量限制,一个线程正在调用对象的摸某一个同步方法,那么此时另一个线程调用这个对象的另一个同步方法也是会被阻塞的
  • 对象锁的不会阻塞非同步的阻塞方法,即使此时一个线程正在调用这个对象的同步方法,其他线程这个时候也是可以调用这个对象的非同步方法的
  • 对象锁的范围仅限自身,对象的成员变量不受外部对象锁的阻塞影响,这符合一个对象一把锁的设计思路
  • 静态同步方法属于类本身,不管这个类有多少个实例,同一时刻只能有一个线程操作这个类的这个静态的同步方法,和对象实例没关系,只和类有关系
  • 同步代码块使用 Object.class 等同于把方法标记为静态同步的
  • 同步代码块使用 this.class 等同于把方法标记为同步的

synchronized 扯了半天,但是只要我们把 synchronized 搞清楚了,同步基本就没问题了,实际编码时,同步我们都是使用 synchronized 的,synchronized 玩好了就差不多成了。


参考文档:

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