Java多线程的锁机制和无锁并行

1.锁机制

1.1  内部锁

Java 平台中的任何一个对象都有着唯一一个与之相关联的锁,这种锁被称为监视器或内部锁,内部锁是一种非公平的排它锁,它能够保障原子性、可见性和有序性。内部锁通过 synchronized 关键字来实现,可以用于修饰方法以及代码块, 被修饰的方法称为同步方法,被修饰的代码块称为同步代码块。示例如下:

线程不安全的示例:

public class J1_ThreadUnsafe {

    private static int i = 0;

    public static void main(String[] args) throws InterruptedException {
        IncreaseTask task = new IncreaseTask();
        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);
        thread1.start();
        thread2.start();
        // 等待线程结束再打印返回值
        thread1.join();
        thread2.join();
        System.out.println(i);
    }

    static class IncreaseTask implements Runnable {
        @Override
        public void run() {
            for (int j = 0; j < 100000; j++) {
                inc();
            }
        }

        private void inc() {
            i++;
        }
    }
}

使用 synchronized 修饰 inr() 方法来保证线程安全:

public class J2_SynchronizedSafe {

    private static int i = 0;

    public static void main(String[] args) throws InterruptedException {
        // 两个线程调用的是同一个IncreaseTask实例,此时是线程安全的
        IncreaseTask task = new IncreaseTask();
        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);
        thread1.start();
        thread2.start();
        //等待结束后 才打印返回值
        thread1.join();
        thread2.join();
        //并打印返回值
        System.out.println(i);
    }

    static class IncreaseTask implements Runnable {
        @Override
        public void run() {
            for (int j = 0; j < 100000; j++) {
                inc();
            }
        }

        private synchronized void inc() {
            i++;
        }
    }
}

通常我们把被修饰的方法体和代码块称为临界区,需要注意的是必须保证多线程锁住的是同一个临界区,否则依然是线程不安全的。如果将上面创建线程的方法修改为如下所示,此时 synchronized 锁住的是不同 IncreaseTask 对象的  inc() 方法,所以仍然是线程不安全的:

Thread thread1 = new Thread(new IncreaseTask());
Thread thread2 = new Thread(new IncreaseTask());

synchronized 除了可以修饰方法外,还可以用于修饰代码块,此时可以使用 this 关键字作为句柄,但仍然需要保证两个线程调用的是同一个 IncreaseTask 实例,示例如下:

public class J3_SynchronizedSafe {

    private static int i = 0;

    static class IncreaseTask implements Runnable {
        @Override
        public void run() {
            for (int j = 0; j < 100000; j++) {
                // 锁住的是同一个对象,此时也是线程安全的
                synchronized (this) {
                    i++;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        IncreaseTask task = new IncreaseTask();
        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(i);
    }
}

如果想要调用不同的 IncreaseTask() 实例,又想保证线程安全,此时可以使用同一个对象作为 synchronized 关键字的句柄。为避免竞态,作为句柄的对象通常使用 private final 关键字进行修饰,示例如下:

public class J4_SynchronizedSafe {

    private static final String s = "";

    private static int i = 0;

    static class IncreaseTask implements Runnable {
        @Override
        public void run() {
            for (int j = 0; j < 100000; j++) {
                // 虽然调用的是不同的 IncreaseTask() 实例,但锁住的仍然是同一个对象,此时也是线程安全的
                synchronized (s) {
                    i++;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new IncreaseTask());
        Thread thread2 = new Thread(new IncreaseTask());
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(i);
    }
}

1.2  显示锁

显示锁是 java.util.concurrent.locks.Lock 接口的实例,该接口对显示锁进行了抽象,定义了如下方法:

lock():获取锁;
lockInterruptibly():如果当前线程未被中断,则获取锁;
tryLock():仅在调用时锁为空闲状态才获取该锁;
tryLock(long time, TimeUnit unit):如果锁在给定的等待时间内存在空闲,并且当前线程未被中断,则获取锁;
unlock():释放锁;
newCondition():返回绑定到此 Lock 实例的新的 Condition 实例。

java.util.concurrent.locks.ReentrantLock 类是 Lock 接口的默认实现,它是一种可重入锁,示例如下:

/** 
 * 利用ReentrantLock实现线程安全
 */
public class J1_ThreadSafe {

    private static ReentrantLock reentrantLock = new ReentrantLock();
    private static Integer i = 0;

    static class IncreaseTask implements Runnable {
        @Override
        public void run() {
            for (int j = 0; j < 100000; j++) {
                try {
                    reentrantLock.lock();
                    i++;
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    reentrantLock.unlock();
                }
            }
        }

        public static void main(String[] args) throws InterruptedException {
            Thread thread1 = new Thread(new IncreaseTask());
            Thread thread2 = new Thread(new IncreaseTask());
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();
            System.out.println(i);
        }
    }
}

ReentrantLock 是一种可重入的锁, 它能够对共享资源进行重复加锁,即持有该锁的线程再次获取该锁时不会被阻塞,但解锁次数与加锁次数必须要保持一致,此时才能完全解锁:

try {
    reentrantLock.lock();
    reentrantLock.lock();
    reentrantLock.lock();
    i++;
} catch (Exception e) {
    e.printStackTrace();
} finally {
    reentrantLock.unlock();
    reentrantLock.unlock();
    reentrantLock.unlock();
}

ReentrantLock 即支持公平锁也支持非公平锁,公平锁在调度时候往往需要频繁切换上下文来保证在等待时间上的公平性,所以默认的 ReentrantLock 锁是非公平的,如果想要使用公平锁,可以在创建时进行指定:

// 参数为true,代表使用公平锁
private static ReentrantLock fairLock = new ReentrantLock(true);

显示锁相比于内部锁提供了更高的灵活性,但容易存在锁泄露(某个线程持有锁后因为异常而导致锁无法被释放)等问题。而内部锁虽然灵活性不足,但不会存在锁泄露,并且虚拟机也会在编译时对内部锁进行适当的锁优化。

1.3  读写锁

由于锁的排它性,导致多个线程无法以安全的方式并发地读取共享变量,这不利于提高系统的并发能力,因此产生了读写锁:

读锁:读锁是共享的,可以被多个线程所持有,即一个线程持有读锁时并不妨碍其他线程获得相应锁的读锁;

写锁:写锁是独占的,一个线程持有写锁时,其他线程无法获取相应锁的写锁或读锁。

基于读写锁的特性,其非常适合于读多写少的场景,示例如下:

public class ReadWriteLock {

    // 可重入锁
    private static ReentrantLock reentrantLock = new ReentrantLock();
    // 读写锁
    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    // 读锁
    private static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
    // 写锁
    private static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

    // 待赋值的变量
    private static String i = "";

    //写方法
    static class Write implements Runnable {

        private Lock lock;
        private String value;

        Write(Lock lock, String value) {
            this.lock = lock;
            this.value = value;
        }

        @Override
        public void run() {
            try {
                lock.lock();
                Thread.sleep(1000);
                i = value;
                System.out.println(Thread.currentThread().getName() + "写入值" + i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    //读方法
    static class Read implements Runnable {

        private Lock lock;

        Read(Lock lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            try {
                lock.lock();
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName() + "读取到值" + i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {

        // 写锁是排它的,但读锁是共享的,耗时3秒左右
        for (int j = 0; j < 2; j++) {
            Thread thread = new Thread(new Write(writeLock, String.valueOf(j)));
            thread.start();
        }
        for (int j = 0; j < 18; j++) {
            Thread thread = new Thread(new Read(readLock));
            thread.start();
        }


        // 使用重入锁时耗时20秒左右
        for (int j = 0; j < 2; j++) {
            Thread thread = new Thread(new Write(reentrantLock, String.valueOf(j)));
            thread.start();
        }
        for (int j = 0; j < 18; j++) {
            Thread thread = new Thread(new Read(reentrantLock));
            thread.start();
        }
    }
}

1.4  锁优化

1.4.1 锁消除

锁消除(Lock Elision)是 JIT 编译器对内部锁所做的一种优化。在编译时,JIT 编译器会通过逃逸分析(Escape Analysis)来判断同步块所使用的锁对象是否只能被一个线程访问而没有逃逸到其他线程,如果是,则编译器在编译这个同步块时就不生成 synchronized 锁所对应的机器码,这种编译器优化就被称为锁消除。示例如下:

public class LockElision {

    private String toJson(Employee employee) {
        StringBuffer buffer = new StringBuffer();
        buffer.append("name:").append(employee.getName());
        buffer.append("age:").append(employee.getAge());
        buffer.append("birthday:").append(employee.getBirthday());
        return buffer.toString();
    }
}

此时的 StringBuffer 实例对象只是一个局部变量,并且该对象并没有被发布到其他线程,因此其对应的内部锁会被消除。

1.4.2 锁粗化

对于相邻的几个同步块,如果这些同步块锁使用的是同一个锁的实例,那么 JIT 编译器就会将这些同步块合并为一个大同步块,从而避免一个线程反复申请、释放同一个锁而导致的开销。

1.4.3 偏向锁

Java 虚拟机在实现 monitorenter 字节码(申请锁)和 monitorexit 字节码(释放锁)时需要借助一个原子操作(CAS操作),这个操作是比较昂贵的,因此内部锁在每次被线程获取时,它都会将对应的线程记录为偏好线程(Biased Thread),之后此线程无论是再次申请该锁还是获取该锁,都无须借助原先昂贵的原子操作,从而减少了锁的申请与释放的开销。

因为锁在每次都获取时,都需要刷新偏好线程的值,这个过程也是需要额外开销的,所以偏向锁只适用于系统中大部分锁争用较少的情况,如果系统中大部分锁的竞争都比较激烈,此时可以考虑在 Java 程序的启动命令行中增加 -XX:-UseBiasedLocking 参数来关闭偏向锁。

1.4.4 适应性锁

如果一个线程在申请锁时,该锁恰好被其他线程所持有,那么该线程就需要等待持有锁的线程释放该锁,此时常用的方案有两种:

暂停该线程进行等待,但暂停操作会导致上下文切换,这会导致额外的开销;

持续执行空循环,进行忙等(Busy Wait)。这不会导致上下文的切换,但是会持续消耗处理器的资源。

暂停策略适用于等待时间较长的场景,此时可以抵消上下文切换带来的开销;忙等策略适用于等待时间较短的场景,此时可以避免持续消耗处理器的资源。Java 虚拟机会根据运行过程中收集到的信息来判断这个锁被线程持有的时间,从而选取最优的策略来进行等待,这就是适应性锁。

2.无锁并行

2.1  比较交换策略-CAS

锁是一种悲观的策略,它假设每一次临界区内的访问都会存在冲突,因此它在同一时刻只允许一个线程进入临界区,而无锁则是一种乐观的策略,它假设临界区内资源的访问很少存在冲突,并采用 CAS 方案来避免这种偶发冲突而导致的线程不安全。CAS 的全称是 Compare And Swap (比较交换),其核心思想如下:

boolean compareAndSwap (Variable V, Object A, Object B){
    if(A == V.get()){  // 检查变量值是否被其他线程修改过
        V.set(B); // 更新变量值
        return true; // 更新成功
    }
    return false; //变量值已被其他线程修改,更新失败
}

如果变量 V 的当前值和调用 CAS 时所提供的变量值 A (即变量的旧值) 一致,那么就说明其他线程并没有修改过变量 V 的值,此时就可以进行更新操作,否则操作失败,整个 CAS 操作的原子性由处理器来进行保证。由于 CAS 操作并不需要频繁的线程调度,因此其通常有着更好的性能表现,为了充分利用 CAS 的特性,JDK 提供了原子包来满足各种场景下的使用需求:

分组

基础数据型

AtomicBoolean、AtomicInteger、AtomicLong

引用型

AtomicReference、AtomicStampedReference、AtomicMarkableReference

数组型

AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

字段更新型

AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater

2.2  引用型                           

除了使用原子包中提供的 AtomicBoolean、AtomicInteger、AtomicLong 来保证基本数据类型操作的原子性外,还可以使用 AtomicReference\<V> 来保证任意类型操作原子性,示例如下:

public class J1_SimpleType {

    private static int i = 0;
    private static AtomicInteger j = new AtomicInteger(0);
    /*使用AtomicReference对普通对象进行封装*/
    private static AtomicReference<Integer> k = new AtomicReference<>(0);

    static class Task implements Runnable {

        private CountDownLatch latch;

        Task(CountDownLatch latch) {
            this.latch = latch;
        }

        @Override
        public void run() {
            i++;
            j.incrementAndGet();
            k.getAndUpdate(-> x + 1);
            latch.countDown();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        int number = 10000;
        CountDownLatch latch = new CountDownLatch(number);
        Semaphore semaphore = new Semaphore(10);
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        Task task = new Task(latch);
        for (int i = 0; i < number; i++) {
            semaphore.acquire();
            executorService.execute(task);
            semaphore.release();
        }
        latch.await();
        System.out.println("输出i的值" + i);
        System.out.println("输出j的值" + j.get());
        System.out.println("输出K的值" + k.get());
        executorService.shutdown();
    }
}

在使用 CAS 的过程中,一个比较常见的隐患是 A-B-A 问题:如果其他线程在将共享变量的值修改为 B 后,又立即修改回原值,此时这次变更对于其他线程而言可能无法感知到。这对于计数等场景而言,是没有问题的,但在一些特别的场景下,就会导致错误。想要解决这个问题,可以在比较时候除了比较变量的值外,还应进行时间戳的比较,AtomicStampedReference 就是这种比较思路的一种实现。其更新值的方法定义如下:

compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)

2.3  数组型

数组型可以保证对数据内元素的操作是线程安全的,示例如下:

public class J3_ArrayElementThreadUnsafe {

    private static int capacity = 10;
    // 保证对集合内元素的操作具有原子性
    private static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(capacity);
    // 对集合内元素的操作线程不安全
    private static Vector<Integer> vector = new Vector<>(capacity);
    // 对集合内元素的操作线程不安全
    private static ArrayList<Integer> arrayList = new ArrayList<>(capacity);

    static {
        for (int i = 0; i < capacity; i++) {
            arrayList.add(0);
            vector.add(0);
        }
    }

    static class Task implements Runnable {

        private CountDownLatch latch;

        Task(CountDownLatch latch) {
            this.latch = latch;
        }

        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                int num = i % capacity;
                atomicIntegerArray.getAndIncrement(num);
                vector.set(num, vector.get(num) + 1);
                arrayList.set(num, arrayList.get(num) + 1);
            }
            latch.countDown();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        int number = 1000;
        CountDownLatch latch = new CountDownLatch(number);
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < number; i++) {
            executorService.execute(new Task(latch));
        }
        latch.await();
        System.out.println("集合内元素的线程安全:");
        System.out.println("atomicIntegerArray size : " + atomicIntegerArray);
        System.out.println("vector size : " + vector);
        System.out.println("arrayList size : " + arrayList);
        executorService.shutdown();
    }
}

// 输出如下:
集合内元素的线程安全:
atomicIntegerArray size : [100000, 100000, 100000, 100000, 100000, 100000, 100000, 100000, 100000, 100000]
vector size : [69966, 81954, 78605, 79144, 66532, 75082, 77324, 78723, 78022, 76294]
arrayList size : [99045, 99173, 99251, 98609, 99248, 99191, 98848, 99181, 99212, 99083]

2.4  字段更新型

如果某个类的基本类型的字段在某一环境中存在线程安全,但该字段在多个环境中都有引用,此时直接修改该字段可能会导致多个环境都需要重新验证,在这种情况下可以使用字段更新型来保证其在特定环境下的线程安全:

public class J5_AtomicIntegerFieldUpdater {

    static class Task implements Runnable {

        private Candidate candidate;
        private CountDownLatch latch;
        private AtomicIntegerFieldUpdater fieldUpdater;

        Task(CountDownLatch latch, Candidate candidate, AtomicIntegerFieldUpdater fieldUpdater) {
            this.candidate = candidate;
            this.latch = latch;
            this.fieldUpdater = fieldUpdater;
        }

        @Override
        public void run() {
            fieldUpdater.incrementAndGet(candidate);
            latch.countDown();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        int number = 100000;
        CountDownLatch latch = new CountDownLatch(number);
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        Candidate candidate = new Candidate("候选人", 0);
        // 使用字段更新型来保证其线程安全
        AtomicIntegerFieldUpdater<Candidate> fieldUpdater = AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");
        for (int i = 0; i < number; i++) {
            executorService.execute(new Task(latch, candidate, fieldUpdater));
        }
        latch.await();
        System.out.println(candidate.getName() + "获得票数:" + candidate.getScore());
        executorService.shutdown();
    }

    private static class Candidate {

        private String name;

        // 1. 不能声明为private  2. 必须用volatile关键字修饰
        public volatile int score;
        .....
    }
}

需要注意的是由于 CAS 只能保证可见性,不能保证原子性,所以该变量必须使用 volatile 关键字修饰,并且由于 FieldUpdater 是采用反射机制来获取该变量的值,所以其也不能声明为 private 。另外 FieldUpdater 也不能用于 static 类型的变量。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shangjg3

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值