JUC并发常用工具学习

今天主要来和大家分享一下JUC相关的一些简单知识,线程池文章就不介绍了,前面的文章有介绍,本文主要介绍Lock和认识synchronized和并发的一些工具类的使用。

Lock

传统的锁有synchronized关键字,我们可以直接在方法和代码块中使用它。

在Java中有ReentrantLock、ReentrantReadWriteLock

常用的ReentrantLock,默认采用的是非公平锁,也有公平锁的实现方式,简单点说,

  • 公平锁需要根据申请锁的顺序来获取锁,按照先来先服务的原则。

  • 非公平锁可以不按照申请锁的顺序,后申请的线程也可以按先申请锁的线程先获取锁。

public ReentrantLock() {
    sync = new NonfairSync();
}
/**
 * Creates an instance of {@code ReentrantLock} with the
 * given fairness policy.
 *
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

下面我们也可以通过一个卖票的小例子来看ReentrantLock的使用,其中最主要的是lock和unlock方法,这方法需要成对出现,否则可能会出现锁未释放的情况。

class Ticket1 {

    private int number = 50;

    Lock lock = new ReentrantLock();
    /**
     * 卖票的方式
     */
    public synchronized void sale() {
        // 加锁
        lock.lock();
        try {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出了" + (number--) + "票,剩余" + number);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

简单对比Lock和synchronized:

  • synchronized和Lock的类型不同,一个是关键字,一个是类。

  • Lock可以判断锁的状态,而synchronized是不可以的。

  • synchronized的锁释放是自动释放的,而Lock是需要手动释放锁。

  • synchronized线程会一直等待,而Lock是不一定等待下去。

  • synchronized是可重入锁,是非公平锁,Lock可以重入锁,可以设置是否公平。

  • Lock适合锁大量代码,synchronized适合锁少量的代码。

生产者的消费者问题

synchronized版

这个问题一直都是比较经典的问题,我们可以使用synchronized关键字实现配置wait方法,notifyAll方法和notify方法实现。

注意wait方法只能出现在同步方法中。

下面的例子需要防止虚假唤醒的问题,即需要采用循环的方式而不能采用if的方式,在JDK1.8文章的也有说明

class Data {

    private int number = 0;
    /**
     * 资源类
     */
    public synchronized void increment() {
//      if (number != 0) {
        while (number != 0) {
            // 等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        number++;
        System.out.println(Thread.currentThread().getName() + "=>" + number);

        // 通知其他线程
        this.notifyAll();
    }

    public synchronized void decrement() {
//      if (number == 0) {
        while (number == 0) {
            // 等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        number--;
        System.out.println(Thread.currentThread().getName() + "=>" + number);
        // 通知
        this.notifyAll();
    }
}

Lock版

可能有人也会说,有synchronize不九可以处理了么,在Java中还有一个配合也是可以实现这个功能的,也就是Lock配置Condition的await方法和signal方法,这个比synchronizd强在哪里呢?这个可以实现精准唤醒,而通过synchronized方法里面的notify不能实现精准唤醒。

下面这个例子就可以实现按顺序实现唤醒。

class Data2 {
    private Lock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();
    private int number = 1;

    public void printA() {
        lock.lock();
        try {
            while (number != 1) {
                // 等待
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName() + "=>" + number);
            // 唤醒指定的人
            number = 2;
            condition2.signal();
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printB() {
        lock.lock();
        try {
            while (number != 2) {
                // 等待
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName() + "=>" + number);
            // 唤醒指定的人
            number = 3;
            condition3.signal();
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printC() {
        lock.lock();
        try {
            while (number != 3) {
                // 等待
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName() + "=>" + number);
            // 唤醒指定的人
            number = 1;
            condition1.signal();
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

}

理解锁

我们如何判断锁的是谁?锁的是调用该方法的对象还是Class模板呢?这样可以帮助我们深入理解锁。

这里还是采用狂神视频的提到的8锁问题,应该看完就可以理解synchronized锁的判断。

场景一

  1. 是输出发短信还是打电话?发短信

  2. 发短信延迟4秒,是发短信还是打电话?发短信

    synchronized锁的是方法的调用者,两个方法用的是同一把锁,谁先拿到先执行

public class Test1 {

    @SneakyThrows
    public static void main(String[] args){
        Phone phone = new Phone();
        new Thread(() -> {
            phone.sendMsg();
        }, "A").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            phone.call();
        }, "B").start();
    }
}


class Phone {

    public synchronized void sendMsg() {
        System.out.println("sendMsg");
    }

    public synchronized void call() {
        System.out.println("call");
    }
}
场景二
  1. 下面先输出哪个结果?发短信还是hello? hello

    hello方法是没有锁的,不是同步方法,不受锁影响

public class Test2 {
    public static void main(String[] args){
        Phone1 phone = new Phone1();
        new Thread(() -> {
            phone.sendMsg();
        }, "A").start();

        new Thread(() -> {
            phone.call();
        }, "B").start();
        new Thread(() -> {
            phone.hello();
        }, "C").start();
    }
}


class Phone1 {

    public synchronized void sendMsg() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sendMsg");
    }

    public synchronized void call() {
        System.out.println("call");
    }

    public void hello() {
        System.out.println("hello");
    }
}
场景三
  1. 下面重新声明两个对象,一个调用同步发短信方法,一个调用call方法,先执行哪个方法? call

因为两个同步方法,锁的是方法调用者的对象,这里有两把锁

public class Test2 {
    public static void main(String[] args){
        Phone1 phone1 = new Phone1();
        Phone1 phone2 = new Phone1();
        new Thread(() -> {
            phone1.sendMsg();
        }, "A").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            phone2.call();
        }, "B").start();

    }
}

class Phone1 {

    public synchronized void sendMsg() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sendMsg");
    }

    public synchronized void call() {
        System.out.println("call");
    }

    public void hello() {
        System.out.println("hello");
    }
}
场景四
  1. 增加两个静态同步方法,只有一个对象,先打印,发短信?打电话?发短信

类一加载就有了,这里锁的Class对象,Class是唯一的

public class Test3 {

    public static void main(String[] args){
        Phone3 phone3 = new Phone3();
        new Thread(() -> {
            phone3.sendMsg();
        }, "A").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            phone3.call();
        }, "B").start();
    }
}

class Phone3 {

    public static synchronized void sendMsg() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sendMsg");
    }

    public static synchronized void call() {
        System.out.println("call");
    }

    public void hello() {
        System.out.println("hello");
    }
}
  1. 两个对象,是发短信还是打电话?发短信

    Class模板只有一个

public class Test3 {

    public static void main(String[] args){
        Phone3 phone3 = new Phone3();
        Phone3 phone2 = new Phone3();
        new Thread(() -> {
            phone3.sendMsg();
        }, "A").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            phone2.call();
        }, "B").start();
    }
}
  1. 一个对象,一个静态同步方法,一个普通同步方法,先发短信,还是先打电话?先打电话

sendMsg锁的是Class模板,call锁的是调用的对象

  1. 如果是两个对象,结果也是一样,结果还是打电话
public class Test4 {

    public static void main(String[] args){
        Phone4 phone4 = new Phone4();
        new Thread(() -> {
            phone4.sendMsg();
        }, "A").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            phone4.call();
        }, "B").start();
    }
}

class Phone4 {

    public static synchronized void sendMsg() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sendMsg");
    }

    public synchronized void call() {
        System.out.println("call");
    }

}

常见的并发辅助类

CountDownLatch

CountDownLatch里面定义了一个减法计算器,和一个阻塞队列,常用的方法有countDown和await方法,countDown相当于-1,await方法会等待计算器归零,然后再被唤醒向下执行。

public class CountDownLatchDemo {

    public static void main(String[] args) throws InterruptedException {
        // 总数6
        CountDownLatch countDownLatch = new CountDownLatch(5);
        // -1
        countDownLatch.countDown();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "go out!");
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }
        // 等待计数器归零,然后再向下执行
        countDownLatch.await();
        System.out.println("close door!");
    }
}

CyclicBarrier

需要等待线程全部达到共同屏障点的同步辅助,其实就是一个加法计算器,主要是可以实现让一组线程达到同一屏障点,然后再进行后续操作。

public class CyclicBarrierDemo {

    public static void main(String[] args){
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () ->{
            System.out.println("召唤神龙成功!");
        });
        for (int i = 1; i <= 7; i++) {
            int temp = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "收集" + temp + "个龙珠");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

Semaphore

一个计数信号量,其中常用的两个方法acquire用于获取许可证,release释放许可证。

可以用于限流等场景,比如下面的停车场例子,当初始化为一个时,也可以用于做互斥锁。

public class SemaphoreTest {

    public static void main(String[] args){
        // 停车位
        Semaphore semaphore = new Semaphore(3);
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                // 等到
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "抢到车位");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName() + "离开车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            }, String.valueOf(i)).start();
        }
    }
}

读写锁

读锁可以允许多个线程同时读,而写锁只允许一个线程写。

下面的例子模拟了一个缓存,这里也采用volatie关键字,用于保证map的数据的可见性。

public class ReadWriteLockDemo {

    public static void main(String[] args){
        MyCache myCache = new MyCache();
        // 多线程写
        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCache.put(temp + "", temp + "");
            }, String.valueOf(i)).start();
        }
        // 多线程读
        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCache.get(temp + "");
            }, String.valueOf(i)).start();
        }
    }
}

class MyCache {

    private volatile Map<String, Object> map = new HashMap<>();
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    /**
     * 一个线程写
     */
    public void put(String key, Object value) {
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "写入");
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写入成功!");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    /**
     * 多个线程读
     */
    public void get(String key) {
        readWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "读取");
            Object value = map.get(key);
            System.out.println(Thread.currentThread().getName() + "读取成功:" + value);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
}

总结

本文章分享的主要是一些并发工具类的使用和介绍,希望大家喜欢!

参考资料

狂神的JUC视频(B站上有,讲的还可以! )

posted @ 2023-04-09 21:53  Leo哥coding~  阅读(31)  评论(0编辑  收藏  举报