最后
即使是面试跳槽,那也是一个学习的过程。只有全面的复习,才能让我们更好的充实自己,武装自己,为自己的面试之路不再坎坷!今天就给大家分享一个Github上全面的Java面试题大全,就是这份面试大全助我拿下大厂Offer,月薪提至30K!
我也是第一时间分享出来给大家,希望可以帮助大家都能去往自己心仪的大厂!为金三银四做准备!
一共有20个知识点专题,分别是:
Dubbo面试专题
JVM面试专题
Java并发面试专题
Kafka面试专题
MongDB面试专题
MyBatis面试专题
MySQL面试专题
Netty面试专题
RabbitMQ面试专题
Redis面试专题
Spring Cloud面试专题
SpringBoot面试专题
zookeeper面试专题
常见面试算法题汇总专题
计算机网络基础专题
设计模式专题
if (!stopRequested)
while (true)
i++;
编译器的优化初衷是好的,但这里却帮了倒忙!
修正这个问题的一种方式是同步访问stopRequested
字段。这个程序会如预期般在大约一秒之内终止:
public class StopThread {
private static Boolean stopRequested;
private static synchronized void requestStop() {
stopRequested = true;
}
private static synchronized Boolean stopRequested() {
return stopRequested;
}
public static void main(String[] args)
throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested())
i++;
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
requestStop();
}
}
注意读和写操作都要被同步,否则无法保证同步能起作用。
还是有其他更正确的替代方法,它更加简洁,性能也可能更好。虽然volatile
修饰符不执行互斥访问,但它可以保证任何一个线程在读取该字段的时候都将看到最近刚刚被写入的值:
public class StopThread {
private static volatile Boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested)
i++;
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
在使用volatile
的时候务必要小心。以下面的方法为例,假设它要产生序列号:
private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
return nextSerialNumber++;
}
这段代码是有问题的,原因在于:增量操作符(++)不是原子的。它在nextSerialNumber
字段中执行两项操作:
-
读取
nextSerialNumber
的值 -
写回一个新值,相当于
nextSerialNumber + 1
如果第二个线程在第一个线程第一步和第二步之间读取nextSerialNumber
,第二个线程就会与第一个线程一起看到同一个值,并返回相同的nextSerialNumber
。
修正 generateSerialNumber
方法:
private static final Atomiclong nextSerialNum = new Atomiclong();
public static long generateSerialNumber() {
return nextSerialNum.getAndIncrement();
}
避免数据不一致的问题的最佳办法是不共享可变的数据或者共享不可变的数据。如果采用这一策略,对它建立文档就很重要,以便它可以随着程序的发展而得到维护。
如果一个线程修改了一个数据对象,然后其他线程也可以读取该对象,只要它没有再被修改。—— 这种对象叫做高效不可变(effectively immutable ) 。
将上面对象从一个线程传递到其他的线程被称作安全发布(safe publication) 。
安全发布对象引用有许多种方法:
-
将它保存在静态字段中,作为类初始化的一部分
-
将它保存在
volatile
字段、final
字段或者通过正常锁定访问的字段中 -
将它放到支持并发的集合中
为了避免死锁和数据破坏,千万不要从同步区字段内部调用外来方法。
在一个被同步的方法或者代码块中,永远不要放弃对客户端的控制。换句话说:在一个被同步的区域内部,不要调用设计成要被覆盖的方法,或者是由客户端以函数对象的形式提供的方法。这样的方法是外来的。
以下面的类为例,它实现了一个可以观察到的集合包装(set wrapper)。该类允许客户端在将元素添加到集合中时预订通知。
观察者(Observer)模式
package com.wjw.effectivejava2;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
public class ObservableSet extends ForwardingSet {
public ObservableSet(Set set) {
super(set);
}
private final List<SetObserver> observers = new ArrayList<>();
public void addObserver(SetObserver observer) {
synchronized (observers) {
observers.add(observer);
}
}
public Boolean removeObserver(SetObserver observer) {
synchronized (observers) {
return observers.remove(observer);
}
}
private void notifyElementAdded(E element) {
synchronized (observers) {
for (SetObserver observer : observers)
observer.added(this, element);
}
}
@Override
public boolean add(E element) {
Boolean added = super.add(element);
if (added)
notifyElementAdded(element);
return added;
}
@Override
public boolean addAll(Collection<? extends E> c) {
Boolean result = false;
for (E element : c)
result |= add(element);
// Calls notifyElementAdded
return result;
}
}
观察者通过调用addObserver
方法预订通知,通过调用removeObserver
方法取消预订。在这两种情况下,这个回调(callback)接口的实例都会被传递给方法:
@FunctionalInterface
public interface SetObserver {
// Invoked when an element is added to the observable set
void added(ObservableSet set, E element);
}
如果只是粗略地检验一下,ObservableSet
会显得很正常。例如,下面的程序打印出0〜99的数字:
public static void main(String[] args) {
ObservableSet set = new ObservableSet<>(new HashSet<>());
set.addObserver((s, e) -> System.out.println(e));
for (int i = 0; i < 100; i++)
set.add(i);
}
尝试一个更复杂的例子,将上面代码里set.addObserver((s, e) -> System.out.println(e));
替换为:
set.addObserver(new SetObserver<>() {
public void added(ObservableSet s, Integer e) {
System.out.println(e);
if (e == 23)
s.removeObserver(this);
}
});
注意:这个调用以一个匿名类SetObserver
实例代替了前一个调用中使用的lambda
。这是因为函数对象需要将自身传给s.removeObserver
,而lambda
则无法访问它们自己
这一节主要是讲线程池的使用。
java.util.concurrent
包中包含了一个Executor Framework它是一个很灵活的基于接口的任务执行工具。
ExecutorService exec = Executors.newSingleThreadExecutor();
为执行而提交一个runnable的方法:
exec.execute(runnable);
告诉executor如何优雅地终止
exec.shutdown();
你可以利用executor service完成更多的工作:
1. 等待一个任务集合中的任何任务完成
利用 invokeAny
或 invokeAll
2. 等待executor service优雅地完成终止
awaitTermination
3. 在任务完成时逐个地获取这些任务的结果
ExecutorCompletionService
4. 调度在某个时间段定时运行或周期运行的任务
ScheduledThreadPoolExecutor
为特殊的应用程序选择executor service是很有技巧的:
-
编写小程序,或者轻量负载的服务器,使用
Executors.newCachedThreadPool
-
大负载的产品服务器中,使用
Executors.newFixedThreadPool
但是后面人们又发现,使用原生的Executors
线程池可能会带来OOM问题,所以又对此作了专门的要求:
在Executor Framework中,工作单元和执行机制是分开的。工作单元称作任务(task)。任务又有两种:Runnable
及 Callable
(会返回值,并且能够抛出任意的异常)
在Java 7中,Executor框架被扩展为支持fork-join
任务
这些任务是通过一种称作
fork-join
池的特殊executor
服务运行的。
fork-join
任务用ForkJoinTask
实例表示,可以被分成更小的子任务,包含ForkJoinPool
的线程不仅要处理这些任务,还要从另一个线程中“偷”任务,以确保所有的线程保持忙碌,从而提高CPU使用率、提高吞吐量,并降低延迟。
并行流Parallel streams就是在fork join池上编写的
先把结论写出来:
-
直接使用
wait
方法和notify
方法就像用“汇编语言”编程一样,而java.util.concurrent
则提供了更高级的语言。没有理由再使用wait
方法和notify
。 -
如果你在维护使用
wait
方法和notify
方法的代码,务必确保始终是利用在while
循环内部调用wait
方法。 -
应该优先使用
notifyAll
方法,而不是使用notify
方法。如果使用notify
方法,一定要确保程序的活性。
并发集合
并发集合为标准的集合接口(如List
、Queue
和 Map
)提供了高性能的并发实现。为了提供高并发性,这些实现在内部自己管理同步。因此,将并发集合锁定没有什么作用,只会使程序的速度变慢。
例如,下面这个方法模拟了 String.intern
的行为:
private static final ConcurrentMap<String, String> map = new ConcurrentHashMap<>();
public static String intern(String s) {
String previousValue = map.putIfAbsent(s, s);
return previousValue == null ? s : previousValue;
}
String.intern
:若常量池中不存在相同的字符串,JVM就会在常量池中创建一个字符串,然后返回该字符串的引用
putIfAbsent
:如果所指定的key
已经在HashMap
中存在,返回和这个key
值对应的value
, 如果所指定的key
不在HashMap
中存在,则返回null
。
ConcurrentHashMap
对get
操作进行了优化。所以可以先用get
操作进行条件判断:
public static String intern(String s) {
String result = map.get(s);
if (result == null) {
result = map.putIfAbsent(s, s);
if (result == null)
result = s;
}
return result;
}
并发集合导致同步的集合大多被废弃了。应该优先使用 ConcurrentHashMap
,而不是使用Collections.synchronizedMap
。
有些集合接口已经通过阻塞操作(blocking operation)进行了扩展,它们会一直等待到可以成功执行为止。这样就允许将阻塞队列用于生产者一消费者队列(producer-consumer queue)。
大多数 ExecutorService
实现(包括ThreadPoolExecutor
)都使用了一个BlockingQueue
同步器
同步器(Synchronizer
)是使线程能够等待另一个线程的对象,允许它们协调动作。最常用的同步器是CountDownLatch
和 Semaphore
。较不常用的是 CyclicBarrier
和 Exchanger
。功能最强大的同步器是Phaser
。
倒计数锁存器(Countdown Latch)是一次性的障碍,允许一个或者多个线程等待一个或者多个其他线程来做某些事情。
要在这个简单的基本类型之上构建一些有用的东西,做起来相当容易。例如,假设想要构建一个简单的框架,用来给一个动作的并发执行定时。
直接在wait
和notify
之上实现这个逻辑会很棍乱,而在CountDownLatch
之上实现则相当简单:
public static long time(Executor executor, int concurrency, Runnable action) throws InterruptedException {
CountDownLatch ready = new CountDownLatch(concurrency);
CountDownLatch start = new CountDownLatch(1);
CountDownLatch done = new CountDownLatch(concurrency);
for (int i = 0; i < concurrency; i++) {
executor.execute(() -> {
ready.countDown();
// Tell timer we’re ready
try {
start.await();
// Wait till peers are ready
action.run();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
done.countDown();
// Tell timer we’re done
}
});
}
ready.await();
// Wait for all workers to be ready
long startNanos = System.nanoTime();
start.countDown();
// And they’re off!
done.await();
// Wait for all workers to finish
return System.nanoTime() - startNanos;
}
代码解释:
这个方法使用了三个倒计数锁存器。第一个是ready ,工作线程用它来告诉 timer
线程它们已经准备好了。然后工作线程在第二个锁存器 start
上等待。
当最后一个工作线程调用ready.countDown
时,timer
线程记录下起始时间,并调用start.countDown
允许所有的工作线程继续进行。
然后 timer
线程在第三个锁存器 done
上等待,直到最后一个工作线程运行完该动作,并调done.countDown
。此时,timer
线程就会苏醒过来,并记录下结束的时间。
对于间歇式的定时,始终应该优先使用System.nanoTime
,而不是使用System.currentTimeMillis
。因为 System.nanoTime
更准确,也更精确,它不受系统时钟调整的影响。
前一个例子中的那三个倒计数锁存器其实可以用一个
CyclicBarrier
或者Phaser
实例代替。这样得到的代码更加简洁,但是理解起来比较困难。
wait
方法被用来使线程等待某个条件。它必须在同步区域内部被调用,这个同步区域将对象锁定在了调用 wait
方法的对象上。
synchronized (obj) {
while ()
obj.wait(); // (Releases lock, and reacquires on wakeup)
… // Perform action appropriate to condition
}
应该使用循环模式来调用wait方法;永远不要在循环之外调用wait方法。循环会在等待之前和之后对条件进行测试:
-
在等待之前测试条件,当条件已经成立时就跳过等待
-
在等待之前测试条件,如果条件不成立的话继续等待
一旦出现下面的这几种情况,线程就可能会醒过来:
- 另一个线程可能意外地或恶意地调用了
notify
方法
公有可访问对象的同步方法中包含的wait 方法都会出现这样的问题
-
即使只有某些等待线程的条件已经被满足,但是通知线程仍然调用
notifyAll
方法 -
在没有通知的情况下,等待线程也可能会苏醒过来。这被称为“伪唤醒”(spurious wakeup)
先把结论写出来:
- 每个类都应该严谨的描述其线程安全属性
最后总结
搞定算法,面试字节再不怕,有需要文章中分享的这些二叉树、链表、字符串、栈和队列等等各大面试高频知识点及解析
最后再分享一份终极手撕架构的大礼包(学习笔记):分布式+微服务+开源框架+性能优化
; // (Releases lock, and reacquires on wakeup)
… // Perform action appropriate to condition
}
应该使用循环模式来调用wait方法;永远不要在循环之外调用wait方法。循环会在等待之前和之后对条件进行测试:
-
在等待之前测试条件,当条件已经成立时就跳过等待
-
在等待之前测试条件,如果条件不成立的话继续等待
一旦出现下面的这几种情况,线程就可能会醒过来:
- 另一个线程可能意外地或恶意地调用了
notify
方法
公有可访问对象的同步方法中包含的wait 方法都会出现这样的问题
-
即使只有某些等待线程的条件已经被满足,但是通知线程仍然调用
notifyAll
方法 -
在没有通知的情况下,等待线程也可能会苏醒过来。这被称为“伪唤醒”(spurious wakeup)
先把结论写出来:
- 每个类都应该严谨的描述其线程安全属性
最后总结
搞定算法,面试字节再不怕,有需要文章中分享的这些二叉树、链表、字符串、栈和队列等等各大面试高频知识点及解析
最后再分享一份终极手撕架构的大礼包(学习笔记):分布式+微服务+开源框架+性能优化
[外链图片转存中…(img-EBXwqtBM-1714887956791)]