Java多线程01_可重入函数、可重入锁

测试环境

OS:windows7_X64
JDK:jdk1.8.0_20
IDE: eclipse_neon

一、可重入函数

相信很多人都听说过可重入函数,可重入函数最重要的两条法则就是:

  1. 只使用非静态局部变量;
  2. 不调用不可重入的函数。
public class Reentrant1 {
    private int count = 0;
    /** * 不可重入 * 当有多个线程调用同一个Reentrant对象的increament(),输出数值是不可预测的。count可能是1,也可能是任何正整数。 */
    public void increament(){
        count ++;
        System.out.println(count);
    }
	
    /** * 可重入 * 无论多少线程调用同一个Reentrant对的象decreament方法,输出数值都是传入参数length的值减1。 */
    public void decreament(int length){
        length--;
        System.out.println(length);
    }
    
}
如上代码所述,increament是不可重入的函数,decreament是可重入的函数。区别在于一个引用了实例变量,一个未引用实例变量。
不可重入函数被视为不安全的函数,无状态的函数则是线程安全的。在spring 的IOC容器中,默认为单例模式,所以在编程时要尽量使用无状态的类和方法。如像incerement()这样类似的方法,为了得到期望的值,则应该每次访问都新建一个实例对象,也就是要在类上标记@Scope(prototype)
二、可重入锁
说完了可重入函数,那么再来看看可重入锁。
可重入锁的意思是当同一个线程获取锁后,在未释放锁的情况下,多次获取同一个锁对象并不会发生死锁现象。
public class Reentrant2 {

    public static void main(String[] args){
        Reentrant2 rt2 = new Reentrant2();
        rt2.getCount("a", 1);
    }
	
    public void getCount(String str, int count){
        // 第一次获取锁
        synchronized (str.intern()) {
            System.out.println(count);
            count++;
            
            // 第二次获取锁
            synchronized (str.intern()) {
                System.out.println(count);
                count++;
            }
        }
    }
    
}
运行Reentrant2的输出结果为:
1
2
getCount方法中第一次获取锁之后在没有释放锁的情况下,第二次获取同一个字符串对象的锁,并没有发生死锁现象,因此使用synchronized修饰的是可重入锁。

递归调用进行同步必须使用可重入的锁,如下代码所示:
public class Reentrant3 {
    public static void main(String[] args){
        Reentrant3 rt3 = new Reentrant3();
        new Thread(rt3.new ReentrantInner()).start();
        new Thread(rt3.new ReentrantInner()).start();
    }
    
    class ReentrantInner implements Runnable{
        public int getCount(String str, int count) throws InterruptedException{
            synchronized (str.intern()) {
                if(count>5){
                    return count;
                }
                System.out.println(Thread.currentThread().getId() + "==" + count);
                count++;
                Thread.sleep(100);
                //递归调用
                return getCount(str, count);
            }
        }
        
        @Override
        public void run() {
            try {
                this.getCount(new String("a"), 1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
运行Reentrant3的输出结果为:
10==1
10==2
10==3
10==4
10==5
9==1
9==2
9==3
9==4
9==5
可以看到与预期结果一致,并没有发生死锁。另外,我想有些人一定注意到了我使用的是new String("a"),同步时又调用了str.intern()方法。因为在实际生产环境中每一个String字符串对象都可能是由不同的线程产生,即使字符串的值完全相同,也可能不是同一个对象。因此需要调用intern()方法,保证相同值的字符串均引用的是同一对象。
synchronized是根据对象进行加锁,如果不是同一个对象,那么锁就会失效。各位可自行去掉intern()方法试试,输出顺序肯定会发生变化。
至于为什么调用intern()返回的会是同一个对象?请参阅String类的api。
三、其它可重入锁
除了使用synchronized以外,ReentrantLock和ReentrantReadWriteLock也是可重入锁,具体的api请自行查阅,这里不再详谈。ReentrantLock的递归实现如下代码所示:
import java.util.concurrent.locks.ReentrantLock;

public class Reentrant4 {
	
	private final ReentrantLock lock = new ReentrantLock();
	
	public static void main(String[] args){
		Reentrant4 rt4 = new Reentrant4();
		new Thread(rt4.new ReentrantInner()).start();
		new Thread(rt4.new ReentrantInner()).start();
	}
	
	class ReentrantInner implements Runnable{
		
		public int getCount(String str, int count){
			lock.lock();
			try{
				if(count>5){
					return count;
				}
				System.out.println(Thread.currentThread().getId() + "==" + count);
				count++;
				Thread.sleep(100);
				return getCount(str, count);
			}catch(Exception e){
				e.printStackTrace();
				return count;
			}finally{
				lock.unlock();
			}
		}

		@Override
		public void run() {
			this.getCount("a", 1);
		}
	}
	
}
运行Reentrant4的输出结果为:
9==1
9==2
9==3
9==4
9==5
10==1
10==2
10==3
10==4
10==5
可以看到与预期结果一致,并没有发生死锁。

四、实现自己的可重入锁

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public class MyReentrantLock implements Lock {

    private AtomicReference<Thread> owner = new AtomicReference<Thread>();
	
    private int count = 0;

    @Override
    public void lock() {
        Thread current = Thread.currentThread();	        //获取当前线程
        if (current == owner.get()) {				            //如果当前线程是持有锁的线程,count计数+1
            count++;
            System.out.println("lock重入次数:"+count);
            return;
        }
        /* * 1.如果持有锁的线程为空,将当前线程设定为持有锁 * 2.如果持有锁的线程不为空,且并非当前线程,全部进入休眠状态100毫秒 */
        while (!owner.compareAndSet(null, current)) {	
        	try {
        		Thread.sleep(100);
        	} catch (InterruptedException e) {
        		e.printStackTrace();
        	}
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
    }
    
    @Override
    public Condition newCondition() {
    	return null;
    }
    
    @Override
    public boolean tryLock() {
    	return false;
    }

    @Override
    public boolean tryLock(long arg0, TimeUnit arg1) throws InterruptedException {
    	return false;
    }
    
    @Override
    public void unlock() {
    	Thread current = Thread.currentThread();
    	/* * 1.如果当前线程为持有锁的线程: * 如果count不为0,说明当前线程至少获得过一次锁,count--; * 如果count为0,说明当前线程为最后一次释放锁,将持有锁的线程设置为空 */
    	if (current == owner.get()) {
    		if(count > 0){
    			count--;
    		}else{
    			owner.compareAndSet(current, null);
    		}
    	}
    	System.out.println("unlock重入次数:"+count+ "ThreadId:"+Thread.currentThread().getId());
    }

}
实际开发时还要考虑锁超时,锁的效率,中断异常等等因素,此代码仅供参考。

五、后记

本文源于最近在玩的一个项目,发现对可重入函数和可重入锁的概念并不是特别清晰,于是看api看书看博客,有些心得,所以发出来分享。
如果您有更好的理解或者文中有不对的地方,欢迎留言探讨补充指正。谢谢。

参考资料: http://ifeve.com/java_lock_see4/ Java锁的种类以及辨析(四):可重入锁 作者:山鸡

    原文作者:java锁
    原文地址: https://blog.csdn.net/coffeelifelau/article/details/51864868
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞