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

智能推荐

while循环&CPU占用率高问题深入分析与解决方案_main函数使用while(1)循环cpu占用99-程序员宅基地

文章浏览阅读3.8k次,点赞9次,收藏28次。直接上一个工作中碰到的问题,另外一个系统开启多线程调用我这边的接口,然后我这边会开启多线程批量查询第三方接口并且返回给调用方。使用的是两三年前别人遗留下来的方法,放到线上后发现确实是可以正常取到结果,但是一旦调用,CPU占用就直接100%(部署环境是win server服务器)。因此查看了下相关的老代码并使用JProfiler查看发现是在某个while循环的时候有问题。具体项目代码就不贴了,类似于下面这段代码。​​​​​​while(flag) {//your code;}这里的flag._main函数使用while(1)循环cpu占用99

【无标题】jetbrains idea shift f6不生效_idea shift +f6快捷键不生效-程序员宅基地

文章浏览阅读347次。idea shift f6 快捷键无效_idea shift +f6快捷键不生效

node.js学习笔记之Node中的核心模块_node模块中有很多核心模块,以下不属于核心模块,使用时需下载的是-程序员宅基地

文章浏览阅读135次。Ecmacript 中没有DOM 和 BOM核心模块Node为JavaScript提供了很多服务器级别,这些API绝大多数都被包装到了一个具名和核心模块中了,例如文件操作的 fs 核心模块 ,http服务构建的http 模块 path 路径操作模块 os 操作系统信息模块// 用来获取机器信息的var os = require('os')// 用来操作路径的var path = require('path')// 获取当前机器的 CPU 信息console.log(os.cpus._node模块中有很多核心模块,以下不属于核心模块,使用时需下载的是

数学建模【SPSS 下载-安装、方差分析与回归分析的SPSS实现(软件概述、方差分析、回归分析)】_化工数学模型数据回归软件-程序员宅基地

文章浏览阅读10w+次,点赞435次,收藏3.4k次。SPSS 22 下载安装过程7.6 方差分析与回归分析的SPSS实现7.6.1 SPSS软件概述1 SPSS版本与安装2 SPSS界面3 SPSS特点4 SPSS数据7.6.2 SPSS与方差分析1 单因素方差分析2 双因素方差分析7.6.3 SPSS与回归分析SPSS回归分析过程牙膏价格问题的回归分析_化工数学模型数据回归软件

利用hutool实现邮件发送功能_hutool发送邮件-程序员宅基地

文章浏览阅读7.5k次。如何利用hutool工具包实现邮件发送功能呢?1、首先引入hutool依赖<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.19</version></dependency>2、编写邮件发送工具类package com.pc.c..._hutool发送邮件

docker安装elasticsearch,elasticsearch-head,kibana,ik分词器_docker安装kibana连接elasticsearch并且elasticsearch有密码-程序员宅基地

文章浏览阅读867次,点赞2次,收藏2次。docker安装elasticsearch,elasticsearch-head,kibana,ik分词器安装方式基本有两种,一种是pull的方式,一种是Dockerfile的方式,由于pull的方式pull下来后还需配置许多东西且不便于复用,个人比较喜欢使用Dockerfile的方式所有docker支持的镜像基本都在https://hub.docker.com/docker的官网上能找到合..._docker安装kibana连接elasticsearch并且elasticsearch有密码

随便推点

Python 攻克移动开发失败!_beeware-程序员宅基地

文章浏览阅读1.3w次,点赞57次,收藏92次。整理 | 郑丽媛出品 | CSDN(ID:CSDNnews)近年来,随着机器学习的兴起,有一门编程语言逐渐变得火热——Python。得益于其针对机器学习提供了大量开源框架和第三方模块,内置..._beeware

Swift4.0_Timer 的基本使用_swift timer 暂停-程序员宅基地

文章浏览阅读7.9k次。//// ViewController.swift// Day_10_Timer//// Created by dongqiangfei on 2018/10/15.// Copyright 2018年 飞飞. All rights reserved.//import UIKitclass ViewController: UIViewController { ..._swift timer 暂停

元素三大等待-程序员宅基地

文章浏览阅读986次,点赞2次,收藏2次。1.硬性等待让当前线程暂停执行,应用场景:代码执行速度太快了,但是UI元素没有立马加载出来,造成两者不同步,这时候就可以让代码等待一下,再去执行找元素的动作线程休眠,强制等待 Thread.sleep(long mills)package com.example.demo;import org.junit.jupiter.api.Test;import org.openqa.selenium.By;import org.openqa.selenium.firefox.Firefox.._元素三大等待

Java软件工程师职位分析_java岗位分析-程序员宅基地

文章浏览阅读3k次,点赞4次,收藏14次。Java软件工程师职位分析_java岗位分析

Java:Unreachable code的解决方法_java unreachable code-程序员宅基地

文章浏览阅读2k次。Java:Unreachable code的解决方法_java unreachable code

标签data-*自定义属性值和根据data属性值查找对应标签_如何根据data-*属性获取对应的标签对象-程序员宅基地

文章浏览阅读1w次。1、html中设置标签data-*的值 标题 11111 222222、点击获取当前标签的data-url的值$('dd').on('click', function() { var urlVal = $(this).data('ur_如何根据data-*属性获取对应的标签对象

推荐文章

热门文章

相关标签