java-锁-Lock

为什么要Lock:synchronized的缺陷

(1)假设两个线程竞争同一个锁,如果获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,这将影响程序执行效率。

(2)因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。

(3)再举个例子:

当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。

但是采用synchronized关键字来实现同步的话,就会导致一个问题:如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。

因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,同样通过Lock就可以办到。

另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。

(4)Lock与synchronized的区别:

  • synchronized是java语言内置的,Lock是一个类,通过这个类可以实现同步访问。
  • synchronized不用手动释放锁,Lock需要手动释放锁。
  • Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
  • 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
  • Lock可以提高多个线程进行读操作的效率。

(5)synchronized:

  • 优点:实现简单,语义清晰,便于JVM堆栈跟踪;加锁解锁过程由JVM自动控制,提供了多种优化方案。
  • 缺点:不能进行高级功能(定时,轮询和可中断等)。

(6)Lock:

  • 优点:可定时的、可轮询的与可中断的锁获取操作,提供了读写锁、公平锁和非公平锁
  • 缺点:需手动释放锁unlock,不适合JVM进行堆栈跟踪。

(7)使用哪个:

在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的,可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是应该优先使用Synchronized。

接口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()

使用最多,获取锁,如果锁已被其他线程获取,则进行等待。

假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法不会中断线程B的等待过程。

Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){
     
}finally{
    lock.unlock();   //释放锁
}

lockInterruptibly()

当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。

public void method() throws InterruptedException {
    Lock lock = ...;
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}

注意:当一个线程获取了锁之后,是不会被interrupt()方法中断的。单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。

因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到进行等待的情况下,是可以响应中断的。

而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

tryLock()

尝试获取锁,如果获取成功,则返回true,如果获取失败,则返回false。

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){
         
     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则直接做其他事情
}

tryLock(long time, TimeUnit unit)

拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

实现类ReentrantLock

ReentrantLock 是“可重入锁”,ReentrantLock 是唯一实现了Lock接口的类,并且 ReentrantLock 提供了更多的方法。

public class ReentrantLock implements Lock, java.io.Serializable {}

使用lock()

public class Main {
    public static void main(String[] args) {
        MyConcurrentList myConcurrentList = new MyConcurrentList();
        Thread t1 = new Thread(() -> myConcurrentList.insert("Thread-1"));
        t1.start();
        Thread t2 = new Thread(() -> myConcurrentList.insert("Thread-2"));
        t2.start();
    }
}

class MyConcurrentList {
    private Lock lock = new ReentrantLock();
    private ArrayList<String> arrayList = new ArrayList<>();

    public void insert(String name) {
        lock.lock();
        try {
            System.out.println(name + " get lock");
            arrayList.add(name);
        } catch (Exception e) {
            // TODO: handle exception
        } finally {
            System.out.println(name + " release lock");
            lock.unlock();
        }
    }
}

输出:

Thread-1 get lock
Thread-1 release lock
Thread-2 get lock
Thread-2 release lock

使用tryLock()

public class Main {
    public static void main(String[] args) {
        MyConcurrentList myConcurrentList = new MyConcurrentList();
        Thread t1 = new Thread(() -> myConcurrentList.insert("Thread-1"));
        t1.start();
        Thread t2 = new Thread(() -> myConcurrentList.insert("Thread-2"));
        t2.start();
    }
}

class MyConcurrentList {
    private Lock lock = new ReentrantLock();
    private ArrayList<String> arrayList = new ArrayList<>();

    public void insert(String name) {
        if (lock.tryLock()) {
            try {
                System.out.println(name + " get lock success");
                arrayList.add(name);
            } catch (Exception e) {
                // TODO: handle exception
            } finally {
                System.out.println(name + " release lock");
                lock.unlock();
            }
        } else {
            System.out.println(name + " get lock fail");
        }
    }
}

结果1:两个线程依次获得锁

Thread-1 get lock success
Thread-1 release lock
Thread-2 get lock success
Thread-2 release lock

结果2:Thread-2获取锁失败后,Thread-1才准备释放锁

Thread-1 get lock success
Thread-2 get lock fail
Thread-1 release lock

结果3:Thread-1将要释放锁了,Thread-2已经获取锁失败

Thread-1 get lock success
Thread-1 release lock
Thread-2 get lock fail

使用lockInterruptibly()

public class Main {
    public static void main(String[] args) {
        MyConcurrentList myConcurrentList = new MyConcurrentList();
        Thread t1 = new MyThread(myConcurrentList);
        t1.start();
        Thread t2 = new MyThread(myConcurrentList);
        t2.start();

        //暂停一段时间,确保t2在等待锁
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //中断t2,让它抛出InterruptedException异常
        t2.interrupt();
    }
}

class MyThread extends Thread {
    private MyConcurrentList myConcurrentList = null;

    public MyThread(MyConcurrentList myConcurrentList) {
        this.myConcurrentList = myConcurrentList;
    }

    @Override
    public void run() {
        //调用并发安全的方法
        try {
            myConcurrentList.insert(this.getName());
        } catch (InterruptedException e) {
            System.out.println(this.getName() + " 被中断");
        }
    }
}

/*并发安全的List*/
class MyConcurrentList {
    private Lock lock = new ReentrantLock();
    private ArrayList<String> arrayList = new ArrayList<>();

    public void insert(String name) throws InterruptedException {
        //注意,如果需要正确中断等待锁的线程,必须将获取锁放在try-catch外面,然后将InterruptedException抛出
        //如果当前线程在等待锁时,当前线程被中断,则会抛出InterruptedException异常
        lock.lockInterruptibly();

        try {
            System.out.println(name + " 得到锁");

            //模拟耗时操作2s
            long startTime = System.currentTimeMillis();
            while (true) {
                if (System.currentTimeMillis() - startTime >= 2000) {
                    break;
                }
            }
        } finally {
            System.out.println(name + " 执行finally");
            lock.unlock();
            System.out.println(name + " 释放了锁");
        }
    }
}

输出:

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

接口ReadWriteLock

ReadWriteLock也是一个接口,在它里面只定义了两个方法:

public interface ReadWriteLock {
    // Returns the lock used for reading.
    Lock readLock();

    // Returns the lock used for writing.
    Lock writeLock();
}

实现类ReentrantReadWriteLock

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {}

ReentrantReadWriteLock里面提供了很多丰富的方法,不过最主要的有两个方法:

  • readLock() 用来获取读锁
  • writeLock() 用来获取写锁

使用readLock()

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Main {
    public static void main(String[] args) {
        ConcurrentFileUtil concurrentFileUtil = new ConcurrentFileUtil();
        Thread t1 = new Thread(() -> concurrentFileUtil.read("Thread-0"));
        t1.start();
        Thread t2 = new Thread(() -> concurrentFileUtil.read("Thread-1"));
        t2.start();
    }
}

class ConcurrentFileUtil {
    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public void read(String name) {
        //先获取锁
        readWriteLock.readLock().lock();
        //拿到锁后
        try {
            System.out.println(name + " 开始读");
            Thread.sleep(1000);//模拟耗时操作
            System.out.println(name + " 结束读");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
            System.out.println(name + " 释放了锁");
        }
    }
}

结果:

Thread-0 开始读
Thread-1 开始读
Thread-0 结束读
Thread-0 释放了锁
Thread-1 结束读
Thread-1 释放了锁

使用writeLock()

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Main {
    public static void main(String[] args) {
        ConcurrentFileUtil concurrentFileUtil = new ConcurrentFileUtil();
        Thread t1 = new Thread(() -> concurrentFileUtil.write("Thread-0"));
        t1.start();
        Thread t2 = new Thread(() -> concurrentFileUtil.write("Thread-1"));
        t2.start();
    }
}

class ConcurrentFileUtil {
    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public void write(String name) {
        //先获取锁
        readWriteLock.writeLock().lock();
        //拿到锁后
        try {
            System.out.println(name + " 开始写");
            Thread.sleep(1000);//模拟耗时操作
            System.out.println(name + " 结束写");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
            System.out.println(name + " 释放了锁");
        }
    }
}

结果:

Thread-0 开始写
Thread-0 结束写
Thread-0 释放了锁
Thread-1 开始写
Thread-1 结束写
Thread-1 释放了锁

Condition

使用ReentrantLock比直接使用synchronized更安全,可以替代synchronized进行线程同步。

但是,synchronized可以配合waitnotify实现线程在条件不满足时等待,条件满足时唤醒,用ReentrantLock我们怎么编写waitnotify的功能呢?

答案是使用Condition对象来实现waitnotify的功能。

举例:任务队列(synchronized + await + notify)

class TaskQueue {
    Queue<String> queue = new LinkedList<>();

    public synchronized void addTask(String s) {
        this.queue.add(s);
        this.notifyAll();
    }

    public synchronized String getTask() throws InterruptedException {
        while (queue.isEmpty()) {
            this.wait();
        }
        return queue.remove();
    }
}

改写(Lock + Condition)

class TaskQueue {
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private Queue<String> queue = new LinkedList<>();

    public void addTask(String s) {
        lock.lock();
        try {
            queue.add(s);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public String getTask() {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                condition.await();
            }
            return queue.remove();
        } finally {
            lock.unlock();
        }
    }
}

可见,使用Condition时,引用的Condition对象必须从Lock实例的newCondition()返回,这样才能获得一个绑定了Lock实例的Condition实例。

Condition提供的await()signal()signalAll()原理和synchronized锁对象的wait()notify()notifyAll()是一致的,并且其行为也是一样的:

  • await()会释放当前锁,进入等待状态;
  • signal()会唤醒某个等待线程;
  • signalAll()会唤醒所有等待线程;
  • 唤醒线程从await()返回后需要重新获得锁。

此外,和tryLock()类似,await()可以在等待指定时间后,如果还没有被其他线程通过signal()signalAll()唤醒,可以自己醒来:

if (condition.await(1, TimeUnit.SECOND)) {
    // 被其他线程唤醒
} else {
    // 指定时间内没有被其他线程唤醒
}

乐观锁StampedLock

悲观读锁:读的过程不允许写。

乐观读锁:读的过程本身不加锁,所以允许写。这样一来,我们读的数据有可能不一致,所以需要额外的代码判断读的过程中是否有写入。

乐观锁的意思就是乐观地估计读的过程中大概率不会有写入。

要进一步提升并发执行效率,Java 8引入了新的读写锁:StampedLock

三种模式:

  • Writing:独占写锁
  • Reading:悲观读锁
  • Optimistic Reading:乐观读。仅当当前未处于 Writing 模式 tryOptimisticRead 才会返回非 0 的邮戳(Stamp),如果在获取乐观读之后没有出现写模式线程获取锁,则在方法validate返回 true。

StampedLock是不可重入锁。

public class Point {
    private final StampedLock stampedLock = new StampedLock();

    private double x;
    private double y;

    public void move(double deltaX, double deltaY) {
        long stamp = stampedLock.writeLock(); // 获取写锁
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            stampedLock.unlockWrite(stamp); // 释放写锁
        }
    }

    public double distanceFromOrigin() {
        long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁
        // 注意下面两行代码不是原子操作
        // 假设x,y = (100,200)
        double currentX = x;
        // 此处已读取到x=100,但x,y可能被写线程修改为(300,400)
        double currentY = y;
        // 此处已读取到y,如果没有写入,读取是正确的(100,200)
        // 如果有写入,读取是错误的(100,400)
        if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生
            stamp = stampedLock.readLock(); // 获取一个悲观读锁
            try {
                currentX = x;
                currentY = y;
            } finally {
                stampedLock.unlockRead(stamp); // 释放悲观读锁
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}

详解乐观读带来的性能提升:

读锁未释放前,获取写锁会阻塞,写操作就不能马上执行。而乐观读锁允许写线程获取锁,写操作能马上执行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值