JDK1.8并发之synchronized和Lock

什么是线程安全?

线程安全是指保证多线程环境下共享的、可修改的状态的正确性。

保证线程安全的两个办法:

  • 封装:将对象的内部状态隐藏、保护起来。
  • 不可变:final变量产生了某种程度地不可变(immutable)效果,可以用于保护只读数据。

线程安全需要保证几个基本特性:

  • 原子性:相关操作不会中途被其他线程干扰,一般通过同步机制实现。
  • 可见性:一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile就是负责保证可见性的。
  • 有序性:保证线程内串行语义,避免指令重排。

synchronized

关键字synchronized可以用来修饰方法和代码块,修饰方法也就相当于将方法放在代码块中。synchronized修饰实例方法时(对象锁),相当于synchronized (this) {}。修饰静态方法时(类锁,也叫静态锁),相当于synchronized (ClassName.class) {}

对象有锁计数器,一个任务可以多次获得对象的锁,每获取一次,计数加1,每释放一次,计数减1,当计数为0时,其他任务才能获取该对象的锁(可重入性)。

Lock

java.util.concurrent.locks.Lock

被synchronized修饰的代码块,如果有一个线程获得了对应的锁,其他线程只能一直等待,直到锁被释放。原线程释放锁的情况有:

  • 执行完synchronized修饰的代码块,然后释放锁。
  • 线程执行发生异常,JVM会让线程自动释放锁。

其他时候,如果代码块执行时间很长,或者等待IO、sleep等发生阻塞,一直没有释放锁,其他线程只能一直等待。

本文学习完毕之后,你将能够理解Lock的优势包括:

  • 可以让尝试获得锁的其他线程只等待一定时间。
  • 读操作和写操作、写操作和写操作会发生冲突,但是读操作和读操作不发生冲突,Lock就可以允许这种可以同时进行的操作。(读写锁)
  • Lock可以知道线程有没有成功获取到锁。
  • 当竞争资源非常激烈时,Lock的性能会更好。
  • sychronized是不可中断锁,Lock可中断。(可中断锁)
  • Lock可设置为公平锁,按照请求锁的顺序分配锁。(公平锁)
  • 可以和条件变量配合使用,对共享数据的多种状态进行监控。

但是

  • Lock对象必须被显式地创建、锁定和释放。
    private Lock lock = new ReentrantLock();
    public void lockedMethod() {
        lock.lock();;
        try{
            // do something
            return; // return必须在try子句中,确保unlock不会过早发生
        } finally {
            lock.unlock();
        }
    }

Lock接口

package java.util.concurrent.locks;
public interface Lock {
    void lock();

    // 尝试获取锁,如果失败,等待的过程中可以响应中断(threadWait.interrupt())
    void lockInterruptibly() throws InterruptedException;

    // 尝试获取锁,如果获取成功,就马上返回true,否则(锁已经被其他线程获取)马上返回false
    boolean tryLock();

    // 尝试获取锁,如果获取失败,会等待unit时间,等待期间还拿不到锁就马上返回false
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    void unlock();
    Condition newCondition();
}

ReentrantLock,重入锁,Lock接口的实现类,提供了更多的方法。

  • isFair(),判断锁是否是公平锁
  • isLocked(),判断锁是否被任何线程获取了
  • isHeldByCurrentThread(),判断锁是否被当前线程获取了
  • hasQueuedThreads(),判断是否有线程在等待该锁

ReadWriteLock接口

public interface ReadWriteLock {
    // 获取读锁
    Lock readLock();

    // 获取写锁
    Lock writeLock();
}

ReentrantReadWriteLock,重入读写锁,实现了ReadWriteLock接口。

  • 如果一个线程已经占用了读锁,其他线程可以马上获得读锁,但需要等待才能获取写锁。
  • 如果一个线程已经占用了写锁,其他线程要获取读锁或写锁都需要等待。
private ReadWriteLock rwl = new ReentrantReadWriteLock();

public static void main(String[] args) {
    final Main main = new Main();

    new Thread(() -> main.testRWL(Thread.currentThread())).start();
    new Thread(() -> main.testRWL(Thread.currentThread())).start();

    // 输出的结果是两个thread交替输出“正在读”
}

public void testRWL(Thread thread) {
    rwl.readLock().lock();
    try {
        long finish = System.currentTimeMillis() + 1;

        while (System.currentTimeMillis() <= finish) {
            System.out.println(thread.getName() + "正在读");
        }

        System.out.println(thread.getName() + "读结束");
    } finally {
        rwl.readLock().unlock();
    }
}

Lock和条件变量

Condition将Object类的waitnotifynotifyAll等操作转化为相应的条件对象操作awaitsignalsignalAll,将复杂而晦涩的同步操作转变为直观可控的对象行为。看JDK源码注释中的一个例子:

class BoundedBuffer {
    final Lock lock = new ReentrantLock();
    final Condition notFull = lock.newCondition(); // 非满条件对象
    final Condition notEmpty = lock.newCondition(); // 非空条件对象

    final Object[] items = new Object[100];
    int putptr, takeptr, count;

    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            // 当数组已满,等待非满条件
            while (count == items.length)
                notFull.await();
            items[putptr] = x;
            if (++putptr == items.length) putptr = 0;
            ++count;
            // 添加元素,就发送一个非空条件信号
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public Object take() throws InterruptedException {
        lock.lock();
        try {
            // 当数组已空,等待非空条件
            while (count == 0)
                notEmpty.await();
            Object x = items[takeptr];
            if (++takeptr == items.length) takeptr = 0;
            --count;
            // 添加元素,就发送一个非满条件信号
            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }
}

对象o的wait方法是让执行线程放弃o的锁并阻塞,而Condition对象的await方法放弃的是Lock锁,因此,一个Lock锁可以同时调整多个条件对象的状态,而synchronized锁只能调整锁隶属的对象的状态。

锁的名词解释

重入锁

重入锁的分配机制是:基于线程的分配,而不是基于方法调用的分配。同一个线程获得锁之后,在其他方法中,不需要再次申请就可以获取锁。synchronized是重入锁,它锁定的是实例的this对象或者静态类的类对象,当锁方法A中又调用同类中的锁方法B时,锁计数增加1,而不用重新申请锁。加锁的ReentrantLock是同一个对象时,Lock锁也满足重入性。

可中断锁

线程A如果在等待sychronized的锁,那么其他线程中用A.interrupt()来中断线程A时,A是不会响应的。这种就是不可中断锁。ReentrantLock使用lockInterrupt方法加锁时,此线程是可中断锁。

公平锁

如果同时有多个线程在等待一个锁时,当锁被释放时,等待时间最长的线程(FIFO)会获得该锁。满足这个条件的就是公平锁。非公平锁无法保证按请求顺序来分配锁。synchronized是非公平锁。ReentrantLock和ReentrantReadWriteLock默认是非公平锁,但是在构造时可以设置为公平锁。

public ReentrantLock() { // 默认非公平锁
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) { // true表示公平锁
    sync = fair ? new FairSync() : new NonfairSync();
}

读写锁

使得多个线程之间的读操作不会发生冲突。比如ReadWriteLock接口就是读写锁。

参考资料

  1. Java并发编程:Lock


上一篇:Jdk1.8集合框架之LinkedHashMap源码解析
下一篇:JDK1.8并发之生产者消费者问题

猜你喜欢

转载自blog.csdn.net/weixin_40255793/article/details/80786249
今日推荐