• 问题整理


    一、HashMap 是不是线程安全?

    1 hashmap的put方法调用addEntry()方法,假如A线程和B线程同时对同一个数组位置调用addEntry,两个线程会同一时间片同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失
    2 HashMap的get操作可能因为resize而引起死循环
    HashMap的扩容机制就是重新申请一个容量是当前的2倍的桶数组,然后将原先的记录逐个重新映射到新的桶里面,然后将原先的桶逐个置为null使得引用失效,
    线程thread1执行到了transfer方法的Entry next = e.next这一句,然后时间片用完了,此时的e = [1,A], next = [2,B]。线程thread2被调度执行并且扩容 此时 [2,B]的next 为[1,A]在取链表的时候从是从尾部开始取,形成了环形链表,如果get的key的桶索引会陷入死循环

        1.1、如何变得安全:
              Hashtable:通过 synchronized 来保证线程安全的,独占锁,悲观策略。吞吐量较低,性能较为低下
              ConcurrentHashMap:JUC 中的线程安全容器,高效并发。ConcurrentHashMap 的 key、value 都不允许为 null
         1.2、jdk1.8相对于jdk1.7的优化
             由 数组+链表 的结构改为 数组+链表+红黑树。
             拉链过长会严重影响hashmap的性能,所以1.8的hashmap引入了红黑树,当链表的长度大于8时,转换为红黑树的结构
             优化了高位运算的hash算法:h^(h>>>16) 将hashcode无符号右移16位,让高16位和低16位进行异或。

    二、ConcurrentHashMap 的实现方式
    1.7
    ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问

    ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁
    put将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry
    1. 计算键所对应的 hash 值;2. 如果哈希表还未初始化,调用 initTable() 初始化,否则在 table 中找到 index 位置,并通过 CAS 添加节点。如果链表节点数目超过 8,则将链表转换为红黑树。如果节点总数超过,则进行扩容操作
    get将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上(get():无需加锁,直接根据 key 的 hash 值遍历 node),由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值
    ConcurrentHashMap的get方法采用了unsafe方法,来保证线程安全
    ConcurrentHashMap迭代器是强一致性,hashmap强一直性(ConcurrentHashMap可以支持在迭代过程中,向map添加新元素,而HashMap则抛出了ConcurrentModificationException)
         1.1、jdk1.8相对于jdk1.7的区别
                 jdk1.7:Segment+HashEntry来进行实现的;
                 jdk1.8:放弃了Segment臃肿的设计,采用Node+CAS+Synchronized来保证线程安全;
                 jdk1.8的实现降低锁的粒度,jdk1.7锁的粒度是基于Segment的,包含多个HashEntry,而jdk1.8锁的粒度就是Node(将 1.7 中存放数据的 HashEntry 改                       为 Node,但作用都是相同的)
    数据结构:jdk1.7 Segment+HashEntry;jdk1.8 数组+链表+红黑树+CAS+synchronized

    三、CountDownLatch 和 CyclicBarrier
    CountDownLatch和CyclicBarrier都有让多个线程等待同步然后再开始下一步动作
    countdownlatch中有个计数器,当计数器减少到0的时候,释放所有等待的线程,coutDown()会让计数器的值减少,await() 进入阻塞状态,直到count为0为止,所有等待的线程都会开始执行。
    而且CountDownLatch只有一次的机会,只会阻塞线程一次
    CyclicBarrier:是回环栅栏,只有等待线程积累到一定的数量的时候才会释放屏障,在释放屏障的时候还可以使用接口初始化 是可以重重复使用的

    四、怎么控制线程,尽可能减少上下文切换
    减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程
    无锁并发编程:多线程处理数据时,可以使用一些方法来避免使用锁。如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据(currenthashmap分段锁思想)
    CAS算法:Java的Atomic包使用CAS算法来更新数据,而不需要加锁
    协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换

    五、乐观锁和悲观锁
    悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁。synchronized和ReentrantLock等独占锁就是悲观锁思想的实现
    乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现
         1.1、乐观锁的ABA 问题
               如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能              的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题
         JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是       否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值

    六、并发特性 - 原子性、有序性、可见性
    原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
    可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
    有序性:即程序执行的顺序按照代码的先后顺序执行,不进行指令重排列
    1.原子性:提供互斥访问,串行线程(atomic,synchronized);
    2.可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
    3.有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则 A操作一定在B操作之前,而是A操作的影响能被操作B观察到)

    七、synchronized
    synchronized锁可以修饰在 普通方法中、静态方法中、代码块,
    synchronized是内置的语言实现,jvm编译器(monitor)去保证锁的加锁和释放 ,synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生
    解决多线程并发访问共享数据的竞争问题
    synchronized使用的锁对象是存储在Java对象头的Mark Word内,Mark Word存储对象的HashCode、分代年龄、锁
    其中锁分为:偏向锁、轻量级锁、自旋锁
    jdk1.6以后对synchronized做了优化, 如自旋锁、偏向锁、轻量级锁、自旋锁等技术来减少锁操作的开销
    一个线程获得了锁,进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作,即可获取锁,但锁竞争比较激烈的场合,偏向锁就失效
    转换为轻量级锁,不存在竞争, 轻量级锁失败后
    转换为自旋锁,若干次循环后 去竞争锁

    八、volatile
    变量定义为 volatile 之后 具备两种特性:保证此变量对所有的线程的可见性;禁止指令重排序优化
    volatile变量通过内存屏障是一个CPU指令,指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么

    九、JMM
    JMM 规定了线程的工作内存和主内存的交互关系,以及线程之间的可见性和程序的执行顺序
    一方面提供足够强的内存可见性保证
    一方面计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排
    在并发编程模式中 线程安全考虑会有3个概念:
    1、可见性
    可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值
    对于串行程序来说,可见性是不存在的在多线程环境中线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中共享变量x进行操作。
    2、有序性
    有序性是指对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的,但对于多线程环境,则可能出现乱序现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致多线程环境下,一个线程中观察另外一个线程,所有操作都是无序的
    3、原子性
    一个操作或者多个操作要么全部执行要么全部不执行。

    通过 volatile、synchronized、final、concurrent 包等 实现。

    十、队列 AQS 队列同步器
    AQS 是构建锁或者其他同步组件的基础框架(如 ReentrantLock、ReentrantReadWriteLock、Semaphore 等), 包含了实现同步器的细节(获取同步状态、FIFO 同步队列)。AQS 的主要使用方式是继承,子类通过继承同步器,并实现它的抽象方法来管理同步状态。
    维护一个同步状态 state。当 state > 0时,表示已经获取了锁;当state = 0 时,表示释放了锁。
    1、如果当前线程获取同步状态失败(锁)时,AQS 则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程
    2、当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态
    AQS 内部维护的是** CLH 双向同步队列**

    十一、锁的特性
    可重入锁:指的是在一个线程中可以多次获取同一把锁。 ReentrantLock 和 synchronized 都是可重入锁。
    可中断锁:顾名思义,就是可以相应中断的锁。synchronized 就不是可中断锁,而 Lock 是可中断锁。
    公平锁:即尽量以请求锁的顺序来获取锁。synchronized 是非公平锁,ReentrantLock 和 ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

    十二、ReentrantLock
    ReentrantLock可重入锁、显示锁ReentrantLock 提供了比synchronized 更强大、灵活的锁机制,可以减少死锁发生的概率
    ReentrantLock 实现 Lock 接口,基于内部的 Sync 实现
    Sync 实现 AQS ,提供了 FairSync(公平锁) 和 NonFairSync(非公平锁) 两种实现
    Condition 和 Lock 一起使用以实现等待/通知模式,通过 await()和singnal() 来阻塞和唤醒线程。

    十三、ReentrantReadWriteLock
    读写锁维护着一对锁,一个读锁和一个写锁。分离读锁和写锁
    在同一时间,可以允许多个读线程同时访问,但是,在写线程访问时,所有读线程和写线程都会被阻塞

    十四、Synchronized 和 Lock 的区别
    synchronized 是 Java 中的关键字,synchronized 是内置的语言实现。Lock 是一个接口 JDK自带
    synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock() 去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;
    Lock 可以让等待锁的线程响应中断,而 synchronized 却不行ReentrantLock 提供了更多,更加全面的功能,具备更强的扩展性。例如:时间锁等候,可中断锁等候,锁投票
    ReentrantLock 提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继续,否则可以等到下次运行时处理,而 synchronized则一旦进入锁请求要么成功要么阻塞,所以相比synchronized 而言,ReentrantLock 会不容易产生死锁些。
    ReentrantLock 支持中断处理

    十五、Java 中线程同步的方式
    sychronized 同步方法或代码块
    volatile、Lock、ThreadLocal、阻塞队列(LinkedBlockingQueue)、使用原子变量(java.util.concurrent.atomic)

    十六、多线程下为什么不使用 int 而使用 AtomicInteger。
    Concurrent 包下的类的源码时,发现无论是 ReentrantLock 内部的 AQS,还是各种 Atomic 开头的原子类,内部都应用到了 CAS
    CAS 中有三个参数:内存值 V、旧的预期值 A、要更新的值 B ,当且仅当内存值 V 的值等于旧的预期值 A 时,才会将内存值 V 的值修改为 B,否则什么都不干
    Unsafe 是 CAS 的核心类,Java 无法直接访问底层操作系统,而是通过本地 native` 方法来访问。不过尽管如此,JVM 还是开了一个后门:Unsafe ,它提供了硬件级别的原子操作
    valueOffset 为变量值在内存中的偏移地址,Unsafe 就是通过偏移地址来得到数据的原值的
    在多线程环境下,int 类型的自增操作不是原子的,线程不安全

    十七、线程池
    使用线程池目的:
    1、创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处理效率
    2、对线程进行一些简单的管理(延时执行、定时循环执行的策略等) 利于扩展
    3、线程并发数量过多,运用线程池能有效的控制线程最大并发数,防止抢占系统资源从而导致阻塞
    线程池有五种状态:RUNNING, SHUTDOWN, STOP, TIDYING, TERMINATED。
    参数:
    corePoolSize线程池中核心线程的数量
    maximumPoolSize 线程池中允许的最大线程数
    keepAliveTime线程空闲的时间,线程的创建和销毁是需要代价的。线程执行完任务后不会立即销毁,而是继续存活一段时间:keepAliveTime
    unit:keepAliveTime 的单位
    workQueue:用来保存等待执行的任务的阻塞队列 (可选ArrayBlockingQueue、LinkedBlockingQueue 等)
    handler:线程池的拒绝策略 (向线程池中提交任务时,如果此时线程池中的线程已经饱和了,而且阻塞队列也已经满了,则线程池会选择一种拒绝策略来处理该任务)

    十八、死锁与活锁的区别
    死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象
    产生死锁的必要条件:
    互斥条件:所谓互斥就是进程在某一时间内独占资源。
    请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
    循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
    活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败

    十九、FutureTask
    用ExecutorService启动任务,FutureTask表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法
    只有当运算完成的时候结果才能取回,如果运算尚未完成get方法将会阻塞

    二十、什么是竞争条件
    当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则我们认为这发生了竞争条件

    二十一、volatile 变量和 atomic 变量的不同
    Volatile不能保证原子性。例如用volatile修饰count变量那么 count++ 操作就不是原子性的
    AtomicInteger类提供的atomic方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作

    二十二、i++
    i++: 读值,+1,写值。在这三步任何之间都可能会有CPU调度产生,造成i的值被修改,造成脏读脏写。
    如果是方法里定义的,一定是线程安全的,因为每个方法栈是线程私有的。
    如果是类的静态成员变量,i++则不是线程安全的,每个线程需要对共享变量操作的时候必须先把共享变量从主内存load到自己的工作内存,登完成对共享变量的操作时再保存到主内存。如果一个线程运算完成后还没刷到主内存,此时这个共享变量的值被另一个线程从主内存读取到了,这个时候读取的数据就是脏数据了
    解决:使用循环CAS使用支持原子性操作的类AtomicInteger

    群交流(262200309)
  • 相关阅读:
    list转datatable,SqlBulkCopy将DataTable中的数据批量插入数据库
    Html.BeginForm 与Section、Partial View 和 Child Action
    e.stopPropagation();与 e.preventDefault();
    NPOI导出
    Excel导入导出(篇二)
    Excel导入导出,通过datatable转存(篇一)
    ajax请求加载Loading或错误提示
    jQuery UI dialog
    Zebra_Dialog 弹出层插件
    Google浏览器导出书签
  • 原文地址:https://www.cnblogs.com/webster1/p/12300801.html
Copyright © 2020-2023  润新知