JUC:Lock接口、ReentrantLock类、Lock与Synchronized的区别、解决虚假唤醒、Condition接口、解决集合类线程不安全、Callable、FutrueTask

 

 

 

Lock 接口 (重点)

常用传教Lock的方法:

Lock lock = new ReentrantLock()

 

1、ReentrantLock 类

实现了 Lock接口

构造方法:

// ReentrantLock类有两个构造器
public ReentrantLock() {
    sync = new NonfairSync(); // 获得非公平锁
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync(); // 如果为true则获得公平锁
}

 

默认构造器创建一个非公平锁,传入true则会获得公平锁

非公平锁:其他线程可以插队,可实现花费时间少的线程可以优先使用

公平锁:当线程被cpu调用了,那么就必须得去执行,不能被改变

// 固定写法 try catch Finally
public class LockTest {
    public static void main(String[] args) {
        SaleTickets saleTickets = new SaleTickets();
        new Thread(()-> {for (int i = 0; i < 50; i++) saleTickets.sale();},"A").start();
        new Thread(()-> {for (int i = 0; i < 50; i++) saleTickets.sale();},"B").start();
        new Thread(()-> {for (int i = 0; i < 50; i++) saleTickets.sale();},"C").start();
    }
}

class SaleTickets{
    private int tickets = 50;
    // 创建非公平锁对象
    Lock lock = new ReentrantLock();
    public void sale(){
        // 设置锁
        lock.lock();
        try{
            if(tickets > 0){
                System.out.println(Thread.currentThread().getName() + ": 卖出了第"+ tickets-- + "号票");
            }
        }catch (Exception e){
            System.out.println("异常");
        }finally {
            // 释放锁
            lock.unlock();
        }
    }
}

 

 

2、Lock与Synchronized的区别 面试

  1. synchronized 是java内置的关键字。Lock是一个类
  2. synchronized 无法判断锁的状态。而Lock锁可以判断锁的状态。
  3. synchronized 是自动释放锁。Lock得调用unlock方法释放锁 (如果不释放锁,则会产生死锁状态)。
  4. synchronized 如果是有两个线程,有一个线程在执行过程中被阻塞,那么另一个线程就会一直等待。Lock锁则不一定会等待下去 (可通过调用tryLock方法去避免这个问题)。
  5. synchronized 可重入锁,不可中断的非公平锁。Lock也是可重入的锁,并可以设置锁的公平与非公平锁
  6. synchronized 适合锁少量的代码同步问题。Lock适合锁大量的同步代码。

 

3、防止线程虚假唤醒

synchronized 来实现防止线程虚假唤醒

public class SynchTest {
    public static void main(String[] args) {
        Number number = new Number();
        // 创建了3个线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    number.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    number.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    number.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"C").start();
        
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    number.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"D").start();

    }
}

class Number{
    private int num = 0;

    public synchronized void increment() throws InterruptedException {
        // 如果num不是0,就进入等待状态
        while (num != 0){
            this.wait();
        }
        num ++ ;
        System.out.println(Thread.currentThread().getName() + " ==> " + num);
        // 通知等待中的线程
        this.notifyAll();
    }

    public synchronized void decrement() throws InterruptedException {
        // 如果num是0,就进入等待状态
        while (num == 0){
            this.wait();
        }
        num -- ;
        System.out.println(Thread.currentThread().getName() + " ==> " + num);
        this.notifyAll();
    }
}

 

 

解决虚假唤醒分析 面试

如上代码,如果在Number类同步代码块中使用if判断num的值进而进行等待操作,在2个线程之间通信是不会出现虚假唤醒的情况。而如果是大于2个线程,还是使用if判断就会出现问题。因为,当调用this.notifyAll();时,会唤醒所有等待的线程,唤醒之后,如果时if的话,就不会再去判断num是否满足条件,会在之前执行等待的代码开始继续往下执行。而使用while,线程醒了进入BLOCK状态,被cpu调用之后,还会去判断num是否符合要求,直到不符合,才会继续执行while以外的代码。这样就保证了线程的安全。while循环的作用就是保证了符合要求的才可以进行之后的操作。

 

4、Condition 接口 JDK 1.5

JUC 线程之间的通信

synchronized 与 Lock 的对应关系

在这里插入图片描述

java.utils.concurrent.locks interface Condition 接口中

  • void await() throws InterruptedException
  • void signal()
  • void signalAll()

与传统的wait方法和notify方法没有什么不同,只是这是Lock接口专门使用的

在这里插入图片描述

public class LockConditionTest {
    public static void main(String[] args) {
        Numbers number = new Numbers();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    number.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    number.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    number.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"C").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    number.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"D").start();
    }
}

class Numbers{
    private int num = 0;
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    public void increment() throws InterruptedException {
        lock.lock();
        try{
            // 如果num不是0,就进入等待状态
            while (num != 0){
                condition.await();
            }
            num ++ ;
            System.out.println(Thread.currentThread().getName() + " ==> " + num);
            // 通知等待中的线程
            condition.signalAll();
        }catch (Exception e){
            System.out.println("异常");
        }finally {
            lock.unlock();
        }
    }

    public void decrement() throws InterruptedException {
        lock.lock();
        try{
            // 如果num不是0,就进入等待状态
            while (num == 0){
                condition.await();
            }
            num -- ;
            System.out.println(Thread.currentThread().getName() + " ==> " + num);
            // 通知等待中的线程
            condition.signalAll();
        }catch (Exception e){
            System.out.println("异常");
        }finally {
            lock.unlock();
        }
    }
}

 

这样写,与普通方式没什么区别。

 

5、Condition实现精准通知唤醒

  • 可以使线程之间有序的执行,精准的通知和唤醒指定的线程
public class LockConditionTest {
    public static void main(String[] args) {
        Numbers number = new Numbers();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                number.printA();
            }
        },"A").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                number.printB();
            }
        },"B").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                number.printC();
            }
        },"C").start();
    }
}

class Numbers{
    private int num = 1;  // num=1 线程A执行, num=2 线程B执行, num=3 线程C执行
    Lock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();

    public void printA(){
        lock.lock();
        // 判断等待 执行 通知
        try{
            // 如果num不等于1,就等待,不执行
            while (num != 1){
                condition1.await();
            }
            num = 2;
            System.out.println(Thread.currentThread().getName());
            condition2.signal(); // 这句话与notify有点区别,该意思是,给condition2发送通知,让condition2去执行
        }catch (Exception e){
            System.out.println("异常");
        }finally {
            lock.unlock();
        }
    }

    public void printB(){
        lock.lock();
        try {
            while ( num != 2){
                condition2.await();
            }
            num = 3;
            System.out.println(Thread.currentThread().getName());
            condition3.signal();
        }catch (Exception e){
            System.out.println("异常");
        }finally {
            lock.unlock();
        }
    }

    public void printC(){
        lock.lock();
        try {
            while ( num != 3){
                condition3.await();
            }
            num = 1;
            System.out.println(Thread.currentThread().getName());
            condition1.signal();
        }catch (Exception e){
            System.out.println("异常");
        }finally {
            lock.unlock();
        }
    }
}

// 线程有顺序的执行并输出:
// A
// B
// C
// ...

 

 

6、关于锁的问题 面试

// 1. 哪个语句先输出
public class LockProblem8 {
    public static void main(String[] args) {
        Info info = new Info();

        new Thread(()->info.msg()).start();

        try {
            TimeUnit.SECONDS.sleep(5); // 使已启动的线程睡眠5秒,常用的方法
        } catch (InterruptedException e) {
            System.out.println("异常");
        }

        new Thread(()->info.call()).start();
    }
}

class Info{
    public synchronized void msg(){
        System.out.println("发短信...");
    }
    public synchronized void call(){
        System.out.println("打电话...");
    }
}
// 这里的锁为同一个对象锁,两个线程的锁都是info这个对象
// 先输出 发短信... 再输出 打电话...
// 2 哪个语句先输出
public class LockProblem8 {
    public static void main(String[] args) {
        Info info1 = new Info();
        Info info2 = new Info();

        new Thread(()->info1.msg()).start();

        new Thread(()->info2.msg()).start();
    }
}

class Info{

    public synchronized void msg(){
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            System.out.println("异常");
        }
        System.out.println("发短信...");
    }

    public synchronized void call(){
        System.out.println("打电话...");
    }
}
// 因为两个线程都是非同一把锁,锁对象都不一样。因为不是同样的所对象,所以互不影响。又因为msg()方法有睡眠
// 先输出 打电话... 在输出 发短信...
// 3 哪个语句先输出
public class LockProblem8 {
    public static void main(String[] args) {
        Info info1 = new Info();
        Info info2 = new Info();

        new Thread(()->info1.msg()).start();

        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            System.out.println("异常");
        }

        new Thread(()->info2.call()).start();
    }
}

class Info{
    public static synchronized void msg(){
        System.out.println("发短信...");
    }

    public static synchronized void call(){
        System.out.println("打电话...");
    }
}
// 因为同步代码被static修饰,所以锁对象都是Info的Class对象,在类加载器加载的时候就生成了的
// 所以先输出 发短信... 在输出 打电话...
// 4 哪个语句先输出
public class LockProblem8 {
    public static void main(String[] args) {
        Info info1 = new Info();
        Info info2 = new Info();

        new Thread(()->info1.msg()).start();

        new Thread(()->info2.call()).start();
    }
}

class Info{
    public static synchronized void msg(){
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            System.out.println("异常");
        }
        System.out.println("发短信...");
    }

    public void call(){
        System.out.println("打电话...");
    }
}
// 由于第二个线程调用的是普通方法,没有锁的竞争
// 所以先输出 打电话... 在输出 发短信...

 

 

解决集合类线程不安全

使用大部分集合 会报异常 ConcurrentModificationException(并发修改异常) 线程不同步原因。

 

解决集合同步

关于List集合

  1. 使用Vector集合,继承了List集合,始于jdk1.0

    Vector之所以线程安全,是因为在每个方法上添加了关键字synchronized

    public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

     

  2. Collections.synchronizedList(new ArrayList<>());

    通过Collections集合的工具类,包装集合,达到线程安全。只不过它不是加在方法的声明处,而是方法的内部

  3. new CopyOnWriteArrayList<>();

    JUC可解决并发线程安全问题。顾名思义:写入时复制。多个线程,每个线程在写入时,将写入的数据进行复制,然后再插入到集合中,保证其他线程写入的数据不被覆盖。

    源码:

     private transient volatile Object[] array;

    遍历Vector/SynchronizedList是需要自己手动加锁的。

    CopyOnWriteArrayList使用迭代器遍历时不需要显示加锁,看看add()、clear()、remove()get()方法的实现可能就有点眉目了。

    public boolean add(E e) {   
        // 加锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
    
            // 得到原数组的长度和元素
            Object[] elements = getArray();
            int len = elements.length;
    
            // 复制出一个新数组
            Object[] newElements = Arrays.copyOf(elements, len + 1);
    
            // 添加时,将新元素添加到新数组中
            newElements[len] = e;
    
            // 将volatile Object[] array 的指向替换成新数组
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

     

    通过代码我们可以知道:在添加的时候就上锁,并复制一个新数组,增加操作在新数组上完成,将array指向到新数组中,最后解锁。

    【总结】

    • 在修改时,复制出一个新数组,修改的操作在新数组中完成,最后将新数组交由array变量指向
    • 写加锁,读不加锁

关于Set集合

  1. Collections.synchronizedSet(Set<E> set) 方法解决并发
  2. CopyOnWriteArraySet 类解决并发

原理与List集合一样


关于Map集合

  1. Collections.synchronizedMap(Map<K,V> map)

  2. ConcurrentHashMap<K,V>()

    要知道ConcurrentHashMap原理

 

解决并发几个方法的区别

  • Vector被CopyOnWriteArrayList替代,是因为Vector在每个方法都是使用的Synchronized关键字,而CopyOnWriteArrayList是使用的Lock锁,因此后者效率高很多
  • Vector和Collections都是使用的Synchronized关键字,前者在方法上,后者在方法中使用。并且两个都在原集合进行操作。

 

JUC解决并发代码:

利用循环创建多个线程,每个线程都需要往list集合中添加数据,模拟高并发

public class Test01{
    public static void main(String[] args) {
        // JUC 解决
        List<String> list = new CopyOnWriteArrayList<>();

        for (int i = 1; i <= 10; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,4));
                System.out.println(Thread.currentThread().getName() + list);
            },String.valueOf(i)).start();
        }
    }
}

 

Callable 进阶 FutureTask

java.util.concurrent interface Callable<V>

函数式接口,只有一个call方法。该接口与Runnble类似。只不过该接口有返回值,可抛出异常。

Thread类没有关于Callable的构造方法。因此Callable只能通过Runnable实现类java.utils.concurrent.FutureTask<V>的构造方法,与Thread类进行连接

在这里插入图片描述

public class CallableUpTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<String> futureTask = new FutureTask<>(new CallableUp());
        new Thread(futureTask).start(); // 创建Callable线程
        // 两个线程去执行Callable中的call方法,只打印一次call,是因为有缓存
        new Thread(futureTask).start(); 
        
        // 获取返回值,可能会产生阻塞,是因为在执行call方法时,可能时间很长,一般最后去获取返回值
        System.out.println(futureTask.get()); 
    }
}

class CallableUp implements Callable<String>{
    @Override
    public String call() throws Exception {
        System.out.println("call");
        return "12345";
    }
}
// 输出
// call
// 12345

 

细节:

  • Callable有缓存
  • 获取返回值可能会发生阻塞

 

感谢:bilibli主播 —— 遇见狂神说
博主的开源教学视频十分良心!超级赞!全栈Java的学习指导!
链接:https://space.bilibili.com/95256449/video
本博客是本人观看 遇见狂神说 的开源课程而自己所做的笔记,与 遇见狂神说 开源教学视频搭配更佳!!

 

posted @ 2020-06-14 16:50  张还行  阅读(237)  评论(0编辑  收藏  举报