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代码块中进行解锁,否则,会跟同步造成的死锁一样,出现锁一直等待的情形。



  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ReentrantLockJava中的一个可重入,它提供了与synchronized关键字类似的功能,但更加灵活和强大。下面是一些与ReentrantLock相关的面试题及其答案: 1. 什么是可重入? 可重入是指同一个线程可以多次获得同一个,而不会造成死。ReentrantLock就是一个可重入。 2. ReentrantLock相对于synchronized关键字有什么优势? 相对于synchronized关键字,ReentrantLock提供了更多的功能和灵活性。它可以实现公平和非公平,支持多个条件变量,可以中断等待的线程,还可以尝试获取等。 3. ReentrantLock如何实现可重入性? ReentrantLock通过记录持有的线程和持有次数来实现可重入性。当一个线程再次获取已经持有的时,它的持有次数会增加,当释放时,持有次数会减少,只有当持有次数为0时,其他线程才能获取该。 4. ReentrantLock如何实现公平性和非公平性? ReentrantLock可以通过构造函数来指定是公平还是非公平。公平会按照线程请求的顺序来获取,而非公平则允许插队,可能会导致某些线程一直获取不到。 5. ReentrantLock如何实现条件变量? ReentrantLock提供了Condition接口来支持条件变量。通过调用ReentrantLock的newCondition()方法可以创建一个Condition对象,然后可以使用该对象的await()、signal()和signalAll()方法来实现线程的等待和唤醒。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值