欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java并发编程:Lock&ReentrantLock&Condition

程序员文章站 2022-04-19 08:23:04
...
在Java中,除了使用synchronized关键字实现线程同步,还可以使用java.util.concurrent.locks包下的重入锁(ReentrantLock)来实现同步。今天我们就来学习ReentrantLock同步。
以下是本文包含的知识点:
1.Lock接口介绍
2.ReentrantLock的使用
3.ReentrantLock与synchronized实现同步的区别
4.ReadWriteLock介绍
5.Condition使用
 
一、Lock接口介绍
ReentrantLock实现自Lock接口,Lock接口中定义了一系列同步的方法,源码如下:
public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}
 在这此方法中lock(),lockInterruptibly(),tryLock(),tryLock(long time, TimeUnit unit)都是用来获取锁的,区别如下:
lock():获取锁。如果锁可用,则获取锁,否则进行等待。
tryLock():如果锁可用,则获取锁,并立即返回值 true,否则返回 false,不会等待。
tryLock(long time, TimeUnit unit):如果在给定的等待时间内,锁可用,则获取锁,并立即返回值 true,否则返回false。
lockInterruptibly():获取锁,等待可中断。如果锁可用,并且当前线程未被中断,则获取锁,否则进行等待。如果被其它线程中断,则抛出InterruptException异常。
unlock()用来释放锁,newCondition()后面再讲。
 
因为采用Lock获取锁,必须手动释放锁,并且在发生异常时也不会自动释放锁,所以lock()/unlock()一般配合try/finally语句块来完成,通常使用形式如下:
Lock lock = ...;
lock.lock();//获取锁
try{
     //处理任务
}catch(Exception e){

}finally{
     lock.unlock();//释放锁
}
 
二、ReentrantLock的使用
ReentrantLock除了实现Serializable接口外,唯一实现Lock接口,下面来看看它的用法:
public class ReentrantLockTest {
      
       private Lock lock = new ReentrantLock();   //lock要定义为成员变量,第个线程都共享这个锁
       private List<Integer> arrList = new ArrayList<Integer>();
      
       public static void main(String[] args) {
             final ReentrantLockTest test = new ReentrantLockTest();
             new Thread(){
                   public void run(){
                        test.testLock(Thread. currentThread());
                  }
            }.start();
             new Thread(){
                   public void run(){
                        test.testLock(Thread. currentThread());
                  }
            }.start();
            
      }
      
       /**
       * lock方法
       * @param thread
       */
       public void testLock(Thread thread){
             //Lock lock = new ReentrantLock();//注意这里,并不会达到互斥的效果
                                              //因为在testLock 方法中的lock变量是局部变量,每个线程执行该方法时都会保存一个副本,那么理所当然每个线程执行到lock.lock()处获取的是不同的锁,所以就不会发生冲突。解决办法:变成全局的,所有线程共享
             lock.lock();//lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
             try {
                  System. out.println(thread.getName()+"得到了锁" );
                   for(int i=0; i<5; i++){
                         arrList.add(i);
                  }
            } catch (Exception e) {
                  e.printStackTrace();
            } finally{
                  System. out.println(thread.getName()+"释放了锁" );
                   lock.unlock();//unlock总是与lock成对出现,且一般采用try,catch,finally的方式
            }
      }
      
       /**
       * tryLock方法
       * tryLock(long time, TimeUnit unit)方法
       * @param thread
       */
       public void testTryLock(Thread thread){
             if(lock .tryLock()){//tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
                                 //tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
                   try {
                        System. out.println(thread.getName()+"得到了锁" );
                         for(int i=0; i<5; i++){
                               arrList.add(i);
                        }
                  } catch (Exception e) {
                        e.printStackTrace();
                  } finally{
                        System. out.println(thread.getName()+"释放了锁" );
                         lock.unlock();
                  }
            } else{
                  System. out.println(thread.getName()+"获取锁失败" );
            }
      }

}
 执行结果:
Thread-0得到了锁
Thread-0释放了锁
Thread-1得到了锁
Thread-1释放了锁
 可以看到实现了同步
 
lockInterruptibly():获取锁,等待可中断实例:
public class TestLockInterruptibly {

       private Lock lock = new ReentrantLock();  
    public static void main(String[] args)  {
      TestLockInterruptibly test = new TestLockInterruptibly();
        MyThread thread1 = new MyThread(test);
        MyThread thread2 = new MyThread(test);
        thread1.start();
        thread2.start();
        
        try {
            Thread. sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.interrupt();
    } 
    
    public void testLockInterruptibly(Thread thread) throws InterruptedException{
        lock.lockInterruptibly();   //注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出
        try { 
            System. out.println(thread.getName()+"得到了锁" );
            long startTime = System.currentTimeMillis();
            for(    ;     ;) {
                if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE )
                    break;
                //。。。
            }
        }
        finally {
            System. out.println(Thread.currentThread().getName()+ "执行finally");
            lock.unlock();
            System. out.println(thread.getName()+"释放了锁" );
        } 
    }
      
}

class MyThread extends Thread {
    private TestLockInterruptibly test = null ;
    public MyThread(TestLockInterruptibly test) {
        this.test = test;
    }
    @Override
    public void run() {
        try {
            test.testLockInterruptibly(Thread.currentThread());
        } catch (InterruptedException e) {
            System. out.println(Thread.currentThread().getName()+ "被中断");
        }
    }
}
 执行结果:
Thread-0得到了锁
Thread-1被中断
可以看到thread2 被中断了
 
三、ReentrantLock与synchronized实现同步的区别
相同点:
1.实现同步功能
2.都具有线程重入性
不同点:
1.代码写法上,ReentrantLock为API层面互斥锁,synchronized为原生语法层面互斥锁。
2.功能上,ReentrantLock比synchronized提高了更高级的功能:等待可中断可实现公平锁锁可以绑定多个条件
等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待。可中断特性对执行时间非常长的同步块很有帮助。Lock的lockInterruptibly()方法就是等待可中断实现。
公平锁是指多个线程在等待同一个锁时,必须按申请锁的时间顺序依次获取锁;而非公平锁则不能保证这一点,在锁被释放的时候,任何一个线程都有机会获得锁。synchronized是非公平锁,ReentrantLock默认是非公平锁,但可以通过带布尔值的构造方法要求使用公平锁。
ReentrantLock构造器源码:
//默认为非公平锁
public ReentrantLock() {
        sync = new NonfairSync();
    }
//可以通过带布尔值的构造方法要求使用公平锁
public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
锁可以绑定多个条件是指一个ReentrantLock对象可以绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()可以实现一个隐含的条件,如果要和多于一个的条件关联,就不得不额外的添加一个锁,而ReentrantLock则无须这样做,只需多次调用newCondition()方法而已。
3.性能上,Jdk6加入了很多针对锁的优化,synchronized与ReentrantLock的性能基本上完全持平了。性能因素不再是选择ReentrantLock的理由,虚拟机未来在性能改进中肯定也会更加偏向于原生的synchronized,所以还是优先考虑使用synchrozied进行同步。
 
四、ReadWriteLock介绍
ReadWriteLock也是一个接口,它只定义了两个方法:
public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading.
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing.
     */
    Lock writeLock();
}
 ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。
ReentrantReadWriteLock类实现了ReadWriteLock接口。我们来看下多个线程同时获取读取锁的情况:
public class ReadWriteLockTest {
      
       private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
      
       public static void main(String[] args) {
             final ReadWriteLockTest test = new ReadWriteLockTest();
             new Thread(){
                   public void run(){
                        test.get2(Thread. currentThread());
                  }
            }.start();
             new Thread(){
                   public void run(){
                        test.get2(Thread. currentThread());
                  }
            }.start();
      }
      
       /**
       * readLock方法:使用多个线程同时执行
       * @param thread
       */
       public void get2(Thread thread) {
             readWriteLock.readLock().lock();
        try {
            long start = System.currentTimeMillis();
            
            while(System.currentTimeMillis() - start <= 1) {
                System. out.println(thread.getName()+"正在进行读操作" );
            }
            System. out.println(thread.getName()+"读操作完毕" );
        } finally {
             readWriteLock.readLock().unlock();
        }
    }

}
 执行结果:
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-1读操作完毕
Thread-0读操作完毕
 根据结果可以看到,两个线程在并发执行,说明读取锁(ReadLock)可以被多个线程同时获得,这也符合实际应用场景。提高读操作的效率。
不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
 
四、Condition使用

synchronized与wait()和nitofy()/notifyAll()方法相结合可以实现等待/通知模型,ReentrantLock同样可以,但是需要借助Condition,且Condition有更好的灵活性,具体体现在:

1、一个Lock里面可以创建多个Condition实例,实现多路通知

2、notify()方法进行通知时,被通知的线程时Java虚拟机随机选择的,但是ReentrantLock结合Condition可以实现有选择性地通知,这是非常重要的

看一下利用Condition实现等待/通知模型的最简单用法,下面的代码注意一下,await()和signal()之前,必须要先lock()获得锁,使用完毕在finally中unlock()释放锁,这和wait()/notify()/notifyAll()使用前必须先获得对象锁是一样的:

import java.util.PriorityQueue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionTest {
	private int queueSize = 10;
	private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);
	private Lock lock = new ReentrantLock();
	private Condition notFull = lock.newCondition();
	private Condition notEmpty = lock.newCondition();

	public static void main(String[] args) {
		ConditionTest test = new ConditionTest();
		Producer producer = test.new Producer();
		Consumer consumer = test.new Consumer();

		producer.start();
		consumer.start();
	}

	class Consumer extends Thread {

		@Override
		public void run() {
			consume();
		}

		private void consume() {
			while (true) {
				lock.lock();
				try {
					while (queue.size() == 0) {
						try {
							System.out.println("队列空,等待数据");
							notEmpty.await();
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
					queue.poll(); // 每次移走队首元素
					notFull.signal();
					System.out.println("从队列取走一个元素,队列剩余" + queue.size() + "个元素");
				} finally {
					lock.unlock();
				}
			}
		}
	}

	class Producer extends Thread {

		@Override
		public void run() {
			produce();
		}

		private void produce() {
			while (true) {
				lock.lock();
				try {
					while (queue.size() == queueSize) {
						try {
							System.out.println("队列满,等待有空余空间");
							notFull.await();
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
					queue.offer(1); // 每次插入一个元素
					notEmpty.signal();
					System.out.println("向队列取中插入一个元素,队列剩余空间:"
							+ (queueSize - queue.size()));
				} finally {
					lock.unlock();
				}
			}
		}
	}
}

Condition的await()方法是释放锁的,原因也很简单,要是await()方法不释放锁,那么signal()方法又怎么能调用到Condition的signal()方法呢?

注意要是用一个Condition的话,那么多个线程被该Condition给await()后,调用Condition的signalAll()方法唤醒的是所有的线程。如果想单独唤醒部分线程该怎么办呢?new出多个Condition就可以了,这样也有助于提升程序运行的效率。使用多个Condition的场景是很常见的,像ArrayBlockingQueue里就有。 
 
参考
《深入Java 虚拟机》