最近看了一下java的同步机制,记录一下,防止遗忘。
为什么要有同步机制?
大家都知道多线程操作,试想这样一个场景:售票
上海-北京只剩下一张票,有三个人在同时抢票,抢到之后系统进行一系列操作(比如算钱,占位等),然后对票数总量减一,告诉其他人没票了。
看上去没毛病,但一旦用上多线程,毛病就来了
三个线程在访问票数总量时,由于系统没计算完或者在占位,导致没能及时将总量减一,所以三个人都抢到票了。
最终的结果就是,有两个用户提示抢到票了,但是占位失败了,这是多么糟糕的用户体验。
怎么解决?粗暴一点就是同一时间只能有一个线程读写票数总量就可以了,这里用到了java的synchronized字段。
Synchronized
- synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果 再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。
- 无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。
- 每个对象只有一个锁(lock)与之相关联。
- 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
Synchronized用法
大概分为三种,首先介绍一下第一种
对对象加锁:
/**
* 锁定调用方法的对象
* 当一个线程调用该方法时,其他线程无法访问持有该TestMethod对象锁的任何方法
* 需要注意的是,该锁只是锁定当前对象,如果新建了一个其他对象,则线程之间访问互不干扰
* */
public synchronized void syncMethodTest() {
Log.e("TestMethods", "syncMethodTest开始执行");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void syncMethodTest() {
synchronized (this) {
Log.e("TestMethods", "syncMethodTest开始执行");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void syncMethodTest2() {
Log.e("TestMethods", "syncMethodTest2开始执行");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
上面syncMethodTest两种加锁方式的效果是一样的,都是锁定对象,这种情况下,不同线程不可以同时访问同一对象的加锁方法,不仅是同一个方法,只要是锁定该对象的方法,都不可以同时访问,比如两个线程无法同时访问同一个对象的syncMethodTest方法,也不可以同时访问同一个对象的syncMethodTest和syncMethodTest2方法。
但如果是两个对象,不同线程之间访问的是不同对象的synchronized方法,他们之间是不会影响的,可以同时访问。
第二种是同步代码块:
private byte[] lock = new byte[0];
/**
* 锁定变量
* 当一个线程调用该方法时,其他线程无法访问持有该变量锁的任何方法
* 需要注意的是,lock变量为对象属性,如果对象不同,比如两个线程中的两个TestMethod对象
* 同时调用该方法,则互不影响,但如果是同一个对象,则需要等其中一个先释放锁才能继续执行
* */
public void syncByteTest() {
synchronized (lock) {
Log.e("TestMethods", "syncByteTest: 开始执行");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
通过第一种对对象加锁,有一个缺点,当我锁定对象时,对象的所有synchronized方法都将被锁定,浪费开销的同时又影响了其他无关函数的调用,所以更为搞笑的方法是锁定代码块。
如上所示,定义一个特殊的变量,给这个变量加锁,锁定的只是一个代码块而已,不同线程之间无法同时访问该代码块,但对象的其他方法还是可以正常访问的。
注:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。
第三种是锁定class
/**
* 锁定class
* 当一个线程调用该方法时,其他线程无法访问持有该class锁的静态变量和静态方法
* 需要注意的是,class只有一个,这也意味着持有该锁的任何方法都不能同时访问
* 持有class锁的方法必须争夺到锁会后才能运行
* */
public synchronized static void syncStaticTest() {
Log.e("TestMethods", "syncStaticTest: 开始执行");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void syncStaticTest() {
synchronized(TestMethods.class) {
Log.e("TestMethods", "syncStaticTest: 开始执行");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上述两种锁定方式是等价的,这种锁一般用在静态方法中,一旦对class锁定,该对象的所有静态方法,静态变量都不能访问,包括通过对象去访问也不行。
具体效果,读者可以通过test方法去测试一下:
public class SyncronizeTest {
TestMethods testMethods = new TestMethods();
public void test() {
Thread thread1 = new Thread(runnable1);
Thread thread2 = new Thread(runnable2);
thread1.start();
thread2.start();
}
Runnable runnable1 = new Runnable() {
@Override
public void run() {
/**
* 锁定调用方法的对象
* */
testMethods.syncMethodTest();
/**
* 锁定变量
* */
// testMethods.syncByteTest();
/**
* 锁定class
* */
// TestMethods.syncStaticTest();
}
};
Runnable runnable2 = new Runnable() {
@Override
public void run() {
/**
* 锁定调用方法的对象
* */
testMethods.syncMethodTest();
/**
* 锁定变量
* */
// testMethods.syncByteTest();
/**
* 锁定class
* */
// TestMethods.syncStaticTest();
}
};
}
快过年了,今天先写到这里,过完年再补充。