ReentrantLock锁的一种误用分析

在网上看到一个兄弟写的关于ReentrantLock的文章,介绍得挺详细的,但是有个例子却讲得有点问题。看下面的代码:

import java.util.concurrent.locks.ReentrantLock;  
  
public class ReentrantLockPractice3 {  
  
    static ReentrantLock lock = new ReentrantLock();  
    private static String[] threadArr = {"A","B","C"};  
      
    public static void main(String[] args){  
        ReentrantLockPractice3 pc = new ReentrantLockPractice3();  
        pc.startDemo();  
    }  
      
    void startDemo(){  
        for(int i = 0;i<10;i++){  
            for(String name : threadArr){  
                TestThread t = new TestThread(name);  
                t.start();  
                try {  
                    Thread.sleep(100);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        }  
    }  
      
  
    class TestThread extends Thread{  
          
        //自定义线程名字  
        TestThread(String str){  
            super(str);           
        }  
          
        public void run(){  
            try {  
                lock.lockInterruptibly();
                System.out.println( Thread.currentThread().getName());                  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            } finally{  
                lock.unlock();  
            }     
        }  
    }  
      
}  

输出结果:

ABCABCABCABCABCABCABCABCABCABC

在这个例子中,作者的意图是通过ReentrantLock来保证多线程输出的有序性:

三个线程,线程名分别为A、B、C,设计程序使得三个线程循环打印“ABC”10次后终止。如:ABCABCABCABCABCABCABCABCABCABC

也许作者对ReentrantLock比较了解,但这个例子却是和ReentrantLock没有什么关系,也就是说,ReentrantLock锁在这里没有起到应有的作用。把ReentrantLock锁去掉,依然能够有序地输出。

 
public class ReentrantLockPractice3a {  
 
    private static String[] threadArr = {"A","B","C"};  
      
    public static void main(String[] args){  
        ReentrantLockPractice3a pc = new ReentrantLockPractice3a();  
        pc.startDemo();  
    }  
      
    void startDemo(){  
        for(int i = 0;i<10;i++){  
            for(String name : threadArr){  
                TestThread t = new TestThread(name);  
                t.start();  
                try {  
                    Thread.sleep(1);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        }  
    }  
      
  
    class TestThread extends Thread{  
          
        //自定义线程名字  
        TestThread(String str){  
            super(str);           
        }  
          
        public void run(){  
            try {
 
                System.out.print(Thread.currentThread().getName());  
                
            } catch(Exception e){  
               e.printStackTrace();
            }     
        }  
    }  
      
}  

输出结果:

ABCABCABCABCABCABCABCABCABCABC

结果仍然是有序的ABCABC...序列,这是为什么呢?

其实真正起让输出有序的是这几行代码:

                try {  
                    Thread.sleep(100);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
    线程每次start后,延时100ms,这让线程有足够的时间进行排队,也就是说,正是因为这个延迟,让原本乱序执行的线程,变得有序起来,即便将延时修改为1ms,也能取得同样的执行效果。因此,ReentrantLock锁在此没有起到应有的作用。

    作者举的这个例子,从目的上讲,是要用到多线程快速处理,并且要让输出的线程有序,实际上的效果是线程都是顺序执行的,也就是说,与使用单线程没有两样,反而增加了创建线程的开销。

    实际上,用到锁,必然涉及到资源的争用,如果不涉及到资源的争用,用锁就没有意义了。为了解释ReentrantLock,可以创建一个资源争用的例子,让锁真正发挥作用:当然也一定会用到多线程,多线程的作用是提高处理效率,其乱序执行是允许的,我们真正要关心的是处理好要顺序执行的变量。看下面的例子:

import java.util.concurrent.locks.ReentrantLock;  
  
public class ReentrantLockPractice3b {  
	
	public int counter = 0;

    static ReentrantLock lock = new ReentrantLock();  
    private static String[] threadArr = {"A","B","C"};  
      
    public static void main(String[] args){  
        ReentrantLockPractice3b pc = new ReentrantLockPractice3b();  
        pc.startDemo();  
    }  
      
    void startDemo(){
    	
        for(int i = 0;i<10;i++){  
            for(String name : threadArr){  
                TestThread t = new TestThread(name, this);  
                t.start();  
            }  
        }  
    }  
      
  
    class TestThread extends Thread{  
    	
    	ReentrantLockPractice3b rl2;
 
        //自定义线程名字  
        TestThread(String str, ReentrantLockPractice3b rl2){  
            super(str);      
            this.rl2 = rl2;
        }  
          
        public void run(){  
            try {  
                lock.lockInterruptibly();

            	System.out.println("counter:" + rl2.counter++);
//              try {
//		    sleep(1000);
//		} catch (InterruptedException e) {
//		    e.printStackTrace();
//		}
                
                System.out.println( Thread.currentThread().getName() );  
                
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            } finally{  
                lock.unlock();  
            }     
        }  
    }  
      
}  

在这个例子中,我们构造了一个会引起资源争用的变量(counter),我们的目的是保证其有序访问。看下面的输出结果:

counter:0
A
counter:1
C
counter:2
C
counter:3
A
counter:4
A
counter:5
B
counter:6
B
counter:7
B
counter:8
C
counter:9
A
counter:10
A
counter:11
C
counter:12
A
counter:13
B
counter:14
B
counter:15
C
counter:16
A
counter:17
C
counter:18
A
counter:19
B
counter:20
C
counter:21
B
counter:22
B
counter:23
C
counter:24
C
counter:25
A
counter:26
B
counter:27
C
counter:28
A
counter:29
B

可以看到,通过ReentrantLock,保证了counter的有序,但是线程的启动并不是按设想的“ABCABC...”的方式执行的,而是无序的,这正是虚拟机高效调度的体现。如果限定了线程的依次执行,那不就等同于单线程了吗?

可以看看,去掉ReentrantLock后,结果会变成什么样子:

import java.util.concurrent.locks.ReentrantLock;  
  
public class ReentrantLockPractice3b {  
	
	public int counter = 0;

    static ReentrantLock lock = new ReentrantLock();  
    private static String[] threadArr = {"A","B","C"};  
      
    public static void main(String[] args){  
        ReentrantLockPractice3b pc = new ReentrantLockPractice3b();  
        pc.startDemo();  
    }  
      
    void startDemo(){
    	
        for(int i = 0;i<10;i++){  
            for(String name : threadArr){  
                TestThread t = new TestThread(name, this);  
                t.start();  
            }  
        }  
    }  
      
  
    class TestThread extends Thread{  
    	
    	ReentrantLockPractice3b rl2;
 
        //自定义线程名字  
        TestThread(String str, ReentrantLockPractice3b rl2){  
            super(str);      
            this.rl2 = rl2;
        }  
          
        public void run(){  
            try {  
                //lock.lockInterruptibly();

            	System.out.println("counter:" + rl2.counter++);
//              try {
//		    sleep(1000);
//		} catch (InterruptedException e) {
//	            e.printStackTrace();
//		}
                
                System.out.println( Thread.currentThread().getName() );  
                
            } /*catch (InterruptedException e) {  
                e.printStackTrace();  
            } */finally{  
                //lock.unlock();  
            }     
        }  
    }  
      
}  

输出结果:

counter:0
counter:3
A
counter:4
B
counter:2
B
counter:6
C
counter:7
A
counter:1
C
counter:5
B
A
counter:8
counter:9
C
A
counter:11
counter:10
B
counter:13
C
counter:14
B
counter:16
A
counter:12
B
counter:17
A
counter:19
C
counter:15
A
counter:22
C
counter:24
C
counter:25
counter:21
C
counter:20
counter:18
B
counter:27
B
B
A
counter:26
B
C
counter:23
A
counter:28
A
counter:29
C

可以看到,counter的值是混乱的,没有按照预设的目标进行计数。ReentrantLock锁发挥了作用。

其实,这里的lock.lockInterruptibly();用lock.lock();更合适一些,因为这里不涉及到锁的中断。

注意:

使用ReentrantLock锁,一定要在finally代码块中进行解锁,否则,会跟同步造成的死锁一样,出现锁一直等待的情形。



posted @ 2017-09-28 18:38  hongweigg  阅读(5)  评论(0编辑  收藏  举报