多线程、并发基础

文章目录

      • 多线程基础
        • 进程、线程区别
        • 怎么理解
        • 为什么要用
        • 应用场景
        • 怎么用、创建方式
        • 守护线程怎么理解
        • 多线程的运行状态(关系图)
        • wait,notify 理解、区别
        • wait为什么要和synchronized一起使用
        • join,yield,sleep
        • 三大特性(原子性、可见性、有序性)怎么理解
        • callable、Future模式
      • 线程安全问题
        • JMM java内存模型怎么理解
        • JVM 内存区域划分
        • 线程安全问题是什么
        • 线程安全解决办法
        • 锁的内存语义
        • 死锁、产生原因、怎么解决
        • threadlocal怎么理解,能干什么,有什么问题
        • threadlocal内存泄漏
        • volatile 原理理解
        • volatile和synchronized区别
        • synchronized使用方法及原理
        • lock锁,用法,condition用法 lock锁与synchronized区别、重入锁、读写锁
        • 悲观锁、乐观锁
        • 原子类怎么理解
      • 并发包
        • 计数器countDownLatch
        • 屏障cyclicBarrier
        • 信号量semaphore
        • AQS(AbstractQueuedSynchronizer)
        • 阻塞容器-阻塞队列
        • 并发容器-非阻塞队列
      • 线程池
        • 为什么要用、使用场景
        • 涉及类简介
        • 分类、创建方式
        • 拒绝策略
        • 实现原理
        • 使用建议
        • 合理配置线程池的思路

多线程基础

进程、线程区别

  • 进程:程序运行的基本单位;进程是所有线程的集合
  • 线程:程序运行的最小单位;每一个线程是进程中的一条执行路径

怎么理解

为什么要用

提高程序运行效率

应用场景

下载、数据库连接池

怎么用、创建方式

  • 继承Thread、实现Runnble,覆写run方法
  • 实现callable 覆写call方法 该方法有返回值支持泛型T,可以抛出异常,通常与线程池一起使用
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<String> submit = executorService.submit(new Callable<String>(){
    @Override
    public String call() throws Exception {
        return "执行结果";
    }
});
String result = submit.get();

守护线程怎么理解

  • 线程分为守护线程(GC垃圾回收器)、非守护线程(用户线程)
  • main线程不是守护线程。当所有非守护线程都结束工作时,守护线程会随着JVM一同结束工作。
  • 线程start方法前,设置setDaemon(true)将该线程设置为守护线程。
    不要将读写、计算操作交给守护线程做,因为一旦非守护进程结束工作,守护线程就会自动结束,从而丢失工作,会发生逻辑问题。
    案列:守护线程往文件中输入内容,但还没开始写入主线程就结束工作了,守护线程随之结束
class TestRunnable implements Runnable{
    public void run(){
        try{
            Thread.sleep(1000);// ① 守护线程阻塞1秒后运行
            FileOutputStream os=new FileOutputStream(new File("daemon.txt"),true);
            os.write("daemon".getBytes());
        }
        catch(Exception e1){
            e1.printStackTrace();
        }
    }
}
public class DaemonTest {
    public static void main(String[] args) {
        Thread thread=new Thread(new TestRunnable());
        thread.setDaemon(true); //设置守护线程
        thread.start(); //开始执行分进程
    }
}

参考:Java中守护线程的总结

多线程的运行状态(关系图)

多线程、并发基础_第1张图片

wait,notify 理解、区别

wait 当前线程进行阻塞,放弃锁,将CPU的执行权让出
notify notifyAll 将持有该资源的阻塞状态的线程唤醒,并释放当前对象的锁(不是立马释放,此方法执行完毕才会释放),只是唤醒(让这些被唤醒的线程拥有重新获取锁的机会,而不是继续阻塞)
需与 synchronized 一起使用
参考:java锁之wait,notify

wait为什么要和synchronized一起使用

wait,notifyobject的方法,语义是释放锁,并阻塞当前线程(当前线程进入锁的阻塞队列,等待拥有这个锁的其他线程唤醒)。
synchronized 中维护了锁,其中有个monitor的概念,也叫监视器,每个对象都有monitor

  • 每一个对象都有一个与之对应的监视器
  • 监视器中维护了该对象的锁、阻塞队列、同步队列
  • 线程因 wait 方法而阻塞,是进入阻塞队列
  • 线程因竞争资源失败是进入同步队列
  • notify/notifyAll 是将阻塞队列中的线程加入到同步队列
    synchronized 底层对同步代码块进行加锁时,使用了monitorEnter、monitorExit,所以wait为什么要和synchronized一起使用

join,yield,sleep

join 相当于插队 让该线程先知行。 join 方法是synchroniezd关键字修饰的,底层原理是wait.
一个案列:配合守护线程、join 一起看

//thread1.join();
//若在主线程中执行此语句后,主线程运行thread1的join方法,主线程进入阻塞,一直等到thread1线程执行完毕,主线程再次执行。

class TestRunnable implements Runnable{
    public void run(){
        try{
            Thread.sleep(1000);//守护线程阻塞1秒后运行
            System.out.println("子线程写完了");
        }
        catch(Exception e1){
            e1.printStackTrace();
        }
    }
}
public class DaemonTest {
    public static void main(String[] args) throws Exception{
        Thread thread=new Thread(new TestRunnable());
        thread.setDaemon(true); //设置守护线程
        thread.start(); //开始执行分进程
        thread.join();
        System.out.println("hello");
    }
}

yield 暂停当前正在执行的线程,使其从运行状态切换到就绪状态,注意此方法可能最终没效果,因为转换到就绪状态后,依然有机会获取CPU的调度。

sleep 当前线程进入休眠,可指定时间,并没有释放锁,暂时让出CPU的资源,等到指定时间,再次得到CPU的调度

对比:
sleepThread的方法,需处理异常,不会释放锁;waitObject 的方法,会释放锁

三大特性(原子性、可见性、有序性)怎么理解

原子性:一个操作或多个操作要么全部执行,要么全部不执行;
可见性:多个线程在对一个变量进行操作时,该变量的值会对其他线程可见。Volatile
有序性:多线程程序执行时,为提高程序运行效率,运行编译器和处理器会对指令进行优化,即重排序。多线程下对指令顺序重排可能会产生问题

callable、Future模式

线程执行可以有返回值,以上有例子说明了Callable用法,但如果在主线程中等待子线程的执行结果,此时主线程时阻塞的,影响了程序的执行效率。
使用场景:图片的加载,刚开始加载图片时,图片未加载好,可显示为模糊。使用子线程1加载图片,子线程2等待加载结果,并将结果替换为原先模糊的图片,而不是使用主线程去等待子线程1的执行结果

线程安全问题

JMM java内存模型怎么理解

主内存、工作内存;定义了线程间通讯时,线程间的可见性。
工作方式:

  • 线程A修改私有变量,直接在该线程自有的工作内存修改即可
  • 线程A修改共享变量,需从主内存中复制数据到工作内存,然后修改数据,把数据再刷到主内存

JVM 内存区域划分

堆、栈(虚拟机栈、本地方法栈)、程序计数器、方法区

  • 程序计数器PC:保存的是程序当前执行的指令的地址
  • 栈:线程私有的区域,每一个栈帧即对应的一个调用的方法。方法的创建运行就是入栈和出栈的操作
  • 堆:公共内存区域,线程共享区域,垃圾回收的区域
  • 方法区:公共区域,线程共享,存储了类的信息(类名、方法名、变量、编译后的代码)
  • 运行时常量池:包括字符串,int(-128~127)
    除了PC计数器其他区域都有可能 outOfMemoryError,栈区域还会 stackOverFlowError
    多线程、并发基础_第2张图片

线程安全问题是什么

多线程环境下,共享变量值发生错乱,程序未按预期执行

线程安全解决办法

加锁、同步

锁的内存语义

当线程释放锁时,会将线程对应的本地内存空间的值刷新到主内存中;
当线程获取锁时,会将线程对应的本地内存空间的值置为无效,从而使得被监视器保护的临界区的代码需从主内从中去获取。

死锁、产生原因、怎么解决

多个线程竞争资源,锁无法释放
破坏死锁四个必要条件:请求和保持,不剥夺,循环等待,互斥条件

threadlocal怎么理解,能干什么,有什么问题

本地线程变量,每一个线程维护自己的本地变量,类似于Map数据结构实现,key为当前线程,valObj变量,支持泛型
数据库连接Connection。存在内存泄漏的风险

threadlocal内存泄漏

synchronized 可以理解为时间换空间;threadLocal 可以理解为空间换时间
当使用大量的线程时,这些线程都使用 threadLocal 本地线程变量,互不影响。假设程序运行结束,这些本地线程变量的 entry 没有被回收,threadLocal 内部实现是一个类Map的结构,threadLocalMap ,threadLocal引用了该map,那么很可能造成堆内存溢出。
其实内存泄漏的原因是entry,强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,导致value对应的Object一直无法被回收,产生内存泄露。

  • 当使用强引用,引用的 threadLocal 对象被回收,threadLocal 引用的 threadLocalMapentryk 为强引用,在GC时没有被回收,如果没有手动回收,产生内存泄漏;
  • 当使用弱引用,引用的 threadLocal对象被回收,threadLocal引用的threadLocalMapentryk为弱引用,在GC时被回收,那么k为空,entry变成 键为nullentry,导致entry不能被回收,最终value没被回收,产生内存泄漏;
  • 使用弱引用,还是会产生内存泄漏,但threadLocal提供的get、set、remove方法在使用时,会将内存中,knullv清除;
  • 无论使用弱引用还是强引用都会产生内存泄漏,但使用弱引用会多一层保证,即knull对应的v会在threadLocalMap调用set、get、remove时被清除

volatile 原理理解

1.为了解决多线程中可见性的问题,JMM中存在一个原则:Happens before 原则。不要求前一个操作在另一操作前发生,要求前一个操作的结果可以被后一个操作获取(对后一个操作可见)。
2.As if serial语义:为了提高程序运行效率,JVM可能会对代码进行指令重排序(不影响结果的前提下),它不会对有依赖关系的指令重排序,保证了在单线程下数据安全,且提高了运行效率。
3.CPU缓存一致性协议(MESI
高速运转的CPU进行数据读写时会与主内存进行交互,但内存的处理的速度与CPU处理的速度不是一个量级,差距很大。便在他们之间引入了缓冲区的概念。

CPU CACHE模型 程序局部执行原理
多线程、并发基础_第3张图片 多线程、并发基础_第4张图片

每一个CPU修改内存数据的步骤:
1)将数据从内存中复制一份到当前CPU的cache中
2)在cache中更新数据
3)将更新的数据刷新到内存中
多个cpu在操作主内存中同一个共享变量时,会出现数据不一致性的问题,解决办法如下:
1)总线加锁,粒度太大,不好控制
2)MESI缓存一致性协议
a.读操作:不做任何事情,将cache的值读到寄存器
b.写操作:通知其他CPU将该变量的cache line 置为无效,其他的cpu要访问这个变量的时候只能从内存中获取
4.原理
禁止指令重排序是添加了内存屏障;
可见性:对使用volatile修饰的共享变量进行写操作时,会使用CPU提供的lock前缀指令,主要做两件事:
1)将当前处理器缓存中的数据写回到系统内存;
2)这个回写操作会使 其他CPU中缓存该内存地址的数据置为无效
5.为什么不能保证原子性
对任意单个volatile变量的读/写具有原子性,对于类似volatile++的操作不具备原子性。
在《Java并发编程的艺术》中有这一段描述:“在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。”我们需要注意的是,这里的修改操作,是指的一个操作。
6.使用场景:
1)作为状态标志、开关等
2)双重检查锁定DCL(double-checked-locking),单例模式懒汉式中会用到

volatile和synchronized区别

  • volatile 保证变量可见性;禁止指令重排序,不能保证原子性,不会发生阻塞,底层使用Lock
  • synchronzied 作用在方法上,或同步代码块上,能保证变量的可见性,能保证原子性,可能因等待锁发生阻塞,底层使用 monitor
  • 都能保证有序性,但synchronzied在1.6之前代价较大,同步之后并行变成串行

synchronized使用方法及原理

1.使用
多线程、并发基础_第5张图片
2.原理
java对象头 和 monitor 是实现关键
例:使用同步代码块,锁主的是类对象

public class Demo1{
 private static int count = 0;
 public static void main(String[] args) {
 	synchronized (Demo1.class) {
 		inc();
 	}
 }
 private static void inc() {
 	count++;
 }
}

编译之后,查看字节码文件

public static void main(java.lang.String[]);
 descriptor: ([Ljava/lang/String;)V
 flags: ACC_PUBLIC, ACC_STATIC
 Code:
 stack=2, locals=3, args_size=1
 0: ldc #2 // class com/thread/SynchronizedDemo
 2: dup
 3: astore_1
 4: monitorenter //注意这个
 5: invokestatic #3 // Method inc:()V
 8: aload_1
 9: monitorexit //注意这个
 10: goto 18
 13: astore_2
 14: aload_1
 15: monitorexit //注意这个
 16: aload_2
 17: athrow
 18: return

占用 monitor 概述:
线程在获取锁的时候,实际上就是获得一个监视器对象(monitor) ,monitor 可以认为是一个同步对象,所有的Java 对象是天生携带 monitor。而monitor是添加Synchronized关键字之后独有的。synchronized同步块使用了monitorentermonitorexit指令实现同步,这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。 线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁,而执行monitorexit,就是释放monitor的所有权。其实wait/notify等方法也依赖于monitor对象,这就是为什么wait/notify只能配合synchronized使用。第二个monitorExit是异常出口。另外以上代码是同步代码块上的过程,若是同步方法,反编译后是没有monitorEntermonitorExit的,而是在方法上加了个标记,ACC_SYNCHRONIZEDJVM通过ACC_SYNCHRONIZED标识,就可以知道这是一个需要同步的方法,进而执行上述同步的过程。
占用monitor过程:
1.如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者
2.如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权

对象被创建在堆中,对象在内存中存储布局方式可以分为3个区域:
对象头(Header)、实例数据(Instance)、对齐填充(Padding)
多线程、并发基础_第6张图片
对象头中主要包括 类型指针标记字段
类型指针指向类元数据,虚拟机通过该指针确定这个对象是哪个类的实例对象
标记字段中存储对象自身运行时的数据,包括哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程Id。

使用同步来保证多线程并发时,数据安全问题。保证了安全性但降低了效率,从日常说法上来说,synchronized 是重量级锁,不过在JDK1.6之后对此进行了优化,锁的种类可以分为
无锁、偏向锁、轻量级锁、重量级锁。也就是synchronized在使用时并不是一直都是重量级锁,锁的状态会根据线程的竞争速度升级。

在大多数情况,加锁的代码块不仅仅不存在多线程竞争,而且总是由同一个线程多次获得。JDK1.6之后对此进行了优化,为了减少获得锁和释放锁带来的性能开销,引入了偏向锁和轻量级锁的概念。即锁的状态会根据线程的竞争速度升级。

  • 偏向锁
    存在同步代码块没有多线程访问以及锁经常由同一线程获取,引入了偏向锁。当一个线程访问了加了同步锁的代码块,会在对象头中存储当前线程的Id,后续这个线程进入和退出这段代码时,不需要再次获取锁和释放锁。当其他的线程想要访问这段同步代码时,尝试获取这个同步锁时,偏向锁会进行撤销,即持有偏向锁的线程会释放锁,偏向锁升级为轻量级锁。
    对原持有偏向锁的线程进行撤销时,会有两种情况:
    1.原获得偏向锁的线程如果已经退出了临界区,也就是同步代码块执行完了,那么这个时候会把对象头设置成无锁状态并且争抢锁的线程可以基于 CAS 重新偏向但前线程
    2.如果原获得偏向锁的线程的同步代码块还没执行完,处于临界区之内,这个时候会把原获得偏向锁的线程升级为轻量级锁后继续执行同步代码块
  • 轻量级锁
    轻量级锁在加锁过程中,用到了自旋锁。当其他线程想要访问同步代码,竞争锁时,这个线程会原地等待(原地自旋,进行空操作的for循环,不会阻塞),直到拥有锁的线程释放锁,这个线程可以马上获取锁。自旋锁,原地等待进行for循环操作时是会消耗CPU的,所以轻量级锁适用于同步代码块执行很快的场景,这样线程原地自旋等待很短的时间就能获取锁了。所以轻量级锁出现的条件就是同步代码执行的时间不能过长,否则等待锁的线程for循环太长时间反而会消耗CPU资源。JDK1.6之前自旋默认的次数是10次,1.6之后引入了自适应自旋锁,根据前一次在同一个锁上自旋等待的时长以及锁的拥有者决定。即上一次自旋等待时间较短,那么认为本次等待差不多的时间即可获取到锁,那么虚拟机认为此次等待获取到锁的概率很大,进而允许自旋等待相对更长的时间,反之亦然(等待时间太长,自旋多次太消耗CPU资源,锁升级为重量级锁,线程直接阻塞)。
    对原持有偏向锁的线程进行撤销时,会有两种情况:
    1.会使用原子的CAS操作替换对象头,如果成功,即没有竞争发生。如果失败,表示锁存在竞争,锁升级为重量级锁
  • 重量级锁
    线程被挂起,阻塞等待被唤醒

多线程、并发基础_第7张图片

lock锁,用法,condition用法 lock锁与synchronized区别、重入锁、读写锁

  • Lock锁
    1.与synchronized 异同
    syn 属于重量级锁,阻塞性的获取锁,Lock 可以非阻塞性的获取锁,且可中断性的获取锁(在等待锁的阻塞过程中终止);sync 锁的获取释放是根据代码的执行自动的,Lock 锁的获取与释放需要手动;sync 是互斥锁,Lock 可以实现互斥锁也可实现共享锁(多个线程获取同一把锁);ReentrantLockSynchronized都属于可重入锁
    2.ReentrantLock
    可重入锁,外层线程获取到锁后,进入内层代码时自动获取锁(锁对象一致),不会因请求锁而阻塞。可实现公平锁和非公平锁
    3.Condition
    AQS 内部类,可以实现比wait、notify更高级的功能。每一个Condition对象维护着一个同步队列和等待队列(FIFO),进入 Lock锁代码块中,调用condition.await()方法,此时持有该锁的线程进入等待队列,并释放锁。当其他线程调用signal()时,唤醒在该condition上注册的等待线程,即从在condition的等待队列中出列,等待这个线程lock方法释放完毕后,出列的线程,加入同步队列获取竞争锁的机会,成功获取锁之后从await()后执行。
    4.ReadWriteLock
    读写锁:读锁(线程共享,一般操作共享资源),写锁(互斥锁)
    锁降级:获取写锁、获取读锁、释放写锁、释放读锁。
    为什么先获取读锁在释放写锁
    目的:1.保证数据的可见性。存在变量i,线程A获取写锁,更改i的值,释放锁,线程B获取写锁(将变量赋值到当前线程的本地内存),更改i的值,其他线程无法感知变量的变化,除非变量提交到主内存,此时线程A获取读锁,获取到变量的值是从主内存获取的,却不是最新的。
    2.总的来说,锁降级就是一种特殊的锁重入机制,JDK 使用 先获取写入锁,然后获取读取锁,最后释放写入锁 这个步骤,是为了提高获取锁的效率,而不是所谓的可见性。 多线程、并发基础_第8张图片
    (https://www.showdoc.cc/server/api/common/visitfile/sign/03ca3dfc2e2dab6cc386e943ff979470?showdoc=.jpg)] 多线程、并发基础_第9张图片

悲观锁、乐观锁

悲观锁:对并发修改出现的情况持有悲观态度,即认为此种情况出现的情况较多,当前线程持有锁,其他线程不允许在持有锁,syn和写锁都属于悲观锁
乐观锁:对并发修改出现的情况持有乐观态度,或较少的出现,或者不加锁,CAS锁,read锁都属于乐观锁

原子类怎么理解

AtomicInteger为例,Integer的原子类,底层使用了CAS(compare and swap,比较并交换),无锁化编程,实际上使用了自旋锁。以对变量a进行自增的操作,++a并不是原子操作,但底层在此操作之后,调用了unsafe的compateAndSwap方法,该方法是native,基于的是CPU 的 CAS指令来实现的。
核心:变量v、valueOffset(指向变量的偏移地址,可理解为变量v的内存地址)、expectVal(期望值)、updateVal(更新值)

    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    private volatile int value;

使用了volatile修饰变量值,多线程可见。unsafe类是一个很特殊的类,功能比较特殊,可以操作对象属性、获得对象偏移地址、线程的挂起与恢复、CAS操作等。
获取unsafe实例,并获取对象的偏移地址。unsafeCAS的核心类,因为Java无法直接访问底层操作系统,而是通过本地(native)方法来访问,但JVM还是开了个后门,使用 unsafe,它提供了硬件级别的原子操作。

     /**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

以上是核心代码,原子类中的操作基本上都是基于这个来做的。

  • this 当前对象
  • valueOffset 偏移地址,unsafe类操作可得到value
  • expect 期望的值
  • update 更改的值
    若当前对象的值与期望值相同,则将该变量值修改为更改值。
/**
* Atomically sets to the given value and returns the old value.
*
* @param newValue the new value
* @return the previous value
*/
public final long getAndSet(long newValue) {
    return unsafe.getAndSetLong(this, valueOffset, newValue);
}

Unsafe.class:
    public final int getAndSetInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var4));

        return var5;
    }

进行do-while操作,不断的比较,原理就是这样。AtomicReference 支持泛型的,原理与Integer一样。
CAS存在的问题:
1.ABA问题
变量 1→2→1CAS并不知道变量经历了2的过程。加版本号、时间戳等方法解决。引入AtomicStampedReference(相比于AtomicReference加了时间戳)来解决这个问题
2.性能消耗问题
较多线程访问变量并更新时,自旋等待尝试更新却一直失败,循环往复,带来性能消耗。
3.不能保证代码块的原子性
只能保证单个变量原子性操作,多个变量一起操作的话,可以使用synchronized代替CAS

并发包

计数器countDownLatch

使一个线程等待其他线程全部执行完毕后再执行;通过一个计数器来实现,计数器的初始值是设置的线程的数量,每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,处于阻塞状态的线程恢复执行。

//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };
//将count值减1
public void countDown() { };

案列:

public class Demo1 {
    private static void tt(CountDownLatch countDownLatch){
        try {
            synchronized (Demo1.class){
                countDownLatch.countDown();//自减
                System.out.println("thread counts = " + (countDownLatch.getCount())+","+Thread.currentThread().getName());
                Thread.sleep(1000);
            }
            countDownLatch.await();
            System.out.println("所有线程执行结束" + countDownLatch.getCount()+","+Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws Exception{
        System.out.println("主线程开始执行");
        ExecutorService pool = Executors.newCachedThreadPool();
        CountDownLatch countDownLatch = new CountDownLatch(5);
        for (int i = 0;i<5;i++){
            pool.execute(()->{
                tt(countDownLatch);
            });
        }
        pool.shutdown();
        countDownLatch.await(2, TimeUnit.SECONDS);
        System.out.println("主线程结束执行");
    }
}
res:
主线程开始执行
thread counts = 4,pool-1-thread-1
thread counts = 3,pool-1-thread-5
主线程结束执行
thread counts = 2,pool-1-thread-4
thread counts = 1,pool-1-thread-3
thread counts = 0,pool-1-thread-2
所有线程执行结束0,pool-1-thread-5
所有线程执行结束0,pool-1-thread-3
所有线程执行结束0,pool-1-thread-4
所有线程执行结束0,pool-1-thread-1
所有线程执行结束0,pool-1-thread-2

屏障cyclicBarrier

循环屏障,循环往复可利用的屏障。所有线程都到达屏障处,才会继续往下执行。

//parties 是参与线程的个数
public CyclicBarrier(int parties)
//Runnable 参数,这个参数的意思是最后一个到达线程要做的任务
public CyclicBarrier(int parties, Runnable barrierAction)
//线程已到达屏障,线程挂起
public int await() throws InterruptedException, BrokenBarrierException
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException

案列:

public class Demo1 {
    static CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
    public static void main(String[] args) {
        ExecutorService pool = Executors.newCachedThreadPool();
        for(int i = 0;i<2;i++){
            pool.execute(()->{
                try {
                    synchronized (Demo1.class){
                        Thread.sleep(1000);
                    }
                    System.out.println("ready,"+Thread.currentThread().getName());
                    cyclicBarrier.await();
                    System.out.println("continue,"+Thread.currentThread().getName());
                }catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
        pool.shutdown();
    }
}
res:
ready,pool-1-thread-1
ready,pool-1-thread-4
ready,pool-1-thread-3
ready,pool-1-thread-2
continue,pool-1-thread-2
continue,pool-1-thread-4
continue,pool-1-thread-1
continue,pool-1-thread-3
  • CyclicBarrier 的用途是让一组线程互相等待,直到全部到达某个公共屏障点才开始继续工作。
  • CyclicBarrier 是可以重复利用的。
  • 在等待的只要有一个线程发生中断,则其它线程就会被唤醒继续正常运行。
  • CyclicBarrier 指定的任务是进行 barrier 处最后一个线程来调用的,如果在执行这个任务发生异常时,则会传播到此线程,其它线程不受影响继续正常运行。
    CountDownLatch区别:
  • CountDownLatch 是一个线程(或者多个),等待另外 N个线程完成某个事情之后才能执行;CyclicBarrierN个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。
    -CountDownLatch 的计数器只能使用一次。而 CyclicBarrier 的计数器可以使用 reset()方法重置;CyclicBarrier 能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。
  • CountDownLatch 采用减计数方式;CyclicBarrier 采用加计数方式。

信号量semaphore

也称之为许可证管理器。API其实都差不多。
acquire()、acquire(permits) 当前线程尝试去阻塞的获取 permits 个许可证。( permits >= 1)
此过程是阻塞的,它会一直等待许可证,直到发生以下任意一件事:
1.当前线程获取了n个可用的许可证,则会停止等待,继续执行。
2.当前线程被中断,则会抛出 InterruptedException 异常,并停止等待,继续执行。
acquierUninterruptibly()、acquireUninterruptibly(permits)
当前线程尝试去阻塞的获取 permits 个许可证。 ( permits >= 1)
此过程是阻塞的,它会一直等待许可证,直到发生以下:
当前线程获取了n个可用的许可证,则会停止等待,继续执行。
释放与此相对。

/**
 * 网上查询到,使用semaphore的一个很好的例子:
 * 食堂打饭案列:一共两个打饭窗口,20个学生按次序排队打饭。
 * 学生1-10:无论等待多久,都会等待,直到打到饭
 * 学生11-15:等待1s,超时则回宿舍吃泡面
 * 学生16-20:中断阻塞式排队打饭
 */
public class Demo2 {
    static class Student implements Runnable {
        private Semaphore semaphore;
        private int no;//编号
        private int type;//三类学生
        public Student(Semaphore semaphore, int no, int type) {
            this.semaphore = semaphore;
            this.no = no;
            this.type = type;
        }
        @Override
        public void run() {
            switch (type) {
                case 1:
                    try {
                        semaphore.acquire();
                        Thread.sleep(1000);
                        System.out.println(no + "号学生排队打到饭了");
                        semaphore.release();
                    } catch (Exception e) {
                    }
                    break;
                case 2:
                    try {
                        if (semaphore.tryAcquire(new Random().nextInt(10000), TimeUnit.MILLISECONDS)) {
                            System.out.println(no + "号学生排队打到饭了");
                            semaphore.release();
                        } else {
                            System.out.println(no + "号学生排队等了1s,没打到饭了,回宿舍吃饭");
                        }
                    } catch (Exception e) {
                    }
                    break;
                case 3:
                    try {
                        semaphore.acquire();
                        Thread.sleep(4000);
                        System.out.println(no + "号学生排队打到饭了");
                        semaphore.release();
                    } catch (Exception e) {
                        System.out.println(no + "号学生排队有异常被中断");
                    }
                    break;
                default:
                    break;
            }
        }
    }
    public static void main(String[] args) throws Exception {
        Semaphore semaphore = new Semaphore(2, true);
        Thread[] threadArr = new Thread[5];
        for (int i = 1; i < 21; i++) {
            if (i < 11) {
                new Thread(new Student(semaphore, i, 1)).start();
            } else if (i < 16 && i > 10) {
                new Thread(new Student(semaphore, i, 2)).start();
            } else {
                Thread thread = new Thread(new Student(semaphore, i, 3));
                threadArr[20 - i] = thread;
                thread.start();
            }
        }
        Thread.sleep(2000);
        for (Thread thread : threadArr) {
            thread.interrupt();
        }
    }
}

AQS(AbstractQueuedSynchronizer)

它提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件,常见的有:ReentrantLockCountDownLatch等
AQS是一个抽象类,主要是通过继承的方式来使用,它本身没有实现任何的同步接口,仅仅是定义了同步状态的获取以及释放的方法来提供自定义的同步组件。
多线程、并发基础_第10张图片
核心:
1.如果当前线程竞争锁失败,则将当前线程封装成node节点,添加到CLH同步队列,并阻塞该线程;

  • 竞争线程竞争失败,封装成节点进入队列
  • 通过CAStail重新指向新的尾部节点
    2.若当前线程释放锁,则会唤醒同步队列的一个阻塞的节点(线程)
  • 修改head节点指向新获得锁得节点
  • 将获取锁得节点得pre节点指向null
    [Java并发-同步器AQS]: https://www.cnblogs.com/hongdada/p/11698260.html “Java并发-同步器AQS”
    参考:[[1]Java并发-同步器AQS][Java并发-同步器AQS]

阻塞容器-阻塞队列

线程安全的支持阻塞性的添加或移除数据。

  • ArrayBlockingQueue
    1把锁ReentrantLock,2condition用于阻塞/唤醒takeput(wating takes,wating puts),维护了1个数组,2个下标用于添加元素和取出元素(take_index,put_index)size()int变量
  • LinkedBlockingQueue
    2ReentrantLock(take,put),2condition(take,put),链表中维护nodetake,put操作分别加锁,队列长度使用原子类维护
  • PriorityBlockingQueue
    支持优先级的任务添加队列
  • SynchronousQueue
    无缓冲阻塞队列,消费一个,才能加入一个
  • DelayQueue
    延迟执行

并发容器-非阻塞队列

ConcurrentLinkedQueue 基于链表的数据结构,使用CAS操作进行入队或出队的节点更新。使用 CAS 非阻塞算法解决了当前节点与 next 节点之间的安全连接和对当前节点的赋值。
添加:找到tail节点,调用 casNext(),只有一个线程会成功,并发下其他线程进行此操作会失败,自旋重新进行此操作。
移除:找到head节点,将其返回,并设置新的head节点(原head.next)。同样也是自旋进行该操作。
如何实现线程安全且非阻塞:保证可见性和原子性
内部维护 Node节点,单向的链表结构,设置 head,tail 节点,使用 volatile修饰,保证了可见性。
tail.casSetNext() CAS 设置尾节点保证操作原子性且非阻塞。

copyOnWriteArrayList 基于数组实现。应用于 读多写少 的场景。可能会出现脏读(读取时,有写入)。
关键字:ReentrantLock 、volatile array 读操作无锁,写操作加锁
初始化:初始化一个长度为0的数组
添加:copy一个数组,将长度+1,将添加的值赋值,并将copy的数组赋给array
移除:同上,也是copy一个数组,长度-1,并将新copy的数组赋给array
获取:同一般数组
即:在更新、移除、添加的操作会copy新数组来实现。

concurrentHashMap
1.7 与 1.8
1.7:
segment分段锁,每一个segment理解为一个片段(可重入锁),其内为数组+链表结构。不同的片段中,并发操作不影响,加锁的粒度较大。最大支持segment数目的并发。
put操作加锁,get不加锁(volatile)Node节点中,val、next都是volatile修饰。volatile只能报账基本数据类型值修改可见性,引用类型(数组,bean等)只能保证引用的可见性。
1.8:
1.7中segment加锁粒度太大,1.8concurrentHashMap1.8hashMap数据结构为基础,使用synchronized+cas+volatile来降低锁的粒度,锁住的是table的首节点。
get原理与1.7类似,put机制如下:

死循环table
1.table为空,初始化
	若是有其他线程也在对其初始化,该线程yield,有个标志位
	若是没有,CAS设置标志位,并对table进行初始化
2.根据hash值获取f=table[index]
3.若f为null
	CAS插入该node,casTabAt
4.检测标志位f.hash==moved
	helpTransfer帮助扩容
5.table不为null,且hash桶处有冲突,加同步锁,锁住f,将粒度降到最低
	内部有链表与树的转换
6.根据插入数据后判断是否扩容

HashTable
线程安全的,基本上所有方法上添加了synchronized关键字。数组(默认长度11)+链表
Entry< k,v >[] table;
count; //table数组中所有entry的个数,可理解为key的个数
capacity;//默认11
thresHold;//阈值 = capacityloadFactor
loadFactor;//负载因子 0.75
reHash;//扩容,数组逆向添加到新的(扩容过)数组中,一次扩容(2
n+1)
HashTabale< k,v > 根据k.hashCode 返回的int 值,在将此值与数组长度取模

int hash = key.hashCode
int index = (hash & 0x7FFFFFFF) % table.length    //做绝对值并取模
for(Entry e = table[index];e != null;e = e.next ){
	if(e.hash == hash && e.key == key){
		return e.v
	}
}

HashMap
线程不安全,数组+链表,链表长度>8,链表转为红黑树。put操作插入链表节点使用头插法
1.7在多线程扩容时会有链表死循环的可能,1.8无。1.7在扩容时,使用的头插法,1.8改为尾插法。

int hash = key.hashCode ^ (key.hashCode <<< 16) //低16位异或高16位
int index = hash & (table.length-1)  //等同于取模,二进制效率高
//避免冲突,分布均匀

线程池

为什么要用、使用场景

线程的的使用从创建开始到使用,最终到结束,都是需要人工去处理的,在使用时,从 new Thread() 开始 到 线程运行.start(),都需要去自己处理,并且创建完成之后,并没有对使用过的线程做后续处理,在使用新的线程,也是新创建,无法管理。
总而言之:提高复用性;解耦(线程的创建和执行分开);减少资源消耗(频繁手动创建)

涉及类简介

Executor 顶级接口
ExecutorService 次级接口,继承顶级接口
AbstractExecutorService 实现类,实现了基本所有的方法
ThreadPoolExecutor 继承上类

分类、创建方式

参数:corePoolSize(核心),maximumPoolSize(最大),keepAliveTime(存活时长),unit(左参数单位),workQueue(阻塞队列,存储任务)

FIXED:new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>())
SINGLED:new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>())
CACHED:new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>())
SCHEDULE:super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue())

阻塞队列简单分为:

  • LinkedListBlockingQueue 无界阻塞队列
  • ArrayBlockingQueue 有界阻塞队列
  • SynchronousQueue 无缓冲阻塞队列 ,出列一个才能入列一个,可以避免在执行有依赖关系的任务出现锁

拒绝策略

  • DiscardPolicy丢弃阻塞队列中的任务
  • AbortPolicy 丢弃阻塞队列中的任务,并抛出异常 (默认策略)
  • DiscardOldestPolicy 丢弃阻塞队列中最老的任务(最先进入的)
  • CallerRunspolicy 由调用线程执行该任务

实现原理

1.每来一个任务,就会创建一个线程去执行该任务,知道线程数到达核心数。
2.核心数到达之后,再来任务,则将任务添加到阻塞队列中,等待核心线程空闲获取去执行
3.核心数到达,且阻塞队列满之后,再来任务,那么再次创建线程执行该任务,直到线程池中线程数达到最大线程数
4.最大线程数到达之后,在来任务,则按照拒绝策略执行

使用建议

  • JDK作者建议使用Executors中提供的方法来创建线程池(FIXED,SINGLED..);
  • 阿里巴巴反之,不推荐使用Executors提供的方法创建,而是自行使用ThreadpoolExecutor自己定义
    如使用FIXED,SINGLE方式创建线程,因为阻塞队列无界,当任务量巨大时,任务积压在阻塞队列中,造成OOM
    如使用CACHED,SCHEDULE方式创建线程,允许创建的线程Integer.MAX 则允许创建大量的线程,造成OOM

合理配置线程池的思路

  • IO密集型 任务 CPU空闲,磁盘很忙 线程数 = 2 * CPU个数
  • CPU密集型 任务 需要耗费大量的CPU进行计算 线程数 = CPU个数

你可能感兴趣的:(多线程,Java,多线程,并发编程)