Fork me on GitHub

多线程进阶JUC:Lock锁、集合类的安全性分析、Callable接口和常用辅助类

文章首发于我的个人博客,欢迎访问:https://blog.itzhouq.cn/juc

多线性系列笔记:

一、Java 线程创建、同步和通信

二、多线程进阶JUC:Lock锁、集合类的安全性分析、Callable接口和常用辅助类

之前写过 Java 线程的创建、同步和通信。这篇文章在此基础之上进一步梳理多线程的一些用法和简单原理。

JUC 是 Java 中 java.util.concurrent 工具包的简称。

1、回顾相关知识点

进程和线程

进程:一个运行起来的程序就是进程,比如 QQ、网易云等。

线程:进程的细分单元,是程序内部执行的路径,拥有独立的运行栈和程序计数器。Java 默认有两个线程,main 线程和 GC 线程。

Java 中线程的开启

Java 本身不能操作硬件,不能开启线程,Java 中使用 start()方法开启线程底层使用了private native void start0()本地方法,本质是调用 C++ 的代码。

并行和并发

  • 并行:多个CPU核心同时执行多个任务。比如:多个人同时做不同的事。
  • 并发:一个CPU核心(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事

使用代码获取 CPU 核心数

public class Demo {
    public static void main(String[] args) {
        // 获取 CPU 核心数
        System.out.println(Runtime.getRuntime().availableProcessors()); // 8
    }
}

并发编程的本质:充分利用 CPU 资源。

线程的状态

public enum State {
   // 新生
    NEW,
    // 运行
    RUNNABLE,
    // 阻塞
    BLOCKED,
    // 等待
    WAITING,
    // 超时等待
    TIMED_WAITING,
    // 终止
    TERMINATED;
}

sleep 和 wait 区别

  • 来源:wait 来源于 Object 类,所有的对象中都有;sleep 属于 Thread 类。
  • 锁的释放:wait 会释放锁;sleep 睡着了,抱着锁睡,不会释放锁。
  • 使用范围:wait 必须在同步代码块中使用;sleep 可以在任意位置使用。
  • 异常:wait 不需要捕获异常;sleep 必须要捕获异常。

2、Lock 锁(重点)

传统的synchronized

还是举个例子:多个窗口卖票的

/**
 * 基本的卖票的例子
 *  线程就是一个单独的资源类,没有任何附属的操作
 */
public class SaleTickerDemo {
    public static void main(String[] args) {
        // 并发:多线程操作同一个资源类,把资源丢进线程
        final Ticket ticket = new Ticket();
        new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                ticket.sale();
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                ticket.sale();
            }
        }, "B").start();

        new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                ticket.sale();
            }
        }, "C").start();
    }
}
// 资源类
class Ticket {
    // 属性、方法
    private int number = 50;
    // 卖票的方式
    public void sale () {
        if (number > 0) {
            System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "张票,剩余" + number + "张票");
        }
    }
}

上述代码是有线程安全问题的,解决并发问题的方式之一是将 sale()方法设置成同步方法。也就是加 synchronized关键词。

// 卖票的方式
public synchronized void sale () {
    if (number > 0) {
        System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "张票,剩余" + number + "张票");
    }
}

Lock 接口

查看 Javadoc Lock接口下面有三个实现类,其中最常用的是 ReentrantLock,。Lock 的使用方法是先创建一个实现类,然后上锁,将业务代码放在 try中,在 finally中解锁。

下面使用 Lock 的方式解决上述线程安全问题。

修改资源类 Ticket:

使用 Lock 加锁主要有以下步骤:

/**
 * Lock 三部曲:
 * 1、new ReentrantLock();
 * 2、lock.lock() 上锁
 * 3、finally 中解锁 lock.unlock()
 */

class Ticket2 {
    private int number = 50;
    Lock lock = new ReentrantLock(); // 锁的实现类
    public void sale () {
        lock.lock(); // 加锁
        try {
            // 业务代码
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "张票,剩余" + number + "张票");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 解锁
        }
    }
}

测试类:

public class SaleTickerDemo2 {
    public static void main(String[] args) {
        Ticket2 ticket = new Ticket2();
        new Thread(() -> { for (int i = 0; i < 50; i++) ticket.sale(); }, "A").start();
        new Thread(() -> { for (int i = 0; i < 50; i++) ticket.sale(); }, "B").start();
        new Thread(() -> { for (int i = 0; i < 50; i++) ticket.sale(); }, "C").start();
    }
}

使用这种方式同样可以解决线程安全问题。

阅读一下 ReentrantLock类的源码:

/**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    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();  // 公平锁
    }

非公平锁:对于需要处理的线程,不分先来后到,可以插队处理。

公平锁:对于需要处理的线程,先来后到的处理,对线程实公平的。

synchronized 和 Lock 的区别

1、synchronized 是 Java 内置的关键词,Lock 是接口;

2、synchronized 无法获取锁的状态;Lock 可以判断是否获取到了锁;

3、synchronized 会自动释放锁;Lock 必须手动释放锁。如果不释放锁,会发生死锁

4、synchronized 中两个线程操作同一个对象,如果 A 线程获得了锁,但是是阻塞状态,那么 B 线程会傻傻的等待获取锁;但是在 Lock 中就不一定会一直等待了,lock 有个尝试获取锁的方法

5、synchronized 是可重入锁,不可中断,非公平锁;Lock 是可重入锁,可以根据需要设置公平性。

6、synchronized 适合锁少量代码同步代码问题,Lock 适合锁大量的同步代码。


3、生产者和消费者问题

synchronized 版本的生产者和消费者问题

首先写一个资源类:

/**
 * 资源类
 * 判断等待,业务,通知
 */
public class Data {
    private int number = 0; // 初始化一个数字,后面对这个数字进行操作

    // 加 1 操作
    public synchronized void increment () throws InterruptedException {
        if (number != 0) {
            this.wait(); // 等待,等到number变成 0 的时候才加1
        }
        number++;
        System.out.println(Thread.currentThread().getName() + "=>" + number);
        // 通知其他线程,我 +1 完毕
        this.notify();
    }

    // 减 1 操作
    public synchronized void decrement () throws InterruptedException {
        if (number == 0) {
            this.wait(); // 等待
        }
        number--;
        System.out.println(Thread.currentThread().getName() + "=>" + number);
        // 通知其他线程,我 -1 完毕
        this.notify();
    }
}

资源类中使用synchronized,将方法变为同步方法解决线程安全问题。

测试:

/**
 * 线程之间的通信:生产者和消费者问题
 *
 * 两个线程交替执行  A和B操作同一个变量  num = 0
 * A num +1
 * B num -1
 */
public class Demo {
    public static void main(String[] args) {
        Data data = new Data();

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

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

两个线程没问题,但是一旦线程多于两个,就出现问题了。

虚假唤醒:虚假唤醒是指当一个条件满足时,很多线程都被唤醒了,但是只有其中部分是有用的,其他的唤醒都是无用功。比如说,商场本来没有货物,突然进来一件商品,所有的线程都被唤醒了,但是只能一个人买,其余的人都是虚假唤醒,获取不到对象的锁。

解决虚假唤醒的方法,文档中有提示。就是将if替换成while。这是因为if只是判断一次,而while可以有多次判断。

// 减 1 操作
public synchronized void decrement () throws InterruptedException {
    while (number == 0) {
        this.wait(); // 等待
    }
    number--;
    System.out.println(Thread.currentThread().getName() + "=>" + number);
    // 通知其他线程,我 -1 完毕
    this.notify();
}

JUC版生产者和消费者问题

类似 synchronized 中有两个配套的方法 wait()notify() 方法用于等待和唤醒线程,Lock中也有两个类似方法 await()signal()。方法位于java.util.concurrent.locks.Condition接口中。用于替换 wait()notify()

资源类:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 资源类
 * 判断等待,业务,通知
 */
public class Data2 {
    private int number = 0;
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    // 加 1 操作
    public void increment () throws InterruptedException {
        lock.lock();
        try {
            while (number != 0) {
                // 等待
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "=>" + number);
            // 通知其他线程,我 +1 完毕
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    // 减 1 操作
    public void decrement () throws InterruptedException {
        lock.lock();
        try {
            while (number == 0) {
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + "=>" + number);
            // 通知其他线程,我 -1 完毕
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }
}

测试:

public class Demo2 {
    public static void main(String[] args) {
        Data2 data = new Data2();

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

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

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

Condition 精准的通知和唤醒线程

为什么需要使用 Condition 中的新方法替换 wait()notify()方法呢?这是因为 Condition 具有精准的通知和唤醒线程功能。

简单说,上面几个线程执行的顺序是随机的,如果需要按照一定的顺序执行线程需要用到 Condition 的精准通知功能。

资源类:

/**
 * 资源类
 * 判断等待,业务,通知
 */
public class Data3 {
    Lock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();
    private int number = 1; // 1A 2B  3C
    public void printA () throws InterruptedException {
        lock.lock();
        try {
            // 业务 --> 执行 ---> 通知
            while (number != 1) {
                // 等待
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName() + "=> AAAAAAA");
            // 唤醒指定的线程B
            number = 2;
            condition2.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printB () throws InterruptedException {
        lock.lock();
        try {
            // 业务 --> 执行 ---> 通知
            while (number != 2) {
                // 等待
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName() + "=> BBBBBBB");
            // 唤醒指定的线程B
            number = 3;
            condition3.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printC () throws InterruptedException {
        lock.lock();
        try {
            // 业务 --> 执行 ---> 通知
            while (number != 3) {
                // 等待
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName() + "=> CCCCCCC");
            // 唤醒指定的线程A
            number = 1;
            condition1.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

测试:

/**
 * A线程执行完成调用线程B,B执行完成调用C
 */
public class Demo3 {
    public static void main(String[] args) {
        Data3 data = new Data3();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.printA();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.printB();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.printC();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();
    }
}

结果是线程按顺序执行。

4、锁的对象问题

1、标准情况下,两个线程先打印 发短信 还是 打电话? 1\发短信 2\打电话

2、发短息你延迟4s,两个线程先打印 发短信 还是 打电话? 1\发短信 2\打电话

先看一个资源类:

/**
 * 资源类
 */
public class Phone {
    public synchronized void sendSms() {
        System.out.println("发短信");
    }

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

现在开启两个线程调用这两个同步方法:

import java.util.concurrent.TimeUnit;

/**
 * 8 锁,就是关于锁的 8 个问题
 * 1、标准情况下,两个线程先打印 发短信 还是 打电话?     1\发短信    2\打电话
 * 2、发短息你延迟4s,两个线程先打印 发短信 还是 打电话?     1\发短信    2\打电话
 */
public class Test1 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> {
            phone.sendSms();
        }, "A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            phone.sendSms();
        }, "B").start();
    }
}

标准情况下,两个线程先打印 发短信 还是 打电话?

答案是先发短信再打电话。

这是因为 A 线程先调用 发短信的方法,所以先打印发短信吗?并不是的。

下面将发短信的方法延迟4秒:

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

结果还是先发短信再打电话。

这是因为锁的存在,在资源类 Phone 中 synchronized 锁的对象是方法的调用者!两个方法使用了同一个锁 phone ,谁先拿到谁就先执行。


1、增加了一个普通方法之后。先执行发短信还是打印Hello 1、打印Hello 2、发短信

2、两个对象,两个同步方法,发短信还是打电话 1、打电话 2、发短信

/**
 * 资源类
 */
public class Phone2 {
    public synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

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

    public void hello() {
        System.out.println("Hello");
    }
}
/**
 * 1、增加了一个普通方法之后。先执行发短信还是打印Hello    1、打印Hello   2、发短信
 */
public class Test2 {
    public static void main(String[] args) {
        Phone2 phone = new Phone2();
        new Thread(() -> {
            phone.sendSms();
        }, "A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

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

这种情况下,先打印 Hello ,因为hello 方法不是同步方法,不受锁的影响。

现在变成两个对象分别调用两个同步方法。

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

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            phone2.call();
        }, "B").start();
    }
}

结果是先打电话。


1、两个静态的同步方法,只有一个对象,先打印发短信还是打电话? 1、发短信 2、打电话

将资源类中的方法修改为静态方法:

/**
 * 资源类
 */
public class Phone3 {
    // synchronized 锁的对象是方法调用者
    // 因为是静态方法,类一加载就有了,锁的是Class
    public static synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

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

测试类:

/**
 * 1、两个静态的同步方法,只有一个对象,先打印发短信还是打电话?    1、发短信     2、打电话
 */
public class Test3 {
    public static void main(String[] args) {
        Phone3 phone = new Phone3();
        new Thread(() -> {
            phone.sendSms();
        }, "A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

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

这种情况下,锁的对象是 Class 对象。


1个静态的同步方法,1个普通的同步方法,只有一个对象,先打印发短信还是打电话? 1、打电话 2、发短信

/**
 * 资源类
 */
public class Phone4 {
    // 静态的同步方法,锁的是 Class 类模板
    public static synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    // 普通的调用方法,锁的调用者
    public synchronized void call() {
        System.out.println("打电话");
    }
}
package cn.itzhouq.lock8;

import java.util.concurrent.TimeUnit;

/**
 * 1个静态的同步方法,1个普通的同步方法,只有一个对象,先打印发短信还是打电话?   1、打电话 2、发短信
 */
public class Test4 {
    public static void main(String[] args) {
        Phone4 phone = new Phone4();
        new Thread(() -> {
            phone.sendSms();
        }, "A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

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

这种情况下还是锁的对象不一样。


小结

使用new关键词创建对象,如果执行的是普通的同步方法,锁的对象是this,当前对象。

如果是静态的同步方法,锁的对象是对象的模板Class,是唯一的。

分析锁的对象时,关注调用者和方法的类型。


5、集合类的安全性问题

首先回顾一下单线程下的 List:s

public class ArrayTest {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("1", "2", "3");
        list.forEach(System.out::println); // 1   2  3
    }
}

单线程下上面的代码没有问题。试试多线程。

public class ArryListTest2 {
    public static void main(String[] args) {
        // java.util.ConcurrentModificationException 并发修改异常
        List<String> list = new ArrayList<>();

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

这里抛出了一个很重要的异常:java.util.ConcurrentModificationException 并发修改异常

解决这个线程安全问题有三种方式:

使用 Vector 代替 ArrayList

public static void main(String[] args) {
    /**
         * 并发下 ArrayList 不安全
         * 方法一:List<String> list = new Vector<>();
         */
    //List<String> list = new ArrayList<>();
    List<String> list = new Vector<>();

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

查看 ArrayList 中 add()的源码:

/**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

没有同步方法。再看 Vector 的 add()方法,有同步方法。

/**
     * Appends the specified element to the end of this Vector.
     *
     * @param e element to be appended to this Vector
     * @return {@code true} (as specified by {@link Collection#add})
     * @since 1.2
     */
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

但是一般不采用这种方法。

使用工具类将 ArrayList 变为安全的

public class ArryListTest2 {
    public static void main(String[] args) {
        /**
         * 并发下 ArrayList 不安全
         * 方法二:List<String> list = Collections.synchronizedList(new ArrayList<>());
         */
        List<String> list = Collections.synchronizedList(new ArrayList<>());

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

使用 CopyOnWrite

public static void main(String[] args) {
    /**
    * 并发下 ArrayList 不安全
    * CopyOnWrite 写入时复制, COW 是计算机程序设计领域的一种优化策略;
    * 多个线程在调用的时候,对于 list 的读取是固定,后面写入操作可能会覆盖已经写好的数据,
    * 为了防止在写入的时候覆盖,造成数据问题
	* 一般都是写入拷贝一份,写完了再set进去。
     */
    List<String> list = new CopyOnWriteArrayList<>();

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

查看 CopyOnWrite 的 add()方法:使用了 Lock 锁。

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;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

Set 的线程安全问题

使用类似的方式,检查 Set 的安全性。

// java.util.ConcurrentModificationException
public class SetTest {
    public static void main(String[] args) {
        /**
         * 解决方法一:使用工具类转换 Set<String> set = Collections.synchronizedSet(new HashSet<>());
         * 解决方式二:使用CopyOnWriteArraySet<>()
         */
//        Set<String> set = new HashSet<>();
//        Set<String> set = Collections.synchronizedSet(new HashSet<>());
        Set<String> set = new CopyOnWriteArraySet<>();
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                set.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(set);
            }, String.valueOf(i)).start();
        }
    }
}

再来回顾一下 Set 的知识:

结构:

Set 的本质:

public HashSet() {
    map = new HashMap<>();
}
// 本质是 map

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}
// set的add 本质就是map的put方法,但是只有key,key是唯一的。PRESENT 是常数

Map 的线程安全问题

public class MapTest {
    // java.util.ConcurrentModificationException
    public static void main(String[] args) {
        /**
         * 解决方式一:Collections.synchronizedMap(new HashMap<>());
         * 解决方式二:new ConcurrentHashMap<>(); 替换
         */
//        Map<String, Object> map = new HashMap<>();
//        Map<String, Object> map = Collections.synchronizedMap(new HashMap<>());
          Map<String, Object> map = new ConcurrentHashMap<>();

        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 5));
                System.out.println(map);
            }, String.valueOf(i)).start();
        }
    }
}

HashMap底层和 ConcurrentHashMap 是重点。

/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

6、Callable

看文档:

@FunctionalInterface
public interface Callable<V>
返回结果并可能引发异常的任务。 实现者定义一个没有参数的单一方法,称为call 。
Callable接口类似于Runnable ,因为它们都是为其实例可能由另一个线程执行的类设计的。 然而,A Runnable不返回结果,也不能抛出被检查的异常。

该Executors类包含的实用方法,从其他普通形式转换为Callable类。

重点:

1、与Runnable 相比,可以有返回值

2、可以抛出异常

3、调用的方法是call()

Callable位于java.util.concurrent包下,它也是一个接口,在它里面也只声明了一个方法,只不过这个方法叫做call():

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

可以看到,这是一个泛型接口,call()函数返回的类型就是传递进来的V类型。

Interface Runnable
All Known Subinterfaces:
RunnableFuture <V>, RunnableScheduledFuture <V>
所有已知实现类:
AsyncBoxView.ChildState , ForkJoinWorkerThread , FutureTask , RenderableImageProducer , SwingWorker , Thread , TimerTask

Runnable 的实现类 FutureTask 有个构造函数:

构造方法
Constructor and Description
FutureTask(Callable<V> callable)
创建一个 FutureTask ,它将在运行时执行给定的 Callable 。
FutureTask(Runnable runnable, V result)
创建一个 FutureTask ,将在运行时执行给定的 Runnable ,并安排 get将在成功完成后返回给定的结果。

可以和Callable产生关联。具体的使用通过下面的代码体现:

package cn.itzhouq.unsafe.callable;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyThread myThread = new MyThread();

        FutureTask futureTask = new FutureTask(myThread); // 适配类
        new Thread(futureTask, "A").start();
        new Thread(futureTask, "B").start(); // 结果会被缓存,效率高

        Integer i = (Integer) futureTask.get(); // 这个 get 方法可能会产生阻塞问题,一般放在最后,或者通过异步通信来处理
        System.out.println(i);
    }
}

class MyThread implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println("call()");
        return 1024;
    }
}

运行结果:

call()
1024

细节:

1、有缓存

2、结果可能需要等待,会阻塞!

7、常用的辅助类

CountDownLatch

看文档:

public class CountDownLatch
extends Object
允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。
A CountDownLatch用给定的计数初始化。 await方法阻塞,直到由于countDown()方法的调用而导致当前计数达到零,之后所有等待线程被释放,并且任何后续的await 调用立即返回。 这是一个一次性的现象 - 计数无法重置。 如果您需要重置计数的版本,请考虑使用CyclicBarrier 。

A CountDownLatch是一种通用的同步工具,可用于多种用途。 一个CountDownLatch为一个计数的CountDownLatch用作一个简单的开/关锁存器,或者门:所有线程调用await在门口等待,直到被调用countDown()的线程打开。 一个CountDownLatch初始化N可以用来做一个线程等待,直到N个线程完成某项操作,或某些动作已经完成N次。

CountDownLatch一个有用的属性是,它不要求调用countDown线程等待计数到达零之前继续,它只是阻止任何线程通过await ,直到所有线程可以通过。

示例用法:这是一组类,其中一组工作线程使用两个倒计时锁存器:

第一个是启动信号,防止任何工作人员进入,直到驾驶员准备好继续前进;
第二个是完成信号,允许司机等到所有的工作人员完成。

另一个典型的用法是将问题划分为N个部分,用一个Runnable来描述每个部分,该Runnable执行该部分并在锁存器上倒计时,并将所有Runnables排队到执行器。 当所有子部分完成时,协调线程将能够通过等待。 (当线程必须以这种方式反复倒数时,请改用CyclicBarrier ))

内存一致性效果:直到计数调用之前达到零,在一个线程操作countDown() happen-before以下由相应的成功返回行动await()在另一个线程。

简单来说,就是一个加法计数器。演示代码:

package cn.itzhouq.add;

import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "Go out");
                countDownLatch.countDown(); // 数量 -1
            }, String.valueOf(i)).start();
        }
        countDownLatch.await(); // 等待计数器归零,然后再向下执行
        System.out.println("Close Door");
    }
}

执行结果:

1Go out
6Go out
4Go out
5Go out
2Go out
3Go out
Close Door

原理:每次有线程调用 countDown()数量 -1 ,假设计数器变为 0 ,countDownLatch.await() 就会被唤醒,继续执行!

CyclicBarrier

看文档:

public class CyclicBarrier
extends Object
允许一组线程全部等待彼此达到共同屏障点的同步辅助。 循环阻塞在涉及固定大小的线程方的程序中很有用,这些线程必须偶尔等待彼此。 屏障被称为循环 ,因为它可以在等待的线程被释放之后重新使用。
A CyclicBarrier支持一个可选的Runnable命令,每个屏障点运行一次,在派对中的最后一个线程到达之后,但在任何线程释放之前。 在任何一方继续进行之前,此屏障操作对更新共享状态很有用。

CyclicBarrier对失败的同步尝试使用all-or-none断裂模型:如果线程由于中断,故障或超时而过早离开障碍点,那么在该障碍点等待的所有其他线程也将通过BrokenBarrierException (或InterruptedException)异常离开如果他们也在同一时间被打断)。

内存一致性效果:线程中调用的行动之前, await() happen-before行动是屏障操作的一部分,进而发生,之前的动作之后,从相应的成功返回await()其他线程。

简单来说,就是一个加法计数器。演示代码:

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        /**
         * 召集7颗龙珠召唤神龙
         */
        // 召唤神龙的线程
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
            System.out.println("召唤神龙成功");
        });

        for (int i = 1; i <= 7; i++) {
            final int temp = i; // lambda 不能直接操作i
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "收集了第" + temp + "颗龙珠");

                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

结果:

Thread-0收集了第1颗龙珠
Thread-3收集了第4颗龙珠
Thread-4收集了第5颗龙珠
Thread-2收集了第3颗龙珠
Thread-1收集了第2颗龙珠
Thread-6收集了第7颗龙珠
Thread-5收集了第6颗龙珠
召唤神龙成功

Semaphore

Semaphore :信号量

看文档:

public class Semaphore
extends Object
implements Serializable
一个计数信号量。 在概念上,信号量维持一组许可证。 如果有必要,每个acquire()都会阻塞,直到许可证可用,然后才能使用它。 每个release()添加许可证,潜在地释放阻塞获取方。 但是,没有使用实际的许可证对象; Semaphore只保留可用数量的计数,并相应地执行。
信号量通常用于限制线程数,而不是访问某些(物理或逻辑)资源。 
在获得项目之前,每个线程必须从信号量获取许可证,以确保某个项目可用。 当线程完成该项目后,它将返回到池中,并将许可证返回到信号量,允许另一个线程获取该项目。 请注意,当调用acquire()时,不会保持同步锁定,因为这将阻止某个项目返回到池中。 信号量封装了限制对池的访问所需的同步,与保持池本身一致性所需的任何同步分开。

信号量被初始化为一个,并且被使用,使得它只有至多一个允许可用,可以用作互斥锁。 这通常被称为二进制信号量 ,因为它只有两个状态:一个许可证可用,或零个许可证可用。 当以这种方式使用时,二进制信号量具有属性(与许多Lock实现不同),“锁”可以由除所有者之外的线程释放(因为信号量没有所有权概念)。 这在某些专门的上下文中是有用的,例如死锁恢复。

此类的构造函数可选择接受公平参数。 当设置为false时,此类不会保证线程获取许可的顺序。 特别是, 闯入是允许的,也就是说,一个线程调用acquire()可以提前已经等待线程分配的许可证-在等待线程队列的头部逻辑新的线程将自己。 当公平设置为真时,信号量保证调用acquire方法的线程被选择以按照它们调用这些方法的顺序获得许可(先进先出; FIFO)。 请注意,FIFO排序必须适用于这些方法中的特定内部执行点。 因此,一个线程可以在另一个线程之前调用acquire ,但是在另一个线程之后到达排序点,并且类似地从方法返回。 另请注意, 未定义的tryAcquire方法不符合公平性设置,但将采取任何可用的许可证。

通常,用于控制资源访问的信号量应该被公平地初始化,以确保线程没有被访问资源。 当使用信号量进行其他类型的同步控制时,非正常排序的吞吐量优势往往超过公平性。

本课程还提供了方便的方法, 一次acquire和release多个许可证。 当没有公平地使用这些方法时,请注意增加无限期延期的风险。

内存一致性效应:在另一个线程中成功执行“获取”方法(如acquire()之前,调用“释放”方法之前的线程中的操作,例如release() happen-before 。

举个抢车位的例子:

现在有 6 个车子,但是只有 3 个停车位,所以需要限流处理。

package cn.itzhouq.add;

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class SemaphoreDemo {
    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();
        }
    }
}

结果:

1抢到车位
3抢到车位
2抢到车位
1离开车位
3离开车位
2离开车位
5抢到车位
4抢到车位
6抢到车位
6离开车位
5离开车位
4离开车位
    ...

细节:

semaphore.acquire(); // 得到。假设已经满了,就等待,等待被释放为止。

semaphore.release(); // 释放。会将当前的信号量释放 + 1,然后唤醒等待的线程!
作用: 多个共享资源互斥的使用!并发限流,控制最大的线程数!

posted @ 2020-04-22 22:03  itzhouq  阅读(313)  评论(0编辑  收藏  举报