java多线程之Lock(3)

前言

从Java 5之后,在java.util.concurrent.locks包下提供了另外一种方式来实现同步访问,那就是Lock,再存在synchronized的情况下,为什么会出现Lock来实现同步呢?它和synchronized又有哪些不同呢?

synchronized的局限性

第一点:

如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

  1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;

  2)线程执行发生异常,此时JVM会让线程自动释放锁。

如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能等待,这样会非常影响程序执行效率。因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。

第二点:

当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是采用synchronized关键字来实现同步的话,就会导致: 如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。

因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。

总结一下:

  1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;

  2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象

java.util.concurrent.locks包下常用的类

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接口主要有获取锁的方法和释放锁的方法

获取锁的方法:

lock() : 获取锁,如果锁已经被其他线程获取,则等待(这是我们使用最多的一个方法)

tryLock() :  尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待

tryLock(long time, TimeUnit unit) :这个方法会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

lockInterruptibly() :获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就是说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程,lockInterruptibly()的声明中抛出了异常

拓展:线程的interrupt方法的作用:对于正在运行的线程,设置中断标志位true,在调用interrupted()或者isInterrupt()方法来获取中断中断状态做出选择,对于阻塞的线程(wait,sleep,join)则会清除中断状态并且抛出异常,即单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。

释放锁的方法:

unlock()释放锁

使用lock锁的演示,把锁对象定义成成员属性:

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意这个地方,成员属性
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
     
    public void insert(Thread thread) {
        lock.lock();//如果使用tryLock(),则把下面代码放在if(tryLock())判断里面
        try {
            System.out.println(thread.getName()+"得到了锁");
            for(int i=0;i<5;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"释放了锁");
            lock.unlock();
        }
    }
}

使用中断锁lockInterruptibly()的演示:线程2在执行lock.lockInterruptibly()方法时因为没有获取到锁

public class PersonTest {
    private Lock lock = new ReentrantLock();   
    public static void main(String[] args)  {
        PersonTest test = new PersonTest();
        MyThread thread1 = new MyThread(test);
        MyThread thread2 = new MyThread(test);
        thread1.start();
        thread2.start();
         
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.interrupt();
    }  
     
    public void insert(Thread thread) throws InterruptedException{
        lock.lockInterruptibly();   //注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出
        try {  
            System.out.println(thread.getName()+"得到了锁");   
            Thread.sleep(1000*4);
        }
        finally {
            System.out.println(Thread.currentThread().getName()+"执行finally");
            lock.unlock();
            System.out.println(thread.getName()+"释放了锁");
        }  
    }
}
 
class MyThread extends Thread {
    private PersonTest test = null;
    public MyThread(PersonTest test) {
        this.test = test;
    }
    @Override
    public void run() {
         
        try {
            test.insert(Thread.currentThread());
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName()+"被中断");
        }
    }
}

执行结果:

Thread-0得到了锁
Thread-1被中断
Thread-0执行finally
Thread-0释放了锁

ReentrantLock可重入锁(实现Lock接口)

可重入锁:如果锁具备可重入性,则称作为可重入锁。

可重入性:如果一个线程试图获取一个已经由它自己持有的锁,那么这个请求就会成功。重入意味着获取锁的操作粒度是线程而不是调用,实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。

重入的实现方法:为每个锁关联一个获取计数值和一个所有者线程,当计数值为0时候,这个锁认为是没有任何线程持有,当线程请求一个未被持有的锁时,JVM将记下锁的持有者并且将获取计数值置为1,如果同一个线程再次获取这个锁,计数值将递增,当线程退出同步代码块的时候。计数器相应地递减,当计数器值为0时候,这个锁将被释放

class MyClass {
    public synchronized void method1() {
        method2();
    }
     
    public synchronized void method2() {
         
    }
}

上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。由于synchronized和Lock都具备可重入性,所以不会发生上述现象。

ReentrantReadWriteLock 读写锁(实现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();//写锁
}

ReentrantReadWriteLock实现类

ReentrantReadWriteLock里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁。

public class Test {
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
     
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
    }  
     
    public void get(Thread thread) {
        rwl.readLock().lock();
        try {
            long start = System.currentTimeMillis();
             
            while(System.currentTimeMillis() - start <= 1) {
                System.out.println(thread.getName()+"正在进行读操作");
            }
            System.out.println(thread.getName()+"读操作完毕");
        } finally {
            rwl.readLock().unlock();
        }
    }
}

执行的时候,线程0和线程1交替执行读操作直至操作完毕,这样就大大的提升了效率

如果有一个线程已经占用了读锁,其他线程如果要申请读锁,也可以申请读锁,多个线程可以同时执行读操作

如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。

如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

可中断锁

可中断锁:顾名思义,就是可以相应中断的锁。

在Java中,synchronized就不是可中断锁,而Lock是可中断锁。

如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。在前面演示lockInterruptibly()的用法时已经体现了Lock的可中断性。

公平锁

公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。

  非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。

  在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。

  而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁

查看ReentrantLock的源码:

在ReentrantLock中定义了2个静态内部类,一个是NotFairSync,一个是FairSync,分别用来实现非公平锁和公平锁。我们可以在构造对象的时候传入boolean fair,即:

ReentrantLock lock = new ReentrantLock(true);//true表示公平锁,false非公平锁

默认的构造方法是非公平锁

    /**
     * Sync object for non-fair locks
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

    /**
     * Sync object for fair locks
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

ReentrantLock其他常用的方法:

  isFair()        //判断锁是否是公平锁

  isLocked()    //判断锁是否被任何线程获取了

  isHeldByCurrentThread()   //判断锁是否被当前线程获取了

  hasQueuedThreads()   //判断是否有线程在等待该锁

  在ReentrantReadWriteLock中也有类似的方法,同样也可以设置为公平锁和非公平锁。不过要记住,ReentrantReadWriteLock并未实现Lock接口,它实现的是ReadWriteLock接口。

Lock和synchronized的区别:

总结来说,Lock和synchronized有以下几点不同:

1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

5)Lock可以提高多个线程进行读操作的效率。

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

通过Condition实现等待通知机制

在synchronized中我们有wait()和notify(),notifyAll()来实现等待通知机制,在lock实现锁的机制中我们如何实现等待通知机制呢?

我们通过Condition对象来实现等待和通知机制,我们通过Lock接口中的newCondition()方法来创建Condition对象

在Condition对象中通过:await()方法就是等待,signal()/signalAll()方法就是通知,condition可以有多个。

public interface Condition {
    
    void await() throws InterruptedException; //等待方法
    
    void awaitUninterruptibly();
  
    long awaitNanos(long nanosTimeout) throws InterruptedException;
 
    boolean await(long time, TimeUnit unit) throws InterruptedException;

    boolean awaitUntil(Date deadline) throws InterruptedException;
  
    void signal(); //唤醒该lock所有等待

    void signalAll();//唤醒程序中所有lock的等待
}

注意:这里的signal()和signalAll()方法和synchronized的notify()/notifyAll()方法有所不同,signal()通知这个lock的所有等待,而signalAll()通知程序中所有的lock的所有等待

public class PersonTest {
	public static void main(String[] args) throws InterruptedException {
		myService myService=new myService();
	    ThreadA a=new ThreadA(myService);
	    a.start();
	    Thread.sleep(3000);
	    myService.signalService();
	}
}

class myService {
private Lock lock=new ReentrantLock();
public Condition condition=lock.newCondition();
public void awaitService(){
	try{
		lock.lock();
		System.out.println("await等待  ");
		condition.await();
		System.out.println("finish await ");
	}catch(InterruptedException e){
		e.printStackTrace();
	}finally{
		lock.unlock();
	}
}
public void signalService(){
	try{
		lock.lock();
		System.out.println("signal 唤醒await");
		condition.signal();
	}finally{
		lock.unlock();
	}
}
}

 class ThreadA extends Thread{
private myService myService;
public ThreadA(myService myService) {
    super();
    this.myService = myService;
}
@Override
public void run() {
    myService.awaitService();
}
} 

执行结果:

await等待  
signal 唤醒await
finish await 

 实现生产-消费模式:

public class PersonTest {
	public static void main(String[] args) throws InterruptedException {
	       MyService myService=new MyService();
	        ThreadA a=new ThreadA(myService);
	        a.start();
	        ThreadB b=new ThreadB(myService);
	        b.start();
	}
}

class MyService {
	private Lock lock=new ReentrantLock();
	private Condition condition=lock.newCondition();
	private boolean hasValue=true;
	public void set(){
		try{
			lock.lock();
			while(hasValue){
				condition.await();
			}
			System.out.println("打印#");
			hasValue=true;
			condition.signal();
		}catch(InterruptedException e){
			e.printStackTrace();
		}finally{
			lock.unlock();
		}
	}
	public void get(){
		try{
			lock.lock();
			while(!hasValue){
				condition.await();
			}
			System.out.println("打印&");
			hasValue=false;
			condition.signal();
		}catch(InterruptedException e){
			e.printStackTrace();
		}finally{
			lock.unlock();
		}
	}
}
 
 class ThreadA extends Thread{
    private MyService myService;
    public ThreadA(MyService myService) {
        super();
        this.myService = myService;
    }
    @Override
    public void run() {
        for(int i=0;i<3;i++){
            myService.set();
        }
    }
}
class ThreadB extends Thread{
    private MyService myService;
    public ThreadB(MyService myService) {
        super();
        this.myService = myService;
    }
    @Override
    public void run() {
        for(int i=0;i<3;i++){
            myService.get();
        }
    }
}

运行结果:

打印&
打印#
打印&
打印#
打印&
打印#

猜你喜欢

转载自blog.csdn.net/weixin_37598682/article/details/81154490