JUC多线程

JUC多线程

线程状态

NEW(新建)

Runnable(就绪)

BLocked(阻塞)

Waiting(等待)

  1. 不见不散
  2. 过时不候 timed_waiting

Terminated(终结)

wait、sleep区别

  1. wait在Object类,sleep在Thread类
  2. sleep不释放锁,wait释放锁
  3. 都可以被interrupted方法打断

并发和并行

串行

按先后顺序执行

并行

多个任务一起执行,之后再汇总(比如泡方便面,一遍撕调料包一遍烧水)

并发

同一时刻多个线程访问同一个资源,多个线程对一个点(秒杀、抢票)

管程(monitor)

锁、是一种同步机制,保证了同一时刻只有一个线程访问被保护的数据或代码

jvm同步基于进入和退出,使用管程对象实现的,管程对象对于临界区加锁

用户线程、守护线程

用户线程:普通线程、自定义的线程(new Thread)

守护线程:运行在后台,如垃圾回收

主线程结束后,用户线程还在运行,JVM存活,如果没有用户线程,都是守护线程,JVM结束

多线程编程步骤

  1. 创建资源类,在资源类创建属性和操作方法
  2. 在资源类写操作方法(判断、实现、通知也就是notify)
  3. 创建多个线程,调用资源类的操作方法
  4. 防止虚假唤醒问题

Synchronized

如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

  1. 获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
  2. 线程执行发生异常,此时JVM会让线程自动释放锁。

如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。

锁升级

针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式,就是先使用偏向锁优先同一线程然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁

Lock

Lock和Synchronized区别

  1. Lock是一个类,synchronized是关键字
  2. synchronized可以自动释放锁,Lock需要手动释放锁,若没有主动释放锁,可能造成死锁
  3. Lock可以让等待锁的线程中断,synchronized等待的线程会一直等待下去
  4. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到
  5. Lock 可以提高多个线程进行读操作的效率。竞争资源 非常激烈时(即有大量线程同时竞争), Lock 的性能要远远优于 synchronized。

Lock的方法

Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}

线程间通信

wait在哪里睡在哪里醒,如果不放while中判断,会造成虚假唤醒

Lock方法实现

Lock 锁的 newContition()方法返回 Condition 对象,Condition 类 也可以实现等待/通知模式。

用 notify()通知时,JVM 会随机唤醒某个等待的线程, 使用 Condition 类可以 进行选择性通知。

  • await()会使当前线程等待,同时会释放锁,当其他线程调用 signal()时,线程会重 新获得锁并继续执行。
  • signal()用于唤醒一个等待的线程。
  • 在调用 Condition 的 await()/signal()方法前,也需要线程持有相关 的 Lock 锁,调用 await()后线程会释放这个锁,在 singal()调用后会从当前 Condition 对象的等待队列中,唤醒 一个线程,唤醒的线程尝试获得锁, 一旦 获得锁成功就继续执行。

线程间定制化通信

请添加图片描述

//第一步 创建资源类
class ShareResource {
    //定义标志位
    private int flag = 1;  // 1 AA     2 BB     3 CC

    //创建Lock锁
    private Lock lock = new ReentrantLock();

    //创建三个condition
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();

    //打印5次,参数第几轮
    public void print5(int loop) throws InterruptedException {
        //上锁
        lock.lock();
        try {
            //判断
            while(flag != 1) {
                //等待
                c1.await();
            }
            //干活
            for (int i = 1; i <=5; i++) {
                System.out.println(Thread.currentThread().getName()+" :: "+i+" :轮数:"+loop);
            }
            //通知
            flag = 2; //修改标志位 2
            c2.signal(); //通知BB线程
        }finally {
            //释放锁
            lock.unlock();
        }
    }

    //打印10次,参数第几轮
    public void print10(int loop) throws InterruptedException {
        lock.lock();
        try {
            while(flag != 2) {
                c2.await();
            }
            for (int i = 1; i <=10; i++) {
                System.out.println(Thread.currentThread().getName()+" :: "+i+" :轮数:"+loop);
            }
            //修改标志位
            flag = 3;
            //通知CC线程
            c3.signal();
        }finally {
            lock.unlock();
        }
    }

    //打印15次,参数第几轮
    public void print15(int loop) throws InterruptedException {
        lock.lock();
        try {
            while(flag != 3) {
                c3.await();
            }
            for (int i = 1; i <=15; i++) {
                System.out.println(Thread.currentThread().getName()+" :: "+i+" :轮数:"+loop);
            }
            //修改标志位
            flag = 1;
            //通知AA线程
            c1.signal();
        }finally {
            lock.unlock();
        }
    }
}

public class ThreadDemo3 {
    public static void main(String[] args) {
        ShareResource shareResource = new ShareResource();
        new Thread(()->{
            for (int i = 1; i <=10; i++) {
                try {
                    shareResource.print5(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"AA").start();

        new Thread(()->{
            for (int i = 1; i <=10; i++) {
                try {
                    shareResource.print10(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"BB").start();

        new Thread(()->{
            for (int i = 1; i <=10; i++) {
                try {
                    shareResource.print15(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"CC").start();
    }
}

集合的线程安全

对应解决方案

ArrayList

ArrayList线程不安全,其add方法底层没有synchronized关键字

解决方案

  • Vector

     List<String> list = new Vector<>();
    
  • Collections

    List<String> list = Collections.synchronizedList(new ArrayList<>());
    
  • CopyOnWriteArrayList(写时复制技术,支持并发读,独立写)

    List<String> list = new CopyOnWriteArrayList<>();
    
Hashset

用CopyOnWriteArraySet

HashMap

用HashTable/ConcurrentHashMap

CopyOnWriteArrayList

写时复制技术:每次加入新的内容,都将原来的复制一份,然后再向其中加入,最后覆盖合并之前的内容

请添加图片描述

多线程锁

synchronized 实现同步的基础:Java 中的每一个对象都可以作为锁。

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的 Class 对象。
  • 对于同步方法块,锁是 Synchonized 括号里配置的对象

公平锁和非公平锁

非公平锁:会导致线程饿死,但是效率高

//Lock演示非公平锁,两种都是
Lock lock =new ReentrantLock();
Lock lock =new ReentrantLock(false);

公平锁:所有线程都可能运行,效率低

//公平锁
Lock lock =new ReentrantLock(false);

可重入锁

synchronized(隐式)和Lock(显式)都是可重入锁

递归锁

所有层共享一个锁,可以递归进入

死锁

两个或两个以上进程在执行过程中,因为争夺资源而造成一种互相等待的现象,若无外力干涉,则无法继续执行下去

产生原因

  1. 互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
  2. 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
  3. 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
  4. 循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有。

查看是否死锁

  1. jps 类似Linux的 ps -ef
  2. jstack:jvm自带堆栈跟踪工具

Callable接口

Runnable与Callable区别

RunnableCallable
是否有返回值×
是否抛出异常×
实现方法名称run()call()

实现Runnable接口

class MyThread1 implements Runnable {
    @Override
    public void run() {

    }
}
//Runnable接口创建线程
new Thread(new MyThread1(),"AA").start();

实现Callable接口

不能直接按照Runnable的方法创建,直接替换 runnable,因为 Thread 类的构造方法根本没有 Callable

需要找一个类,既和Runnable有关系,又和Callable有关系,即FutureTask

//实现Callable接口
class MyThread2 implements Callable {

    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName()+" come in callable");
        return 200;
    }
}
//FutureTask
FutureTask<Integer> futureTask1 = new FutureTask<>(new MyThread2());
//lam表达式
FutureTask<Integer> futureTask2 = new FutureTask<>(()->{
    System.out.println(Thread.currentThread().getName()+" come in callable");
    return 1024;
});
//创建一个线程
new Thread(futureTask2,"lucy").start();
//调用FutureTask的get方法
System.out.println(futureTask2.get());
FutureTask原理

未来任务

不影响主线程情况下,单开一个线程做其他事情,主线程直接get

//FutureTask原理  未来任务
        /**
          1、老师上课,口渴了,去买票不合适,讲课线程继续。
            单开启线程找班上班长帮我买水,把水买回来,需要时候直接get
         
          2、4个同学, 1同学 1+2...5   ,  2同学 10+11+12....50, 3同学 60+61+62,  4同学 100+200
               第2个同学计算量比较大,
              FutureTask单开启线程给2同学计算,先汇总 1 3 4 ,最后等2同学计算位完成,统一汇总
         
          3、考试,做会做的题目,最后看不会做的题目
         
          只汇总一次
         
         */

JUC三大辅助类

CountDownLatch(减少计数)

• CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这 些线程会阻塞

• 其它线程调用 countDown 方法会将计数器减 1(调用 countDown 方法的线程 不会阻塞)

• 当计数器的值变为 0 时,因 await 方法阻塞的线程会被唤醒,继续执行

//演示 CountDownLatch
public class CountDownLatchDemo {
    //6个同学陆续离开教室之后,班长锁门
    public static void main(String[] args) throws InterruptedException {

        //创建CountDownLatch对象,设置初始值
        CountDownLatch countDownLatch = new CountDownLatch(6);

        //6个同学陆续离开教室之后
        for (int i = 1; i <=6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+" 号同学离开了教室");

                //计数  -1
                countDownLatch.countDown();

            },String.valueOf(i)).start();
        }

        //等待
        countDownLatch.await();

        System.out.println(Thread.currentThread().getName()+" 班长锁门走人了");
    }
}

CyclicBarrier(循环栅栏)

CyclicBarrier 的构造方法第一个参数是目标障碍数,每次执行 CyclicBarrier 一 次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后 的语句。可以将 CyclicBarrier 理解为加 1 操作。

//集齐7颗龙珠就可以召唤神龙
public class CyclicBarrierDemo {

    //创建固定值
    private static final int NUMBER = 7;

    public static void main(String[] args) {
        //创建CyclicBarrier
        CyclicBarrier cyclicBarrier =
                new CyclicBarrier(NUMBER,()->{
                    System.out.println("*****集齐7颗龙珠就可以召唤神龙");
                });

        //集齐七颗龙珠过程
        for (int i = 1; i <=7; i++) {
            new Thread(()->{
                try {
                    System.out.println(Thread.currentThread().getName()+" 星龙被收集到了");
                    //等待
                    cyclicBarrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

Semaphore(信号灯)

Semaphore 的构造方法中传入的第一个参数是最大信号量(可以看成最大线 程池),每个信号量初始化为一个最多只能分发一个许可证。使用 acquire 方 法获得许可证,release 方法释放许可。

//6辆汽车,停3个车位
public class SemaphoreDemo {
    public static void main(String[] args) {
        //创建Semaphore,设置许可数量
        Semaphore semaphore = new Semaphore(3);

        //模拟6辆汽车
        for (int i = 1; i <=6; i++) {
            new Thread(()->{
                try {
                    //抢占
                    semaphore.acquire();

                    System.out.println(Thread.currentThread().getName()+" 抢到了车位");

                    //设置随机停车时间
                    TimeUnit.SECONDS.sleep(new Random().nextInt(5));

                    System.out.println(Thread.currentThread().getName()+" ------离开了车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //释放
                    semaphore.release();
                }
            },String.valueOf(i)).start();
        }
    }
}

读写锁

JAVA 的并发包提供了读写锁 ReentrantReadWriteLock, 它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称 为排他锁

表锁

整张表上锁

行锁

只锁操作的某一行,会死锁

读锁(共享锁)

会死锁

请添加图片描述

互相等待造成死锁

线程进入读锁的前提条件

  • 没有其他线程的写锁
  • 没有写请求, 或者==有写请求,但调用线程和持有锁的线程是同一个(可重入锁)

写锁(独占锁)

请添加图片描述

线程1对第一条记录写,同时操作第二条记录

线程2对第二条记录写,同时操作第一条记录

造成死锁

线程进入写锁的前提条件

  • 没有其他线程的读锁
  • 没有其他线程的写锁

读写锁有以下三个重要的特性

(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公 平优于公平。

(2)重进入:读锁和写锁都支持线程重进入。

(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为 读锁。

请添加图片描述

阻塞队列

ArrayBlockingQueue

基于数组的阻塞队列实现,在 ArrayBlockingQueue 内部,维护了一个定长数 组,以便缓存队列中的数据对象

内部还保存着两个整形变量,分别标识着队列的 头部和尾部在数组中的位置。

在生产者放入数据和消费者获取数据,都是共用同一个 锁对象,由此也意味着两者无法真正并行运行

在插入或删除 元素时不会产生或销毁任何额外的对象实例

LinkedBlockingQueue

基于链表的阻塞队列,同 ArrayListBlockingQueue 类似,其内部也维持着一 个数据缓冲队列(该队列由一个链表构成)

在插入或删除 元素时会生成一个额外的 Node 对象

DelayQueue

使用优先级队列实现的延迟无界阻塞队列。

PriorityBlockingQueue

不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者

生产者生产数据的速度绝对不能快于消费者消费 数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。

SynchronousQueue

不存储元素的阻塞队列,也即单个元素的队列

• 公平模式:采用公平锁,并配合一个 FIFO 队列来阻塞 多余的生产者和消费者,从而体系整体的公平策略

• 非公平模式(SynchronousQueue 默认):采用非公平锁,同时配合一个 LIFO 队列来管理多余的生产者和消费者,而后一种模式, 如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有 某些生产者或者是消费者的数据永远都得不到处理。

LinkedTransferQueue

由链表组成的无界阻塞队列

消费者线程取元素时,如 果队列不为空,则直接取走数据,若队列为空,那就生成一个节点(节点元素 为 null)入队,然后消费者线程被等待在这个节点上,后面生产者线程入队时 发现有一个元素为 null 的节点,生产者线程就不入队了,直接就将元素填充到 该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素,从调用的 方法返回。

LinkedBlockingDeque

由链表组成的双向阻塞队列,即可以从队 列的两端插入和移除元素。

• 插入元素时: 如果当前队列已满将会进入阻塞状态,一直等到队列有空的位置时 再讲该元素插入,该操作可以通过设置超时参数,超时后返回 false 表示操作 失败,也可以不设置超时参数一直阻塞,中断后抛出 InterruptedException异常

• 读取元素时: 如果当前队列为空会阻塞住直到队列不为空然后返回元素,同样可 以通过设置超时参数

核心方法

请添加图片描述

线程池

Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors, ExecutorService,ThreadPoolExecutor 这几个类

newFixedThreadPool

创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。

一池N线程

• 线程池中的线程处于一定的量,可以很好的控制线程的并发量

• 线程可以重复被使用,在显示关闭之前,都将一直存在

• 超出一定量的线程被提交时候需在队列中等待

newSingleThreadExecutor

创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该 线程。

可保证顺序地执行各 个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的 newFixedThreadPool 不同,可保证无需重新配置此方法所返回的执行程序即 可使用其他的线程。

一池1线程

newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空 闲线程,若无可回收,则新建线程.

一池可扩容线程

• 线程池中数量没有固定,可达到最大值(Interger. MAX_VALUE)

• 线程池中的线程可进行缓存重复利用和回收(回收默认时间为 1 分钟)

• 当线程池中,没有可用线程,会重新创建一个线程

//演示线程池三种常用分类
public class ThreadPoolDemo1 {
    public static void main(String[] args) {
        //一池五线程
        ExecutorService threadPool1 = Executors.newFixedThreadPool(5); //5个窗口

        //一池一线程
        ExecutorService threadPool2 = Executors.newSingleThreadExecutor(); //一个窗口

        //一池可扩容线程
        ExecutorService threadPool3 = Executors.newCachedThreadPool();
        //10个顾客请求
        try {
            for (int i = 1; i <=10; i++) {
                //执行
                threadPool3.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" 办理业务");
                });
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            //关闭
            threadPool3.shutdown();
        }

    }

}

7个参数

corePoolSize
int corePoolSize//线程池的核心线程数
maximumPoolSize
int  maximumPoolSize//最大线程数量
keepAliveTime
long keepAliveTime//空闲线程存活时间
unit
TimeUnit unit //存活的时间单位
workQueue
BlockingQueue<Runnable> workQueue//阻塞队列
threadFactory
ThreadFactory  threadFactory//线程工厂
handler
RejectedExecutionHandler  handler //队列满后的拒绝策略

线程池工作流程

线程池最大线程数是5,常住线程数是2,也就是说,主线程调用execute()后,创建线程,当线程数超过两个,第三个线程进入与阻塞队列,当阻塞队列满了之后,在有线程进来会在线程池再次创建,当超过最大线程数后,在来进程会实施拒绝策略。

  1. 在创建了线程池后,线程池中的线程数为零
  2. 当调用 execute()方法添加一个请求任务时,线程池会做出如下判断:
    1. 如 果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务
    2. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入 队列
    3. 如果这个时候队列满了且正在运行的线程数量还小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务
    4. 如 果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程 池会启动饱和拒绝策略来执行
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行
  4. 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
    1. 如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉
    2. 所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

请添加图片描述

拒绝策略

请添加图片描述

分支合并框架(Fork/Join)

以将一个大的任务拆分成多个子任务进行并行处理,最后将子 任务结果合并成最后的计算结果,并进行输出。

  1. 任务分割:首先 Fork/Join 框架需要把大的任务分割成足够小的子任务,如果 子任务比较大的话还要对子任务进行继续分割
  2. 执行任务并合并结果:分割的子任务分别放到双端队列里,然后几个启动线程 分别从双端队列里获取任务执行。子任务执行完的结果都放在另外一个队列里, 启动一个线程从队列里取数据,然后合并这些数据。

Fork 方法的实现原理

当我们调用 ForkJoinTask 的 fork 方法时,程序会把任务放在 ForkJoinWorkerThread 的 pushTask 的 workQueue 中,异步地 执行这个任务,然后立即返回结果

Join 方法

主要作用是阻塞当前线程并等待获取结果,它首先调用 doJoin 方法,通过 doJoin()方法得到当前任务的状态来判断返回 什么结果,任务状态有 4 种: 已完成(NORMAL)、被取消(CANCELLED)、信号(SIGNAL)和出现异常(EXCEPTIONAL)

• 如果任务状态是已完成,则直接返回任务结果。

• 如果任务状态是被取消,则直接抛出 CancellationException

• 如果任务状态是抛出异常,则直接抛出对应的异常

Fork/Join 框架的异常处理

ForkJoinTask 在执行的时候没办法在主线程里直接捕获异常,所以 ForkJoinTask 提供了 isCompletedAbnormally()方法来检查 任务是否已经抛出异常或已经被取消了,并且可以通过 ForkJoinTask 的 getException 方法获取异常。

getException 方法返回 Throwable 对象,如果任务被取消了则返回 CancellationException。

如果任务没有完成或者没有抛出异常则返回 null。

volatile关键字

内存可见性问题

当多个线程操作共享数据时,彼此不可见

volatile

当多个线程操作共享数据时,保证内存中数据可见

禁止进行指令重排序。(实现有序性

相较于synchronized是一种较为轻量级的同步策略

注意:volatile不具备“互斥性”,不能保证变量的“原子性”

原子变量

java.util.concurrent.atomic 包下提供了一些原子操作的常用类:

➢ AtomicBoolean、AtomicInteger、AtomicLong、 AtomicReference

➢ AtomicIntegerArray、AtomicLongArray

➢ AtomicMarkableReference

➢ AtomicReferenceArray

➢ AtomicStampedReference

特性

  1. volatile:保证内存可见性

  2. CAS算法:保证数据原子性

    硬件对于并发操作共享数据的支持

    包含了3个操作数:内存值V、预估值A、更新值B,当且仅当V==A时,V=B,否则不进行任何操作

ConcurrentHashMap

锁分段机制,其中 Segment 继承于 ReentrantLock。

默认分为16段,实现了并行,每个段一个锁

jdk1.7

由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表

HashEntry跟HashMap差不多的,但是不同点是,他使用volatile去修饰了他的数据Value还有下一个节点next。

put

首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。

  1. 尝试自旋获取锁。
  2. 如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。
get

整个过程都不需要加锁

只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。

jdk1.8

采用CAS + synchronized

put
  1. 根据 key 计算出 hashcode 。
  2. 判断是否需要进行初始化。
  3. 为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
  4. 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
  5. 如果都不满足,则利用 synchronized 锁写入数据。
  6. 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

CountDownLatch闭锁

在完成某些运算时,只有其他所有运算全部完成,当前运算才会继续执行

egment 继承于 ReentrantLock。

默认分为16段,实现了并行,每个段一个锁

jdk1.7

由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表

HashEntry跟HashMap差不多的,但是不同点是,他使用volatile去修饰了他的数据Value还有下一个节点next。

put

首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。

  1. 尝试自旋获取锁。
  2. 如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。
get

整个过程都不需要加锁

只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。

jdk1.8

采用CAS + synchronized

put
  1. 根据 key 计算出 hashcode 。
  2. 判断是否需要进行初始化。
  3. 为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
  4. 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
  5. 如果都不满足,则利用 synchronized 锁写入数据。
  6. 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

CountDownLatch闭锁

在完成某些运算时,只有其他所有运算全部完成,当前运算才会继续执行

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值