小言_互联网的博客

Java多线程——深入理解"脏读"

316人阅读  评论(0)

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
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场