通过上篇博文(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();
不出所料,同一个对象里同一个同步方法只能有一个 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();
可以看到,同步和非同步方法一起执行。着说明对象锁的阻塞范围不包括非同步方法。这下我们心里有跟了,没有同步标记的方法多线程中没有使用限制
测试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();
可以看到,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();
结果可能出乎我们意料,但是想想又是非常合理的,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();
不愧为 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();
恩,可以看到,2个 Animal 对象的 Object.class 同步代码块方法同时只能有一个线程跑。这说明在代码块中使用 Object.class 相当于把这个方法标记为静态同步的。
总结下对象锁的阻塞范围:
- 对象锁的阻塞先于自身的同步方法,同步方法没有数量限制,一个线程正在调用对象的摸某一个同步方法,那么此时另一个线程调用这个对象的另一个同步方法也是会被阻塞的
- 对象锁的不会阻塞非同步的阻塞方法,即使此时一个线程正在调用这个对象的同步方法,其他线程这个时候也是可以调用这个对象的非同步方法的
- 对象锁的范围仅限自身,对象的成员变量不受外部对象锁的阻塞影响,这符合一个对象一把锁的设计思路
- 静态同步方法属于类本身,不管这个类有多少个实例,同一时刻只能有一个线程操作这个类的这个静态的同步方法,和对象实例没关系,只和类有关系
- 同步代码块使用 Object.class 等同于把方法标记为静态同步的
- 同步代码块使用 this.class 等同于把方法标记为同步的
synchronized 扯了半天,但是只要我们把 synchronized 搞清楚了,同步基本就没问题了,实际编码时,同步我们都是使用 synchronized 的,synchronized 玩好了就差不多成了。