关于java:一文看懂JUC和高并发

54次阅读

共计 9540 个字符,预计需要花费 24 分钟才能阅读完成。

大家好,我是小菜,一个渴望在互联网行业做到蔡不菜的小菜。可柔可刚,点赞则柔,白嫖则刚!死鬼~ 看完记得给我来个三连哦!

本文次要介绍 JUC 和高并发

如有须要,能够参考

如有帮忙,不忘 点赞

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

一、Volatile

volatile 是 Java 虚拟机提供的轻量级的同步机制

1)保障可见性

JMM 模型的线程工作: 各个线程对主内存中共享变量 X 的操作都是各个线程各自拷贝到本人的工作内存操作后再协会主内存中。

存在的问题: 如果一个线程 A 批改了共享变量 X 的值还未写回主内存,这是另外一个线程 B 又对内存中的一个共享变量 X 进行操作,然而此时线程 A 工作内存中的共享变量对线程 B 来说事并不可见的。这种工作内存与主内存提早的景象就会造成了可见性的问题。

解决(volatile): 当多个线程拜访同一个变量时,一个线程批改了这个变量的值,其余线程可能立刻看到批改的值

2)不保障原子性

原子性: 不可分割、完整性,即某个线程正在做某个具体业务时,两头不能够被加塞或者被宰割,须要整体残缺,要么同时胜利,要么同时失败

解决办法:

  • 退出 synchronized
  • 应用 JUC 下的 AtomicInteger

3)禁止指令重排

指令重排: 多线程环境中线程交替执行,因为编译器优化重排的存在,两个线程中应用的变量是否保障一致性是无奈确定的,后果无奈预测。

指令重排过程: 源代码 -> 编辑器优化的重排 -> 指令并行的重排 -> 内存零碎的重排 -> 最终执行的指令

内存屏障作用:

  • 保障特定操作的执行程序
  • 保障某些变量的内存可见性(利用该个性实现 volatile 的内存可见性)

二、CAS

1)什么是 CAS

  • CAS 全称:Compare-And-Set , 它是一条 CPU 并发祥语
  • 它的性能就是_判断内存某个地位的值是否为预期值,如果是则更新为新的值_,这个过程是 原子 的。
  • CAS 并发祥语体现在 Java 语言中就是_sun.miscUnSafe_类中的各个办法,调用 UnSafe 类中的 CAS 办法,JVM 会帮我实现 CAS 汇编指令,这是一种齐全依赖于 硬件 性能,通过它实现了原子操作,再次强调,因为 CAS 是一种零碎源语,源语属于操作系统用于领域,是由若干个指令组成,用于实现某个性能的一个过程,并且源语的执行必须是间断的,在 执行过程中不容许中断,也即是说 CAS 是一条原子指令,不会造成所谓的数据不统一的问题

    public class CASDemo{public static void main(String[] args) {AtomicInteger atomicInteger = new AtomicInteger();
             System.out.println(atomicInteger.compareAndSet(0,5));       //true
             System.out.println(atomicInteger.compareAndSet(0,2));       //false
             System.out.println(atomicInteger);                          //5

 }

2)CAS 原理

public final int getAndIncrement() {return unsafe.getAndAddInt(this, valueOffset, 1); // 引出问题 --> 何为 unsafe
}

3)何为 UnSafe

  • UnSafe 是_CAS 的外围类_,因为 Java 办法无奈间接拜访底层,须要通过本地(native)办法来拜访,UnSafe 相当于一个前面,基于该类能够间接操作额外的内存数据。UnSafe 类在于_sun.misc_包中。其中外部办法能够向 C 的指针一样间接操作内存,因为 Java 中 CAS 操作的次要依赖于 UnSafe 类的办法
  • 变量 ValueOffset,是该变量在内存中偏移地址,因为_UnSafe 就是依据内存偏移地址来获取数据的_。
  • 变量 value 由 volatile 润饰,保障了多线程之间的可见性。

4)CAS 毛病

  1. 循环工夫开销很大

  1. 只能保障一个共享变量的原子性 当对一个共享变量执行操作的时候,咱们能够应用循环 CAS 的形式来保障原子操作,然而对多个共享变量操作时,循环 CAS 就无奈保障操作的原子性,这个时候就能够用锁来保障原子性。
  2. 存在 ABA 问题

5)ABA 问题

何为 ABA 问题 :在一个时间差的时段内会造成数据的变动。比如说一个线程 AA 从内存中取走 A,这个时候另一个线程 BB 也从内存中取走 A,这个时候 A 的值为 X,而后线程 BB 将 A 的值改为 Y,过一会又将 A 的值改为 X,这个时候线程 AA 回来进行 CAS 操作发现内存中 A 的值依然是 X,因而线程 AA 操作胜利。 然而只管线程 AA 的 CAS 操作胜利,然而不代表这个过程就是没问题的

原子援用

解决 :( 工夫戳原子援用:AtomicStampedReference

三、汇合类不平安问题

1)故障景象

呈现 java.util.ConcurrentModificationException 异样

2) 导致起因

public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1); // 引出问题 –> 何为 unsafe
}
并发争抢批改导致

 public static void main(String[] args) {
  List<String> stringList = new ArrayList<>();
  for (int i = 0; i < 30; i++) {
  new Thread(()->{
  stringList.add(UUID.randomUUID().toString().substring(0,8));
  System.out.println(stringList);
  },” 线程 ”+i).start();
  }
 }

3)解决办法

  • Vector:线程平安
  • Collections.synchronizedList(new ArrayList<>())
  • new CopyOnWriteArrayList<>()

    • List 线程:new CopyOnWriteArrayList<>();
    • Set 线程:new CopyOnWriteArraySet<>();
    • Set 线程:ConcurrentHashMap();

四、锁

1)偏心锁 / 非偏心锁

_定义_:

偏心锁: 是指多个线程依照申请锁的程序来获取锁,相似于排队,FIFO 规定 非偏心锁: 是指在多线程获取锁的程序并不是依照申请锁的程序,有可能后申请的线程比先申请的线程优先获到锁,在高并发的状况下,有可能造成优先级反转或者饥饿景象。

_两者的区别_:

并发包 ReentrantLock 的创立能够指定函数的 boolean 类型来失去偏心锁或者非偏心锁,默认是非偏心锁

偏心锁: 就是很偏心,在并发环境中,每个线程在获取锁时会先查看此锁保护的期待队列,如果为空,或者以后线程是期待队列的第一个,就占有锁,否则就会退出到期待队列中,当前会依照 FIFO 的规定从队列中抽取到本人。非偏心锁: 非偏心锁比拟粗鲁,上来就间接尝试占有锁,如果尝试失败,就再采纳相似偏心锁的那种形式。

就 Java ReentrantLock 而言,通过构造函数指定该锁是否是偏心锁,默认 非偏心锁,非偏心锁的长处在于吞吐量比偏心锁大,就 synchronized 而言,它是一种非偏心锁。

2)可重入锁(递归锁)

可重入锁也称之为 递归锁 ,指定是同一个线程外层函数取得锁之后,内层递归函数依然能获取该锁的代码,在同一个线程在外层办法获取锁的时候,在进入内层办法会主动获取锁。也就是说, 线程能够进入任何一个它曾经领有的锁所同步着的代码块

ReentrantLock 和 syschronized 就是一个典型的可重入锁

ReentrantLock 举例:

syschronized 举例:

3)自旋锁

是指尝试获取锁的线程不会立刻阻塞,而是采纳 循环 的形式去尝试获取锁,这样的益处是缩小线程上下文切换的耗费,毛病是循环会耗费 CPU。

下面的 CAS 问题中的 unsafe 用到的就是自旋锁。

 public final int getAndAddInt(Object var1, long var2, int var4) {
      int var5;
      do {var5 = this.getIntVolatile(var1, var2);
      } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
      return var5;
 }

_例子_:

4)独占锁(写)/ 共享锁(读)/ 互斥锁

  • 独占锁: 指该锁一次只能被一个线程所持有。对 ReentrantLock 和 Synchronize 而言都是独占锁。
  • 共享锁: 指该锁可被多个线程所持有。

对 ReentrantReadWriteLock 而言,其读锁是共享锁,其写锁是独占锁。读锁的共享锁能够保障并发度是十分高效的。读写,写读,写写的过程是互斥的。

_例子_:

5)CountDownLatch

  • 让一些线程阻塞直到另外一些线程实现后才别唤醒
  • CountDownLatch 次要有两个办法,当一个或多个线程调用await 办法时,调用线程会被阻塞,其余线程调用countDown 办法计数器减 1(调用countDown 办法时线程不会阻塞),当计数器的值变为 0,因调用await 办法被阻塞的线程会被唤醒,进而继续执行。

    _要害办法_:1)await() 办法 2)countDown() 办法

    _例子_:

    一个教室有 1 个班长和若干个学生,班长要等所有学生都走了能力关门,那么要如何实现。

6)CyclicBarrier

  • CyclicBarrier 的字面意思是可循环 (Cyclic) 应用的屏障 (Barrier)。它要做的事件是,让一组线程达到一个屏障(也能够叫做同步点)时被阻塞,晓得最初一个线程达到屏障时,屏障才会开门,所有被屏障拦挡的线程才会持续干活,线程进入屏障通过 CyclicBarrier 的 await() 办法。

    _例子_:

    跟下面一样,一个班级有六个学生,要等学生都来到后班长能力关门。

CountDownLatch 和 CyclicBarrier 其实是相同的操作,一个是相减到 0 开始执行,一个是相加到指定值开始执行

7)Semaphore

  • 信号量的次要用户两个目标,一个是用于 共享资源的互相排挤应用 ,另一个是用于 并发资源数的管制
  • 例子:抢车位问题,此时有六部车辆,然而只有三个车位的问题。

五、阻塞队列

概念: 阻塞队列,拆分为“阻塞”和“队列”,所谓阻塞,在多线程畛域,某些状况下会刮起线程(即线程阻塞),一旦条件满足,被挂起的线程优先被主动唤醒。Tread 1 往阻塞队列中增加元素,Thread 2 往阻塞队列中移除元素

  1. 当阻塞队列是空时,从队列中 获取 元素的操作将会被阻塞。
  2. 当阻塞队列是满时,从队列中 增加 元素的操作将会被阻塞。

1)品种

  1. ArrayBlockingQueue: 是一个基于 数组构造 的有界阻塞队列,此队列依照 FIFO(先进先出)规定排序。
  2. LinkedBlockingQueue: 是一个基于 链表构造 的有界阻塞队列(大小默认值为 Integer.MAX_VALUE),此队列依照 FIFO(先进先出)对元素进行排序,吞吐量通常要高于 ArrayBlockingQueue。
  3. SynchronusQueue: 是一个 不贮存元素 的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作始终处于阻塞状态,吞吐量通常要高于 LinkedBlockingQueue。
  4. PriorityBlockingQueue:反对优先级排序的无界阻塞队列
  5. DelayQueue:应用优先级队列实现的提早无界阻塞队列。
  6. LinkedTransferQueue:由链表构造组成的无界阻塞队列。

_吞吐量_:SynchronusQueue > LinkedBlockingQueue > ArrayBlockingQueue

2)应用益处

咱们不须要关怀什么时候胡须要阻塞线程,什么时候须要唤醒线程,因为 BlockingQueure 都一手给你办好了。在 concurrent 包,公布以前,在多线程环境下,咱们必须本人去管制这些细节,尤其还要兼顾效率和线程平安, 而这会给咱们的程序带来不小的复杂度

3)外围办法

办法类型 抛异样 非凡值 阻塞 超时
插入方法 add(o) offer(o) put(o) offer(o, timeout, timeunit)
移除办法 remove(o) poll() take() poll(timeout, timeunit)
查看办法 element() peek() 不可用 不可用
  • 抛异样:如果操作不能马上进行,则抛出异样
  • 非凡值:如果操作不能马上进行,将会返回一个非凡的值,个别是 true 或者 false
  • 始终阻塞:如果操作不能马上进行,操作会被阻塞
  • 超时退出:如果操作不能马上进行,操作会被阻塞指定的工夫,如果指定工夫没执行,则返回一个非凡值,个别是 true 或者 false

4)用途

  • 生产者消费者模式
  • 线程池
  • 消息中间件

_生产者消费者模式 – 传统版_:

_生产者消费者模式 – 阻塞队列版_:

六、线程池

概念: 线程池做的工作次要是管制运行的线程的数量,解决过程中将工作退出队列 ,而后在线程创立后启动这些工作, 如果线程超过了最大数量,超出的线程将排队等待 ,等其余线程执行结束,再从队列中取出工作来执行。 特点:

  • 线程复用
  • 管制最大并发数
  • 治理线程

长处:

  • 升高资源耗费,通过反复利用本人创立的线程减低线程创立和销毁造成的耗费。
  • 进步响应速度,当工作达到时,工作可不须要等到线程创立就能立刻执行。
  • 进步线程的可管理性,线程是稀缺西苑,如果无限度的创立,不仅会耗费系统资源,还会升高体统的稳定性,应用线程能够进行统一分配,调优和监控。

1)线程创立几种办法

1)_继承 Thead_

class ThreadDemo extends Thread{
    @Override
    public void run() {System.out.println("ThreadDemo 运行中...");
    }
    public static void main(String[] args) {ThreadDemo threadDemo = new ThreadDemo();
        threadDemo.start();}
}

2)_实现 Runnable 接口_

class RunnableDemo{public static void main(String[] args) {new Thread(new Runnable() {
            @Override
            public void run() {System.out.println("RunnableDemo 运行中...");
            }
        }).start();}
}

3)_实现 Callable_

public static void main(String[] args) throws ExecutionException, InterruptedException {FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {return 1;}
    });
    new Thread(futureTask).start();
    System.out.println(futureTask.get());
}

2)架构阐明

Java 中的线程池应用过 Excutor 框架实现的,该框架中用到了 ExecutorExecutorsExecutorServiceThreadPoolExecutor 这几个类。

3)重点理解

  • Executors.newFixedThreadPool()

    特点:

    1. 创立一个定长线程池,可控制线程的最大并发数,超出的线程会在队列中期待。
    2. newFixedThreadPool 创立的线程池 CorePoolSize 和 MaximumPoolSize 是相等的,它应用的是LinkedBlockingQueue
   public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads, 0, 
        TimeUnit.MICROSECONDS, new LinkedBlockingDeque<Runnable>());
    }
  • Executors.newSingleThreadExecutor()

    特点:

    1. 创立一个单线程化的线程池,它只会用惟一的工作线程来执行工作,保障所有工作都依照指定的程序执行。
    2. newSingleThreadExecutor 将 corePoolSize 和 MaximumPoolSize 都设置为 1,它应用的是LinedBlockingQueue
     public static ExecutorService newSingleThreadExecutor() {
        return new ThreadPoolExecutor(1, 1, 0,
        TimeUnit.MICROSECONDS, new LinkedBlockingDeque<Runnable>());
      }
  • Executors.newCachedThreadPool()

    特点:

    1. 创立一个可缓存线程池,如果线程池长度超过解决须要,可灵便回收闲暇线程,若无可回收,则创立新线程。
    2. newCacheThreadPool 将 corePoolsize 设置为 0,MaximumPoolSize 设置为 Integer.MAX_VALUE,它应用的是SynchronousQueue,也就是说来了工作就创立线程运行,如果线程闲暇超过 60 秒,就销毁线程
     public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60,
        TimeUnit.SECONDS, new SynchronousQueue<>());
      }

4)七大参数

参数 作用
corePoolSize 线程池中常驻外围线程数
maximumPoolSize 线程池可能包容同时执行的最大线程数,需大于 1
keepAliveTime 多余闲暇线程的存活工夫,当空间工夫达到 keepAliveTime 值时,多余的线程会被销毁直到只剩下 corePoolSize 个线程为止
TimeUnit keepAliveTime
workQueue 阻塞工作队列
threadFactory 示意生成线程池中工作线程的线程工厂,用户创立新线程,个别用默认即可
RejectedExecutionHandler 回绝策略,示意当线程队列满了并且工作线程大于线程池的最大显示数(maximumPoolSize)时如何来回绝

5)线程池工作原理

例子:

假如一家银行总共有六个窗口(maximumPoolSize),周末开了三个窗口提供业务办理(corePoolSize),下班期间来了 3 集体办理业务,三个窗口可能应酬的过去,这个时候又来了 1 个,三个窗口便忙不过来了,,只好让新来的客户去期待区(workQueue)期待,接下来如果还有来客户的话便让客户去期待区(workQueue)期待。然而如果期待区也坐满了。业务经理(threadFactory)便告诉剩下的窗口开启来进行业务办理,然而如果六个窗口都占满了,而且期待区也坐不下了。这个时候银行便要思考采纳什么形式(RejectedExecutionHandler)来回绝客户。工夫缓缓的过来了,办理业务的客户也差不多走了,只剩下 3 个客户在办理。这个时候闲暇了 3 个新增的窗口,他们便开始期待(keepAliveTime)肯定工夫,如果工夫到了还没有客户来办理业务的话,这 3 个新增窗口便能够敞开,回去劳动。然而原来的三个窗口(corePoolSize)还得持续开着。

6)回绝策略

期待队列曾经排满,再也塞不下新的工作,而且也达到了 maximumPoolSize 数量,无奈持续为新工作服务,这个时候咱们便要采取回绝策略机制正当的解决这个问题。以下内置回绝策略均实现了 RejectExecutionHandler 接口

  • AbortPolicy(默认)

间接抛出 RejectedException 异样来阻止零碎失常运行。

  • CallerRunPolicy

“调用者运行”一种调节机制,该策略既不会摈弃工作,也不会抛出异样。线程调用运行该工作的 execute 自身。此策略提供简略的反馈管制机制,可能减缓新工作的提交速度。

  • DiscardOldestPolicy

摈弃队列中期待最久的工作,而后把当前任务退出队列中尝试再次提交(如果再次失败,则反复此过程)。

  • DiscardPolicy

间接抛弃工作,不予任何解决也不抛出异样,如果容许工作失落,这是最好的回绝策略。

7)为何不必 JDK 创立线程池的办法

阿里巴巴 java 开发手册 【强制】线程资源必须通过线程池提供,不容许在利用中自行显示创立线程。阐明:应用线程池的益处是 缩小在创立和销毁线程上所耗费的工夫以及系统资源的开销,解决资源有余的问题。如果不应用线程池,有可能造成零碎创立大量同类线程而导致耗费完内存或者“适度切换”的问题。 【强制】 线程池不容许应用 Executors 去创立,而是通过 ThreadPoolExecutor 的形式,这样的解决形式让写的同学更加明确线程池的运行规定,躲避资源耗尽的危险。

  1. FixedThreadPoolSingleThreadPool:容许的申请队列长度为 Integer.MAX_VALUE,可能会沉积大量的申请,从而导致 OOM。
  2. CacheThreadPoolScheduledThreadPool:容许创立线程的数量为 Integer.MAX_VALUE,可能会创立大量的线程,从而导致 OOM。例子:

8)合理配置线程池

_CPU 密集型_:

  • 查看本机 CPU 核数:Runtime.getRuntime().availableProcessors()
  • CPU 密集的意思是该工作须要大量的运算,而没有阻塞,CPU 需始终全速运行。
  • CPU 密集工作只有在真正的多核 CPU 上才可能失去减速(通过多线程)
  • CPU 密集型工作配置尽可能少的线程数量 => 公式:CPU 核数 + 1 个线程的线程池

_IO 密集型_:

  • 因为 IO 密集型工作线程并不是始终在执行工作,则应配置尽可能多的线程,如 CPU 核数 * 2
  • IO 密集型,是阐明该工作须要大量的 IO,即大量的阻塞。所以在单线程上运行 IO 密集型的工作会导致节约大量的 CPU 运算能力节约在期待上,所以要应用多线程能够大大的减速程序运行,即便在单核 CPU 上,这种减速次要就是利用了被节约掉的阻塞工夫。
  • 配置线程公式:CPU 核数 / 1- 阻塞系数(0.8~0.9)=> 如 8 核 CPU:8 / 1 – 0.9 = 80 个线程数

七、死锁编码及定位剖析

1)什么是死锁

死锁是指两个或两个以上的过程在执行过程中,因抢夺资源而造成的一种相互期待的景象,如果无外力的干预那么它们将无奈推动上来,如果零碎的资源短缺,过程的资源申请都可能失去满足,死锁呈现的可能性就很低,否则会因抢夺无限的资源而陷入死锁。

2)造成起因

  • 资源零碎有余
  • 过程运行推动的程序不适合
  • 资源分配不当

_例子_:

打印后果 陷入死锁状态:

3)解决办法

  • jps 命令定位过程编号

  • jstack 找到死锁查看

[END]

以上便是 Java 中 JUC 和高并发 的大略知识点啦!路漫漫,小菜与你一起求索~

明天的你多致力一点,今天的你就能少说一句求人的话!

我是小菜,一个和你一起学习的男人。 ????

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

正文完
 0