简单谈谈Juc并发编程——上

简单谈谈Juc并发编程——上

前言

本课程学习与B站狂神说Java的JUC并发编程

本课程的代码都放在了我的个人gitee仓库上了

什么是JUC?

  • java.util.concurrent juc
  • java.util.concurrent.atomic 原子性
  • java.util.concurrent.locks 锁

平时业务中可能用Thread

或者像Runnable接口实现,没有返回值,而且效率相对于callable较低

java.util.concurrent Interface Callable

进程与线程

我们都知道计算机的核心是CPU,它承担了所有的计算任务,而操作系统是计算机的管理者,它负责任务的调度,资源的分配和管理,统领整个计算机硬件

  • 进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体
  • 线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)

进程:一个程序,QQ.exe Music.exe 程序的集合; 一个进程往往可以包含多个线程,至少包含一个!

Java默认有几个线程? 2 个 main、GC 线程:开了一个进程 Typora,写字,自动保存(线程负责的)

对于Java而言:Thread、Runnable、Callable Java 真的可以开启线程吗? 开不了

// 本地方法,调用了底层的C++,因为java运行在jvm虚拟机上,Java无法直接操作硬件
private native void start0();
复制代码

线程有几个状态?

Thread.State可以看到,是一个枚举

扫描二维码关注公众号,回复: 13171533 查看本文章
   public enum State {
        //尚未启动的线程的线程状态
       //新生
        NEW,
		//运行
        RUNNABLE,
		//阻塞
        BLOCKED,
		//等待,死等
        WAITING,
       	//超时等待,过期不候
        TIMED_WAITING,
		//终止
        TERMINATED;
    }
复制代码

并发和并行

  • 并发是指两个或多个事件在同一时间间隔发生->交替进行
    • 一核cpu,模拟出来多个线程,快速交替运行
  • 并行是指两个或者多个事件在同一时刻发生 ->同时进行
    • 多核cpu,多个线程同时执行,线程池

并发编程的目标是充分的利用cpu的每一个核,以达到最高的处理性能

    //获取cpu的核数
    System.out.println(Runtime.getRuntime().availableProcessors());
复制代码

wait/sleep 区别

1、来自不同的类

wait => Object

sleep => Thread

2、关于锁的释放

wait 会释放锁

sleep 睡觉了,抱着锁睡觉,不会释放!

3、使用的范围是不同的

wait必须在synchronized同步代码块中使用

sleep可以在任何地方睡

4、是否需要捕获异常(存疑)

throws InterruptedException

wait 也需要捕获异常(实测提示需要捕获异常,且不捕获会报错!)

sleep 必须要捕获异常

Lock锁

只要是并发编程,就一定需要有锁!

传统的synchronized锁

此处不谈线程池,讲普通的方法

解耦线程类,不必要再去写一个单独的线程类继承Runnable接口

而是使用lambda表达式()->{}实现Runnable接口来创建线程

        new Thread(()->{
            //do something
        },"Name").start();
复制代码

然后synchronized锁方法上,锁住这个对象

        public synchronized void sale(){
            if(num<=0)return;
            System.out.println(Thread.currentThread().getName()+" 买到第"+(num--)+"张票,剩下"+num+"张票");
        }
复制代码

还是老生常谈的卖票

public static void main(String[] args) throws InterruptedException {

        Ticket ticket = new Ticket();

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

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

        new Thread(()->{
            for(int i=0;i<100;i++){
                ticket.sale();
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"C").start();
    }
    static class Ticket{
        private int num=100;
        public synchronized void sale(){
            if(num<=0)return;
            System.out.println(Thread.currentThread().getName()+" 买到第"+(num--)+"张票,剩下"+num+"张票");
        }
    }
复制代码

Lock锁

java.util.concurrent.locks.Lock的Lock是一个接口

建议的做法是始终立即跟随locktry块的通话

最常见的是在之前/之后的上锁lock.lock(); 和解锁lock.unlock()

它有几个实现类:

  • ReentrantLock可重入锁
  • ReentrantReadWriteLock.ReadLock读锁
  • ReentrantReadWriteLock.WriteLock写锁

我们来看看可重入锁ReentrantLock的构造器

   //默认创建非公平锁Nonfair
	public ReentrantLock() {
        sync = new NonfairSync();
    }
	//boolean参数为true创建公平锁Fair,反之创建非公平锁Nonfair
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
复制代码

公平锁:公平:需要先来后到 非公平锁:不公平:可以插队 (默认)

使用三部曲

  1. Lock lock=new ReentrantLock();实例化锁对象
  2. lock.lock();上锁
  3. finally中lock.unlock();解锁
    static class Ticket{
        private int num=100;
        //可重入锁
        Lock lock=new ReentrantLock();
        public void sale(){
            //上锁
            lock.lock();

            try{
                //do something 业务代码
                if(num<=0)return;
                System.out.println(Thread.currentThread().getName()+" 买到第"+(num--)+"张票,剩下"+num+"张票");
            }catch(Exception e){
                e.printStackTrace();
            }finally{
                lock.unlock();
            }
        }
    }
复制代码

synchronized和lock的区别

  1. Synchronized 内置的Java关键字, Lock 是一个Java
  2. Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁
  3. Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,死锁
    • 如果在Synchronized中出现异常,会自动释放锁
  4. Synchronized 线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去;
    • lock.tryLock()尝试获取锁 ,获取不到就自己掉头走了不会等下去
  5. Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以判断锁,非公平(可以自己设置)
  6. Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码!

生产者、消费者问题

面试:单例模式、排序算法、生产消费者、死锁

老版的synchronized实现

当num为0时,消费者等待,生产者生成消息

当num>=0时,生产者等待,消费者进行消费

我们先来看一下这段问题代码

    public static void main(String[] args) throws InterruptedException {
        Data data = new Data();
        new Thread(()->{
            for(int i=0;i<100;i++){
                try {
                    data.pro();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"ProducerA").start();

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

    }
    static class Data{
        private int num=0;

        public synchronized void pro() throws InterruptedException {
            if(num!=0){
                System.out.println(Thread.currentThread().getName()+"正在等待");
                this.wait();
            }
            num++;
            System.out.println(Thread.currentThread().getName()+" 生产者生产了一条消息,此时num="+num);
            this.notifyAll();
        }
        public synchronized void con() throws InterruptedException {
            if(num==0){
                System.out.println(Thread.currentThread().getName()+"正在等待");
                this.wait();
            }
            num--;
            System.out.println(Thread.currentThread().getName()+" 消费者消费了一条消息,此时num="+num);
            this.notifyAll();
        }
    }
复制代码

这个时候代码会正确运行嘛,结论是会的

那如果我们放置多个producer和consumer呢?

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

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

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

        new Thread(()->{
            for(int i=0;i<100;i++){
                try {
                    data.con();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"ConsumerB").start();
复制代码

可以看见有很大几率会出问题

ConsumerA 消费者消费了一条消息,此时num=-92 ConsumerA 消费者消费了一条消息,此时num=-93 ConsumerA 消费者消费了一条消息,此时num=-94 ConsumerA 消费者消费了一条消息,此时num=-95 ConsumerA 消费者消费了一条消息,此时num=-96 ConsumerB 消费者消费了一条消息,此时num=-97 ProducerB 生产者生产了一条消息,此时num=-96

正在等待

这里出现的就是虚假唤醒

查看Object的wait方法的api文档可以看见

线程也可以唤醒,而不会被通知,中断或超时,即所谓的虚假唤醒

比如说买货,如果商品本来没有货物,突然进了一件商品,这是所有的线程都被唤醒了 ,但是只能一个人买,所以其他人都是假唤醒,获取不到对象的锁

虽然这在实践中很少会发生,但应用程序必须通过测试应该使线程被唤醒的条件来防范,并且如果条件不满足则继续等待。

换句话说,等待应该总是出现在循环中

为什么if块会存在虚假唤醒的情况?

在if块中使用wait方法,是非常危险的,因为一旦线程被唤醒,并得到锁,就不会再判断if条件,而执行if语句块外的代码

所以建议,凡是先要做条件判断,再wait的地方,都使用while循环来做

	synchronized (obj) {
         while (<condition does not hold>)
             obj.wait(timeout);
         ... // Perform action appropriate to condition
     } 
复制代码

所以我们将原有的代码将if改为while

        public synchronized void pro() throws InterruptedException {
            while (num!=0){
                System.out.println(Thread.currentThread().getName()+"正在等待");
                this.wait();
            }
            num++;
            System.out.println(Thread.currentThread().getName()+" 生产者生产了一条消息,此时num="+num);
            this.notifyAll();
        }
        public synchronized void con() throws InterruptedException {
            while (num==0){
                System.out.println(Thread.currentThread().getName()+"正在等待");
                this.wait();
            }
            num--;
            System.out.println(Thread.currentThread().getName()+" 消费者消费了一条消息,此时num="+num);
            this.notifyAll();
        }
复制代码

juc版的生产者和消费者实现

使用Lock和Condition两个接口

其中lock对象我们这使用ReentrantLock实例化

condition对象使用lock.newCondition()获取

Condition实现可以提供Object监视器方法的行为和语义,例如有保证的通知顺序,或者在执行通知时不需要

锁定

一个Condition实例本质上绑定到一个锁。 要获得特定Condition实例的Condition实例,请使用其

newCondition()方法

Condition因素出Object监视器方法( wait,notify 和 notifyAll )成不同的对象,以得到具有多个等待集的每个对象,通过将它们与使用任意的组合的效果Lock个实现。

Lock替换synchronized方法和语句的使用, Condition取代了对象监视器方法的使用。

    static class Data{
        private int num=0;

        //获取锁,取代了wait和notify
        Lock lock=new ReentrantLock();
        //获取condition,取代synchronized
        Condition condition = lock.newCondition();

        public void pro(){
            lock.lock();
            try{

                while (num!=0){
                    System.out.println(Thread.currentThread().getName()+"正在等待");
                    //等待
                    condition.await();
                }
                num++;
                System.out.println(Thread.currentThread().getName()+" 生产者生产了一条消息,此时num="+num);
                //唤醒
                condition.signalAll();

            }catch(Exception e){
                e.printStackTrace();
            }finally{
                lock.unlock();
            }

        }
        public void con(){
            lock.lock();
            try{
                while (num==0){
                    System.out.println(Thread.currentThread().getName()+"正在等待");
                    condition.await();
                }
                num--;
                System.out.println(Thread.currentThread().getName()+" 消费者消费了一条消息,此时num="+num);
                condition.signalAll();
            }catch(Exception e){
                e.printStackTrace();
            }finally{
                lock.unlock();
            }

        }
    }
复制代码

也许会有人问:既然synchronized更简洁,这里反而还多加了一层condition,岂不是更麻烦了?

当然不是

Lock+Condition与synchronized的区别

设置多个Condition监视器可以实现精准的通知唤醒线程

个人理解:不用就等待,需要则唤醒

Condition监视器的精准唤醒

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

/**
 * @Author if
 * @Description: 使用Condition实现精准唤醒
 * @Date 2021-11-05 上午 12:04
 */
public class Test03 {
    public static void main(String[] args) {
        Data data=new Data();
        new Thread(()->{
            for(int i=0;i<10;i++){
                data.soutA();
            }
        },"A").start();
        new Thread(()->{
            for(int i=0;i<10;i++){
                data.soutB();
            }
        },"B").start();
        new Thread(()->{
            for(int i=0;i<10;i++){
                data.soutC();
            }
        },"C").start();
    }
    static class Data{

        private Lock lock=new ReentrantLock();
        private Condition condition1=lock.newCondition();
        private Condition condition2=lock.newCondition();
        private Condition condition3=lock.newCondition();
        private int num=1;

        public void soutA(){
            lock.lock();
            try{
                //num不为1时,condition1等待
                while(num!=1){
                    condition1.await();
                }
                System.out.println("AAAAAA");
                num=2;
                //A输出完后,唤醒condition2来输出B
                condition2.signal();
            }catch(Exception e){
                e.printStackTrace();
            }finally{
                lock.unlock();
            }
        }

        public void soutB(){
            lock.lock();
            try{
                //num不为2时,condition2等待
                while(num!=2){
                    condition2.await();
                }
                System.out.println("BBBBBB");
                num=3;
                //B输出完后,唤醒condition3来输出C
                condition3.signal();
            }catch(Exception e){
                e.printStackTrace();
            }finally{
                lock.unlock();
            }
        }

        public void soutC(){
            lock.lock();
            try{
                //num不为3时,condition3等待
                while(num!=3){
                    condition3.await();
                }
                System.out.println("CCCCCC");
                num=1;
                //B输出完后,唤醒condition1来输出A
                condition1.signal();
            }catch(Exception e){
                e.printStackTrace();
            }finally{
                lock.unlock();
            }
        }
    }
}
复制代码

结果

AAAAAA BBBBBB CCCCCC AAAAAA BBBBBB CCCCCC

其实condition还有awaitNanos超时等待和awaitUntil超时时间等待,下文ArrayBlockingQueue会讲到

八锁问题

1.锁对象的同步锁synchronized

import java.util.concurrent.TimeUnit;

/**
 * @Author if
 * @Description:
 *
 * 问题:main主线程中  没有  添加TimeUnit.SECONDS.sleep(1);之前,输出顺序?
 * 答题:看cpu的时间片分配,随机的(多开几个线程测试就行)
 *
 * 问题:mail方法中  没有  添加TimeUnit.SECONDS.sleep(4);之前,输出顺序?
 * 答题:先发邮件再打电话
 *
 * 问题:mail方法中添加了TimeUnit.SECONDS.sleep(4);后,输出顺序?
 * 答题:还是先发邮件再打电话,因为synchronized不会释放锁,直到代码结束才释放
 *
 * @Date 2021-11-05 上午 12:20
 */
public class Test1 {

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

        try{
            //main线程休眠
            TimeUnit.SECONDS.sleep(1);
        }catch(Exception e){
            e.printStackTrace();
        }

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

    static class Phone{
        //synchronized锁住调用者(实例化的对象)
        public synchronized void mail(){
            try{
                //休眠了锁也没被释放
                TimeUnit.SECONDS.sleep(4);
            }catch(Exception e){
                e.printStackTrace();
            }
            System.out.println("发邮件");
        }
        public synchronized void call(){
            System.out.println("打电话");
        }
    }
}
复制代码

2.synchronized和普通方法不同步

import java.util.concurrent.TimeUnit;

/**
 * @Author if
 * @Description:
 *
 * 问题:如果我将mail方法上synchronized,call方法不上,此时调用顺序
 * 答题:先打电话再发邮件,因为call不需要获得锁就能执行,只是需要等待main线程的1秒睡眠
 *
 * @Date 2021-11-05 上午 12:29
 */
public class Test2 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        /**
         * 问题:假如现在有两个phone,且mail和call方法都是synchronized时,调用俩对象的方法时的执行顺序
         * 答题:当然是按正常顺序来了,因为synchronized锁的调用者,即实例化对象,而这有两个不同的对象
         */
//        Phone phone1 = new Phone();
//        Phone phone2 = new Phone();
//        new Thread(()->{
//            phone1.mail();
//        },"A").start();
//        try{
//            //main线程休眠
//            TimeUnit.SECONDS.sleep(1);
//        }catch(Exception e){
//            e.printStackTrace();
//        }
//        new Thread(()->{
//            phone2.call();
//        },"B").start();


        new Thread(()->{
            phone.mail();
        },"A").start();

        try{
            //main线程休眠
            TimeUnit.SECONDS.sleep(1);
        }catch(Exception e){
            e.printStackTrace();
        }

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

    static class Phone{
        //synchronized锁住调用者(实例化的对象)
        public synchronized void mail(){
            try{
                //休眠了锁也没被释放
                TimeUnit.SECONDS.sleep(4);
            }catch(Exception e){
                e.printStackTrace();
            }
            System.out.println("发邮件");
        }
        //这里没有上同步锁,当然不受影响了
        public void call(){
            System.out.println("打电话");
        }
    }
}
复制代码

3.锁class的同步锁synchronized

import java.util.concurrent.TimeUnit;

/**
 * @Author if
 * @Description:
 *
 * 问题:mail和call方法改为static静态方法后,调用顺序
 * 答题:先短信再电话,因为此时锁住的Phone.class,而不是实例化对象
 *
 * 问题:实例化两个对象,phone1和phone2,调用顺序
 * 答题:还是先短信再电话,锁住的是Phone.class,而不是实例化对象
 *
 *
 * @Date 2021-11-05 上午 12:37
 */
public class Test3 {
    public static void main(String[] args) {

        Phone phone = new Phone();

        new Thread(()->{
            phone.mail();
        },"A").start();

        try{
            //main线程休眠
            TimeUnit.SECONDS.sleep(1);
        }catch(Exception e){
            e.printStackTrace();
        }

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


        Phone phone1 = new Phone();
        Phone phone2 = new Phone();

        new Thread(()->{
            phone1.mail();
        },"A").start();

        try{
            //main线程休眠
            TimeUnit.SECONDS.sleep(1);
        }catch(Exception e){
            e.printStackTrace();
        }

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

    }

     static class Phone{
        //synchronized锁住class,和对象相不相同没有关系,因为多个对象的class都是这同一个
        public static synchronized void mail(){
            try{
                TimeUnit.SECONDS.sleep(4);
            }catch(Exception e){
                e.printStackTrace();
            }
            System.out.println("发邮件");
        }
        public static synchronized void call(){
            System.out.println("打电话");
        }
    }
}
复制代码

4.静态synchronized和普通synchronized锁

import java.util.concurrent.TimeUnit;

/**
 * @Author if
 * @Description:
 *
 * 问题:此时mail是静态synchronized方法,call是普通synchronized方法,问执行顺序?
 * 答题:
 * 先打电话再发邮件,因为两把锁不同,mail虽然先start,但是他锁住class且睡眠4秒
 * 此时main线程的1秒已经结束,开始call的start,而call锁的对象锁,和mail的class锁不同
 * 所以不需要同步,则执行call
 *
 * @Date 2021-11-05 上午 12:46
 */
public class Test4 {
    public static void main(String[] args) {

        Phone phone = new Phone();

        new Thread(()->{
            phone.mail();
        },"A").start();

        try{
            //main线程休眠
            TimeUnit.SECONDS.sleep(1);
        }catch(Exception e){
            e.printStackTrace();
        }

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


    }

    static class Phone{

        public static synchronized void mail(){
            try{
                TimeUnit.SECONDS.sleep(4);
            }catch(Exception e){
                e.printStackTrace();
            }
            System.out.println("发邮件");
        }
        public synchronized void call(){
            System.out.println("打电话");
        }
    }
}
复制代码

总结

synchronized在代码块或者普通方法中,锁住的是方法的调用者(实例化对象)

静态方法中,锁住的类的class

对象锁和class锁不同,所以不需要同步

不安全的List类

我们之前使用的集合都是在单线程情况下,所以没有出现问题,但是其实很多都是不安全的

例如我们平时经常使用的ArrayList

        //多线程下的ArrayList插入报错
        // 并发修改异常:java.util.ConcurrentModificationException
        List<String> stringList=new ArrayList<>();
        for(int i=1;i<=100;i++){
            new Thread(()->{
                stringList.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(stringList);
            },i+"").start();
        }
复制代码

单线程玩多了,乍一看没什么问题,可是这是在多线程的情况下,就会出现并发修改异常

如何优化让他变成线程安全的呢?

1.使用Vector代替

Vector的增删改查都加上了同步锁synchronized,使得线程安全

但是效率怎么样呢?我们下文再说

        //多线程下的Vector没有报错,因为底层加了synchronized
        List<String> stringVector=new Vector<>();
        for(int i=1;i<=100;i++){
            new Thread(()->{
                stringVector.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(stringVector);
            },i+"").start();
        }
复制代码

2.使用Collections转synchronizedList

使用Collections.synchronizedList()方法将普通list转为线程安全的list

        //如果我想用安全的list呢
        //使用Collections.synchronizedList将普通list转为线程安全的list
        List<String> stringList=new ArrayList<>();
        List<String> synchronizedList = Collections.synchronizedList(stringList);
        for(int i=1;i<=100;i++){
            new Thread(()->{
                synchronizedList.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(synchronizedList);
            },i+"").start();
        }
复制代码

如何既保证线程安全,效率也高呢

使用JUC的CopyOnWriteArrayList

JUC:使用CopyOnWriteArrayList,解决并发

COW :写入时复制,一种优化策略

list是唯一固定的,多个线程读取时是固定的,但是写入时有可能会覆盖

COW写入时避免了覆盖,防止了数据问题

        /**
         * JUC:使用CopyOnWriteArrayList,解决并发
         */
        List<String> list = new CopyOnWriteArrayList<>();
        for(int i=1;i<=100;i++){
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(list);
            },i+"").start();
        }
复制代码

怎么解决的?

写入时先复制一份长度+1的数组,然后末尾插入数据,再把数组赋给原数组完成插入

插入源码为例

    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();
        }
    }
复制代码

CopyOnWriteArrayList比vector好在哪?

Vector 的增删改查方法都加上了synchronized锁,保证同步的情况下,每个方法都要去获得锁,所以性能会下降

CopyOnWriteArrayList 方法只是在增删改方法上增加了ReentrantLock锁

但是他的读方法不加锁,==读写分离==,所以在读的方面就要比Vector性能要好

CopyOnWriteArrayList适合读多写少的并发情况

不安全的Set类

和上面list差不多,我就不多做讲解了,直接贴代码

多线程下报错

        //并发修改异常:java.util.ConcurrentModificationException
        HashSet<String> set = new HashSet<>();
        for(int i=1;i<=100;i++){
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(set);
            },i+"").start();
        }
复制代码

解决方案

1.转synchronizedSet

        HashSet<String> set = new HashSet<>();
        Set<String> synchronizedSet = Collections.synchronizedSet(set);
        for(int i=1;i<=100;i++){
            new Thread(()->{
                synchronizedSet.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(synchronizedSet);
            },i+"").start();
        }
复制代码

2.使用CopyOnWriteArraySet

        Set<String> set = new CopyOnWriteArraySet<>();
        for(int i=1;i<=100;i++){
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(set);
            },i+"").start();
        }
复制代码

简单说明一下HashSet的实现

这里提一嘴HashSet的实现,说了不一定加分,但是说不出来一定扣分

本质就是就是new的HashMap,然后new的Object当做HashMap的value,add的参数当做key

因为是hash算法,所以HashSet是无序的

因为key不能重复,所以HashSet的的元素是不能重复的

    private transient HashMap<E,Object> map;
	private static final Object PRESENT = new Object();

	public HashSet() {
        map = new HashMap<>();
    }

    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
复制代码

不安全的Map类

单线程中我们经常使用的HashMap在多线程下也是不安全的

        //并发修改异常:ConcurrentModificationException
        HashMap<String, String> map = new HashMap<>();
        for(int i=1;i<=100;i++){
            new Thread(()->{
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));
                System.out.println(map);
            }).start();
        }
复制代码

1.用Hashtable代替

        Map<String, String> hashtable = new Hashtable<>();
        for (int i = 1; i <= 100; i++) {
            new Thread(() -> {
                hashtable.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 5));
                System.out.println(hashtable);
            }).start();
        }
复制代码

和之前的Vector代替ArrayList一样,用synchronized简单粗暴的加上同步保证线程安全,只是效率可能会低一些

2.转synchronizedMap

        HashMap<String, String> map = new HashMap<>();
        Map<String, String> synchronizedMap = Collections.synchronizedMap(map);
        for (int i = 1; i <= 100; i++) {
            new Thread(() -> {
                synchronizedMap.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 5));
                System.out.println(synchronizedMap);
            }).start();
        }
复制代码

3.使用ConcurrentHashMap

使用java.util.concurrent.ConcurrentHashMap

并发的HashMap,在保证了线程安全的情况下也保证了效率的高效,推荐使用

对ConcurrentHashMap不熟悉的小伙伴可以看看我的《简单谈谈ConcurrentHashMap》

        Map<String, String> concurrentHashMap = new ConcurrentHashMap<>();
        for (int i = 1; i <= 100; i++) {
            new Thread(() -> {
                concurrentHashMap.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 5));
                System.out.println(concurrentHashMap);
            }).start();
        }
复制代码

走进Callable

返回结果并可能引发异常的任务。 实现者定义一个没有参数的单一方法,称为call

Callable接口类似于Runnable ,因为它们都是为其实例可能由另一个线程执行的类设计的

然而,A Runnable不返回结果,也不能抛出被检查的异常

  • 可以有返回值
  • 可以抛出异常
  • 方法不同
    • Runnable是run()
    • Callable是call()

老版本创建线程的两种方式

1.extends Thread

public static void main(String[] args) {
        //老版方式:线程类继承Thread重写run方法,然后启动
        new MyThread().start();
    }

    static class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println("class MyThread extends Thread");
        }
    }
复制代码

2.实现Runnable接口

public static void main(String[] args) {
        //老版方式:线程类实现Runnable的run,将实例化对象放入Thread参数启动线程
        new Thread(new MyRun()).start();
    }
    static class MyRun implements Runnable{
        @Override
        public void run() {
            System.out.println("class MyRun implements Runnable");
        }
    }
复制代码

使用Callable创建线程

我们这里需要一个适配类FutureTask

这个类实现了RunnableFuture类,FutureTask<V> implements RunnableFuture<V>

RunnableFuture类继承了Runnable,RunnableFuture<V> extends Runnable, Future<V>

所以Thread(Runnable target)可以将其传入

注意:futureTask.get()可以获取返回结果,但是可能会抛异常,需要捕获或抛出

因为要等待执行完毕才返回,所以有可能会阻塞,最好把它放在最后,或者异步通信来处理

public static void main(String[] args) {
        /**
         * 使用Callable
         * 线程类实现Callable实现call()方法,这个方法可以有返回值,就是定义的泛型类型
         * 创建FutureTask,将线程类对象传入
         * 将FutureTask对象传入Thread再启动
         *
         * Thread(Runnable target)
         * FutureTask<V> implements RunnableFuture<V>
         * RunnableFuture<V> extends Runnable, Future<V>
         */
        MyCall callable = new MyCall();
        //适配类
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start();
        //获取返回结果,可能会抛异常
        //可能会阻塞,因为要等待执行完毕,所以最好把他放在最后,或者异步通信来处理
        try {
            Integer integer = futureTask.get();
            System.out.println(integer);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

    }

    static class MyCall implements Callable<Integer>{
        @Override
        public Integer call() throws Exception {
            System.out.println("class MyCall implements Callable<Integer>");
            return 1024;
        }
    }
复制代码

FutureTask的状态

如果我们此时用同一个FutureTask传入两条线程,会输出两次结果吗?

        MyCall callable = new MyCall();
        //适配类
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        //如果此时用同一个futureTask对象开启两条线程会有什么结果呢?
        //答案是:只会有一个线程进行运行,且只输出一次结果
        //因为FutureTask的state的状态从初始化NEW变了完成状态COMPLETING
        //然后在run方法中判断不为NEW则直接返回不执行了
        new Thread(futureTask,"A").start();
        new Thread(futureTask,"B").start();
复制代码

答案是:不会,只会执行一次!

为什么?

我们看看源码

可以看到FutureTask有一个state表示状态变量,还有很多int类型的常量表示具体状态

这里我们暂时只关注NEW和COMPLETING

    public class FutureTask<V> implements RunnableFuture<V> {
    private volatile int state;
    private static final int NEW          = 0;
    private static final int COMPLETING   = 1;
    private static final int NORMAL       = 2;
    private static final int EXCEPTIONAL  = 3;
    private static final int CANCELLED    = 4;
    private static final int INTERRUPTING = 5;
    private static final int INTERRUPTED  = 6;
复制代码

在构造器中,默认给state为NEW

    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }
复制代码

第一条线程进入

在run方法中,执行了callable的call方法后,会将判断变量ran设置为trueif (ran) set(result);

而在set方法中UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)

将NEW状态变为了COMPLETING

也就是说此条FutureTask已经完成了他的使命,变为COMPLETING完成状态

当下一条线程进来判断state != NEW时,直接return

所以执行了一次之后,其他的线程都无法继续执行run,也就是Callable的call方法了

所以可以得出结论:正常情况下,一个FutureTask只能执行一次call

    public void run() {
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }
复制代码

常用的辅助类

CountDownLatch减法计数器

java.util.concurrent.CountDownLatch

减法计数器

允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助类

可用于某些线程的强制执行

CountDownLatch用给定的计数初始化。 await方法阻塞,直到由于countDown()方法的调用而导致当前计数达到零,之后所有等待线程被释放,并且任何后续的await 调用立即返回。 这是一个一次性的现象 - 计数无法重置。 如果您需要重置计数的版本,请考虑使用CyclicBarrier 。

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

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

import java.util.concurrent.CountDownLatch;

/**
 * @Author if
 * @Description: 倒计时锁存器(减法计数器)
 * 每次调用countDown方法进行-1
 * 当总数不为0时,会一直阻塞下去
 * 可以用于线程的强制执行(因为不执行会阻塞)
 * @Date 2021-11-06 上午 12:00
 */
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        //count总数,用于倒计时
        //配合await方法,在倒计时结束前不会再向下执行代码
        CountDownLatch countDownLatch = new CountDownLatch(12);

        for(int i=1;i<=6;i++){
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("第"+i+"次执行countDown");
            countDownLatch.countDown();
//            new Thread(()->{
//                System.out.println(Thread.currentThread().getName()+"线程走了");
//                //数量-1
//                countDownLatch.countDown();
//            },i+"").start();
        }
        //等待计数器归零,然后再向下执行
        countDownLatch.await();
        System.out.println("关门");
    }
}
复制代码

CyclicBarrier加法计数器

java.util.concurrent.CyclicBarrier

加法计数器

允许一组线程全部等待彼此达到共同屏障点的同步辅助类

可以用于某些线程的强制等待

循环阻塞在涉及固定大小的线程方的程序中很有用,这些线程必须偶尔等待彼此。 屏障被称为循环 ,因为它可以在等待的线程被释放之后重新使用。

A CyclicBarrier支持一个可选的Runnable命令,每个屏障点运行一次,在派对中的最后一个线程到达之后,但在任何线程释放之前。 在任何一方继续进行之前,此屏障操作对更新共享状态很有用。

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

/**
 * @Author if
 * @Description: 循环屏障(加法计数器)
 * 使用await方法阻塞当前线程
 * 当执行的await方法次数达不到构造器传入的参数parties时,会一直阻塞下去
 * @Date 2021-11-06 上午 12:08
 */
public class CyclicBarrierDemo {
    public static void main(String[] args) throws InterruptedException, BrokenBarrierException {
        //集齐7颗龙珠召唤神龙
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
            System.out.println("集齐7颗龙珠召唤神龙成功");
        });
        for(int i=1;i<=7;i++){
            Thread.sleep(500);
            int finalI = i;
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"收集了第"+finalI+"颗龙珠");
                try {
                    //执行等待,直到等待了7次
                    cyclicBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            },i+"").start();
        }
    }
}
复制代码

Semaphore信号量

一个计数信号量,在概念上,信号量维持一组许可证。

如果有必要,每个acquire()都会阻塞,直到许可证可用,然后才能使用它。 每个release()添加许可证,潜在地释放阻塞获取方

但是,没有使用实际的许可证对象; Semaphore只保留可用数量的计数,并相应地执行

信号量通常用于限制线程数,而不是访问某些(物理或逻辑)资源

我们这假设有3个车位和6辆车需要停车,所以只能有3台车能停进去,其他的车需要等待车位空出才能停

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

/**
 * @Author if
 * @Description: 信号量
 * 参数permits许可证,可以用于限制线程数
 * @Date 2021-11-06 上午 12:17
 */
public class SemaphoreDemo {
    public static void main(String[] args) {
        //permits许可证
        //我们这假设有3个车位和6辆车需要停车
        Semaphore semaphore = new Semaphore(3);

        for(int i=1;i<=6;i++){
            new Thread(()->{
                try{
                    //acquire得到许可证
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"线程抢到车位");
                    TimeUnit.SECONDS.sleep(3);
                    System.out.println(Thread.currentThread().getName()+"线程离开车位");
                }catch(Exception e){
                    e.printStackTrace();
                }finally{
                    //release释放许可证
                    semaphore.release();
                }
            },i+"").start();
        }
    }
}
复制代码

猜你喜欢

转载自juejin.im/post/7031104891653193736