因为并行的并发任务导致资源竞争:synchronized及Lock

168 阅读4分钟

回顾上文:我们知道

  1. 通过继承Thread类可以实现任务和线程相结合。
  2. 在A线程中调用B线程的join,则A线程会被阻塞,直到B线程执行完毕后A线程才能执行,实现了某种意义上的顺序执行。
  3. 通过线程对象调用interrupt的方式,可以将该线程设置一个中断标志,程序内部可以使用isInterrupted去判断是否有这个终端标志,如果被中断了则调用isInterrupted会返回true
  4. 通过实现Thread.UncaughtExceptionHandler定义异常捕获器,如何在线程中设置异常捕获器,即可捕获在线程中抛出的异常。

本文内容

  1. 使用线程共享资源时出现的问题。
  2. 解决共享资源竞争
  3. 学习两种锁synchronizedlock

模拟一个抢票的程序

该Demo程序,定义了一个属性资源为5,它有一个方法是sub方法,就是一个递减方法。

Demo中还定义了一个静态类,作为线程,这个线程就是不断的去执行int res = demo.sub();就是去抢夺资源。

在主线程中,我们使用线程池去驱动线程,用循环开启线程,相当于开启了100个线程去抢夺资源。

public class Demo {
    private int num = 5;

    public int sub() {
        --num;
        return num;
    }

    static class Thread1 extends Thread{
        private Demo demo;

        public Thread1(Demo demo) {
            this.demo = demo;
        }

        @Override
        public void run() {
            while (true) {
                try {
                    TimeUnit.MILLISECONDS.sleep(50);
                }catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                int res = demo.sub();
                if (res >= 0) {
                    System.out.println(currentThread().getName() + "执行了操作-1 还有:" + res);
                } else {
                    return;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Demo demo = new Demo();
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 100; i++) {
            executorService.execute(new Thread1 (demo));
        }
        executorService.shutdown();
    }
}

我们观察输出结果:

pool-1-thread-47执行了操作-1 还有:0
pool-1-thread-41执行了操作-1 还有:1
pool-1-thread-44执行了操作-1 还有:3
pool-1-thread-50执行了操作-1 还有:3
pool-1-thread-48执行了操作-1 还有:2
pool-1-thread-42执行了操作-1 还有:4

发现有两个操作,44线程和50线程在操作时,操作了相同的num资源,这并不是我们想看到的,但这是线程共享资源造成的原因,我们必须要解决这个问题。

解决共享资源竞争--锁

synchronized

锁对象建议使用final进行修饰

概念

我们需要某种方式来防止两个任务访问相同的资源。该方法就是当资源被一个任务使用时,在其上加锁,使得在一段时间内只有一个任务可以运行这段代码。因为锁语句产生了一种相互排斥的效果,所以这种机制常称为互斥量

synchronized方法声明

标记在方法上或用在方法体中。

Java提供synchronized关键字,用来防止资源冲突。synchronized是一个加锁的动作,而在一个类中锁有两种:

  1. 对象都自动含有单一的锁(监视器)。
  2. 类本身也有个锁,可以对static数据进行并发的控制。

上面这两种其实意思是,你可以用synchronized去防止对static数据进行并发访问,因为可以防止对非static数据进行并发访问。

声明synchronized方法

synchronized void f()

synchronized void g()

synchronized static void f()

如果一个类对象中有两个synchronized方法,则要明白锁只有一个,如果A线程拿到了锁,操作了f(),其他线程只能等待A线程释放了锁才可以调用g()

另外锁还有一个计数机制,就是一个线程任务可以多次获得对象的锁(同一把锁),简单点说,A线程拿到了锁操作f(),它现在获得了锁,能被允许继续获得多个锁。加锁时执行+1的操作,释放锁的时候-1,当计数变为0时,完全释放锁。

注意:锁是保护资源的,所以要将资源的域设置为private,这样可以设置一个统一的public方法去操作资源,而这个public方法可以使用synchronized去保护操作资源时发生的资源竞争的问题。

什么时候需要用synchronized

  1. 涉及到资源的操作
  2. 如果这个资源(变量)接下来将被不同的线程读取
  3. 或者操作一个已经被其他线程操作过的资源(变量)

这就需要用到加锁的操作了。

上述用synchronized解决资源抢夺的问题

public synchronized int sub() {
    --num;
    return num;
}

Lock对象

更加灵活的锁

相较于对象锁、类内建锁。我们可以显式的声明一把锁,这把锁的生命周期由我们手动控制:

  1. 显式地创建
  2. 锁定
  3. 释放

改写用lock加锁

private Lock lock = new ReentrantLock(); // 声明锁
private  int num = 5;

public  int sub() {
    lock.lock(); // 加锁
    try {
        --num;
        return num;
    }finally {
        lock.unlock(); // 释放锁
    }
}

如果光是这样,还不足以有趣,我们再写个Demo,感受一下

本示例来自《Java编程思想》,我在此基础上做了一些修改

这个Demo主要是想介绍一下ReentrantLock对象

public class AttemptLocking {
    private ReentrantLock lock = new ReentrantLock();
    
    // 正常锁操作 和 解锁操作
    public void untimed(){
        boolean captured = lock.tryLock();
        try {
            System.out.println("tryLock():" + captured);
        }finally {
            if (captured){
                lock.unlock();
            }
        }
    }
    
    // 等待锁操作和解锁操作,如果没有拿到锁就反复等待,直到拿到锁
    public  void timed(){
        boolean captured = false;
        try {
            captured = lock.tryLock(2,TimeUnit.SECONDS); // 两秒去锁一次
            while (!captured){
                captured = lock.tryLock(2,TimeUnit.SECONDS); // 反复去锁
                System.out.println("正在尝试");
            }
            System.out.println("拿到锁");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        final AttemptLocking al = new AttemptLocking();
        // al.untimed();
        // al.timed();
        
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                al.lock.lock(); //忘记解锁
                System.out.println("acquired");
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }finally {
                    al.lock.unlock(); // 尝试解锁,如果把这个 try...catch...finally去掉,可以再感受一下
                }
            }
        });
        thread.setDaemon(true);
        thread.start();
        Thread.yield();
        al.untimed();
        al.timed();
    }
}