目录
1、简介
在多线程的的使用场景中,如果对于同一个对象中的实例变量并发进行访问的话,可能会产生“脏数据”的情况,而想要避免出现这种情况的方法就是进行同步处理。常用的就是使用synchronized、volatile来实现。但在日常开发中可能经常会出现使用不当而导致各种各样的异常问题,例如死锁、带来的执行性能问题等。下面就对两种类型做详细的介绍。
2、synchronize
一般非线程安全的问题只会存在于“实例变量”中,如果是方法内部的变量,不会存在非线程安全的问题。但如下逻辑,则会出现输出不符合预期的情况。
先执行完b的逻辑,才继续执行a。
com.ccg.multithreaddemo D/ThreadLog: a set over!
com.ccg.multithreaddemo D/ThreadLog: b set over!
com.ccg.multithreaddemo D/ThreadLog: b num = 200
com.ccg.multithreaddemo D/ThreadLog: a num = 200
这个时候只需要在方法 addI 前面增加 synchronized 即可。
com.ccg.multithreaddemo D/ThreadLog: a set over!
com.ccg.multithreaddemo D/ThreadLog: b num = 200
com.ccg.multithreaddemo D/ThreadLog: b set over!
com.ccg.multithreaddemo D/ThreadLog: a num = 200
- 1)、锁对象
在方法前面增加synchronized的方式,锁的是对象。而不是将一段代码或者方法当做锁,所以哪个线程先执行了带synchronized关键字的方法,哪个线程就持有了该方法所属对象的锁Lock,其他的线程只能是等待状态。但如果多个线程访问的不是同一个对象,如下,修改测试的代码,创建ThreadB的时候,也传入另外一个PrivateNum对象,这个时候的输出就会跟上面的不一样。
public class SynchronizeTestCase {
public static void run() {
PrivateNum privateNum = new PrivateNum();
ThreadA threadA = new ThreadA(privateNum);
threadA.start();
PrivateNum privateNum2 = new PrivateNum();
ThreadB threadB = new ThreadB(privateNum2);
threadB.start();
}
}
//对应的输出:
com.ccg.multithreaddemo D/ThreadLog: a set over!
com.ccg.multithreaddemo D/ThreadLog: b num = 200
com.ccg.multithreaddemo D/ThreadLog: b set over!
com.ccg.multithreaddemo D/ThreadLog: a num = 200
如果类里面的有其他没有关键字synchronized的其他方法,则还是能够异步执行的。
- 2)、锁类
synchronized(class)的实现方式就是对类进行加锁,也就是Class锁,另外,如果是对静态方法加锁的方式,对应的锁也是Class锁。下面的两个方法,都能够实现同样的效果,保证逻辑同步执行。
//类加锁
public void classSync() {
try {
synchronized (Task.this) {
Thread.sleep(10000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//静态方式加锁
public synchronized static void staticMethodSync() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 3)、锁代码块
用关键字synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个长时间的任务,那么B线程就必须等待比较长的时间,在这种情况下,可以使用synchronized同步语句块来解决,synchronized方法是对当前对象进行加锁,而synchronized代码块是对某一个对象进行加锁。如下两种方式都是同步代码块的实现方式。
public void doTimeTask() {
try {
synchronized (this) {
Log.d(TAG, "<-getData1->" + getData1);
}
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void doTask() {
try {
synchronized (getData1) {
Log.d(TAG, "<-getData1->" + getData1);
}
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
其中 synchronized (this) 里面的 this 代表的是当前对象,也可以是某个具体的对象,如上面的getData1。当使用synchronized (this) 这种同步代码块实现方式的时候,如果已经有另外一个线程在执行这个代码块,则这个对象里面的其他synchronized 方法也需要进行等待,也就是同步的方式。
上面提到的,同步代码块的方式,有时候可以解决耗时的问题,例如,耗时的操作放在同步代码块的外面,也即不用进行等待即可先完成所需的耗时操作,但程序也还是正常运行。
//同步方式的实现方式
public synchronized void doLongTimeTask() {
try {
Log.d(TAG, "处理耗时操作");
Thread.sleep(100000);
getData1 = "网络请求返回数据";
getData2 = "从本地文件io返回数据";
Log.d(TAG, "<-getData1->" + getData1);
Log.d(TAG, "<-getData2->" + getData2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//同步代码块的实现方式
public synchronized void doLongTimeTask2() {
try {
Log.d(TAG, "处理耗时操作");
Thread.sleep(100000);
String tempData1 = "网络请求返回数据";
String tempData2 = "从本地文件io返回数据";
synchronized (this) {
getData1 = tempData1;
getData2 = tempData2;
}
Log.d(TAG, "<-getData1->" + getData1);
Log.d(TAG, "<-getData2->" + getData2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 4)、可重入锁
意思是自己可以再次获取自己的内部锁,例如有某个线程获取了某个对象的锁,此时这个对象还没有释放,当其需要再次获取这个对象的锁的时候,还是可以获取到的,比如说在一个synchronized方法里面调用了这个类里面另外一个synchronized方法,是能够正常执行的。如果不可锁重入的话,就会造成死锁了。
3、volatile
volatile的主要作用是使变量在多个线程间可见,他能够强制程序从公共堆栈中取的变量的值,而不是从线程私有数据栈中取得变量的值。
public void stopPrint() {
final PrintString printString = new PrintString();
new Thread() {
@Override
public void run() {
printString.printData();
}
}.start();
Log.d(TAG, "停止打印");
printString.setContinuePrint(false);
}
public static class PrintString {
private boolean isContinuePrint = true;
public boolean isContinuePrint() {
return isContinuePrint;
}
public void setContinuePrint(boolean continuePrint) {
isContinuePrint = continuePrint;
}
public void printData() {
while (isContinuePrint == true) {
Log.d(TAG, "run printData threadName=" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
以上逻辑,运行的结果是,程序一直停不下来,原因是printData方法会一直从私有线程栈中获取isContinuePrint的值,也就是在主线程设置的 printString.setContinuePrint(false) 操作其实并没有生效。解决方式就是增加volatile进行修饰。
private volatile boolean isContinuePrint = true;通过volatile关键字,它会强制线程从公共内存中读取变量的值,也就是保证数据的可见性。
4、总结
1、如果A线程先持有了object对象的Lock锁,B线程可以以异步的方式调用object对象中的非synchronized类型的方法;
2、A线程先持有了object对象的Lock锁,B线程如果在这时调用object对象中的synchronized类型方法,则需要等待,也就是同步;
3、volatile只能保证变量的可见性,无法保证其原子性;
4、synchronized既有办法保证原子性,也能够保证可见性。