JUC| 青训营笔记

95 阅读18分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 15 天

Java线程常用方法及其概念

Thread类API

Thread对象的run()方法

  • 使用

创建线程的几种方式

继承于Thread类

public static void main(String[] args) {
    new Thread(()->{
        System.out.println("hello");
    }).start();
}

步骤

  • 使用子类继承于Thread类
  • 重写父类run方法
  • 创建字类对象,这样就获得了一个线程对象
  • 调用线程对象start方法:启动线程后自动调用run方法

run方法由jvm调用,自己调用不是多线程,一个线程对象只能调用一次start

实现Runable接口

步骤

  • 定义子类实现Runnable接口
  • 子类重写Runnable接口中的run方法
  • 通过Thread类含参构造器创建线程对象
  • 将Runnable接口给子对象传入Thread类的构造器
  • 调用Thread类的start方法:调用了Runnable子类的run方法

实现对比于继承好处

  • 避免了单一继承的局限性
  • 多线程可以共享一个接口类的对象,适合多个线程处理同一个资源

Thread类的相关方法

  • void start() 启动线程,执行run方法
  • run() 线程被调用时候执行的操作
  • String getName() 获取线程名称
  • void setName(String name) 设置线程名称
  • static Thread currentThread() 返回当前线程
  • static void yield() 线程让步,让jvm重新分配
  • join() 调用线程将阻塞,被调用线程被优先执行
    • 低优先级的线程也能够执行
  • static void sleep(long millis):休眠x毫秒,
  • stop() 不推荐使用,强制线程生命结束
  • boolean isAlive() :线程是否存活

线程优先级

线程生命周期

  • 新建
  • 就绪
  • 运行
  • 阻塞
  • 死亡

同步问题

详细见我操作系统相关,或者下文并发编程

锁问题

详细见我操作系统相关,或者下文并发编程

线程通信

下面三个方法只有在synchronized方法或代码块中才能够使用

wait();释放锁,进入wait状态

notify();释放锁,唤醒一个wait对象

notifyAll();释放锁,唤醒所有wait对象

概念

java的_start到start0进行调用c++。

1.锁,sychonized。

2.并发,并行。

3.程序,线程,管程。

管程:获取monitor的线程才叫管程。

4.用户线程,守护线程。User Thread Daemon Thread

守护线程:垃圾回收就是守护线程。守护线程默默守护。isDaemon()判断是否守护线程。

Callable接口:

特点:对比Runnable能够有返回值抛出异常,call()方法

辅助类:

CountDownLatch

计数器:

  • countDown每次降低;
  • await就是等待计数器为0;

CyclicBarrier

互相等待:

  • 等待条件然后执行,
  • 构造方法设置参数,和执行方法。
  • 调用await等待。

Semaphore

信号量:构造方法设置信号量数;

  • acquire()获取信号量
  • release()释放信号量

BlockingQueue阻塞队列:

一个队列:

  • 没有元素时候获取阻塞
  • 队列满的时候放入阻塞

子类:

  • ArrayBlockingQueue
  • LinkedBlockingQueue
  • DelayQueue,优先级队列
  • PriorityBlockingQueue
  • 等:

API:

ThreadPool线程池:

用于创建线程:

七个参数的含义

  • corePoolSize:常驻线程数量。
  • maximumPoolSize:最大线程数量。
  • keepAliveTime:和下一个参数一起看,线程存活时间。
  • unit:和上一个参数一起看,线程存活时间,这是单位。
  • workQueue:阻塞的工作队列。常驻线程用完,放入这个队列。
  • threadFactory:线程工厂,用于创建线程。
  • handler:拒绝策略。线程接受容量用完了

Fork/Join分支合并框架:

Fork分支:一个任务拆分成多个任务

Join合并:多个任务合并成一个结果

多线程集合

List集合线程不安全.解决方法

  • Vector,因为方法上添加了synchronized关键字。
  • Collections,中的synchronizedList。
  • CopyOnWriteArrayList:实际生产中使用的类

CopyOnWriteArrayList解析:写时复制技术。

  • 并发读,独立写,拷贝需要写的东西进行修改。再进行合并。

HashSet和HashMap的线程安全问题。

  • CopyOnWriteSet来解决。
  • ConcurrentHashMap来解决

future相关:

future接口技术。

出现原因:异步任务。有返回值

future特点:底层通过callable来进行异步方法。

  • 异步
  • 返回值

runable接口:

  • 多线程
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<String> stringFutureTask = new FutureTask<String>(()->{
            Thread.sleep(1000);
            System.out.println("hello");
            return "hello";
        });
//        stringFutureTask.run(); 调用run方法开启的不是多线程。
        new Thread(stringFutureTask).start();
        System.out.println("111");
        String s = stringFutureTask.get();
        System.out.println(s);
    }

FutureTask();

get();阻塞获取返回值。

CompletableFuture接口

实现了两个接口,Future和CompletionStage接口。

  • 减少了future阻塞和轮询。future能做的CompletableFuture都能做。

不推荐使用无参构造,推荐使用6个静态方法创建实例。

  • 默认会使用一个线程池。

CompletableFuture.runAsync(runable());

CompletableFuture.supplyAsync(runable());

对象.get();

对象.join();与对象.get()没有太大区别。特点就是这个不会抛出异常。

对象.whenComplete(v,e);
exceptionally(e);
这个线程池可能会关闭。因为默认线程池默认为守护线程。

链式语法:新的一种代码风格,对象.setAA().setBB().setCC();

插叙:项目实例:

流式编程需要学习,stream。

常用方法:

  • 获取结构和触发计算。
  • 对计算结果进行处理

类似try,catch,finally

thenApply(v),handle(v,e);

  • 对计算结果进行消费

对前方线程的返回结果的限制。

thenRun();不需要前方的返回值,

thenAcccept();需要前方返回值,消费型(有输入,无返回)。

thenApply();需要前方返回值,加工型,需要返回。

注意有一个Async的。自己传入线程池。使用的线程会不相同。

  • 对计算速度进行选用

a.applyToEither(b,Function f);,谁快执行方法。。

  • 对计算结果进行合并

A与B的任务进行合并。

a.thenCombine(b,Function f);合并结果返回。

锁相关:

乐观锁悲观锁:

悲观锁:以为别人会改,一把大锁。

乐观锁:不会有人改,更新数据进行判断。版本号,CAS算法。

sycho和lock就是悲观锁,

锁相关理解,案例解析。

口诀:线程-操作-资源

synchronized解析

使用的锁对象。

  • 实例方法是对象本身(this)对象锁。
  • 静态方法是类本身类(xxx.class)模板锁。
  • 代码块是以对象(传入的对象)为锁。

字节码反编译解析:

  • 代码块:一个锁,有一个monitorenter和两个exit,对应正常退出和异常退出。
  • 实例方法:ACC_SYNCHRONIZED.标识。
  • 静态方法:ACC_SYNCHRONIZED.标识。和STATIC标识为静态。

对象可成为锁的理由:

  • 每个对象都带有一个ObjectMonitor对象。来源于C++ObjectMonitor.cpp

公平锁和非公平锁:

持有锁的概率不同。

ReentranLock();

公平锁:先到先得。概率相同

非公平锁:获取锁的顺序不是申请锁的顺序。线程饥饿。减小了空闲状态上下文切换。

可重入锁:

嵌套的锁对象相同。可以重复获得,不用释放。

Synchonized是可重入锁,ReentranLock()也是(lock和unlock配对)。

原理是:有一个锁计数器和指向该线程的指针。计算器为0就释放锁。

死锁:

两个或者两个以上的锁互相争夺资源,造成互相等待。

排查方法:

  • 1、jps -l
  • 2、jstack 进程号。
  • 3、图形化界面jconsole。

未完待续.................

LockSupport与线程中断:

概念:

一个线程不应该由其他线程给强制中断和停止。

java提供了一种停止线程的协商机制-中断。也即中断标识协商策略。中断标识符变为true。标识为有人发送了中断请求。

API解析:

interrupt();设置中断标识符位true,不会立刻停止线程。自行处理。

  • 底层调用interrapt0。

interrupted();静态方法。判断是否被中断,清楚线程标识位false。

  • 底层调用isInterrupt(true);

isInterrupted();判断是否被中 断。 对结束的线程不影响。

  • 底层调用isInterrupt(false);

如何停止线程

1.volatile变量:

  • 自旋方式检测

2.AtomicBoolean:

  • 和volatile很类似。

3.通过中断API:

当前线程处于阻塞状态,被调用interrupt后,会结束阻塞,返回异常。这样还会清楚中断标识,很可能造成死循环。避免死循环方法,在catch中调用interrupt来中止线程,否则默认的异常处理机制清楚中断标识为false。

LockSupport

创建锁和其他同步类的基本线程同步阻塞原语。

API:

许可证最多只有一个不会累积。传统的唤醒需要在等待之后,许可证机制解决了线程调用顺序的问题。

park():没有许可证,阻塞线程。

unpark():发放许可证,解除阻塞。

就是对线程等待唤醒的方法。

唤醒线程的方法:

  • synchronized使用Object中的wait和notify
    • notify唤醒wait的线程。
  • lock使用JUC包中Condition的await方法和signal方法。
  • LockSupport类的阻塞和唤醒方法。

JMMjava内存模型

类似内存和CPU和缓存关系。解决读写一致性问题。屏蔽掉各种硬件和OS对内存访问的差异。达到java程序在各种平台上达到一致性的内存访问效果。Java程序把自己限制在了内存划分的区域上(运行时数据区),而运行时候又会分为线程的私有区和共享区,GC不会发生在私有区。

  • 方法区:共享区,存放类信息,常量,静态变量,即时编译器编译后的代码等。
  • JVM堆:共享区,存放对象实例的。
  • 程序计数器:私有区,程序执行位置的
  • 虚拟机栈:私有区,栈帧组成
  • 本地方法栈:私有区,维护了本地方法登记表,记录那个线程调用那个本地接口。

JVM内存模型是处于Java的JVM虚拟机层面的,实际上对于操作系统来说,本质上JVM还是存在于主存中,而JMM是Java语言与OS和硬件架构层面的,主要作用是规定硬件架构与Java语言的内存模型,而本质上不存在JMM这个东西,JMM只是一种规范,并不能说是某些技术实现。

定义和作用:

  • JMM是围绕原子性,有序性、可见性拓展延伸的

原子,一个线程只要开始就不会被其他线程干扰。

有序,指令排序,指令重排序。

可见,线程修改数据,数据立马对所有线程可见,不能改主内存,避免脏读,通过主内存进行中间转换。

  • JMM实现线程和主内存的抽象关系。
  • 屏蔽硬件和OS对程序的影响。

happens-before(先行发生):

一个操作的执行结果对第二个操作可见,第一个执行在第二个执行之前。不会导致重排序后代码乱序与逻辑不附和。

满足上面这8个原则,就能满足happens-before.

volatile

  • 可见性,有序性。没有原子性
  • 可见性:
    • 写后立即刷新到主内存,读取回到主内存重新读取。

保证可见性和有序性的原因:

    • 内存屏障,可见性,修改后马上写回主内存发送通知。有依赖关系,禁止重排。这里加锁,只是对读取和写入进行上锁,没有对整体进行上锁。

没有原子性:由于没有进行整体加锁导致了不能够同步。

内存屏障

  • 插入内存屏障指令,使得在所有读写操作后,才能执行该点后的操作。
  • 屏障前,写都要写操作回主内存。
  • 屏障后,所有读操作获取内存中的最新结果。

对主内存中的共享变量进行上锁

  • 在volatile写的时候:前面插入storestore,后面插入storeload
  • 在volatile读的时候:后面插入loadload和loadstore。

重排序

为了优化性能,对指令进行重排序。

不存在依赖关系,可以重排序。

使用:

  • 对于变量的复制
  • 对于系统状态的判断
  • 对于读操作多写操作少使用volatile
  • 使用DCL双端锁的发布,
  • 就是不进行重写顺序

小总结:

  • volatile写之前的操作,都会禁止重排序到volatile之后
  • volatlie读之后的操作,都会禁止重排序到volatile之前
  • volatile写之后的volatile读操作,都会禁止重排序

CAS

compare and swap

原子类:

  • 非阻塞的,是个原子指令cmpxchg,硬件保证,
  • 底层调用unsafe类。

AtomicReference:原子类的结构

unsafe:

用于落地cas机制,可以直接操作内存。底层通过自旋的方式进行访问内存进行比较。比较耗费CPU时间

缺点:

  • 循环时间自旋开销大,高并发不友好.
  • ABA问题:

线程1获取A,线程2A改成B再改成A。结果线程1没有发现。

添加版本号就可以解决ABA问题。使用AtomicStampedReference带有版本号。使用时间戳也能解决。

原子类:

有18个原子类。

小结:

ThreadLocal

提供线程局部变量,每个线程有自己的变量,人手一份。避免了线程安全问题。

api

  • get();获取
  • initialValue();初始值,只能调用一次。匿名内部类创建。
  • remove():删除,推荐使用threadlocal后释放。减轻线程负担。使用finally释放。
  • set();设置
  • withInital:静态方法,获取初始值。替代initialValue();快速创建

threadlocal源码分析:

Thread,ThreadLocal,ThreadLocalMap关系。

Thread中有一个ThreadLocal。

ThreadLocal中有一个静态类:ThreadLocalMap。

ThreadLocalMap中有一个弱引用WeakReference:key做弱引用指向ThreadLocal

就是一个当前线程为Key,以值为value就是能够存储了。需要使用就用这个线程作为key获取

弱引用和内存泄漏的问题:

强软弱虚引用:

强引用:就算出现OOM,也不会回收,这是最常用的。

软引用:SoftReference。系统内存不够的时候就会回收。

弱引用:WeakReference。只要GC就会回收内存。读取图片

虚引用:和引用队列一起用,在任何时候都可能被回收。get方法总是返回null,处理监控通知,比finalize更加灵活。

这个图说明了这个为什么使用弱引用,如果使用强引用,那么ThreadLocal将不会回收。

key为null的Entry。

这图解释了key为null的entry存在于堆内存中,ThreadLocal被释放,key就是null。,然而gc不能够回收掉这些key为null的entry对象,因为一直有引用指向这entry对象。消除key堆null的需要我们手动调用remove来删除。虽然线程回收的时候就会回收这个,但是线程池中线程回复用,不会清楚Thread对象的引用,导致Entry残留在内存。程序也有set、get和remove都自动调用expungeStaleEntry来清楚value为null释放Entry。还有将Entry设置为null;

  • ThreadLocal可以用static来修饰,只会申请一个空间。
  • 强制:使用try和finally来remove释放。

Java对象内存布局及对象头

对象布局:

构成三部分:对象头,实例数据,对齐填充。

  • 对象头由两部分:对象标记Markword,类元信息(类型指针)。
  • 实例数据:
  • 对齐填充:填充到16字节。

对象标记MarkWord

包含哈希码,GC标记,GC次数,同步锁标记,偏向锁持有者。

64位寄,MarkWord占8字节, 类型指针占8字节。

类元信息

指向类元信息指向方法区。

实例数据与对象填充:

对象的属性。数组多一个length字段。

JOL工具

在maven中导入,

一个对象最小16字节。markword8字节,类型指针4字节,填充4字节。

压缩指针:

默认启动了压缩指针。导致类元指针只有四个字节,关闭压缩指针后,类型指针占用8字节,最后也就不用进行填充了

Synchronized与锁升级

能用块锁,就不用方法锁。能用对象锁,就不用类锁。

权衡安全性与性能问题。

  • 无锁-偏向锁-轻量级锁-重量级锁

每个对象都能成锁,因为每个对象都有objectMonitor。

Synchronized锁升级流程:

markword中得锁标记位得变化。锁的指向变化如下图。

hashcode调用才会产生。

无锁:

就是001上图的绿色。

偏向锁:

修改记录线程信息,只有一个线程没有其他线程竞争,就不用上锁会自动获得锁。偏向于第一个访问的线程(偏向线程)。我们记录偏向线程ID,以后就检查是否记录的线程ID,不用再上锁。如果ID匹配不同发生竞争,竞争成功。那么就会用CAS来交换markword的ID位新ID。竞争失败就升级位轻量级锁,保证公平性。

不用上锁,就不用进入操作系统内核。

操作:

  • 开启有延迟
  • 默认开启偏向锁
  • 关闭偏向锁

偏向锁撤销:

后来的线程竞争失败,使用轻量锁来替代。等待全局安全点撤销偏向锁进行升级。竞争失败是原来偏向锁线程还执行,就会失败。

Java15后废弃偏向锁。维护偏向锁成本太高了

轻量级锁

是在多线程锁的竞争当中出现的轻量级锁。

markword前62bit为LockRecord指针。JVM会为每个线程再栈帧中创建存储锁记录的空间,Displaced Mark Word。用于存储markword中的LockRecord指针。获得锁失败的线程CAS进行自旋,尝试获取锁。

轻量级锁多次自旋,自旋达到一定次数后升级为重量级锁。

java6之前是10次:或者超过核数的一半

java8使用了成功加倍,失败减半的方式。

重量级锁:

前62bit指向互斥量monitor的指针。

总结:

hashcode

在锁升级的过程中hashCode不见了,这些信息保存到。要是计算过hashcode就不会进行偏向锁。要是偏向锁后计算hashcode会直接进入重量级锁。hashcode和偏向锁不共存,重量级锁有ObjectMonitor有hashcode作为重量级锁的hashcode。轻量级锁会在线程栈中创建一个锁记录的拷贝。

锁的优缺点:

JIT锁消除和锁粗化:

锁消除:

每一个线程使用不同的锁,这样JIT编译器会无视这个synchronized。根本没有加锁,被编译器发现了

锁粗化:

重复使用锁,重复使用合并成一个。

AQS

AbstractQueuedSynchronizer,最重要这是底层的锁原理。抽象队列同步器

主要用来解决锁给谁的问题。

  • FIFO的等待队列实际双向队列,和一个int的state来分配锁。

AQS相关类,都是基于AQS类

  • ReentrantLock,-
  • CountDownLatch
  • ReentrantReadWriteLock
  • Semaphore

state状态位用于标识是否有线程占用。通过CAS对state进行修改。

FIFO的队列,作为等候区。用于锁的分配。每个线程都封装为Node结点,Node结点中封装了很多线程信息。

体系架构

双向队列CLH,状态位state,Node结点。

state标识是否阻塞。有阻塞就要排队,排队就要队列,使用双向链表的CLH,tail和head指针标识头和尾。

Node节点:

里面包含了很多状态状态。waitstatus是node的等待状态。

AQS源码设计解析:

底层就是操作Sync对象(AQS的子类对象)。

  • 先来判断是否有线程占用了锁,state,然后调用acquire();
  • acquire()

  • tryAcquire()尝试直接获取锁
  • addWaiter()当前线程添加到等待队列
  • acquireQueued();设置线程状态在队列中有序等待,用了LockSupport.park();

unlock():

  • 就是将队列的头结点进行唤醒。唤醒需要进行获取state,不一定公平,非公平锁不会按照顺序来进行获取state的状态。

cancelAcquire();

在acquireQueued();中还有取消获取。

  • 也就是从队列中将这个结点取消出来。使用CAS来进行交换,防止多线程进行干扰。

ReentranLock ReentrantReadWrite StampedLock详解

发展历程: 无锁-独占锁-读写锁-邮戳锁

ReadWriteLock接口:

读写锁:可以被多个读线程访问,或者被一个写线程访问。

  • 读写互斥,读读共享:。

几种锁导入:

独占锁:

  • 由于无锁,并发访问导致数据,天下大乱,独占锁,只能允许获取锁的线程访问。

读写锁:

  • 独占锁不管怎么样只能由一个线程访问,读写锁将独占锁的访问量提高了,读的时候让多个线程访问,一旦有一个写线程,可能导致抢不到CPU片饿死,锁饿死。读多写少性能好
  • 锁降级:写入锁降级为读锁。
  • 写锁中可以插入读锁,读锁中不能插入写锁

邮戳锁:

stamp为long类型,

由于读写锁在有线程读的时候写线程不能进入,导致锁饥饿,能不能让写线程在读的时候也能进去。于是java8中出现比读写锁更加快的锁邮戳锁诞生。这是乐观锁的思想。

  • 在读取过程当中和邮戳号进行对比判断是否被修改。
  • 在获取锁的时候,获取一个邮戳号stamp
  • 在释放锁的是否,把获取的stamp与锁进行对比,不同就是失败,相同就是成功。
  • 不可重入。

邮戳锁缺点:

不支持重入,不支持条件变量,不要调用中断。

总结:

多看多背