JUC编程的使用(基于尚硅谷视频)

文章目录

1.核心概念

  • 并发和并行

并发:一台处理器处理多个任务
并行:多台处理器处理多个任务

  • 进程,线程和管程

进程:简单来说,在系统中运行的一个应用程序就是一个进程,每一个进程都有自己的内存空间和系统资源
线程:进程中的一个执行任务,一个进程中至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
管程:Monitor(监视器),也就是我们常说的锁

  • 用户线程和守护线程

用户线程:是系统的工作线程,他会完成程序需要完成的业务操作,不做特殊说明,默认为用户线程。
守护线程:一种特殊的线程,为其它线程服务,比如GC回收线程,如果用户线程全部销毁,虚拟机中只剩下守护线程,此时虚拟机将会退出。

2.线程

2.1线程的实现

  • 继承Thread,重写run方法,用start方法开启线程
  • 实现runnable接口,实现run方法
  • 继承Callable接口,实现call方法

Thread、Runnable和Callable实现多线程的区别

  • 继承Thread类会使当前类不满足OOP原则,局限性高。
  • 实现Runnable接口的类,必须作为传参传入到Thread中。
  • 实现Callable接口的类,启动的线程具有返回值。

2.2 多线程的使用方法(重点)

  • 首先需要准备资源类,准备属性和操作方法。
  • 在资源类的操作方法中可以【判断,干活,通知】操作。
  • 随后创建线程,调用资源类的操作方法。
  • 防止虚假唤醒的功能,条件判断改为while判断,不再使用if判断

2.3.Synchronized关键字(自动上锁和解锁)

常用形式:

  • 方法上添加synchronized(如果没有static关键字,锁的就是当前实例,如果有static锁的就是Class对象)
  • 锁住的是当前方法的this对象

2.4Look锁(手动上锁和解锁)

Look和Synchronized的区别:

  • Synchronized是java内置的关键字,Look是接口,并且不是java内置的
  • Synchronized是自动上锁和解锁,Look是手动上锁和解锁。
  • Lock可以时等待的线程中断,synchronized不能
  • 可以通过Lock知道有没有成功获取到锁,synchronized不能
  • 当竞争非常激励的时候,Lock的性能要远远优于synchronized

2.4.1常用方法

lock()//上锁操作
unlock()//解锁操作,为了避免死锁问题,所以应放在final代码块中
newCodition()//创建一个监听器,我的理解是,condition更像是一个标志
/**
* 监听器的重要方法
**/
await(); //线程等待
single(); //线程唤醒
singleAll(); //唤醒所有线程

2.4.2常用类

ReentrantLock

可重入互斥锁,同一个线程可以对同一个共享资源重复的加锁或释放锁,只允许一个线程获得锁。

2.5可定制化线程

实现方案:

利用Lock可以创建多个Condition(监听器)的性质,可以通过唤醒和等待指定的线程

2.6线程的状态

在这里插入图片描述
在这里插入图片描述

3.ArrayList的线程不安全性

3.1产生原因

如下程序,当我们在遍历集合的过程中对数组进行了增删改操作,就会报ConcurrenModificationException异常,代码如下:

ConcurrentModificationException的概念:通常不允许一个线程在另一个线程对其进行迭代时修改 Collection

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();

    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            list.add(1);
            System.out.println(list);
        }).start();
    }
}

3.2解决方案

  • 将ArrayList替换为Vector
  • 使用Collections.synchronizedList()将现有集合转换为线程安全的
  • 将CopyOnWirteArrayList替换ArrayList

CopyOnWirteArrayList原理:将原集合进行复制,再对复制后的集合进行操作,操作后的数组替换集合

4.HashSet和HashMap的线程不安全

当我们在遍历Set和Map的过程中对数组进行了增删改操作,依旧会出现上述问题。

4.2解决方案

  • HashSet 替换为CopyOnWirteArraySet
  • HashMap替换为ConcurrentHashMap

5.Callable接口实现创建线程(JDK1.5之后)

5.1Callable接口的概念

不同于Runnable接口,Callable接口可以返回结果,但是不能直接将Callable接口的实现类作为参数给Thread的,需要借用中间类(FuturnTask)

5.2Callable创建线程

public static void main(String[] args) throws ExecutionException, InterruptedException {

        FutureTask<Integer> futureTask = new FutureTask<>(() -> {
            return 1;
        });
        Thread thread = new Thread(futureTask, "AA");
        thread.start();
    }

6.辅助类

6.1计时器(CountDownLatch)

常用的有两个方法

  • new CountDownLatch(初始计数值);
  • await();//直到计数值为0,才会执行下面的代码。

6.2循环阻塞(CyclicBarrier)

常用的方法

  • new CycicBarrier(循环次数,Runnable接口的实现类); //只有经过了循环次数的await(),才会开启Runnable的实现类
  • await();//循环技术

6.3信号量类(Semaphore)

常用的方法:

  • new Semaphore(许可证的个数);
  • semaphore.acquire();//获取许可证
  • semaphore.release();//释放许可证

7.阻塞队列(BlockingQueue)

7.1概念

在这里插入图片描述

  • 当队列为空的时候,向队列中取元素将会阻塞
  • 当队列满的时候,向队列中添加元素将会阻塞

7.2阻塞队列的分类

  • ArrayBlockingQueue:数组组成的有界的阻塞队列
  • LikedBlockingQueue:链表组成的有界的阻塞队列
  • DelayQueue:使用优先级队列实现的延时无界阻塞队列
  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列
  • SynchronizedQueue:不存储元素的阻塞队列,也即单个元素的队列
  • LikedTranferQueue:有链表组成的无界队列
  • LikedBlockDeque:有链表组成的双向阻塞队列

7.3常用的方法

在这里插入图片描述

8.线程池

8.1概念

线程池做的工作主要是控制运行的线程数量,处理过程中将任务放到队列中,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超过数量的线程排队等待,等待其他线程执行完毕,再从该队类中取出任务来执行。

8.2线程池的创建方式

  • 创建一个固定线程数量的线程池
ExecutorService executorService = Executors.newFixedThreadPool(固定数量);
  • 线程池中的线程处于一定的量,可以很好的控制线程的并发量
  • 线程可以重复被使用,在现实关闭之前,都将一直存在。
  • 超出一定量的线程被提交时候需要在队列中等待。
  • 创建单个线程的线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();
  • 创建可扩容的线程池
ExecutorService executorService = Executors.newCachedThreadPool();

8.3ThreadPoolExecutor

8.3.1参数介绍

在这里插入图片描述

8.3.2工作流程

在这里插入图片描述

8.3.3线程池拒绝策略

在这里插入图片描述

8.3.4自定义线程池(推荐,不建议用Executors创建)

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 5, 2L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

9.Future接口

9.1概念

Future接口定义了操作异步任务执行一些方法,如获取异步任务的执行结果,取消任务的执行,判断任务是否被取消,判断任务执行是否完毕。

9.2FutureTask类

不同于Runnable接口,Callable接口可以返回结果,但是不能直接将Callable接口的实现类作为参数给Thread的,需要借用中间类(FuturnTask)

  • 优点:FutureTask结合线程池可以有效的提升系统性能
  • 缺点:get方法会使程序进行阻塞(程序一旦执行到get方式时,会一直等待线程执行完毕,就会导致系统阻塞),使用isDone()轮询方式,耗费CPU的资源

10.CompletableFuture

10.1产生原因

由于Future接口的get方法会造成阻塞,而使用isDone()以轮询的方式会占用CPU的资源,因此再JDK8就提出了CompletableFuture类

10.2构造方法

不建议我们直接使用new CompletableFuture()的形式直接创建,而是建议我们使用以下方法进行创建

//无返回值
public static CompletableFuture<Void> runAsync(Runnable runnable)//从默认的线程池创建
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)//从自定义的线程池创建
//有返回值
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)//从默认的线程池中创建
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor) //从自定义的线程池中创建

10.3CompletableFuture如何防止阻塞和CPU损耗问题

通过传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。

11.4get()方法和join()方法的区别

get方法编译期间必须抛出异常,而join方法再编译期间不用抛出异常

10.5常用的方法

/**获取结果**/
get(); //获取线程返回内容,需要抛出异常
get(long timeout, TimeUnit unit);//获取线程内容,如果超过指定时间,抛出异常【java.util.concurrent.TimeoutException】
join();//获取线程执行结果,不需要抛出异常
getNow(T valueIfAbsent);//如果线程执行完成,返回结果,否则返回ValueIfAbsent

/**主动触发计算**/
boolean complete(T value); //如果尚未完成,将value作为返回值返回

/**对计算结果进行处理**/
thenApply();//将多个线程串行化,如果其中一个步骤抛出异常,余下步骤不做处理
handle();//将多个线程串行化,如果其中一个步骤抛出异常,余下步骤依然做处理

/**计算结果进行消费**/
thenRun();//不接受上一步的传输结果,不向下一步传输结果
thenApply()://接受上一步传输的结果,并向下传输结果
thenAccept();//接受上一步传输结果,不向下传输结果
== 上述方法携带了Async,将从指定线程切换到默认线程(如果上一步执行过快,为了性能优化,将会切换到main先成功)==

/**速度比较**/
applyToEither(CompletionStage<? extends T> other, Function<? super T, U> fn);//哪个线程的速度快,就将那个线程的返回值作为参数返回到“Function<? super T, U> ”中

/**合并结果**/
thenCombine(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn) //将当前线程和线程other的结果合并处理。

11.锁

11.1.悲观锁和乐观锁

  • 悲观锁:当一个线程占用资源的时候,另一个线程不能占用线程,只有第一个线程归还资源时,另一个线程才能占用

多用在写操作比较多的地方
在这里插入图片描述

  • 乐观锁:多个线程都可以占用资源,但是当一个线程做了修改后,资源的版本号就做了修改,而另外的线程持有的还是原来的版本号,此时就不能做修改。
    在这里插入图片描述

11.2管程

  • 管程(monitors,也称为监视器):是一种数据结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。
  • 每个对象在创建的时候就会生成一个monitors,底层的程序通过一次初始化和两个退出控制锁(第一次退出正常退出,第二次退出是为了防止异常而实现的)
    +

11.3公平锁和非公平锁的概念

公平锁:当检验到资源的状态为0时,会先判断此时是否有队列,如果有,排队(先到先得
非公平锁:检验到资源的状态为0时,不考虑是否有队列,直接占用(抢夺

公平锁和非公平锁的特点:

  • 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在还是很明显的,所以非公平锁能充分地利用CPU的时间片,尽量减少CPU空闲状态时间。
  • 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步锁的概率就变得非常大,所以就减少了线程的开销。

11.4可重入锁(递归锁)

11.4.1概念

可重入锁:是指在同一线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象为同一个对象),不会因为之前已经获取过还没有释放而阻塞。
常见的可重入锁:synchronized,ReentrantLock

11.4.2实现原理

1.每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针,当中性格monitor enter时,如果目标计数器为零,说明它没有被其他线程所持有,Java虚拟机会将该所对象的持有线程设置为当前线程,并且将其计数器加1。
2.在目标锁对象的计数器不为零的情况下,如果锁对象持有的是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,只支持有线程释放该锁。
3.当执行monitor exit时,java虚拟机则需将锁对象的计数器减1.计数器为零代表锁已经被释放

11.5死锁

11.5.1死锁的概念

两个或两个以上的线程在执行过程中,因为争夺资源而造成互相等待的现象,如果没有外力干涉,他们无法再执行下去。
在这里插入图片描述

11.5.2死锁代码

public class Test {

    final static Object a = new Object();
    final static Object b = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (a) {
                System.out.println("AA尝试获取b");
                synchronized (b) {
                    System.out.println("AA获取了b");
                }
            }
        }, "AA").start();

        new Thread(() -> {
            synchronized (b) {
                System.out.println("BB尝试换取a");
                synchronized (a) {
                    System.out.println("BB获取了a");
                }
            }
        }, "BB").start();
    }
}

11.5.3如何检验死锁

# 第一种
jsp -l #查看所有进程
jstack 端口号 # 检验当前进程中是否有死锁

#第二种(图形化界面)
jconsole

11.6表锁和行锁

  • 表锁:数据库中table的锁,不会发生死锁
  • 行锁:数据库中每个table中对应row的锁,会发生死锁

12.中断机制

12.1概念

一个线程不应该有其他线程来强制中断或停止,而是应该由线程自行停止。所以Java提供了一种用于停止线程的协商机制【中断】,中断只是一种协作协商机制,Java没有给终端增加任何语法,终端的过程完全由程序员自行实现。

12.2常用API

interrupt(); //实例方法,清除中断标志,中断标志为false,不会停止线程
interrupted();//静态方法,返回中断标志位,然后将中断标志位置为false。
isInterrupted();//实例方法,返回中断标志位状态。

12.3注意点

  • 终端是一种协商机制,只会将中断标志位置为true,不会立即停止线程
  • 当我们在执行中断的时候,遇到线程阻塞和睡眠,会抛出异常,同时将中断标志位置为false,就会导致线程一直执行,所以解决方法就是,在catch块中进行二次中断Thread.currentThread.interrupted();

13.LockSupport

13.1概念

LockSupport是一个线程阻塞工具类,所有的方法都是静态的,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法,。

13.2常用的方法

park(); //使用此方法会使当前线程在此处阻塞,知道发送凭证
unpark(Thread t);//向指定线程发送凭证,凭证不会累加,一个线程醉倒只有一张凭证

13.3相较于wait/notify,await/single方法的优点。

wait/notify,await/single方法都必须执行在synchronized/lock代码块中,而且唤醒必须在等待之前。而相比之下,LockSupport方法就不会出现以上效果。

14.JMM

14.1JMM的由来

由于CPU的运行并不是直接操作内存而是先把内存里边的数据读到缓存中,而内存的渡河写操作的时候就会造成不一致的问题。JVM规范中视图定义一种Java内存模型来频闭掉各种硬件和操作系统的内存访问差异。

14.2JMM的学术定义

JMM本身是一种抽象的概念并不真实存在,他仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写如何使以及如何变成对另一个线程可见,关键技术点都是围绕着多线程的原子性,可见性和有序性展开的。

14.3三大特性

14.3.1可见性

是指一个线程修改了某一个共享变量的值,其他线程是否能够立即知道变更,JMM规定了所有的变量都存储在主内存中。

系统主内存共享变量修改被写入的时机是不确定的,多线程并发很可能出现脏读,所以每个线程都有自己的工作内存,线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝,先成功对变量的所有操作(读取,复制等)都必须在自己线程自己的工作内存中进行,而不能够直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

14.3.2原子性

指一个操作是不可打断的,即多线程环境下,操作不能被其他线程干扰。

14.3.3有序性

概念:
对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行,但是为了提升性能,编译器和处理器通常会对指令序列进行重新排序,Java规范规定JVM线程内部维持顺序化语义,即只要程序的最终结果与它顺序化执行的结果相等,那么程序的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。

优缺点:
JVM能根据处理其他行(CPU多级缓存系统,多核处理器等)适当的对机器指令进行重新排序,是机器指令能更符合CPU的执行他姓,最大限度地发挥机器性能,但是指令重排可以保证串行语义一致,但没有义务保证多线程间语义也一致,简单来说,就是两行以上或不相关的代码在执行的时候有可能限制性的不是第一条,不见得是从上到下顺序执行,执行顺序会被优化。

14.4JMM 多线程对变量的读写过程

  • 我们定义的所有共享变量都存储到物理内存中
  • 每个线程都有自己的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
  • 线程对共享变量所有的操作都必须现在线程自己的工作内存中进行后写回主内存,不能直接从主内存中读写(不能越级)
  • 不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行(同级不能相互访问)

14.5happens-before原则

14.5.1总原则

  • 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
  • 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则指定的顺序来执行,如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。

14.5.2 官网8原则

  • ==次序原则:==一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作。
  • ==锁定原则:==一个unLock操作先行发生于Lock操作
  • ==volatile原则:==对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的对后面的读是可见的。
  • ==传递原则:==如果操作A先行发生于操作B,而操作B先行发生于操作C,则可得出操作A先行发生于操作C
  • 线程启动原则: Thread对象的start()方法先行发生与此线程内执行的操作
  • 线程中断原则: 对线程interrupt()方法的调用先行发生于被”中断线程的代码检验到中断事件“的发生。
  • 线程终止原则: 线程中的所有操作都先行发生于对此线程的终止检测。
  • 对象终结原则: 一个方法的初始化操作先行与他的finalize()方法之前。

15.Volatile

15.1特点

  • 当写一个Volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
  • 当读一个Volatile变量时,JMM会把该线程对应的本地缓存设置为无效,重新回到主内存中读取最新共享变量。

15.2内存屏障

国庆玩了两天,感觉整个人都废了,不好意思,我又来卷各位了。

15.2.1两大特性

  • 可见性:写完后立即刷新会主内存并及时发送通知,大家可以去主内存那最新版,前面的修改队友所有线程有效。
  • 有序(禁重排):指编译器和处理器为了优化性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序,不存在依赖关系,可以重排序。存在依赖关系的,禁止重排序,但重拍后的指令绝对不能改变原有的串行语义。

15.2.2概念

内存屏障(也称内存栅栏,屏障指令等,是一类同步屏障指令,是CPU或编译器在堆内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行才可以执行此点之后的操作),避免代码重排序。内存屏障是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性(禁重排),但volatile不能保证原子性。

15.2.3特点

  • 内存屏障之前的所有写操作都要写到主内存。
  • 内存屏障之后的读操作都能获得内存屏障之前的所有写操作的最新结果。
  • 读写屏障:

写屏障(Store Memory Barrier):告诉处理器在写屏障之前将所有储存在缓存中的数据同步到主内存,也就是说当看到Store屏障指令,就必须把该指令之前所有写操作指令执行完毕才能继续往下执行。在读指令之前插入读屏障,让工作内存或CPU高速缓存当中的缓存数据失效,重新去主内存中获取
读屏障(Load Memory Barrier):处理器在读屏障之后的读操作,都在读屏障之后执行,也就是说在Load屏障指令之后就能保证后面的读取数据指令一定能够读取到最新的数据。在写指令之前插入写屏障,强制把写缓冲区的数据刷回到主内存中
在这里插入图片描述
在这里插入图片描述

15.2.4读写插入策略

在这里插入图片描述
在这里插入图片描述

15.2.5volatile变量的读写过程

读取(load) -> 存储(store) -> 使用(use) -> 分配(assign) -> store(存储) ->write(写入,多线程下,需要lock && unlock)
在这里插入图片描述
在这里插入图片描述

15.2.6volatile变量没有原子性的原因

在volitail变量的读写过程中,如果一个线程完成了操作,改变了主内存中的内容,而另一个变量虽然由于可见性的原因,改变了从主内存中接收到的变量值,但是变量已经结束了变量计算,导致本应在改变后的操作变为了原数据的操作,造成操作丢失,从而证明volatile没有原子性

15.2.7volatile的使用场所

  • 单一赋值,且不含符合运算
  • 状态标志
  • 开销较低的读,写策略
  • DCL双端锁实现高并发下的单例模式

16.CAS

16.1概念

CAS(compare and swap),中文翻译为比较并交换,实现并发算法是常用到的一种技术,它包含三个操作数【内存位置,预期原值,更新值】
执行CAS操作的时候,将内存位置的值与预期原值比较。

  • 如果相匹配,那么处理器会自动将该位置值更新为新值。
  • 如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个成功。

16.2AtomicReference缺点

  • 循环时间长,开销大

由于CAS是采用自旋的思想,因此就会导致开销很大

  • ABA问题

当A线程采集到的数据是1,B线程在此过程中把‘1’改为了‘2’,然后在A再次验证前把‘2’重新改为了‘1’。虽然对于开始和结束的值来说是没问题的,但是中间环节还是做了修改,是不安全的。

16.3如何解决AtomicReference中的ABA问题

使用AtomicStampedReference替代,AtomicStampedReference使用了乐观锁的思想,在更新值的时候,向原子类中放入了一个标签。

17.原子类

17.1基本原子类

  • 类型
    • AtomicInteger
    • AtomicLong
    • AtomicBoolean
  • 常用API

在这里插入图片描述

17.2数组类型的原子类

  • 类型
    • AtomicIntegreArray
    • AtomicLongArray
    • AtomicReferenceArray

17.3引用类型的原子类

  • 类型
    • AtomiceReference(基本类型原子类)
    • AtomicStampedReference(带有邮签的类型原子类,解决修改过多少次)
    • AtomicMarkableReference(带标记的类型原子类,解决修改过一次的)

17.4属性修改原子类

  • 目的

以一种线程安全的方式操作非线程安全对象中的某些字段

  • 类型
    • AtomicIntegerFieldUpdate
    • AtomicLongFieldUpdate
    • AtomicReferenceFieldUpdate
  • 使用要求
    • 更新的对象属性必须使用public volatile修饰符
    • 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdate()创建一个更新器,并且需要设置想要更新的类型。

17.5原子增强操作类

在大数据和高并发的情况下,synchronized和AtomicLong的性能不好,所以,sun公司在Java8提出了以下四种类,用来代替AtomicLong和AtomicDouble

  • 类型
    • LongAdder(只能实现加减操作)
    • LongAccumulator(可实现复杂操作)
    • DoubleAdder
    • DoubleAccumulator
  • 常用API
    • add(long x) 添加给定值
    • decrement 相当于add(-1)
    • increment 相当于add(1)
    • reset() 将保持总和的总量重置为0
    • sun() 返回当前总和
    • sunThenReset()返回当前总和后置0

17.5.1 Striped64

17.5.1.1常用的成员函数
static final int NCPU = Runtime.getRuntime().availableProcessors(); //CPU数量,即cells数组的最大长度
transient volatile Cell[] cells; //cells数组,为2的幂,2,4,8......,方便以后位运算
transient volatile long base; // 基础数据值,当并发量比较低时,只累加该值主要用于没有竞争的情况下,通过CAS更新。
transient volatile int cellsBusy; //创建或者扩容Cells数组时使用的自旋锁变量调整单元格大小(扩容),创建单元格使用的锁

在这里插入图片描述

17.5.2 底层执行原理

  LongAdder的基本思想就是分散热点,将value值分散到一个Cell数组中,不同的线程会命中到不同的槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就会被分散,冲突的概率就小了很多,如果要获取真正的long值,只要将各个槽中的变量累加返回。
  sum()会将所有Cell数组中的value和base累加作为返回值,核心的思想就是将之前AtomicLong一个value的更新压力分散到各个value中,从而降级更新热点。

18 ThreadLocal

18.1 作用

实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份)主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题。

18.2remove方法的作用

由于ThreadLocal是为每个线程创建局部变量,因此在我们不用这个线程的时候,线程中的私有变量仍然是存在的。而这一现象在线程池中是非常危险的,因此阿里巴巴代码规范要求我们在使用ThreadLocal时,使用后要执行remove()方法。

18.3 Thread,ThreadLocal,ThreadLocalMap的关系

Thread对象中维护了一个ThreadLocalMap属性,在这个属性中,key存储的是ThreadLocal对象,value是我们set的值。因此,每当我们要使用ThreadLocal对象的set方法时,实际就是向当前Thread对象的ThreadLocalMap属性中set一个值,key为ThreadLocal对象,value为设置的值。

18.4强引用

当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行垃圾回收,死都不收,强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就表明对象还活着,垃圾回收机制器不会碰到这种情况。在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,他是不可能被垃圾回收机制回收的。即使该对象以后永远都不会被用到,JVM也不会回收,因此强引用是造成Java内存泄漏的主要原因之一。对于一个普通的对象如果没有其他的引用关系,只要超过了引用的作用域或者显示地将相应强引用赋为null,一般认为就是可以被垃圾收集的(当然具体回收机制还是要看垃圾回收策略)。

18.5软引用(SoftReference)

对于软引用对象来说,当系统内存充足时,他不会被回收,当系统内存不足时会被回收

18.6弱引用(WekaReference)

对于弱引用对象,不论系统内存是否充足,只要系统进行GC操作,都将被回收。

18.7虚引用(PhantomReference)

  • 虚引用必须和引用队列(ReferenceQueue)联合处理

虚引用需要java.lang.ref.PhantomReference类来实现,顾名思义,就是形同虚设,与其他几种引用不同,虚引用并不会决定对象的生命周期,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用,引用在finalize后,会放入到引用队列中。

  • PhantomReference的get方法总是返回null

虚引用的主要作用是跟踪对象被垃圾回收的状态,仅仅是提供了一种确保对象被finalize以后,做某些事情的通知机制。PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。

  • 处理监听通知使用

换句话说,设置虚引用关联对象的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续操作添加进一步的处理,用来实现比finalize机制更灵活的回收操作。

18.8Entry为什么使用弱引用

当function01方法执行完毕后,战阵摧毁强引用t1也就没有了,但此时线程的ThreadLocalMap里某个Entry的key引用还指向这个对象。若这个key引用是强引用,就会导致key指向的ThreadLocal对象以及v指向的对象不能被gc回收,造成内存泄露。若这个key引用是弱引用就大概率会减少内存遗漏的问题,使用弱引用就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null
在这里插入图片描述

18.9Entry使用弱引用带来的问题

  • 问题描述

当ThreadLocal被强制置为null的时候,系统GC的时候,ThreadLocalMap中的key就为key,但是此时value的值依然存在。就会遗留下一条强引用链,就会造成内存泄露问题。

  • 解决方案

在java中,使用到ThreadLocal的get(),set(),remove()方法都会调用expungeStaleEntry方法,此方法会删除ThreadLocalMap中key为null的value数据。

18.10总结

  • ThreadLocal必须设置初始化,使用withInitValue方法
  • ThreadLocal建议设置为静态的
  • ThreadLocall用完后必须remove

19.对象内存布局

在HotSpot虚拟机中,对象在堆内存中的存储分布可以划分为三个部分:对象头(Header),实例数据(Instance Data),对齐填充(Padding)
在这里插入图片描述

19.1对象头

19.1.1对象标记

对象标记中存储着哈希码,GC标记,GC次数,同步锁标记,偏向锁持有者。
在这里插入图片描述
标志位信息
在这里插入图片描述

在这里插入图片描述
在64位系统中,Mark Work占了8个字节,类型指针占了8个字节,一共是16个字节。

19.2类元指针

对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例
在java虚拟机启动的时候默认是开启压缩指针,此时类元指针的大小是4个字节,如果关闭类元指针,类元指针的大小为8个字节

19.3实例数据和对齐填充

  • 实例数据:对象中属性所占的位数
  • 对齐填充:如果当前实例的对象头和实例数据的和是8的倍数的数据,对齐填充就是补足8的倍数的填充值

20.Synchronized锁升级机制

20.1锁的升级流程

在这里插入图片描述

  • synchronized用的锁是存在Java对象头里的MarkWork中,锁升级功能主要依赖MarkWork中锁标记位和释放偏向锁标志

20.2锁的分类

  • 无锁

调用hashCode方法,才会将hashcode的值存入到对象的对象标志中
在这里插入图片描述

  • 偏向锁:MarkWork存储的是偏向线程的id
    在这里插入图片描述
    偏向锁默认的情况下是有4秒钟的延迟的,因此一般情况下,如果有锁产生,直接跳过偏向锁,到轻量级锁

在实际应用过程中,“锁总是被同一个线程持有,很少发生竞争”,也就是说锁总是被第一个占用他的线程拥有,这个线程就是所得偏向线程。
那么只需要在锁第一次被拥有的时候,记录下偏向线程ID,这样偏向线程就一直持有锁(后续这个线程进入或退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接回去检查锁的MarkWork里面是不是放的自己的线程id》
如果相等,表示偏向锁是偏向于当前线程的,就不需要再次尝试获得锁了,知道竞争关系发生才释放锁,以后每次同步,检查锁的偏向线程ID与当前线程id是否一致,如果一致直接进入同步,无需每次加锁解锁都去CAS更新对象头,如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外的开销,性能及高。
如果不等,表示发生了竞争,所已经不是总是偏向于一个线程,这个时候会尝试使用CAS来替代MarkWork里面线程ID为新线程的ID。
竞争成功,表示之前的线程不存在了,MarkWork里面的线程id为新线程的ID,锁不会升级,仍然为偏向锁。
竞争失败,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。

偏向锁常用命令

  • 关闭延时参数 -XX:BiasedLockingStartupDelay=0(也可以在主线程启动的时候,睡4秒钟,等待偏向锁开启)

偏向锁的撤销

偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行。

  • 第一个线程正在执行synchronized方法(处于同步块),他还没有执行完,其他线程来抢夺,该偏向锁会被取消掉并出现锁升级,此时轻量级锁由原来持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。
  • 第一个线程完成synchronized方法(退出同步块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向。
  • 轻量锁:MarkWork存储的是只想线程栈中Lock Record的指针。

轻量级锁是为了在线程近乎交替执行同步块时提高性能。主要目的是在没有多线程竞争的前提下,通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗,说白了先自旋,不行才升级阻塞。

升级时机:当关闭偏向锁功能或多线程竞争会导致偏向锁升级为轻量级锁
在这里插入图片描述

轻量锁的加锁

JVM会为每个线程在当前线程的栈帧中创建用于存储记录的空间,官方称为Displaced Mark Word。若一个线程获得锁时发现是轻量级锁,会把锁的MarkWord复制到自己的Displaced Mark Word里面,然后线程尝试用CAS将锁的MarkWork替换为指向锁记录的指针,如果成功,当前线程获得锁,如果失败表示MarkWork已经被替换成其他线程的锁记录,说明在与其他线程竞争锁,当前线程就尝试使用自旋获取锁。

轻量级锁的释放

在释放锁时,当前线程会使用CAS将Displaced Mark Work的内容赋值回锁的Mark Work里面,如果没有发生过竞争,那么这个复制的错做就会成功,如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么CAS操作就会失败,此时会释放锁并唤醒被阻塞的线程

轻量级锁的自旋次数

Java6以前轻量级锁的自旋次数是用户设置的,默认的为10。在Java6以后,做了改变,使用了自适应自旋锁(线程自旋成功,那下次自选的最大次数会增加,因为JVM认为既然上次成功了,那么这一次也很大概率会成功,反之如果很少自旋,那么下次会减少自旋的次数甚至不自旋,避免CPU空转)

  • 重量锁:MarkWork存储的是指向堆中monitor对象的指针

Java中synchronized的重量级锁,是基于进入和退出Monitor实现的,在编译时会将同步块的开始位置插入monitor enter命令,在结束位置插入monitor exit指令。
当线程执行到monitor enter指令时,会尝试获取对象所对应的Monitor所有权,如果获取到了,及获取了锁,会在Monitor的ower中存放当前线程的id,这样它处于锁定状态,否则其他线程无法获取到了这个Monitor
在这里插入图片描述

20.2.1锁升级和HashCode的关系

  • 在无所状态下,MarkWork中可以存储对象的identity hash code值,当对象的hashCode方法第一次被调用时,JVM会生成对应的identity hash code值并将该值存储到MarkWork中。
  • 对于偏向锁,在线程获取偏向锁时,会用ThreadID和epoch值覆盖identity hash code所在的位置,如果一个对象的hashCode()方法已经被调用过一次之后,这个对象不能被设置偏向锁,因为如果可以的话,那Mark Work的identity hash code必然会被偏向线程id覆盖,这样就会造成前后hashCode不一致的问题。
  • 升级为轻量级锁时,JVM会在当前线程的栈帧中创建一个锁记录空间(Lock Record)空间,用于存储锁对象的MarkWork拷贝,这些拷贝中包含了identity hash code。
  • 升级为重量级锁后,MarkWork保存的重量级锁指针,代表重量级锁的ObjectMonitor类里有字段记录非加锁状态下的MarkWork,所释放后会将信息协会到对象头。

21AQS

21.1概念

整体就是一个抽象的FIFO队列(CLH队列)来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态

21.2内部体系架构

  • state:AQS的同步状态
  • CLH:双向队列
  • Node类型:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

21.3源码分析(以ReentryLock为例)

21.3.1基本架构

在这里插入图片描述

21.3.2公平锁和非公平锁的区别

//公平锁
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
}
//非公平锁
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

从上述两段代码可以看出公平锁和非公平锁的区别在于,前者在改变state的时候,会判断一下是否有前置节点(hasQueuedPredecessors())。

21.3.3Lock和Unlock

在这里插入图片描述
在这里插入图片描述

22.读锁和写锁

22.1 概念

  • 读锁:共享锁

多个线程可以共同读取一把锁

  • 写锁:独占锁

一把锁只能被一个线程占去

有上述两个概念,我们不由得出,当多个线程同时对一个资源(缓存)进行读写时,我们如何保证读出的数据一定在写入的数据后呢?由此,我们引出了读写锁的概念
读写锁:已给资源可以被多个线程访问,或者一个资源可以被一个写线程访问,但是不能同时存在读写线程,读写互斥,只允许读读共享

22.2读写锁的降级

ReentrantReadWriteLock锁降级:将写入锁降级为读锁(类似于Linux文件读写权限理解,就像写权限高于读权限一样),锁的严苛程度叫做升级,反之叫做降级。遵循获取写锁,获取读锁在释放写锁的次序,写所能降级成读锁,也即一个线程占有了写锁,在不释放写锁的情况下,他还能占有读锁,即写锁降级为读锁
在这里插入图片描述

写锁能够降级为读锁,读锁不能升级为写锁
实现方式:

主要通过在写锁释放之前,占有读锁的方式实现

22.3锁饥饿

ReentrantReadWriteLock实现了读写分离,想要获取读锁就必须确保当前没有其他任何读写锁了,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,因为当前有可能会一直存在读锁。而无法获得写锁。

23StampedLock

23.1特点

ReentrantReadWriteLock允许共享读,但是不允许共享写。但是ReentrantReadWriteLock的读锁被占用的时候,其他线程获取写锁的时候会被阻塞。但是StampedLock采用了乐观锁机制,其他线程尝试写锁时不会被阻塞,这就是对读锁的优化。但是在获取乐观读锁后,还需要对结果进行校验。

23.2模式

  • readLock():同ReentrantLock()相同
  • writeLock():同ReentrantLock()相同
  • tryOptimisticRead():乐观读,如果当前数据改变,重新读取

23.3缺点

在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值