Java使用ReentrantLock与Synchronize解决并发问题

概述

在前面学习了在Java中使用多线程的方法,但是遇到了一个问题:
当多个线程同时访问一个资源的时候,会出现线程安全问题,与原以为的答案并不相符。
举个例子,我们开辟三个线程去模拟“抢票”,代码如下:


public class Buytickets {
    static int Nums = 10;

    public static void main(String[] args) {
        Buy buy = new Buy();
        new Thread(buy, "张三").start();
        new Thread(buy, "李四").start();
        new Thread(buy, "王二").start();
    }

    static class Buy implements Runnable {

        @Override
        public void run() {
            while (Nums > 0) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                buy();

            }
        }

        private void buy() {
            if (Nums < 1) return;
            System.out.println(Thread.currentThread().getName() +
                    "抢到了第" + Nums-- + "张票。");

        }

    }

}

我们执行之后发现一个问题——

Snipaste_2020-04-02_14-27-38.png
这是为什么呢?了解了一下,当一个线程运行的时候会先把目标资源拿到自己线程的工作内存之中,然后才对其修改,而不是直接作用在内存里。所以刚开始每一个线程都拿到工作内存的Nums实际上都是10,因此就有很大可能会出现重复的问题。
不过可以明确,像我们这里编写的抢票是很简陋的,如果没有Thread.sleep()函数的话,线程安全问题的发生概率还是很小的。

但是假设我们当前的场景是面对上千、上万流量同时进行访问,那么出现线程安全问题是必然的。所以我们使用sleep函数将线程睡眠,这个时候可以模拟出高并发的效果(三个线程同时争夺一个资源)。

那么如何解决呢? 很显然,我们的办法就是当出现争夺资源情况的时候,让它们进行“排队”。当第一个线程拿到这个资源的时候,就对这个资源“加上一把锁”,这样的话就能保证我们的线程安全。

但是很遗憾,这样我们的程序又变成了单线程,但是涉及到这样情况的时候,我们不得不为了安全性而降低性能。

Synchronize

前面概述大致解释了并发问题。接下来我们来学习如何进行加锁。
Synchronize是一个Java关键字,有两种使用方法:

1.作用在方法的头部

private synchronized void buy() {

2.作用在代码块

首先需要明确,如果想作用在代码块之中,我们的目标必须是一个对象,所以如果前面的案例想要以代码块方式,其实并不适合。
首先我们需要把原来的Nums变成一个Intger类型的变量。

        private void buy() {
            if (Nums < 1) return;
            Integer temp = Nums;
            synchronized (temp) {
                System.out.println(Thread.currentThread().getName() +
                        "抢到了第" + Nums-- + "张票。");

            }
        }

在Synchronizd()中,我们设置要“锁住”哪一个对象,然后把相关的代码放进这个函数体之中。

ReentrantLock

如果你理解了前面Synchronize的用法,那么接下来lock(单词太长了,缩短一下,代指ReentrantLock...)的使用方式就很简单了。

首先建立一个对象。

        private final ReentrantLock lock = new ReentrantLock();

如何使用呢?其实很简单,.lock()加锁,.unlock()解锁

        private void buy() {
            lock.lock();
            try {
                if (Nums < 1) return;
                System.out.println(Thread.currentThread().getName() +
                    "抢到了第" + Nums-- + "张票。");

            } finally {
                lock.unlock();
            }
    }

之所以使用finally是因为如果try中某一句代码出现问题,导致崩溃那么就会出现下面说的死锁问题(线程一直等待目标线程结束,造成线程阻塞)。

二者之间的区别

  • Synchronize 是Java内置的关键字,lock是一个类。
  • Synchronize 无法判断锁的状态,lock可以判断是否获取。
  • Synchronize 当对应的代码结束后会自动释放锁,lock需要手动释放,如果不释放将面临死锁
  • Synchronize 因为是作用于方法或代码块,如果不执行完毕是不能中断的。
  • Synchronize 性能没有lock更好。
  • lock支持公平锁和非公平锁,而Synchronize只支持非公平锁(new ReentrantLock(true)便是公平锁)。

非公平锁与公平锁的区别

简单的来说,当几个线程同时争夺一个锁的时候,获得的顺序是随机的,这是非公平锁。而通过对比先来后到的,则是公平锁。

不过阅读了网上文章对此的分析来看,非公平锁并不是意味着随机,而是可能会造成新增加的线程比之前等待的线程优先拿到锁。现在姑且按照上面的方式进行理解。