并发编程一些基础知识

并发编程三要素:

  • 原子性:一个操作集要么全部成功,要么全部失败
  • 可见性:一个线程对共享线程变量修改,另一个线程能够立刻看到
  • 有序性:程序执行顺序按照代码先后顺序执行。

解决方法:

  • 原子类(Atomic),synchronized,lock,可以解决原子性问题。
  • synchronized,volatile,lock,可以解决可见性问题
  • Happeds-Before 规则可以解决有序性问题

进程与线程

  • 进程
    内存中应用程序都有自己的一块内存空间,一个进程有多个线程
  • 线程
    负责当前进程中任务的执行,一个进程至少有一个线程,同进程中的多线程共享数据。
    区别
    1.进程是操作系统资源分配的基本单位,线程是处理器任务调度和执行的基本单位
    2.同一个进程中线程资源共享,而进程间资源是独立的。
    3.一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃后整个进程都会死掉。

守护线程与用户线程

  • 用户线程:运行在前台,执行具体任务,如程序的主线程、连接网络的子线程都是用户线程
  • 守护线程:运行在后台,为其他前台线程服务,一旦所有用户线程结束,守护线程会随JVM一起结束工作。
    main 函数就是一个用户线程,main函数启动同时在JVM内部还启动了很多守护线程,比如垃圾回收。

线程创建

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口
  • 使用Excutors工具类创建线程池(一般不会使用,后面会详解)
  • ThreadPoolExecutor自定义线程池(推荐使用)

run() start() 区别

start() 方法用于启动线程,run()方法用于执行线程执行时的代码,run()方法可以重复调用,start()只能调用一次
为什么我们需要先调用start,再调用run?
new Thraed 线程进入新建状态。调用start()方法,会启动一个线程并使线程进入就绪状态,当分配到时间片就可以运行了。start()会执行线程的相应准备工作,然后自动执行run()方法的内容。
而直接执行run()方法,会把run方法当成一个main线程下的普通方法执行,并没有指定哪个线程执行它。

线程生命周期状态

在这里插入图片描述

  • 新建
  • 可运行
    线程新建后,调用线程对象的start()方法,该线程处于就绪状态,等待被线程调度选中,获得CPU使用权
  • 阻塞
    处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直至进入到就绪状态
    • 等待阻塞
      运行状态的线程调用wait()方法,JVM会把该线程放入等待队列中,使本线程进入到等待阻塞状态。
    • 同步阻塞
      线程在获取synchronized同步锁失败(被其它线程加锁),JVM会把该线程放入锁池中,线程会被同步阻塞
    • 其它阻塞
      当调用线程的sleep()或join或发出了I/O请求时,线程会进入阻塞状态。
  • 死亡
    线程run、main方法结束后,或者异常退出了,则结束生命周期

CPU调度算法

  • 分时调用
    让所有线程都能轮流获取CPU资源,并且平均分配每一个线程占用cpu的时间片
  • 抢占式调用
    是指优先让可运行池中优先级高的线程占用cpu,如果有相同的,那么就随机选择一个。

线程调用相关方法

  • wait()
    使一个线程处于等待状态,并且释放当前对象的锁
  • sleep()
    使一个正在运行的线程处于睡眠状态,不会释放当前锁,需要处理异常
  • notify()
    唤醒一个处于等待状态的线程,当然在调用此方法时,并不能确定唤醒哪个等待状态的线程,而是JVM决定
  • notifyAll():唤醒所有等待状态的线程,释放出来竞争CPU资源。

sleep()和wait()有什么区别
两者都能暂停线程

  • 类不同
    sleep是Thread的静态方法,wait是Object类方法
  • 是否释放锁
    sleep不释放锁,wait释放锁
  • 用途不一样
    wait通常用于线程间通信,sleep被用于暂停执行

synchronized是如何工作的

在java虚拟机中,每一个对象通过某种逻辑关联监视器,每个监视器和一个对象引用相关联,为了实现监视器互斥功能,每个对象都关联着一把锁。
一旦方法或者代码块被synchronized修饰,那么这个部分的代码就会被放入监视器区域,确保一次只能有一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分的代码。

如果提交任务是,线程池队列已满,会发生什么?

1.如果是无界队列LinkedBlockingQueue,会继续添加任务到阻塞队列中等待执行(高并发,多调用链路时会有OOM的问题,这也就是为什么不建议用Excutors创建线程池,因为创建出的线程池默认都是无界的)。
2.如果是使用有界队列比如:ArrayBlockingQueue,任务会首先被添加到ArrayBlockingQueue中,如果ArrayBlockingQueue满了,会根据maximimPookSize增加线程数量,如果增加了线程数量还是不行,则会执行拒绝策略(默认是丢弃执行)。

一般如何获取dump文件

1.首先使用ps -ef | grep “java” 获取java进程相关信息
2.使用jmap来获取dump信息 jmap -dump:format=b,file=/home/app/dump.out 17740
3.使用eclipse memory analyzer分析

并发理论

如果一个对象引用被设置为null,垃圾收集器是否会立即释放对象占用的内存?
不会,而是在下一次垃圾回收才会释放。

finalize()

GC决定回收某对象时,就会执行该对象finalize方法,在垃圾回收时会调用被回收对象的finalize方法,可以覆盖此方法来实现对其资源的回收。这里的顺序是第一次回收时并不会回收占用内存空间,而是调用finalize,第二次回收时才会回收对象内存
finalize使用

并发关键字

synchronized

synchronized 用来控制线程同步,就是在多线程环境下,synchronized控制的代码段不能被多个线程执行。
ps:synchronized在java1.6前是重量级锁,在1.6后经过自旋、锁消除、锁粗化,偏向锁、轻量级锁等来减少锁的开销

synchronized底层实现原理
在执行同步代码块之前之后都有一个 monitor字样,其中前面的是monitorenter,后面的是离开 monitorexit,不难想象一个线程也执行同步代码块,首先要获取锁,而获取锁的过程就是 monitorenter,在执行完代码块之后,要释放锁,释放锁就是执行 monitorexit指令

锁自旋
很多 synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和內核态切换的冋题。既然 synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized的边界做忙循环,这就是自旋。如果做了多次循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。

锁升级
synchronized锁升级原理:在锁对象的对象头里面有一个 threadid字段,在第一次访问的时候 threadid为空,jvm让其持有偏向锁,并将 threadid设置为其线程id,再次进入的时候会先判断 threadid是否与其线程id一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized锁的升级锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在Java6之后优化synchronized的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。

volatile

对于可见性,Java提供了 volatile夭键字来保证可见性和禁止指令重排。
volatile提供 happens- before的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
从实践角度而言, volatile的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见 javautil. concurrent atomic包下的类,比如 AtomicInteger volatile常用于多线程环境下的单次操作单次读或者单次写)。

为什么转换为重量级锁就很耗时

主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。

synchronized与volatile区别

  • volatile是变量修饰符; synchronized可以修饰类、方法、变量。
  • volatile仅能实现变量的修改可见性,不能保证原子性;而 synchronized则可以保证变量的修改可见性和原子性。
  • volatile不会造成线程的阻塞; synchronized可能会造成线程的阻塞。
  • volatile标记的变量不会被编译器优化; synchronized标记的变量可以被编译器优化。
  • volatile关键字是线程同步的轻量级实现,所以 volatile性能肯定比synchronized关健字要好。但是 volatile关键字只能用于变量而
  • synchronized关键字可以修饰方法以及代码块。 synchronized关键字在JavaSE16之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏冋锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized关键字的场景还是更多—些。

Lock体系

lock对比 synchronized优势

  • 可以使锁更加公平
  • 可以使线程在等待锁时响应中断
  • 可以让线程尝试获取锁,并在无法获取锁的时候立刻返回或等待一段时间
  • 可以在不同范围内,以不同顺序获取/释放锁
    整体上来说Lock是 synchronized的扩展版,Lock提供了无条件的、可轮询的tryLock方法)、定时的 (tryLock带参方法)、可中断的( ockInterruptibly)、可多亲件队列的( new Condition方法)锁操作。另外Lock的实现类基本都支持非公平锁默认和公平锁, synchronized只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。

CAS

CAS是—种基于锁的操作,而且是乐观锁
悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁釆取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加 version来获取数据,性能较悲观锁有很大的提高。
CAS操作包含三个操作数一一内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B.CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。
java util. concurrent atomic包下的类大多是使用CAs操作来实现的
(AtomicInteger, Atomic Boolean, AtomicLong)

CAS可能带来的问题

  • ABA 问题
    比如说一个线程one从内存位置v中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但可能存在潜藏的问题。从Java1.5开始JK的 atomic包里提供了一个类 AtomicStampedReference来解决ABA问题。
  • 自旋时间长开销大
    对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的cpU资源,效率低于 synchronized
  • 只能保证一个共享变量的原子操作
    当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作但是对多个共享变量操作时,循环cAS就无法保证操作的原子性,这个时俣就可以用锁。

AQS

AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 Reentrantlock, Semaphore,其他的诸如ReentrantReadwritelock, SynchronousQueue, FutureTask等等皆是基于AQS的。
在这里插入图片描述
AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。

ReentrantLock(可重入锁)

Reentrantlock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。

并发容器

ConcurrentHashMap

ConcurrentHashMap是Java中的一个线程安全且高效的 HashMap实现。平时涉及高并发如果要用map结构,那第一时间想到的就是它。相对于 hashmap来说, ConcurrentHashMap就是线程安全的map,其中利用了锁分段的思想提高了并发度。
JDK1.6

  • segment继承了 Reentrantlock充当锁的角色,为每一个 segment提供了线程安全的保障;
  • segment维护了哈希散列表的若干个桶,每个桶由 HashEntry构成的链表。
    JDK1.8 ConcurrentHashMap抛弃了原有的 Segment分段锁,而采用了CAS +
    synchronized来保证并发安全性。
    弃用原因:
    1.加入多个分段锁浪费内存空间。
    2.生产环境中, map 在放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待。
    3.为了提高 GC 的效率。
    ps: ConcurrentHashMap并发度为hash桶的大小(因为多线程可同时对一个hash桶的不同值做修改)

CopyOnWriteArrayList

CopyOnWrite ArrayList(免锁容器的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModification Exception。在 CopyOnWrite ArrayList中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。
缺点:

  • 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的內寳比较多的情况下,可能导致 young gc或者 full gce
  • 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然 CopyOnWriteArrayList能做到最终一致性但是还是没法满足实时性要求。
  • 由于实际使用中可能没法保证 Copyon WriteArrayList到底要放置多少数据,万一数据稍微有点多,每次add/set都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。

CopyOnWrite ArrayList的设计思想:
1.读写分离,读和写分开。
2.最终一致性。
3.使用另外开辟空间的思路,来解决并发冲突。

线程池

这里不推荐在线上使用Executors,原因:

  • newFⅸedThreadpool和 newSingleThreadExecutor:
    主要问题是堆积的请求处理队列可能会耗费非常大的內存,甚至OOM
  • newCachedThreadPool #A newScheduledThreadPool:
    主要问题是线程数最大数是 Integer. MAX VALUE,可能会创建数量非常多的线程,甚至OOM。

ThreadPoolExecutor

参数

ThreadpoolExecutor3个最重要的参数:

  • corePoolSize:核心线程数,线程数定义了最小可以同时运行的线程数量。
  • maximumpoolsize:线程池中允许存在的工作线程的最大数量。
  • workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放在队列中。

ThreadpoolExecutor其他常见参数

  • keepAliveTime:线程池中的线程数量大于 corePoolsize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毀,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
  • unit:keepAliveTime参数的时间单位。
  • threadFactory:为线程池提供创建新线程的线程工厂
  • handler:线程池任务队列超过 maxinumpoolsize之后的拒绝策略

执行流程:
在这里插入图片描述

注意

  1. 核心线程数内的线程是不会被回收的(除非设置allowCoreThreadTimeOut = true)
  2. 核心线程数外的线程是会被回收的(超过keepAliveTime)

执行结果是如何通过Future返回的

   Future<Integer> future = Executors.newSingleThreadExecutor().submit(() -> 1);
   // 打印返回的future对象的具体类
   System.out.println(f.getClass());

输出如下:

class java.util.concurrent.FutureTask

JDK1.8看FutureTask中get方法的实现:

    /**
     * @throws CancellationException {@inheritDoc}
     */
    public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        return report(s);
    }

    /**
     * @throws CancellationException {@inheritDoc}
     */
    public V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException {
        if (unit == null)
            throw new NullPointerException();
        int s = state;
        if (s <= COMPLETING &&
            (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
            throw new TimeoutException();
        return report(s);
    }

report方法如下:

    /**
     * Returns result or throws exception for completed task.
     *
     * @param s completed state value
     */
    @SuppressWarnings("unchecked")
    private V report(int s) throws ExecutionException {
        Object x = outcome;
        if (s == NORMAL)
            return (V)x;
        if (s >= CANCELLED)
            throw new CancellationException();
        throw new ExecutionException((Throwable)x);
    }

总的来说就是根据state变量判断任务是否执行完成(包括正常和异常),如果未完成就等,正常完成就返回结果,其他情况抛出对应异常。

并发工具

CountDownLatch与 CyclicBarrier

CountDownLatch 与 CyclicBarrier都是用于控制并发的工具类,都可以理解成維护的就是一个计数器,但是这两者还是各有不同侧重点的:

  • CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;而 Cyclic Barrier一般用于一组线程互相等待至某个状态,然后这组线程再同时执行; CountDownLatch强调一个线程等多个线程完成某件事情。 CyclicBarrier是多个线程互等,等大家都完成,再携手共进。
  • 调用CountDownLatch的 countDown方法后,当前线程并不会阻塞,会继续往下执行;而调用 Cyclic Barrier的awat方法,会阻塞当前线程,直CyclicBarrier指定的线程全部都到达了指定点的时候,才能继续往下执行;
  • CountDownLatch方法比较少,操作比较简单,而 CyclicBarrier提供的方法更多,比如能够通过 getNumber Waiting0, isBroken这些方法获取当前多个线程的状态,并且 Cyclic Barrier的构造方法可以传入 barrier Action,指定当所有线程都到达时执行的业务功能;
  • CountDownLatch是不能复用的,而CyclicLatch是可以复用的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值