在上篇中我们深入学习了JUC中的lock锁与synchronized关键字的区别,以及一些关键问题,特点的探讨,这一篇我们继续进行JUC的学习。

线程安全是什么意思呢?

线程安全是指在多线程运行的情况下,不会导致代码逻辑顺序发生异常。

比如我们常常听说的超卖情况,明明100件产品却卖给了110个甚至更多的人,这就是线程不安全导致的,所以我们这篇文章就是要解决这个问题。

集合的安全性问题

我先附上一段代码,希望小伙伴们先理解如下代码

package com.test.rabbitmq.lockTest;

import java.util.ArrayList;
import java.util.UUID;

/**
 * @author ME
 * @date 2022/2/5 20:31
 */
public class Test {
    public static void main(String[] args) {
        // 创建集合对象
        ArrayList<String> list = new ArrayList<>();
        // 循环添加元素
        for (int i = 1; i <= 10; i++) {
            // 添加一个五位的随机数
            list.add(UUID.randomUUID().toString().substring(0, 5));
            //打印集合内容
            System.out.println(list);
        }
    }
}

上图代码正常执行是没有问题的,但是当我们将for循环中逻辑放入到多个线程中是否会有问题呢?代码如下

package com.test.rabbitmq.lockTest;

import java.util.ArrayList;
import java.util.UUID;

/**
 * @author ME
 * @date 2022/2/5 20:31
 */
public class Test {
    public static void main(String[] args) {
        // 创建集合对象
        ArrayList<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();
        }
    }
}

执行如上代码后,会出现ConcurrentModificationException(并发修改异常),说明ArrayList是不安全的,那么此时我们应该如何解决呢?

java线程安全的list集合 线程安全集合 java_后端

解决方法如下

package com.test.rabbitmq.lockTest;

import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * @author ME
 * @date 2022/2/5 20:31
 */
public class Test {
    public static void main(String[] args) {
        // 创建集合对象
        // 解决ArrayList线程不安全问题方案
        // 方案一:使用Vector集合,List<String> list = new Vector<>();
        // 方案二:使用集合工具类中的同步方法,List<String> list = Collections.synchronizedList(new ArrayList<>());
        // 方案三:List<String> list = new CopyOnWriteArrayList<>();
        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();
        }
    }
}

Vector集合如何实现线程安全的呢?

add方法上添加了synchronized关键字

java线程安全的list集合 线程安全集合 java_线程安全_02

 Collections.synchronizedList如何实现线程安全呢?

他锁住了整个集合对象

java线程安全的list集合 线程安全集合 java_juc_03

 CopyOnWriteArrayList如何实现线程安全呢?

他使用了JUC可重入锁,效率比synchronized关键字效率高

java线程安全的list集合 线程安全集合 java_后端_04

 Set集合同样是线程不安全的,想要让Set集合变得安全,与ArrayList同理

Collections.synchronizedSet方法,使用同步代码块。

CopyOnWriteArraySet方法,底层与CopyOnWriteArrayList调用同一个方法,都是用lock锁实现线程安全的。

Map集合也是线程不安全的,线程安全的Map集合为ConcurrentHashMap

CountDownLatch

减法计数器,当我们需要确定子线程全部结束后继续向下执行时,我们就可以使用CountDownLatch计数器,当计数器清零后继续向下执行。代码如下

package com.test.rabbitmq.lockTest;

import java.util.concurrent.CountDownLatch;

/**
 * @author ME
 * @date 2022/2/5 20:31
 */
public class Test {
    public static void main(String[] args) throws InterruptedException {
        // 创建线程计数器
        CountDownLatch count = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            // 循环创建线程执行逻辑
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "线程执行完毕");
                // 每一次子线程执行完毕后进行计数器的-1
                count.countDown();
            }).start();
        }
        //等待线程计数器为0时继续向下执行,如果不写的话不会校验计数器是否清零,直接继续执行
        count.await();
        System.out.println("子线程执行完毕");
    }
}

CyclicBarrier

加法计数器,当我们执行await()方法时,计数器加1,当达到了对象设置的最大值时,可以进行返回并执行对象设置的线程逻辑。代码如下

package com.test.rabbitmq.lockTest;

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

/**
 * @author ME
 * @date 2022/2/5 20:31
 */
public class Test {
    public static void main(String[] args) throws InterruptedException {
        // 线程计数器,参数:1.线程计数器计数到多大的值时会结束,2.当计数器达到了参数一的值后,我们可以执行再去执行一个线程
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5, ()->{System.out.println("5个子线程全部执行结束"); });
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "执行完毕");
                try {
                    // 计数器进行+1后进入等待
                    cyclicBarrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

Semaphore

线程信号量,可以用来限流,确定程序中可存在的最大线程数量,当线程数量达到了最大线程数量时,其他线程进行等待。

package com.test.rabbitmq.lockTest;

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

/**
 * @author ME
 * @date 2022/2/5 20:31
 */
public class Test {
    public static void main(String[] args) {
        // 限制了最大的线程数量为2个,可以添加第二个参数是否为公平锁/非公平锁
        // 当线程达到2个的时候,则开始等待,待一个线程运行结束后,再开启下一个线程
        Semaphore semaphore = new Semaphore(2);
        for (int i = 0; i < 5; 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();
                }

            }).start();
        }
    }
}

ReadWriteLock读写锁

当我们多个线程进行读写的时候,一般时候希望读的时候可以多个线程一起读取,但是写入的时候只能有一个线程进行写入,所以需要使用读写锁。

我们先来看一下不使用读写锁的情况

1.创建一个写入方法与读取方法

package com.test.lock.readWriteLock;

import java.util.HashMap;
import java.util.Map;

/**
 * @author ME
 * @date 2022/2/6 17:18
 */
public class MyData {
    private volatile Map<String, String> data = new HashMap<>();
    // 写入方法
    public void put(String key, String value) {
        System.out.println(Thread.currentThread().getName() + "写入了" + key);
        data.put(key, value);
        System.out.println(Thread.currentThread().getName() + "写入完成");
    }

    // 读取方法
    public void get(String key) {
        System.out.println(Thread.currentThread().getName() + "读取了" + key);
        String s = data.get(key);
        System.out.println(Thread.currentThread().getName() + "读取完成");
    }
}

2.多线程进行读写

package com.test.lock.readWriteLock;

/**
 * @author ME
 * @date 2022/2/6 17:18
 */
public class ReadWriteLock {
    public static void main(String[] args) {
        MyData myData = new MyData();
        // 10个线程进行写入操作
        for (int i = 0; i < 10; i++) {
            final int temp = i;
            new Thread(() -> {
                myData.put(String.valueOf(temp), String.valueOf(temp));
            }, String.valueOf(i)).start();
        }
        // 10个线程进行读取操作
        for (int i = 0; i < 10; i++) {
            final int temp = i;
            new Thread(() -> {
                myData.get(String.valueOf(temp));
            }, String.valueOf(i)).start();
        }
    }
}

执行结果如下

java线程安全的list集合 线程安全集合 java_java线程安全的list集合_05

使用读写锁进行改造

package com.test.lock.readWriteLock;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @author ME
 * @date 2022/2/6 17:18
 */
public class MyData {
    private volatile Map<String, String> data = new HashMap<>();
    // 创建一个读写锁,相比于ReentrantLock,颗粒度更小
    private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    // 写入方法
    public void put(String key, String value) {
        // 进行写锁上锁
        reentrantReadWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "写入了" + key);
            data.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写入完成");
        } finally {
            // 进行写锁解锁
            reentrantReadWriteLock.writeLock().unlock();
        }
    }

    // 读取方法
    public void get(String key) {
        // 进行读锁上锁
        reentrantReadWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "读取了" + key);
            String s = data.get(key);
            System.out.println(Thread.currentThread().getName() + "读取完成");
        } finally {
            // 进行读锁解锁
            reentrantReadWriteLock.readLock().unlock();
        }
    }
}

此时执行结果如下

java线程安全的list集合 线程安全集合 java_juc_06

读写锁总结:

看到这里的同学我觉得是赚到了,因为我在学习读写锁的时候想到了一个问题,奈何比较愚笨,思考了比较长的时间,接下来我就把这个问题分享出来。

我们要先提出一个概念——读锁是共享锁,写锁是排他锁

共享锁:可以多个线程进行共享的锁

排他锁:同一时刻只能由一个线程运行的锁

了解到了上面的概念后我想提出两个问题:

1.共享锁既然可以多个线程共享,那么为什么还需要加锁?

2.排他锁既然约束力同一时刻只能有一个线程运行,那他跟普通的锁有什么区别?

答:

1.我们要注意创建的读写锁是同一个对象,只是分别调用了不同方法才分出来读锁与写锁。读锁是共享锁,说明上了读锁后的线程,可以一起使用,这个锁是用来锁定此时的状态是读取状态,只有读取数据的线程才能一起共享,此时如果有写锁来争抢锁,则需要等待,而不是共享。

2.上面问题一的答案中说过了,读写锁是同一个对象,相比于普通的锁,他的颗粒度更细,不会像普通锁一样锁住数据,只有在写锁的时候才会锁定数据,且写入的时候不可读取,读取的时候不可写入,但是当写锁争抢到锁以后,他就与普通的锁没有了区别。

BlockingQueue阻塞队列

阻塞队列这里我单拿出了一篇文章

工作中需要用到的Java知识(Queue队列篇)

SynchronousQueue同步队列

同步队列与普通队列的区别在于同步队列中只能有一个元素,只有元素被取出后才能放入下一个元素。

新增元素put()方法;取出元素take()方法。