java通关整理汇总-Java基础、计算机网络、数据库、设计模式、框架、算法模板、笔试
一、多线程和高并发
1.缓存一致性问题
当程序在运行过程中,会将运算需要的数据从主存复制一份到 CPU 的高速缓存当中,那么 CPU 进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
在单线程中,运行是没有问题的,但是在多线程中,由于每个线程可能是由不同的cpu执行(单核cpu也会出现缓存一致性的问题),当两个线程同时进行写操作时,就有可能在读写过程中出现一个修改后,另外一个线程只是在原来的基础上修改,导致结果和预期不大一样,出现缓存不一致的问题
2.怎么解决缓存一致性问题
1)通过在总线加 LOCK#锁的方式
2)通过缓存一致性协议
- 早期的话,可以在总线上加锁,因为 CPU 和其他部件进行通信都是通过总线来进行的,如果对总线 加 LOCK#锁的话,也就是说阻塞了其他 CPU 对其他部件访问,从而使得只有一个CPU能使用这个变量。
第一种方式的问题:在锁住总线的期间,其他CPU无法访问内存,就会导致CPU无法访问内存,导致效率低下。
所以就出现了缓存一致性协议
- 该协议保证了每个缓存中使用的共享变量副本是一致的。
- 核心思想:当 CPU 向内存写入数据时, 如果发现操作 的变量是共享变量,即在其他 CPU 中也存在该变量的副本,会发出信号通知其 他 CPU 将该变量的缓存行置为无效状态,因此当其他 CPU 需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
3. 简述volatile关键字
一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰 之后,那么就具备了两层语义:
- 1.保证了不同线程对这个变量进行读取时的可见性,即一个线程修改 了某个变量的值,这新值对其他线程来说是立即可见的。(volatile 解决了 线程间共享变量的可见性问题)。
这里说一下可见性的过程(加volatile关键字后线程读取过程)
- 2.禁止进行指令重排序,阻止编译器对代码的优化。
4. 说一下java的内存模型
这里面试可以先说java模型规定,然后再说三个主要特性,把volatile留着,先把原子性、可见性、有序性还有happens-before说清楚,然后在解释volatile,对比synchronized、lock一起说
- java内存模型规定所有的变量都是存在主内存当中,每个线程都有自己的工作内存。线程对变量的操作都是在工作内存上进行的,而不能直接对主内存进行操作,且不访问其他线程的工作内存。
三个主要特性:
- Volatile 关键字
- 要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。
只要有一个没有被保证,就有可能会导致程序运行不正确。 - happens-before 关系。(先行发生原则)
原子性: 即一个操作或者多个操作要么全部执行并且执行的过程不会被任
何因素打断,要么就都不执行。
可见性: 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线 程能够立即看得到修改的值。
- 通过 Synchronized 和 Lock 和 volatile 实现“可见性”。
有序性: 即程序执行的顺序按照代码的先后顺序执行。
happens-before 原则(先行发生原则):
- 程序次序原则:写在前面的代码会先于后面的代码先执行
- volatile变量规则:对一个变量的写操作先于读操作
- 线程启动规则:线程的start()先于其他动作
- 线程中断规则、线程终结规则、对象终结规则
4. synchronized、lock 和 volatile
synochronizd和volatile关键字区别:
-
volatile关键字解决的是变量在多个线程之间的可见性;而sychronized关键字解决的是多个线程之间访问共享资源的同步性
-
volatile只能用于修饰变量,而synchronized可以修饰方法,以及代码块。(volatile是线程同步的轻量级实现,所以volatile性能比synchronized要好,并且随着JDK新版本的发布,sychronized关键字在执行上得到很大的提升,在开发中使用synchronized关键字的比率还是比较大)
-
多线程访问volatile不会发生阻塞,而sychronized会出现阻塞。
-
volatile能保证变量在多个线程之间的可见性,但不能保证原子性;而sychronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公有内存中的数据做同步。
-
线程安全包含原子性和可见性两个方面。
-
对于用volatile修饰的变量,JVM虚拟机只是保证从主内存加载到线程工作内存的值是最新的。
一句话说明volatile的作用:实现变量在多个线程之间的可见性。
synchronized和lock区别:
1)Lock是一个接口,而synchronized是Java中的关键字
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率(读写锁)。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
5. 锁类型(可重入锁、可中断锁、公平锁、非公平锁)
可重入锁:比如说,当一个线程执行method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
又比如,如果线程加载的两个方法都是加了synchronized的,在加载第二个的时候就不用加载了。
可中断锁:synchronized就不是可中断锁,而Lock是可中断锁。
公平锁:公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
非公平锁:非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
6. Java 中的活锁,死锁,饥饿有什么区别?
死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一 种互相等待的现象,若无外力作用,它们都将无法推进下去,此时称系统处于 死锁状态或系统产生了死锁。
饥饿锁:考虑一台打印机分配的例子,当有多个进程需要打印文件时,系统 按照短文件优先的策略排序,该策略具有平均等待时间短的优点,似乎非常合 理,但当短文件打印任务源源不断时,长文件的打印任务将被无限期地推迟, 导致饥饿以至饿死。
活锁:与饥饿相关的另外一个概念称为活锁 ,在忙式等待条件下发生的饥饿,称为活锁。
比如,事务1给一个数据加锁,事务2也来加锁,事务2就等待,这时候事务3也来加锁,当事务1释放锁后,事务3首先加锁,事务2还是等待,这时候事务4也来了,就这样事务2总是不断的尝试,称为活锁
避免活锁的简单方法是采用先来先服务的策略。
二、线程池问题
1. 什么是线程池?
线程池就是创建若干个可执行的线程放入一个池(容器)中, 有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,而是仍然在线程池中等待下一个任务。
2. 为什么使用线程池?
因为java创建一个线程,需要调用系统内核的API,需要操作系统为线程分配一系列的资源,成本很高,应该尽量避免创建、销毁,所以使用线程池
线程池是一种生产者-消费者模式
3. 线程池的工作原理
- a.如果线程池中的数量小于corePoolSize,而线程池中的线程都处于空闲状态,也需要创建新的线程来处理任务。
- b.如果线程池中的数量大于等于corePoolSize,但是缓冲队列未满,那么任务就会被放入缓冲队列中,等待空闲线程来去取出执行。
- c.如果线程池中的数量大于等于corePoolSize,但是缓冲队列满时,并且线程池中的数量小于 maximumPoolSize,就会建立新的线程。
- d.如果线程池中的数量大于等于corePoolSize,但是缓冲队列满时,并且线程池中的数量等于maximumPoolSize,就可以通过任务拒绝策略来处理次任务。
处理任务的优先级为:当corePoolSize、缓冲队列、maximumPoolSize都满时,就会使用handler处理被拒绝的任务
4. 线程池的核心参数
-
corePoolSize 核心线程数,即就是中线程池中长时间稳定存活的线程数
-
maxPoolSize
最大线程数,重点强调线程中最大可包含的线程数。最大线程数的上限需要根据实际情况而定 -
keepAliveTime
线程的存活时间,该参数是指非核心线程的存活时间,用来严格控制线程池中线程的数量尽可能的保持在一定的范围内,若要修改核心线程的存活时长,可参考相关参数 -
ThreadFactory
线程创建的工厂,新的线程都是由ThreadFactory创建的,系统默认使用的是Executors.defaultThreadFactory创建的,用它创建出来的线程的优先级、组等都是一样的,并且他都不是守护线程。我们也可以使用自定义的线程创建工厂,并对相关的值进行修改 -
WorkQueue
线程的工作队列,常见的类型有三种,如下
1.直接交换:SynchronousQueue,任务不多,是没有容量的,maxPoolSize需要大一点
2.无界队列:LinkedBlockingQueue,可产生OOM
3.有界队列:ArrayBlockingQueue
三、线程
1. 创建线程的方法
- 继承Thread方法
- 实现Runnable接口
- 实现Callable接口
- 线程池
2. Runnable和Callable有什么不同?
- Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,支持泛型
- Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;
Callable 接口 call 方法允许抛出异常,可以获取异常信息