线程相关问题

多线程
计算机在同一时间可以执行多个线程
并行
多个事情在同一时间点内发生,并行的发生是不会抢占资源的
并发
多个事情在一段时间内同时发生,并发的产生会抢占资源
多线程的好处
如果为单线程计算机一次只能处理一个线程,那么当处理的线程需要等待获取其他资源时,CPU就会处于空闲状态,这时CPU就可以执行其他的线程,从而提高CPU的利用率。由于一个程序的执行是由多个线程组成的,相比于单线程也能提高程序的执行速度。
并不是只有当正在执行的线程需要获取其他资源时线程才能切换,由于是多线程,为了满足所有的线程都能被平等地执行,于是采用的是分时的线程调度机制(当一个线程被执行的时间到达指定的时间后,就会进行等待,而CPU就会去执行其他的进程),于是会引发如下的问题。
多线程引发的问题
共享资源的安全性问题,由于计算机在同一时间执行了多个线程,当多个线程对同一资源进行修改时(访问同一资源),会导致变量出现问题。
例如:
JVM的主内存存在一个变量int a = 0;此时有两个线程A和B,都要对变量a进行加一操作,A线程先对变量a进行加一操作,此时线程A还没有对变量a的值进行覆盖,但是时间片进行了切换,此时如果线程B读取主内存中的变量a,那么变量a的旧值就会被线程B获取,线程A和线程B最后进行结果覆盖,我们就会发现此时变量a的结果只是加一了一次,与正确的结果不一样。
上下文切换问题
如今我们使用的计算机都是多线程的,所以我们才可以边听音乐边打游戏,这是由于播放音乐线程和游戏运行线程在非常短的时间内交替执行,时间短到我们无法察觉,因此我们才会觉得我们是在边听音乐边打游戏,但是线程进行上下文切换时也需要耗费一定的资源,所以当执行的线程过多时,在相同的一段时间内同一线程被执行的次数就会变少,也就会导致我们觉得计算机变卡了。
并发编程
为了解决多线程引发的共享资源的安全性问题(多个线程同一个时间点访问同一资源),我们将同时访问同一资源的线程使用编程进行管理,转换为线程依次对同一资源进行访问,这就是并发编程。
并发编程如何实现?
synchronized关键字
public class Cat {
public static int age = 0;
public static Object lock = new Object();

public synchronized void set() {// synchronized关键字修饰的方法不会同时被多个线程执行age++;
}public void play() {// synchronized关键字修饰的代码块不会同时被多个线程执行synchronized (lock) {// lock的对象头中记录的是锁的状态,为了保证不同线程获取的锁对象为同一个,age++;}
}

}

ReentrantLock类
Java内存模型(Java Memory Model)JMM
JMM指的是Java虚拟机在执行线程时内存的工作过程
Java虚拟机将所有的变量都存储在主内存当中,当一个线程需要对主内存当中的变量进行操作时,会先将主内存中的变量复制一份到线程中的工作内存当中(每一个线程都会有一个属于自己的工作内存,是线程私有的),然后对工作内存中的变量副本进行操作,最后用操作完成后的变量副本对主内存中的变量进行覆盖,如果只是查询操作就不需要对主内存中的变量进行覆盖。
Java内存模型存在的问题
不可见性
由于线程在对变量进行操作时是在线程自己私有的工作内存中,因此一个线程在对变量进行操作时,另一个线程是不知道的,所以另一个线程无法准确并适时地获取当前真正的变量,就有可能导致对变量操作后的结果存在问题。
无序性
当线程中正在执行的代码需要等待获取其他资源时,为了提高CPU的运行效率,CPU就会跳过当前代码而执行后面可以执行的代码,这种情况在大多数情况下都是没有问题的,但是毕竟运行的顺序和程序中代码写的不一样,难免会出现一些问题。
非原子性
当一个线程的时间片结束时,会进行上下文的切换,由于Java虚拟机中只能保证机器指令的执行是原子性的,而正常的一条对数据进行更改的操作,例如count++,就需要3条机器指令来完成:
指令1:获取主内存中的变量count
指令2:在工作内存中执行count++操作
指令3:将结果赋值给主内存中
只要其中一条指令被分开执行,都会导致主内存中的数据出现问题
volatile关键字
用于修饰类中的成员变量(静态和非静态),保证了这个变量在被不同线程操作时的可见性,一个线程在改变了变量的值之后,对于另一个线程来说是立即可见的,可以禁止指令的重排序,volatile不能保证对变量操作的原子性。
volatile底层实现原理
使用Memory Barrier(内存屏障)
如何保证原子性?
需要保证同一个时刻多个线程在访问同一个资源时,只有一个线程可以进行访问

synchronized关键字
synchronized关键字用于修饰方法或者代码块,修饰的方法以及代码块中的代码在同一时刻只能有一个线程进行访问,synchronized关键字可以保证原子性,同时由于关键字将代码块中的代码变为了一个整体,所以也能够保证可见性和有序性
原子变量
原子类
CAS(Compare And Swap):比较并交换,是乐观锁的一种实现方式,采用的是自旋锁的思想
CAS中包含了三个操作数内存值、预估值、更新值,当一个线程对主内存中的变量操作完成后,需要将结果赋值给主内存时,需要对内存值和预估值进行判断,内存值就是当前主内存中变量的值,预估值就是线程将主内存中变量的值复制到工作内存时的值,如果这两个值相等,就认为这个变量在线程进行结果计算时并没有发生改变,这时可以直接将得到的结果赋值给主内存中的变量,如果这两个值不相等,那么就认为,线程在对变量进行结果计算的过程中又有其他的线程对主内存中的变量进行了修改,这时这个线程就需要重新获取新的内存值,并重新进行一次结果计算,直到内存值和预估值相等。
CAS的缺点:
CAS采用自旋锁的方式(无锁),不断地重复就绪执行的状态,直到内存值和预估值相等,会占用一定的CPU,所以如果这样的线程过多的时候,就会导致CPU开销过大。
ABA问题
虽然说CAS可以通过比较内存值和预估值是否相等来判断变量是否被更改,但是有一种情况即使内存值和预估值相等的情况下,变量依然被修改了,只不过进行多次修改后的内存值仍然与预估值是相等的
解决方法
给变量添加版本号,每次在对变量进行修改时改变版本号,虽然有可能内存值和预估值比较是相等的,但是如果过程中发生了修改,那么版本号也是不同的,可以通过比较内存值和预估值,以及版本号来确定变量在内存中是否更新过。

Java中锁的分类
锁的分类可以根据特性、设计、状态进行分类,并不都是具体的实现
乐观锁/悲观锁
根据如何看待线程并发问题进行分类
乐观锁
乐观锁认为当一个线程访问数据时,另一个线程不会对这个数据进行修改,所以只会在更新数据时进行判断,如果访问数据的过程中数据修改了,那么就会不断尝试更新数据,如果数据没有修改,就直接执行赋值操作。乐观锁没有加锁。乐观锁适合读操作较多的场景,不加锁性能会提升。
悲观锁
悲观锁认为当一个线程访问数据时,另一个线程一定会对这个数据进行修改,即使没有更改数据,也会认为进行了修改。因此当多个线程对同一个数据进行访问时,会进行加锁,使同一时刻只能有一个线程访问同一资源。悲观锁适合写操作较多的场景。
可重入锁
同一个线程获取了外层方法的锁时,在进入内层方法时会自动获取锁(前提:获取的锁对象为同一个)。获取一个类中的不同方法时,synchronized和ReentrantLock获取的是同一把锁。
代码举例:
public static synchronized void testA() {
System.out.println(“A方法”);
testB();
}

public static synchronized void testB() {
System.out.println(“B方法”);
}

public static void main(String[] args) {
new Thread(() -> {
testA();
}).start();
}
synchronized锁是可重入锁,如果不是可重入锁,那么就会产生死锁。
读写锁
读写锁读读不互斥,读写互斥,写写互斥,防止一个线程在修改数据时另一个线程读取到了更改之前数据。
ReentrantReadWriteLock是一种读写锁的实现。
分段锁
分段锁简单来说就是将锁的粒度细化,对数据进行分段,并给每个分段加锁,提高并发效率。
Hashtable集合是线程安全的,使用的是synchronized关键字对每个集合中的方法都进行加锁,这样虽然是线程安全的但是同时也会降低并发效率,为了提高集合的并发效率,于是采用了分段锁,将锁的粒度进行细分,Hashtable底层实现是哈希表+链表/红黑树,并不是每次并发访问集合都会对哈希表中同一个位置进行访问,所以对哈希表的每一个位置加锁,保证同一时刻不会对哈希表中同一个位置的元素进行访问。
自旋锁
线程在获取锁失败后,不断循环尝试获取锁,不断尝试过程中该线程不会被切换,这种情况只适合低并发的场景,因为自旋锁会占用CPU的资源,如果并发数量过多,会导致CPU占用过大。自旋锁适合加锁时间(获取锁的线程的执行时间)非常短的场景,这样不断尝试获取锁的线程的等待时间也会很短,相比于将线程
共享锁/独占锁
共享锁:锁对象可以被多个线程持有,读锁就是共享锁
独占锁:锁对象只能被同一线程持有,写锁就是独享锁
共享锁可以提高对同一个数据的并发读操作的效率
公平锁/非公平锁
非公平锁:多个线程谁先获取锁对象,谁就可以执行
公平锁:多个线程按照先来先服务的原则,依次获取锁对象
synchronized锁是非公平锁
ReentrantLock锁默认是非公平的,也可以实现公平锁
ReentrantLock底层实现类AbstractQueuedSynchronizer(AQS)中存在队列,将没有获取锁对象的线程存放入队列中,来实现公平锁。
偏向锁/轻量级锁/重量级锁(锁的状态)
每一个对象都会有对象头,锁的状态就存放在对象头中的Mark Word区域中
偏向锁
锁的状态一开始是无锁状态(没有一个线程访问同步代码块),当同步代码块一直被同一个线程访问时,锁的状态就会变为偏向锁的状态,这时锁对象会将该线程的id记录在对象头中,降低之后再次获取锁对象的成本,提高运行效率。
轻量级锁
当锁是偏向锁的状态时,其他线程获取不到锁对象,此时锁状态就会升级为轻量级锁,此时访问不到锁对象的线程会采用自旋锁的方式不断尝试获取锁对象。
重量级锁
当锁是轻量级锁的状态时,采用自旋锁方式获取锁对象的线程,经过一定的自旋次数仍然没有获取到锁对象,就会进入阻塞状态,此时锁状态上升位重量级锁,进入阻塞状态的线程只能等待CPU的调度。
对象结构
对象在Java虚拟机内存中有三部分区域:对象头、实例数据和对齐填充
对象头中有一块区域Mark Word用于存放对象运行时的数据,如HashCode、经历垃圾回收的次数、锁状态标志(用于记录锁对象是否正在被线程使用)、线程持有的锁状态(偏向、轻量级、重量级)、偏向锁的线程id
Synchronized锁实现
synchronized关键字用于修饰同步代码块和同步方法,同步方法和同步代码块都是通过moniterenter和moniterexit指令实现,同步方法在调用时会有一个ACC_SYNCHRONIZED标识,用于说明该方法为同步方法,所以在方法开始执行时执行moniterenter指令,当方法执行完成后执行moniterexit指令。同步代码块没有标识,只是会在进入同步代码块之前指令moniterenter指令,在同步代码块执行结束时执行moniterexit指令。
在执行moniterenter指令时,线程会尝试获取该同步代码块或同步方法的锁对象,如果获取成功,就将锁对象的计数器加一,在执行moniterexit指令时,会将锁对象的计数器减一,当锁对象的计数器为0时,该锁对象就会被释放,也就是说同步代码块或同步方法中的内容已经执行完成。
AQS(AbstractQueuedSynchronizer)
ReentrantLock锁、ReentrantReadWriteLock锁都是基于AQS抽象类中的方法实现的,AQS位于java.util.concurrent.locks包中,也就是说AQS是解决线程安全问题的锁的实现类,AQS是抽象类但是其中没有抽象方法。
AQS类中有一个int类型成员变量state,用于线程判断锁对象是否正在被使用,默认情况下state的值为0,当有线程正在使用锁对象时state的值变为1。使用volatile关键字修饰state变量可以确保可见性和有序性,所以为了确保对变量修改的原子性,AQS类中提供了特定的原子性的方法,保证对变量的操作是线程安全的。
当多个线程获取锁对象时,AQS会将获取不到锁对象的线程存放到队列当中,队列底层是由双向链表实现,链表的每一个节点(Node:AQS的内部类)封装着一个线程,链表的头部为获取到锁对象的线程。
acquire()方法
用于获取锁对象
方法中的第一步调用tryAcquire(int age)方法尝试获取锁对象,如果获取成功返回true,acquire()方法执行完毕,否则返回false,于是尝试获取锁对象的线程就会被封装到Node节点中,并插入到双向链表的尾部。
AQS的锁存在独占锁和共享锁
独占锁:ReentrantLock锁就是独占锁
共享锁:ReentrantReadWriteLock锁中的读锁就是共享锁
ReentrantLock锁的实现
ReentrantLock锁中又分为公平锁和非公平锁,他们的区别在于公平锁是当线程获取锁对象时,直接将线程放入队列中进行等待,而非公平锁时当线程获取锁对象时,会先尝试获取锁对象,如果尝试获取不到才会将线程放入队列中进行等待。
如下为公平锁和非公平锁获取锁的实现代码
final void lock() {// 公平锁获取锁对象
acquire(1);// 排队
}

final void lock() {// 非公平锁获取锁对象
if (compareAndSetState(0, 1))// 先尝试获取锁对象,底层使用AQS实现,根据变量state判断是否可以获取锁对象
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);// 排队
}
为什么公平锁和非公平锁都需要将线程进入等待队列中?
进入等待队列中的线程会被阻塞,当锁释放后,系统会对阻塞的线程进行唤醒,使线程获取锁对象,如果阻塞的线程没有进入等待队列,系统就会随机唤醒阻塞线程,这样有可能导致有些阻塞的线程永远不会被唤醒。
ReentrantLock:悲观锁、公平锁(可以实现非公平锁)、互斥锁、可重入锁
Synchronized:悲观锁、非公平锁、互斥锁、可重入锁
JUC常用类(Java.util.concurrent)
ConcurrentHashMap
ConcurrentHashMap和HashMap一样都是线程安全的,但是ConcurrentHashMap的效率高于HashMap。原因是因为ConcurrentHashMap采用分段锁的思想,降低锁的粒度,提高了执行效率。
HashMap在方法中使用synchronized关键字,使方法变为同步方法,多个线程不能同时调用同一个方法,而ConcurrentHashMap使用分段锁,将锁的粒度变为链表或者红黑树,这样多个线程在同一时刻就可以调用同一个方法,提高了执行效率。
JDK1.8之后放弃分段锁的原因?
分段锁是给链表或者红黑树加锁,当集合中的元素越来越多时,会导致链表变得越来越长,进而会使锁的粒度变得越来越大,从而导致分段锁的性能变低。
分段锁是给Hash表中的每一个节点都添加锁,不管该索引位置下是否存在元素都会存在锁,有可能该索引下一直没有元素,导致内存空间的浪费。
JDK1.8之后放弃了分段锁而使用粒度更小的Node锁,在添加元素时首先判断该索引位置下是否存在元素,如果不存在元素,就直接添加元素,如果存在元素,就给该位置下的Node头节点添加Node锁(synchronized锁),确保添加时的原子性。
ConcurrentHashMap不支持存储null键和null值的原因?
不支持null值是为了消除歧义,如果键值对的值为null,那么在使用键获取对应的值时,如果返回值为null,我们不清楚是输入键所对应的值为null,还是输入键因为在集合中不存在所以返回null。
CopyOnWriteArrayList
CopyOnWriteArrayList相比于ArrayList是线程安全的,虽然Vector同样也是线程安全的,但是CopyOnWriteArrayList的效率要高于Vector。
Vector给每一个方法都添加了synchronized关键字,同一时刻只能有一个线程访问同一个方法,CopyOnWriteArrayList允许同一时刻有多个线程进行读操作,但是同一时刻只能有一个线程进行写操作,CopyOnWriteArrayList在进行写操作时,会先对集合中的底层数组进行复制一份,并对复制出来的数组进行数据修改操作,当修改完成后再使用修改完成后的数组对原数组进行覆盖,这样在多线程的情况下,当一个线程对集合进行写操作的同时,其他线程还能对集合进行读操作,提高了运行效率。CopyOnWriteArrayList使用ReentrantLock锁实现对集合写操作的并发执行。
CopyOnWriteArraySet
CopyOnWriteArraySet是基于CopyOnWriteArrayList实现的,不能存储重复元素。
CountDownLatch
CountDownLatch允许一个线程在等待其他线程执行完毕之后在去执行,在创建对象时指定参数用于表示线程等待其他线程的数量。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/142623.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JNDI注入

1、什么是 JNDI JNDI(Java Naming and Directory Interface, Java命名和目录接口),JNDI API 映射为特定的命名(Name)和目录服务(Directory)系统,使得Java应用程序可以和这些命名(Name&#xff…

【Shell脚本11】Shell 函数

Shell 函数 linux shell 可以用户定义函数,然后在shell脚本中可以随便调用。 shell中函数的定义格式如下: [ function ] funname [()]{action;[return int;]}说明: 1、可以带function fun() 定义,也可以直接fun() 定义,不带任何…

SQL基础理论篇(一):什么是SQL

文章目录 什么是SQLSQL的四大部分常用的SQL标准参考文献 什么是SQL SQL的全称是Structured Query Language,即结构化查询语句。 其最早诞生于1974年,IBM研究员发布的一篇论文"SEQUEL:一门结构化的英语查询语言"。这几十年里&…

旺店通·企业版对接打通金蝶云星空查询调拨单接口与分布式调入单新增接口

旺店通企业版对接打通金蝶云星空查询调拨单接口与分布式调入单新增接口 源系统:旺店通企业版 旺店通是北京掌上先机网络科技有限公司旗下品牌,国内的零售云服务提供商,基于云计算SaaS服务模式,以体系化解决方案,助力零售企业数字化…

Android framework添加自定义的Product项目,lunch目标项目

文章目录 Android framework添加自定义的Product项目1.什么是Product?2.定义自己的Product玩一玩 Android framework添加自定义的Product项目 1.什么是Product? 源码目录下输入lunch命令之后,简单理解下面这些列表就是product。用于把系统编…

OpenCV+特征检测

检测 函数cv.cornerHarris()。其参数为: img 输入图像,应为灰度和float32类型blockSize是拐角检测考虑的邻域大小ksize 使用的Sobel导数的光圈参数k 等式中的哈里斯检测器自由参数 import numpy as np import cv2 as cv filename chessboard.png img…

如何显示标注的纯黑mask图

文章目录 前言一、二分类mask显示二、多分类mask显示 前言 通常情况下,使用标注软件标注的标签图看起来都是纯黑的,因为mask图为单通道的灰度图,而灰度图一般要像素值大于128后,才会逐渐显白,255为白色。而标注的时候…

sass 生成辅助色

背景 一个按钮往往有 4 个状态。 默认状态hover鼠标按下禁用状态 为了表示这 4 个状态&#xff0c;需要设置 4 个颜色来提示用户。 按钮类型一般有 5 个&#xff1a; 以 primary 类型按钮为例&#xff0c;设置它不同状态下的颜色&#xff1a; <button class"btn…

IP-guard Webserver view 远程命令执行漏洞【2023最新漏洞】

IP-guard Webserver view 远程命令执行漏洞【2023最新漏洞】 一、漏洞描述二、漏洞影响三、漏洞危害四、FOFA语句五、漏洞复现1、手动复现yaml pocburp发包 2、自动化复现小龙POC检测工具下载地址 免责声明&#xff1a;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传…

R程序 示例4.3.2版本包 在centos进行编译部署

为了在CentOS上下载和编译R语言4.3.2包&#xff0c;可以按照以下步骤进行操作&#xff1a; 1.首先&#xff0c;需要安装一些必要的依赖项。可以使用以下命令安装它们&#xff1a; sudo yum install -y epel-release sudo yum install -y gcc gcc-c gcc-gfortran readline-dev…

Linux 使用随记

Linux 使用随记 shell 命令行模式登录后所取得的程序被成为shell&#xff0c;这是因为这个程序负责最外层的跟用户&#xff08;我们&#xff09;通信工作&#xff0c;所以才被戏称为shell。 命令 1、命令格式 command [-options] parameter1 parameter2 … 1、一行命令中第…

UML建模语言

UML建模语言 类的关系 依赖关系 类的方法中使用形参、局部变量或者静态方法的方式调用其他类&#xff0c;表示当前类依赖其他类。 public class Main {public void eat(Person person) {person.play();// 方法参数Student student new Student();student.study();// 局部变…

4 条件判断和循环

文章目录 一、条件判断和循环1.1 if语句1.2 if-else1.3 if-elif-else1.4 for循环1.5 while循环1.6 break退出循环1.7 continue继续循环1.8 多重循环 二、练习题小结 一、条件判断和循环 1.1 if语句 输入用户年龄&#xff0c;根据年龄打印不同的内容&#xff0c;在Python程序中…

C#几种截取字符串的方法

在C#编程中&#xff0c;经常需要对字符串进行截取操作&#xff0c;即从一个长字符串中获取所需的部分信息。本文将介绍几种常用的C#字符串截取方法&#xff0c;并提供相应的示例代码。 目录 1. 使用Substring方法2. 使用Split方法3. 使用Substring和IndexOf方法4. 使用Regex类…

JVM之垃圾回收

1. 如何判断对象可以回收 1.1 引用计数法 引用计数法是一种内存管理技术&#xff0c;其中每个对象都有一个与之关联的引用计数。引用计数表示当前有多少个指针引用了该对象。当引用计数变为零时&#xff0c;表示没有指针再指向该对象&#xff0c;该对象可以被释放&#xff0c…

HBase学习笔记(3)—— HBase整合Phoenix

目录 Phoenix Shell 操作 Phoenix JDBC 操作 Phoenix 二级索引 HBase整合Phoenix Phoenix 简介 Phoenix 是 HBase 的开源 SQL 皮肤。可以使用标准 JDBC API 代替 HBase 客户端 API来创建表&#xff0c;插入数据和查询 HBase 数据 使用Phoenix的优点 在 Client 和 HBase …

C++虚基类详解

多继承&#xff08;Multiple Inheritance&#xff09; 是指从多个直接基类中产生派生类的能力&#xff0c;多继承的派生类继承了所有父类的成员。尽管概念上非常简单&#xff0c;但是多个基类的相互交织可能会带来错综复杂的设计问题&#xff0c;命名冲突就是不可回避的一个。…

云原生Kubernetes系列 | 通过容器互联搭建wordpress博客系统

云原生Kubernetes系列 | 通过容器互联搭建wordpress博客系统 通过容器互联搭建一个wordpress博客系统。wordpress系统是需要连接到数据库上的,所以wordpress和mysql的镜像都是需要的。wordpress在创建过程中需要指定一些参数。创建mysql容器时需要把mysql的数据保存在宿主机本…

uni-app报错“本应用使用HBuilderX x.x.x 或对应的cli版本编译,而手机端SDK版本是x.x.x不匹配的版本可能造成应用异常”

uniapp开发的一个跨平台软件&#xff0c;在安卓模拟器上启动的时候报警告&#xff1a; 官方给的解释&#xff1a;uni-app运行环境版本和编译器版本不一致的问题 - DCloud问答 解决办法有两个 方法一&#xff1a;添加忽略警告的配置 项目根目录下找到 manifest.json&#xf…

C现代方法(第20章)笔记——底层程序设计

文章目录 第20章 底层程序设计20.1 位运算符20.1.1 移位运算符20.1.2 按位取反运算符、按位与运算符、按位异或运算符和按位或运算符20.1.3 用位运算符访问位20.1.4 用位运算符访问位域20.1.5 程序——XOR加密 20.2 结构中的位域20.2.1 位域是如何存储的 20.3 其他底层技术20.3…