欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 新闻 > 国际 > 并发编程 面试速记

并发编程 面试速记

2025/4/3 12:07:59 来源:https://blog.csdn.net/red_Apple_3_11/article/details/146445587  浏览:    关键词:并发编程 面试速记

复习线程基础内容

线程的概念

线程是进程中的一个最小执行单元,是一个独立的任务,是cpu执行的最小单元.

把一些独立的任务放在线程中执行,多个线程可以同时并行的执行,提高了程序效应处理的速度.

创建线程

  1. 类 继承 Thread类

  2. 类 实现Runnable接口 重写public void run() { } new Thread(任务)

  3. 类 实现Callable接口 重写public T call()throws Exception{} 可以抛出异常

  4. 使用线程池创建

线程方法

 run() 执行的任务
​
start() 启动线程
​
sleep(时间);让线程休眠
​
join() ;   线程加入   让其他线程等待当前线程执行完后再执行

currentThread() 获得当前正在执行的线程

线程的状态

创建 new Thread 还不能执行

就绪状态 start() 把线程注册到操作系统

运行状态 获得了cpu的执行

阻塞状态 sleep() join wait 等待同步锁 期间操作系统就不再调用了 等待阻塞动作完成后,再回到就绪状态

死亡状态 任务运行结束,出异常没有处理

多线程访问共享数据

存在资源竟用问题

加锁

使用synchronized关键字修饰代码块和方法

同步锁对象,任意类的对象都可以,但是只能是唯一的一个对象,记录有没有线程进入到同步代码块

synchronized(同步锁对象){ 
​}

synchronized修饰方法时,对象是自动提供的,

synchronized修饰方法时,同步锁对象不需要我们指定 同步锁对象会默认提供: 1.非静态的方法--锁对象默认是this 2.静态方法--锁对象是当前类的Class对象(类的对象,一个类的对象只有一个)

public synchronized void print(){ if(num>0){ System.out.println(Thread.currentThread().getName()+"买到了第"+num+"张票"); num--; } }

ReentrantLock类实现 java类实现

lock()

unlock()

synchronized 和 ReentrantLock区别:

synchronized是一个关键字,控制依靠底层编译后的指令去实现
synchronized可以修饰一个方法,还可以修饰一个代码块
synchronized是隐式的加锁和释放锁,一旦方法或代码块中运行结束或出现异常,会自动释放锁
​
ReentrantLock是一个类,是依靠java代码去控制(底层有一个同步队列)
ReentrantLock只能修饰代码块
ReentrantLock需要手动的加锁,手动的释放锁,所以释放锁最好写在finally中,一旦出现异常,保证锁能释放

线程通信(生产者,消费者模型)

wait() notify() notifyAll() 都只能在同步代码块中使用, 他们是Object类中定义的方法, 调用的对象只能是锁对象

sleep()和wait()区别

sleep(时间) 休眠指定的时间,时间到了后,会自动进入到就绪状态, 休眠期间不会释放锁 是Thread类中的方法

wait() 线程等待,不会自己醒来,需要其他线程唤醒, wait()是会释放锁的 是Object类中的方法

多线程优点

提高程序的响应处理速度,提高cpu的利用率,压榨硬件的剩余价值

问题:

  多线程访问同一资源

现在cpu是多内核的,在理论上是可以同时执行多个线程的.

并行执行: 在同一个时间节点上,多个线程同时执行

并发执行: 在一个时间段内,多个线程交替执行 微观上是一个一个的执行, 宏观上感觉是同时执行

并发编程核心问题

不可见性,乱序性,非原子性

也就是多个线程访问共享数据时,出现问题的根本原因.

不可见性

java内存模型 Java Memory Model,JMM

java内存模型是变量数据(票数)都存在主内存中,每个线程还有自己的工作内存(本地内存),

规定线程不能直接对主内存中的数据操作,只能把主内存数据加载到自己的工作内存中操作,

操作完成后,再写回主内存.

这样的设计就会引发不可见性问题. 一个线程在自己的工作内存中操作了数据后,另一个线程也正在操作,但是不知道数据已经被另一个线程修改了,

乱序性 cpu 读指令的同时可以同时执行不影响其他的指令

a = 3

a= 4

c =a +b

为了优化指令执行,在执行一些等待时间长的执行时,可以把其他的一些指令提前执行,提高速度.

但是在多线程场景下,就可能会出现问题

非原子性

线程的切换执行带来非原子性问题

cpu保证的原子性执行时cpu指令级别的,但是对于高级语言的一条代码,有时是要拆分成多条执令的

线程在执行到某条执行时,操作系统会切换到其他线程去执行,这样这条高级语言指令执行就是非原子性的.

例如: ++操作 分成3条指令, 1.加载主内存数据到工作内存,2.在工作内存操作数据,3.写回主内存

总结: 工作内存的缓存导致了不可见性, 指令的优化导致了乱序(无序)性,线程的切换执行导致非原子性

volatile关键字 - 解决不可见性 乱序性

volatile关键字修饰的变量,在一个工作内存中操作后,底层会将工作内存中的数据同步到其他线程的工作内存,使其立即可见. 解决了不可见性的问题

volatile关键字修饰后的变量,在执行时,不会让优化重排序,解决了乱序性.

但是volatile无法解决非原子性问题.

如何保证原子性 ----加锁

解决非原子性问题,可以通过加锁的方式实现,

synchronized 和 ReentrantLock都可以实现.

java中还提供一种方案,在不加锁的情况下,实现++操作的原子性,

就是原子类.AtomicInteger

在java.util.concurrent包下,定义了许多与并发编程相关的处理类, 此包一般大家也简称JUC.

 private  static AtomicInteger atomicInteger = new AtomicInteger(0);
​public static void main(String[] args) {for (int i = 0; i < 10; i++) {new Thread() {@Overridepublic void run() {//不加锁的方式,在多线程中实现++操作,满足原子性的System.out.println(atomicInteger.incrementAndGet());}}.start();}

AtomicInteger

实现方式: 采用CAS(比较并交换)思想,当多个线程对同一个内存数据库操作时,

假设A线程把主内存数据加载到自己工作内存中,这个工作内存中的值就是预期值,

然后在自己的工作内存中操作后,当写回主内存时,先判断预期值和主内存的值是否一致,如果一致,说明还没有其他线程修改,直接写回主内存,

一旦预期值和主内存中的值不一样,说明有其他线程已经修改过了,线程A需要重新获取主内存中值,重新操作,判断.

直到预期值和主内存值相同,才结束,否则自旋一直判断.

由于采用自旋方式实现,使得线程都不会阻塞,一直自旋,适合并发量低的情况.

如果并发了过大,线程一直自旋,会导致cpu开销大.

还会有一个ABA问题: 线程A拿到主内存值后,期间有其他线程已经多次修改内存数据,最终又修改的和线程A拿到值相同,

可以通过带版本号的原子类,每次操作时改变版本号即可.

private  AtomicStampedReference atomicStampedReference= new AtomicStampedReference(100,0);//预期值和版本号解决aba问题

A->B->A

A C

锁分类

锁分类,不全是指java中的锁,有的指锁的特征,有的指锁的实现,有的指锁的状态

乐观锁,悲观锁

乐观锁: 是一种不加锁的实现,例如原子类, 认为不加锁,采用自旋方式尝试修改共享数据,是不会有问题的.

悲观锁: 是一种加锁实现,例如synchronized 和 ReentrantLock, 认为不加锁修改共享数据会出问题

可重入锁

可重入锁又名递归锁, 当同一个线程,获取锁进入到完成方法后,可以在内存进入到另一个方法(内存方法与外层方法使用的是同一把锁)

synchronized 和 ReentrantLock都是可以重入的锁

读写锁

ReentrantReadWriteLock 读写锁实现

ReentrantReadWriteLock.WriteLock
ReentrantReadWriteLock.ReadLock读读不互斥
读写互斥
写写互斥
只要有写操作,其他线程就不能写,也不能读,保证读不到脏数据
最大的保证读的效率

分段锁

将锁的粒度进一步细化,提高并发效率

Hashtable是线程安全的,方法上都加了锁 假如有两个线程同时读,也只能一个一个的读,并发效率低

ConcurrentHashMap没有给方法上加锁,使用hash表中的每个位置上的第一个对象作为锁对象,这样就可以多个线程对不同的位置进行操作,相互不影响,只有对同一个位置操作时,才互斥.

有多把锁,提高并发操作的效率

自旋锁

线程尝试不断的获取锁,当第一次获取不到时,线程不阻塞,尝试继续获取锁,有可能后面几次尝试后,有其他线程释放了锁,此时就可以获取锁, 当尝试获取到一定次数后(默认10次),任然获取不到锁,那么可以进入阻塞状态.

synchronized 就是一种自旋锁

并发量的低的情况下适合自旋

共享锁/独占锁

共享锁: 锁可以内多个线程共享, 读写锁中读锁就是共享锁

独占锁: 一把锁只能有一个线程使用,读写锁的写锁,synchronized 和 ReentrantLock都是独占锁.

公平锁/非公平锁

公平锁: 可以按照请求获得锁的顺序来得到锁

非公平锁:不按照请求获得锁的顺序得到锁

synchronized 是非公平

ReentrantLock默认是非公平

public ReentrantLock() {sync = new NonfairSync();
}

可以通过构造方法参数设置选择公平实现或非公平实现

public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();
}

锁状态

java中为了synchronized 进行优化,提供了3种锁状态,

偏向锁: 一段同步代码块一直由一个线程执行,那么会在锁对象中记录下了线程信息,可以直接获得锁.

轻量级锁: 当锁状态为偏向锁时,此时又有其他线程访问,锁状态升级为轻量级锁,线程不阻塞,采用自旋方式获取锁.

重量级锁: 当锁状态为轻量级锁时,如果有大量的线程到来,大量的线程自旋,锁状态升级为重量级锁,自旋的线程会进入到阻塞状态,由操作系统去调度管理.

synchronized

是一个关键字,实现同步,还需要我们提供一个同步锁对象,记录锁状态,记录线程信息

控制同步,是依靠底层的指令实现的.

如果是同步方法,在指令中会为方法添加ACC_SYNCHRONIZED标志

如果是同步代码块,在进入到同步代码块时,会执行monitorenter, 离开同步代码块时或者出异常时,执行monitorexit

AQS

AQS(AbstractQueuedSynchronizer 抽象同步队列) 是一个实现线程同步的框架

并发包中很多类的底层都用到了AQS

链表 状态,改变状态的方法

class AbstractQueuedSynchronizer {private transient volatile Node head;private transient volatile Node tail;private volatile int state; //表示有没有线程访问共享数据  默认是0 表示没有线程访问//修改状态的方法   cas机制   protected final boolean compareAndSetState(int expect, int update) {return unsafe.compareAndSwapInt(this, stateOffset, expect, update);}//内部类   static final class Node {volatile Node prev;volatile Node next;volatile Thread thread;}}

ReentrantLock实现

三个内部类 公平非公平 都继承sync sync继承aqs

 class ReentrantLock{abstract static class Sync extends AbstractQueuedSynchronizer {abstract void lock();}//非公平锁static final class NonfairSync extends Sync {void lock(){}}//公平锁static final class FairSync extends Syn c {void lock(){}}}

公平和非公平区别

JUC 常用类

在集合类中,像Vector,Hashtable这些类加锁时都是直接把锁加载方法上了,性能就低, 在并发访问量小的情况下,还可以使用, 大并发访问量下,性能就太低了.

ConcurrentHashMap

hashmap 双列集合键不能重复 ,如何判断键不能重复 ,hashcode 和equals  先用hashcode方法判断不同肯定不同,然后相同用equals方法 
HashMap适合单线程场景下的,不允许多个线程同时访问操作,如果有多线程访问会报异常Hashtable 是线程安全的 直接给方法加锁,效率低ConcurrentHashMap 是线程安全的,没有直接给方法加锁, 用哈希表中每一个位置上的第一个元素(第一个是存在元素)作为锁对象哈希表长度是16,那么就有16把锁对象,锁住自己的位置即可,这样如果多个线程如果操作不同的位置,那么相互不影响,只有多个线程操作同一个位置时,才会等待如果位置上没有任何元素,那么采用cas机制插入数据到对应的位置Hashtable ,ConcurrentHashMap  键值都不能为null为什么这样设计,键值都不能为null?map.put("b","b")为了消除歧义  System.out.println(map1.get("a"));//null  值是null  还是键不存在返回null

CopyOnWriteArrayList 写少读多

ArrayList 是单线程场景下使用的,在多线程场景下会报异常
Vector 是线程安全的,在方法上加了锁,效率低
CopyOnWriteArrayList  写方法操作加了锁(ReentrantLock实现的),
在写入数据时,先把原数组做了备份,把要添加的数据写入到备份数组中,当写入完成后,再把修改的数组赋值到原数组中去
给写加了锁,读没有加锁,读的效率变高了, 这种适合写操作少,读操作多的场景

CopyOnWriteArraySet

CopyOnWriteArraySet 的实现基于 CopyOnWriteArrayList,不能存储重复数据。

辅助类 CountDownLatch

.countDown(); 计数器的一种操作 让前面的线程先执行,然后执行后面的线程 底层也是 aqs同步队列

池的概念 缓冲

字符串常量池

String s1 = "abc"; String s2="abc"; s1==s2//true

Integer自动装箱 缓存了-128 --+127之间的对象

Integer a = 100; Integer b = 100; a==b //true IntegerCache.cache[i + (-IntegerCache.low)];

数据库连接池

阿里巴巴Druid数据库连接池

帮我们缓存一定数量的链接对象,放在池子里,用完还回到池子中,

减少了对象的频繁创建和销毁的时间开销

线程池

为减少频繁的创建和销毁线程,

jdk5开始引入了线程池,

建议使用ThreadPoolExecutor类来创建线程池, 这样提高效率.

Java.uitl.concurrent.ThreadPoolExecutor

 public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)

7个参数

corePoolSize: 核心线程池中的数量(初始化的数量) 5

maximumPoolSize:线程池中最大的数量 10 5

keepAliveTime: 空闲线程存活时间 当核心线程池中的线程足以应付任务时, 非核心线程池中的线程在指定空闲时间到期后,会销毁.

unit: 时间单位

workQueue: 5 等待队列, 当核心线程池中的线程都在使用时,如果有任务继续到来,会先将等待的任务放到队列中,如果队列也满了,才会创建新的线程(非核心线程池中的线程)

threadFactory:线程工厂,用来创建线程池中的线程

handler:拒绝处理任务时的策略 4种拒绝策略

线程池工作流程

当有大量的任务到来时,先判断核心线程池中的线程是否都忙着,

     有空闲的,直接让核心线程中的线程执行任务没有空闲的,   判断等待队列是否已满,如果没满,把任务添加到队列等待如果已满,判断非核心线程池中的线程是否都忙着如果有空闲的,没满,交由非核心线程池中的线程执行如果非核心线程池野已经满了,那么就使用对应的拒绝策略处理.

4种拒绝策略:

AbortPolicy: 抛异常

CallerRunsPolicy: 由提交任务的线程执行 例如在main线程提交,则由main线程执行拒绝的任务 DiscardOldestPolicy: 丢弃等待时间最长的任务

DiscardPolicy: 丢弃最后不能执行的任务

提交任务的方法

void   execute(任务);  提交任务没有返回值Future<?> submit = executor.submit(myTask);//提交任务可以接收返回值
submit.get();  

关闭线程池

shutdown();  //执行shutdown()后,不再接收新的任务,会把线程池中还有等待队列中已有的任务执行完,再停止
shutdownNow(); //立即停止,队列中等待的任务就不再执行了.

ThreadLocal

是什么

本地线程变量,为每个线程提供一个变量副本,只在当前线程中使用,相互是隔离的

底层构造

为每个线程对象创建ThreadLocalMap对象,赋给Thread类中threadLocals,用threadLocals存储每个线程的变量副本

内存泄漏

对象已经不用了,但是垃圾回收不能回收该对象.(例如数据库连接对象,流对象,socket....)

对象引用分为四种:

强引用

Object obj = new Object(); 强引用

obj.hashCode();

obj=null; 没有引用指向对象

对象如果有强引用关联,那么肯定是不能被回收的

软引用

被SoftReference类包裹的对象, 当内存充足时,不会被回收,当内存不足时,即使有引用指向,也会被回收

 Object o1 = new Object();SoftReference<Object> softReference = new SoftReference<Object>(o1);

弱引用

被WeakReference类包裹的对象,只要发送垃圾回收,该类对象都会被回收掉,不管内存是否充足

Object o1 = new Object();WeakReference<Object> weakReference = new WeakReference<Object>(o1);

ThreadLocal 被弱引用管理static class Entry extends WeakReference<ThreadLocal<?>> {}

当发生垃圾回收时,被回收掉,但是value还与外界保持引用关系,不能被回收. 造成内存泄漏

threadLocal.remove();//不再使用时,调用remove方法,删除键值对,可以避免内存泄漏问题

虚引用

被PhantomReference类包裹的对象,随时都可以被回收,

通过虚引用对象跟踪对象回收的状态

深层拷贝的概念

深层拷贝指的是创建一个新对象,新对象和原对象不但拥有不同的内存地址,而且它们内部所包含的所有引用类型的成员变量也会被递归地复制,这就保证了新对象和原对象在任何层次上都是相互独立的,对其中一个对象的修改不会影响到另一个对象。

序列化与反序列化的原理

  • 序列化:把对象的状态转换为字节序列,这些字节序列包含了对象的所有信息,像对象的类名、成员变量的值等。这个字节序列能够存储在文件中,也可以通过网络传输。

  • 反序列化:把字节序列再转换回对象。在反序列化的过程中,Java 会依据字节序列里的信息创建一个新的对象,并且把字节序列中的值赋给新对象的成员变量。

序列化实现深层拷贝的原因

  • 创建新对象:反序列化时会创建一个全新的对象,这个对象和原对象在内存中的地址是不同的,这就保证了两者在最外层是相互独立的。

  • 递归复制引用类型成员变量:序列化过程会递归地处理对象的所有成员变量,包括引用类型的成员变量。当对引用类型的成员变量进行序列化时,会把其内部的状态也转换为字节序列;在反序列化时,会依据这些字节序列创建新的对象,并将其赋值给新对象的对应成员变量。这样一来,新对象和原对象内部的引用类型成员变量也是相互独立的

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com