Java多线程——深入理解"脏读"
脏读:某线程取到的数据是被其他线程所修改过的。
在Java中,若没有使用加锁操作,所有的线程之间是异步执行的,因此就会产生"脏读"导致数据的丢失或错误。
首先来看根本没有任何加锁操作的情况。
class MyThread implements Runnable{
private int num = 5;
@Override
public void run() {
showNum();
}
public void showNum(){
while (num>0){
num--;
System.out.println(Thread.currentThread().getName()+":"+num);
}
}
}
public class DirtyReadTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
new Thread(myThread,"A").start();
new Thread(myThread,"B").start();
}
}
// 执行结果
B:3
A:3
A:2
A:1
A:0
程序解读:从这个执行结果可以看出,A:3并且B:3,同一个3出现了两次,明显与逻辑不符,出现了"脏读"。
此时需要给该方法加上synchronized关键字,从而保证一次只会有一个线程进入该方法,保证同步即线程安全。
public synchronized void showNum(){
while (num>0){
num--;
System.out.println(Thread.currentThread().getName()+":"+num);
}
}
// 执行结果
A:4
A:3
A:2
A:1
A:0
程序解读:此时线程A始终拥有该对象的锁,由于线程A和线程B都是在调用对象MyThread,因此A反复进行打印知道num=-1,而当A释放锁后,B获取到锁,此时第一次判断num>0的结果就是false,while循环不会进入,程序执行结束。
现在就产生一个疑惑,有了synchronized关键字修饰真的就完全保证了同步吗?
实际上与程序是如何编写的有很大的关系,举个栗子:
class PublicVar{
private String username = "A";
private String password = "AA";
public synchronized void setValue(String username,String password){
try {
this.username = username;
Thread.sleep(5000);
this.password = password;
System.out.println("setValue method thread name="
+Thread.currentThread().getName()+" username="
+username+" password="+password);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void getValue(){
System.out.println("getValue method thread name="
+Thread.currentThread().getName()+" username="
+username+" password="+password);
}
}
class ThreadA extends Thread{
PublicVar publicVar = null;
public ThreadA(PublicVar publicVar) {
this.publicVar = publicVar;
}
@Override
public void run() {
publicVar.setValue("B","BB");
}
}
public class DirtyReadTest2 {
public static void main(String[] args) {
try {
PublicVar publicVar = new PublicVar();
ThreadA threadA = new ThreadA(publicVar);
threadA.start();
// 程序的结果与这个睡眠时间相关
// 此处的睡眠时间>setValue()中的睡眠时间:
等设置完值后才取得值,不会出现脏读。
// 此处的睡眠时间<setValue()中的睡眠时间:
没等设置完值就取得值,会出现脏读。
Thread.sleep(200);
publicVar.getValue();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 打印结果
getValue method thread name=main username=B password=AA
setValue method thread name=Thread-0 username=B password=BB
程序解读:简单的说一下这段代码,就是在用户设置完对象的属性前,直接过去的对象的属性,导致该对象设置属性过程中(仅设置了username,还未设置password),就直接使用getValue()获取该对象的值,导致了"脏读"的产生。
由上述代码及可看出,编写了synchronized但并不能完全保证同步,此时疑问又来了,synchronized本来就是因为同步而产生了,为什么现在不能保证了呢?
其实synchronized是可以保证同步的,这段代码出现"脏读"的原因是getValue()并不是同步的,而某个线程获得对象锁时,其他线程可以随意调用非同步方法
。因此,要想实现设置完值后才允许获得值,就需要getValue()与setValue()同步,因此需要将getValue()也设置为同步方法即可。
public synchronized void getValue(){
System.out.println("getValue method thread name="
+Thread.currentThread().getName()+" username="
+username+" password="+password);
}
// 打印结果
setValue method thread name=Thread-0 username=B password=BB
getValue method thread name=main username=B password=BB
程序解读:getValue()一定会等setValue()执行完才执行,因为这两个方法锁的是同一个对象(this),而在主函数中setValue()一定是优先于getValue()调用,避免"脏读"的产生。
某个线程获得对象锁时,其他线程不能调用其他同步方法,必须要等待该线程执行结束。
同样再举一个难以判断的栗子,明明已经使用了synchronized锁,却因为程序设计的问题而导致脏读的产生。
class MyOneList{
private List list = new ArrayList();
public synchronized void add(String data){
list.add(data);
}
public synchronized int getSize(){
return list.size();
}
}
/**
* 创建一个只含1个元素的list
*/
class MyService{
public MyOneList addServiceMethod(MyOneList list,String data){
try {
if (list.getSize()<1){
// 模拟从远程花费2s取回数据
Thread.sleep(2000);
list.add(data);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return list;
}
}
// 两个线程分别插入"A"和"B"
class MyThread1 extends Thread{
private MyOneList list;
public MyThread1(MyOneList list) {
this.list = list;
}
@Override
public void run() {
MyService myService = new MyService();
myService.addServiceMethod(list,"A");
}
}
class MyThread2 extends Thread{
private MyOneList list;
public MyThread2(MyOneList list) {
this.list = list;
}
@Override
public void run() {
MyService myService = new MyService();
myService.addServiceMethod(list,"B");
}
}
public class DirtyReadTest3 {
public static void main(String[] args) throws InterruptedException {
MyOneList list = new MyOneList();
MyThread1 thread1 = new MyThread1(list);
thread1.setName("A");
MyThread2 thread2 = new MyThread2(list);
thread2.setName("B");
thread1.start();
thread2.start();
Thread.sleep(6000);
System.out.println("listSize = "+list.getSize());
}
}
// 打印结果
listSize = 2
程序解读:这段代码明明使用了synchronized同步代码块了,但却没有输出正确的结果,其原因是两个线程调用了非同步的方法addServiceMethod(),虽然add()是同步的,但两个线程都可以异步进入addServiceMethod(),并在进行if判断时,两个线程都可以进入if语句块中(异步,因此两个线程进入是size都==0),只不过是两个线程顺序的执行了add()方法(同步方法),因此最终的结果为2。
因此这段代码需要进行改进,即"同步化",将list作为对象放在synchronized代码块中,正是需要对list参数的getSize()方法做同步的调用,所以就对list参数进行同步处理。
public MyOneList addServiceMethod(MyOneList list,String data){
try {
synchronized (list){
if (list.getSize()<1){
// 模拟从远程花费2s取回数据
Thread.sleep(2000);
list.add(data);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return list;
}
// 打印结果
listSize = 1
程序解读:此时将list作为对象进行同步化处理,两个线程在进行判断前是按照顺序执行的,因此,若线程1先抢到list锁,线程2此时需要等待线程1执行完同步块中的语句后再进入同步块,而当线程1执行完后,size==1,此时线程2在执行到if判断时就不会进入语句块,因此会输出正确的结果。
总结:
使用synchronized同步机制可以很好的解决"脏读"的问题,使得线程安全。但一定要考虑全面,合理正确的使用synchronized机制,避免"脏读"问题的产生。
转载:https://blog.csdn.net/weixin_43490440/article/details/102227041