Java多线程相关知识点汇总_java多线程相关概念-程序员宅基地

1.ThreadLocal
2.如何保证高并发场景下的线程安全?
3.JUC(java.util.concurrent)包
4.volatile
5.信号量同步
6.线程池
7.线程同步类
8.并发集合类
9.锁机制

1.ThreadLocal
ThreadLocal如何实现多线程数据隔离?
ThreadLocal内存泄漏问题?
ThreadLocal脏数据问题?

ThreadLocal主要功能:
进行对象跨层传输,使用ThreadLocal,打破层次间的约束。
线程间数据隔离。
进行事务操作,存储线程事务信息。

注意ThreadLocal并不是解决多线程下共享资源的技术

首先需要了解下四类引用:

强引用
对象有强引用指向,并且GC Roots可达,不会回收。
软引用
引用强度弱与“强引用”,用在非必须的场景,在OOM即将发生的时候,垃圾回收器会将这些软引用指向的对象放入回收范围。
弱引用
引用强度弱于前两者,弱引用指向的对象只存在弱引用这一条路径,那么在下次YGC时会被回收。YGC时间不确定,则弱引用何时被回收也不确定。
虚引用
极其虚弱的引用对象,完成定义就无法通过该引用获取对象了。一般场景很难使用到。

ThreadLocal 与Thread类图关系,自己拍的图片,大家先看一下,等有时间我自己画一下。

在这里插入图片描述
ThreadLocal有一个静态内部类ThreadLocalMap和Entry。
Thread中持有一个ThreadLocalMap属性,在ThreadLocal->createMap()赋值的。ThreadLocal与ThreadLocalMap联系的方法:get() set() remove()
Entry继承自WeaReference,Entry只有一个value成员,它的key是ThreadLocal对象。

下面是栈与内存的的角度分析Thread ThreadLocal ThreadLocalMap三者之间的关系。
在这里插入图片描述
1个Thread有且仅有1个ThreadLocalMap对象
1个Entry对象的Key弱引用指向1个ThreadLocal对象
1个ThreadLocalMap独享存储多个Entry对象
1个ThreadLocal对象可以被多个线程共享
ThreadLocal对象不持有Value,Value由线程的Entry对象持有。

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

所有Entry对象都会被ThreadLocalMap类实例化的threadLocals持有,线程执行完毕,线程对象内的实例属性均会被垃圾回收。Entry中的Key是ThreadLocal弱引用,即使线程正在运行过程中,只有ThreadLocal对象引用被置为null,Entry的Key就会自动在下次YGC时被垃圾回收。而ThreadLocal使用set() get()时会自动将key==null的value置为null,使value也能被垃圾回收。
ThreadLocal内存泄漏问题:

在这里插入图片描述
Entry中的Key-ThreadLocal对象失去引用后,触发弱引用机制来回收Entry中的Value是不可能的,在上面的图中,ThreadLocal被回收了,但是如果没有显式的回调remove(),这时候Entry中的Value是不会被立即回收的,可能会造成内存泄漏。

线程池操作可能产生的ThreadLocal脏数据

2.如何保证高并发场景下的线程安全?
2.1 数据单线程可见
2.2 只读对象
final修饰的变量
2.3 线程安全类
StringBuffer,以及采用synchronized修饰的其他类。
2.4 同步锁工作机制

3.JUC(java.util.concurrent)包
3.1 线程同步类

object.wait()/Object.notify()
CountDownLatch
Semaphore
CyclicBarrier

3.2 并发集合类
ConcurrentHashMap
ConcurrentSkipListMap
CopyOnWriteArrayList
BlockingQueue

3.3 线程管理类
ThreadLocal
Executors静态工厂
ThreadPoolExecutor
ScheduledExecutorService

3.4 锁相关类
ReentrantLock

4.volatile

volatile解决的是多线程共享变量的可见性问题,但是不具备synchronized的互斥性,所以对volatile变量的操作并非都具有原子性。

//Author:[email protected]

public class VolatileAtomic {

private static volatile long count = 0L; private static final int
NUMBER = 10000;

public static void main(String[] args) {
Thread subtraceThread = new SubtraceThread();
subtraceThread.start();
for(int i=0;i<NUMBER;i++) {
count++;
}
while(subtraceThread.isAlive())
{

}
System.out.println("count = " + count);

}

private static class SubtraceThread extends Thread {

@Override
public void run(){
  for(int i=0;i<NUMBER;i++) {
    count--;
  }
}

} }

在这里插入图片描述

这个结果不为0就说明volatile变量是不具备synchronized的互斥性的,这很简单,因为count++与count–在底层是分为三步操作的,并不是原子操作。所以如果想得到结果为0,只需要在count++与count–上加锁即可。

volatile非常适合“一写多读”的场景,“一写多读”的经典场景是CopyOnWriteArrayList
JVM的happens-before原则:

程序次序原则:一个线程内,代码按照编写时的顺序执行。
锁住规则:unlock操作优先于lock操作
volatile变量规则:如果一个变量使用volatile关键字来修饰,一个线程对它进行读操作,另一个线程对它进行写操作,那么写操作肯定要优先于读操作。
volatile具有保证顺序性的语义。

volatile与synchronized的区别:
1.使用上区别

1.1 volatile关键字只能修饰实例变量和类变量,不能修饰方法以及方法参数、局部变量、常量等。
1.2 synchronized关键字不能修饰变量,只能修饰方法和代码块。
1.3 volatile修饰的变量可以为null,但是synchronized修饰的monitor不能为null

2.对原子性的保证

2.1 volatile无法保证原子性
2.2 synchronized 执行中无法被打断,可以保证代码的原子性。

3.对可见性的保证

3.1 两者均可以保证共享资源在多线程间的可见性,实现机制不同。
3.2 synchronized使用monitor enter与monitor exit通过排他方式使得同步代码串行化,monitor exit之后所有的共享资源都会被刷新到主内存中。
3.3 volatile使用机器指令“:lock;”方式迫使其他线程工作内存中的数据失效,不得不到主内存中再次加载。

4.对有序性的保证

4.1 volatile关键字禁止JVM编译器以及处理器对其进行重排序,可以保证有序性。
4.2 synchronized修饰的代码块或者函数中也可能发生指令重排

5.其他区别

5.1 volatile不会使线程陷入阻塞
5.2 synchronized关键字会使线程进行阻塞状态。

5.信号量同步

信号量实现多线程交替打印ABC的问题:

//[email protected]

import java.util.concurrent.Semaphore;

public class ABC_Semaphore {

public static Semaphore A = new Semaphore(1); public static
Semaphore B = new Semaphore(0); public static Semaphore C = new
Semaphore(0);

static class ThreadA extends Thread {

@Override
public void run() {
  for(int i=0;i<10;i++) {
    try {
      A.acquire();
      System.out.print('A');
      B.release();
    } catch(Exception e) {

    }
  }
}   }

static class ThreadB extends Thread {

@Override
public void run() {
  for(int i=0;i<10;i++) {
    try {
      B.acquire();
      System.out.print('B');
      C.release();
    } catch(Exception e) {

    }
  }
}   }

static class ThreadC extends Thread {

@Override
public void run() {
  for(int i=0;i<10;i++) {
    try {
      C.acquire();
      System.out.print('C');
      A.release();
    } catch(Exception e) {

    }
  }
}   }

public static void main(String[] args) {
new ThreadA().start();
new ThreadB().start();
new ThreadC().start(); } }

在这里插入图片描述
6.线程池

线程池的作用:
1.利用线程池管理并复用线程、控制最大并发数等。
2.实现任务线程队列缓存策略和拒绝机制。
3.实现某些与时间相关的功能,如定时执行、周期执行等。
4.隔离线程环境。例如交易服务和搜索服务在同一台服务器上,分别开启两个线程池,其中交易线程的消耗肯定大一点。这种将不同的服务隔离开来,便于处理较慢和较快的服务。

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}

1.corePoolSize表示常驻核心线程数。如果等于0,则任务完成之后,没有任何请求进入时销毁线程池的线程;如果大于0,即使本地任务执行完毕,核心线程也不会销毁。这个值的设置非常关键,设置过大会浪费资源,设置过小会导致线程频繁地创建或销毁。

2.maximumPoolSize表示线程池能够容纳同时执行的最大线程数。如果执行的线程数大于maximumPoolSize,需要借助第五个参数来将其缓存在队列中。如果maximumPoolSize与corePoolSize相等,就是固定大小的线程池。

3.keepAliveTime表示线程池中的线程空闲时间,当空闲时间达到keepAliveTime,线程会被销毁,直到剩下corePoolSize个线程,避免浪费内存资源。当线程池的线程数大于corePoolSize时keepAliveTime才会起作用。当ThreadPoolExecutor的allowCoreThreadTimeOut变成设为true,核心线程超时也会被回收。

4.unit表示时间单位。
5.workQueue表示缓存队列。当请求的线程数大于maximumPoolSize,线程进入BlockingQueue阻塞队列。

6.threadFactory表示线程工厂。它用来生产一组相同任务的线程。
7.handler表示执行拒绝策略的对象。当超过第5个参数workQueue的任务缓存区上限的时候,可以通过该策略处理请求,这是简单的限流保护。

线程池相关的类图:
在这里插入图片描述
7.线程同步类
在这里插入图片描述
AQS是一个抽象类,主是是以继承的方式使用。AQS本身是没有实现任何同步接口的,它仅仅只是定义了同步状态的获取和释放的方法来供自定义的同步组件的使用。AQS抽象类包含如下几个方法:
AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。共享模式时只用 Sync Queue, 独占模式有时只用 Sync Queue, 但若涉及 Condition, 则还有 Condition Queue。在子类的 tryAcquire, tryAcquireShared 中实现公平与非公平的区分。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源volatile state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。
整个 AQS 分为以下几部分:

Node 节点, 用于存放获取线程的节点, 存在于 Sync Queue, Condition Queue, 这些节点主要的区分在于 waitStatus 的值(下面会详细叙述)

Condition Queue, 这个队列是用于独占模式中, 只有用到 Condition.awaitXX 时才会将 node加到 tail 上(PS: 在使用 Condition的前提是已经获取 Lock)

Sync Queue, 独占 共享的模式中均会使用到的存放 Node 的 CLH queue(主要特点是, 队列中总有一个 dummy 节点, 后继节点获取锁的条件由前继节点决定, 前继节点在释放 lock 时会唤醒sleep中的后继节点)

ConditionObject, 用于独占的模式, 主要是线程释放lock, 加入 Condition Queue, 并进行相应的 signal 操作。

独占的获取lock (acquire, release), 例如 ReentrantLock。

共享的获取lock (acquireShared, releaseShared), 例如 ReeantrantReadWriteLock, Semaphore, CountDownLatch

参考这篇文章的应用 Java并发编程:CountDownLatch、CyclicBarrier和Semaphore

经典的面试题:多线程交替打印ABC的多种实现方法
7.1 CountDownLatch
CountDownLatch初始时定义了资源总量 state=count,执行countDown()不断-1,直到为0,当state=0时时才能获取锁。释放后state就一直为0。CountDownLatch是一次性的,用完之后如果再想使用就重新创建一个。

7.2 Semaphore
见《5.信号量同步》
Semaphore定义了资源总量state=permits,当state>0可以获取锁,将state减1,当state=0时只能等待其他线程释放锁,当释放锁时state加1,其他等待的线程又能获得这个锁。当permits定义为1时,就是互斥锁,当permits>1时就是共享锁。

7.3 CyclicBarrier

CyclicBarrier是基于ReentrantLock实现的,它比CountDownLatch优越的地方是可以循环使用。
8.并发集合类

这在Java底层数据结构中总结这一块内容。
8.1 ConcurrentHashMap
8.2 ConcurrentSkipListMap
8.3 CopyOnWriteArrayList
8.4 BlockingQueue
9.锁机制
9.1 ReentrantLock
要想支持重入性,就要解决两个问题:1.在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;2. 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。我们知道,同步组件主要是通过重写AQS的几个protected方法来表达自己的同步语义。
公平锁 VS 非公平锁

公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序,而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象。
公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量。

9.2 ReentrantLockReadWriteLock
ReadWriteLock: 允许读操作并发执行;不允许“读/写”, “写/写”并发执行。当数据结构需要频繁的读时,ReadWriteLock相比ReentrantLock与synchronized的性能更好。
9.3 StampedLock
读不阻塞写的实现思路:
在读的时候如果发生了写,则应当重读而不是在读的时候直接阻塞写!
因为在读线程非常多而写线程比较少的情况下,写线程可能发生饥饿现象,也就是因为大量的读线程存在并且读线程都阻塞写线程,

因此写线程可能几乎很少被调度成功!当读执行的时候另一个线程执行了写,则读线程发现数据不一致则执行重读即可。所以读写都存在的情况下,

使用StampedLock就可以实现一种无障碍操作,即读写之间不会阻塞对方,但是写和写之间还是阻塞的!

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/zhuguang10/article/details/88670537

智能推荐

oracle 12c 集群安装后的检查_12c查看crs状态-程序员宅基地

文章浏览阅读1.6k次。安装配置gi、安装数据库软件、dbca建库见下:http://blog.csdn.net/kadwf123/article/details/784299611、检查集群节点及状态:[root@rac2 ~]# olsnodes -srac1 Activerac2 Activerac3 Activerac4 Active[root@rac2 ~]_12c查看crs状态

解决jupyter notebook无法找到虚拟环境的问题_jupyter没有pytorch环境-程序员宅基地

文章浏览阅读1.3w次,点赞45次,收藏99次。我个人用的是anaconda3的一个python集成环境,自带jupyter notebook,但在我打开jupyter notebook界面后,却找不到对应的虚拟环境,原来是jupyter notebook只是通用于下载anaconda时自带的环境,其他环境要想使用必须手动下载一些库:1.首先进入到自己创建的虚拟环境(pytorch是虚拟环境的名字)activate pytorch2.在该环境下下载这个库conda install ipykernelconda install nb__jupyter没有pytorch环境

国内安装scoop的保姆教程_scoop-cn-程序员宅基地

文章浏览阅读5.2k次,点赞19次,收藏28次。选择scoop纯属意外,也是无奈,因为电脑用户被锁了管理员权限,所有exe安装程序都无法安装,只可以用绿色软件,最后被我发现scoop,省去了到处下载XXX绿色版的烦恼,当然scoop里需要管理员权限的软件也跟我无缘了(譬如everything)。推荐添加dorado这个bucket镜像,里面很多中文软件,但是部分国外的软件下载地址在github,可能无法下载。以上两个是官方bucket的国内镜像,所有软件建议优先从这里下载。上面可以看到很多bucket以及软件数。如果官网登陆不了可以试一下以下方式。_scoop-cn

Element ui colorpicker在Vue中的使用_vue el-color-picker-程序员宅基地

文章浏览阅读4.5k次,点赞2次,收藏3次。首先要有一个color-picker组件 <el-color-picker v-model="headcolor"></el-color-picker>在data里面data() { return {headcolor: ’ #278add ’ //这里可以选择一个默认的颜色} }然后在你想要改变颜色的地方用v-bind绑定就好了,例如:这里的:sty..._vue el-color-picker

迅为iTOP-4412精英版之烧写内核移植后的镜像_exynos 4412 刷机-程序员宅基地

文章浏览阅读640次。基于芯片日益增长的问题,所以内核开发者们引入了新的方法,就是在内核中只保留函数,而数据则不包含,由用户(应用程序员)自己把数据按照规定的格式编写,并放在约定的地方,为了不占用过多的内存,还要求数据以根精简的方式编写。boot启动时,传参给内核,告诉内核设备树文件和kernel的位置,内核启动时根据地址去找到设备树文件,再利用专用的编译器去反编译dtb文件,将dtb还原成数据结构,以供驱动的函数去调用。firmware是三星的一个固件的设备信息,因为找不到固件,所以内核启动不成功。_exynos 4412 刷机

Linux系统配置jdk_linux配置jdk-程序员宅基地

文章浏览阅读2w次,点赞24次,收藏42次。Linux系统配置jdkLinux学习教程,Linux入门教程(超详细)_linux配置jdk

随便推点

matlab(4):特殊符号的输入_matlab微米怎么输入-程序员宅基地

文章浏览阅读3.3k次,点赞5次,收藏19次。xlabel('\delta');ylabel('AUC');具体符号的对照表参照下图:_matlab微米怎么输入

C语言程序设计-文件(打开与关闭、顺序、二进制读写)-程序员宅基地

文章浏览阅读119次。顺序读写指的是按照文件中数据的顺序进行读取或写入。对于文本文件,可以使用fgets、fputs、fscanf、fprintf等函数进行顺序读写。在C语言中,对文件的操作通常涉及文件的打开、读写以及关闭。文件的打开使用fopen函数,而关闭则使用fclose函数。在C语言中,可以使用fread和fwrite函数进行二进制读写。‍ Biaoge 于2024-03-09 23:51发布 阅读量:7 ️文章类型:【 C语言程序设计 】在C语言中,用于打开文件的函数是____,用于关闭文件的函数是____。

Touchdesigner自学笔记之三_touchdesigner怎么让一个模型跟着鼠标移动-程序员宅基地

文章浏览阅读3.4k次,点赞2次,收藏13次。跟随鼠标移动的粒子以grid(SOP)为partical(SOP)的资源模板,调整后连接【Geo组合+point spirit(MAT)】,在连接【feedback组合】适当调整。影响粒子动态的节点【metaball(SOP)+force(SOP)】添加mouse in(CHOP)鼠标位置到metaball的坐标,实现鼠标影响。..._touchdesigner怎么让一个模型跟着鼠标移动

【附源码】基于java的校园停车场管理系统的设计与实现61m0e9计算机毕设SSM_基于java技术的停车场管理系统实现与设计-程序员宅基地

文章浏览阅读178次。项目运行环境配置:Jdk1.8 + Tomcat7.0 + Mysql + HBuilderX(Webstorm也行)+ Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。项目技术:Springboot + mybatis + Maven +mysql5.7或8.0+html+css+js等等组成,B/S模式 + Maven管理等等。环境需要1.运行环境:最好是java jdk 1.8,我们在这个平台上运行的。其他版本理论上也可以。_基于java技术的停车场管理系统实现与设计

Android系统播放器MediaPlayer源码分析_android多媒体播放源码分析 时序图-程序员宅基地

文章浏览阅读3.5k次。前言对于MediaPlayer播放器的源码分析内容相对来说比较多,会从Java-&amp;amp;gt;Jni-&amp;amp;gt;C/C++慢慢分析,后面会慢慢更新。另外,博客只作为自己学习记录的一种方式,对于其他的不过多的评论。MediaPlayerDemopublic class MainActivity extends AppCompatActivity implements SurfaceHolder.Cal..._android多媒体播放源码分析 时序图

java 数据结构与算法 ——快速排序法-程序员宅基地

文章浏览阅读2.4k次,点赞41次,收藏13次。java 数据结构与算法 ——快速排序法_快速排序法