查漏补缺第十五期(华为一面)
前言
目前正在出一个查漏补缺专题
系列教程, 篇幅会较多, 喜欢的话,给个关注❤️ ~
本专题主要以Java
语言为主, 好了, 废话不多说直接开整吧~
volatile和synchronized的区别
在Java中,volatile
和synchronized
都是用于实现多线程编程时的同步机制,但它们有着不同的特性和应用场景。
volatile
关键字用于声明变量,以确保该变量在多个线程之间的可见性。具体来说,当一个线程修改了一个被volatile
修饰的变量的值时,这个修改会立即被其他线程可见,即其他线程可以立即读取到最新的值。volatile
关键字通过强制线程从主内存中读取变量的值,而不是从线程的本地缓存中读取,来实现可见性。因此,volatile
适用于多个线程访问同一个共享变量的场景。
然而,volatile
不能保证原子性。当多个线程同时修改一个volatile
变量时,不能保证线程安全。例如,当多个线程对一个volatile
变量进行递增操作时,可能会出现竞态条件(race condition)问题,导致结果不符合预期。
相比之下,synchronized
关键字是一种更加强大的同步机制。它用于修饰方法或代码块,确保在同一时间只有一个线程可以执行被synchronized
修饰的方法或代码块。当一个线程获取到了对象的锁时,其他线程将被阻塞,直到该线程释放锁。
synchronized
关键字不仅保证了可见性,还保证了原子性。当一个线程执行synchronized
代码块时,它会获取到对象的锁,然后执行代码块中的操作,执行完成后释放锁。这样可以确保在同一时间只有一个线程执行该代码块,避免了竞态条件问题。
需要注意的是,synchronized
关键字在获取和释放锁的过程中会导致一定的性能开销。而volatile
关键字的性能开销相对较小。因此,在性能要求较高的情况下,volatile
可能是更好的选择。但在需要确保线程安全和复杂同步操作的情况下,synchronized
是更常用的选择。
总结:
volatile
关键字用于保证可见性,适用于多个线程访问同一个共享变量的场景。但不能保证原子性。synchronized
关键字用于保证线程安全,同时保证可见性和原子性。适用于需要复杂同步操作或线程安全的场景。volatile
的性能开销相对较小,而synchronized
的性能开销较大。
在性能要求较高且只需要保证可见性的情况下,可以使用volatile
。在需要确保线程安全和复杂同步操作的情况下,使用synchronized
。
大顶堆小顶堆怎么删除根节点
在Java中,删除大顶堆(Max Heap)或小顶堆(Min Heap)的根节点,需要进行堆的调整以维持堆的性质。
删除大顶堆的根节点的步骤如下:
- 将堆的根节点与最后一个叶子节点交换位置。
- 删除最后一个叶子节点(即原来的根节点)。
- 将新的根节点与其子节点进行比较,将其与较大的子节点交换位置,以满足大顶堆的性质。
- 重复步骤3,直到新的根节点满足大顶堆的性质。
以下是一个示例代码来删除大顶堆的根节点:
public void deleteMax(int[] heap, int size) {
int max = heap[0];
heap[0] = heap[size - 1]; // 将根节点与最后一个叶子节点交换位置
size--;
int index = 0;
while (true) {
int leftChild = 2 * index + 1;
int rightChild = 2 * index + 2;
int largest = index;
if (leftChild < size && heap[leftChild] > heap[largest]) {
largest = leftChild;
}
if (rightChild < size && heap[rightChild] > heap[largest]) {
largest = rightChild;
}
if (largest != index) {
int temp = heap[index];
heap[index] = heap[largest];
heap[largest] = temp;
index = largest;
} else {
break;
}
}
}
删除小顶堆的根节点的步骤与删除大顶堆的根节点类似,只是在比较子节点和交换位置时要选择较小的子节点,以满足小顶堆的性质。
以下是一个示例代码来删除小顶堆的根节点:
public void deleteMin(int[] heap, int size) {
int min = heap[0];
heap[0] = heap[size - 1]; // 将根节点与最后一个叶子节点交换位置
size--;
int index = 0;
while (true) {
int leftChild = 2 * index + 1;
int rightChild = 2 * index + 2;
int smallest = index;
if (leftChild < size && heap[leftChild] < heap[smallest]) {
smallest = leftChild;
}
if (rightChild < size && heap[rightChild] < heap[smallest]) {
smallest = rightChild;
}
if (smallest != index) {
int temp = heap[index];
heap[index] = heap[smallest];
heap[smallest] = temp;
index = smallest;
} else {
break;
}
}
}
注意,这里的示例代码是基于数组实现的堆,需要传入一个表示堆的数组和堆的大小。在实际应用中,通常会使用堆数据结构的类库,如PriorityQueue
来进行堆的操作,它已经封装了相关的方法供使用。
CSRF攻击是什么,怎么预防
CSRF(Cross-Site Request Forgery)
攻击是一种恶意利用用户在已认证的网站上的身份验证凭据进行非授权操作的攻击方式。攻击者通过诱导用户访问恶意网站或点击包含恶意代码的链接,在用户不知情的情况下发送伪造的请求来执行恶意操作,例如修改用户信息、发送未授权的请求等。
为了预防CSRF
攻击,可以采取以下几种常用的防御措施:
-
随机化请求参数:为每个请求添加一个随机生成的参数(称为
CSRF
令牌或同步令牌),并将其与用户会话关联。服务器验证请求参数中的令牌与用户会话中的令牌是否匹配,只有在匹配的情况下才接受请求。这样可以防止攻击者伪造有效请求。 -
检查Referer头部:在服务器端验证请求的
Referer
头部,确保请求来自可信任的源。但需要注意的是,Referer
头部并不是100%可靠,因为它可能会被篡改或被一些浏览器禁用。 -
使用验证码:在进行敏感操作时,要求用户输入验证码,以防止自动化的CSRF攻击。
-
同源检测:在服务器端验证请求的来源是否与目标网站具有相同的源(协议、域名和端口),以确保请求来自同一个源。可以通过检查
Origin
或Referer
头部来实现。 -
限制敏感操作的
HTTP
方法:将敏感操作限制为POST、PUT、DELETE
等需要明确用户意图的HTTP
方法,而不是GET
请求。这样可以减少被CSRF攻击的风险。 -
使用双重身份验证:要求用户进行双重身份验证,例如使用短信验证码、二次密码等。这样即使攻击者成功伪造了请求,也无法通过第二层验证。
-
定期更改会话标识符:在用户登录时,为其分配一个新的会话标识符,并在每次会话过期或重要操作后重新生成。这样即使攻击者窃取了会话标识符,也很快失效。
线程通信方式
在Java中,有以下几种常见的线程通信方式:
-
共享变量:线程之间通过读写共享变量来进行通信。多个线程可以共享同一个变量,并在读写该变量时进行同步操作,如使用
volatile
关键字或synchronized
关键字来保证可见性和原子性。 -
对象锁(
wait()
和notify()
):线程通过等待和通知机制来进行通信。在对象锁机制中,线程可以调用对象的wait()
方法将自己阻塞,等待其他线程调用相同对象的notify()
或notifyAll()
方法来唤醒它们。这种方式常用于生产者-消费者模式等场景。 -
管道(
PipedInputStream
和PipedOutputStream
):线程通过管道进行通信。一个线程将数据写入管道的输出流,另一个线程从管道的输入流中读取数据。这种方式可以用于实现线程间的单向数据传输。 -
信号量(
Semaphore
):线程通过信号量来进行通信。信号量维护了一个计数器,线程可以通过调用acquire()
方法来获取信号量,如果计数器为0,则线程将被阻塞。其他线程可以通过调用release()
方法释放信号量,增加计数器的值,从而允许其他线程获取信号量。 -
栅栏(
CyclicBarrier
):线程通过栅栏进行同步。栅栏是一个同步辅助类,它允许一组线程互相等待,直到所有线程都达到某个共同的状态。当所有线程都到达栅栏时,它们会被释放继续执行。 -
阻塞队列(
BlockingQueue
):线程通过阻塞队列进行数据交换。阻塞队列提供了线程安全的生产者-消费者模型,生产者线程可以将数据放入队列,消费者线程可以从队列中取出数据。当队列为空时,消费者线程会被阻塞等待,当队列满时,生产者线程会被阻塞等待。
如何高效拷贝数组
在Java中,有几种高效拷贝数组的方式:
- 使用
System.arraycopy()
方法:System.arraycopy()
方法是Java提供的高效数组拷贝方法。它可以在两个数组之间进行数据的拷贝,并且具有较高的性能。下面是使用System.arraycopy()
方法进行数组拷贝的示例代码:
int[] sourceArray = {1, 2, 3, 4, 5};
int[] destinationArray = new int[sourceArray.length];
System.arraycopy(sourceArray, 0, destinationArray, 0, sourceArray.length);
- 使用
Arrays.copyOf()
方法:Arrays.copyOf()
方法也是一种方便的数组拷贝方式。它会创建一个新的数组,并将指定长度的元素拷贝到新数组中。下面是使用Arrays.copyOf()
方法进行数组拷贝的示例代码:
int[] sourceArray = {1, 2, 3, 4, 5};
int[] destinationArray = Arrays.copyOf(sourceArray, sourceArray.length);
- 使用
Arrays.copyOfRange()
方法:如果只需要拷贝原数组的一部分元素,可以使用Arrays.copyOfRange()
方法。该方法会创建一个新的数组,并将指定范围内的元素拷贝到新数组中。下面是使用Arrays.copyOfRange()
方法进行数组拷贝的示例代码:
int[] sourceArray = {1, 2, 3, 4, 5};
int[] destinationArray = Arrays.copyOfRange(sourceArray, 0, 3);
这些方法在进行数组拷贝时都具有较高的性能,并且能够处理不同类型的数组(如int[]
、byte[]
等)。
算法题: 跳跃游戏(力扣55题)
给定一个非负整数数组 nums
,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标。
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
使用贪心算法可以解决这个问题。我们可以从数组的第一个位置开始,不断更新能够到达的最远位置,直到最后一个位置或者无法继续前进。
public class Q55 {
public static boolean canJump(int[] nums) {
int maxReach = 0; // 当前能够到达的最远位置
int n = nums.length;
for (int i = 0; i < n; i++) {
if (i > maxReach) {
// 如果当前位置已经超过了能够到达的最远位置,说明无法到达最后一个位置
return false;
}
maxReach = Math.max(maxReach, i + nums[i]);
if (maxReach >= n - 1) {
// 如果当前能够到达的最远位置已经超过或等于最后一个位置,说明可以到达最后一个位置
return true;
}
}
return false;
}
public static void main(String[] args) {
int[] nums1 = new int[]{2,3,1,1,4};
System.out.println(canJump(nums1)); // true
int[] nums2 = new int[]{3,2,1,0,4};
System.out.println(canJump(nums2)); // false
}
}
我们使用maxReach
来记录当前能够到达的最远位置。遍历数组,对于每个位置,更新maxReach
为当前位置加上当前位置所能跳跃的最大长度与maxReach
之间的较大值。如果遍历过程中发现当前位置已经超过了maxReach
,说明无法到达最后一个位置,返回false。如果遍历结束时,maxReach已经超过或等于数组的最后一个位置,说明可以到达最后一个位置,返回true。如果遍历结束后仍然没有到达最后一个位置,返回false。
时间复杂度为O(n),其中n是数组的长度 空间复杂度O(1)
结束语
大家可以针对自己薄弱的地方进行复习, 然后多总结,形成自己的理解,不要去背~
本着把自己知道的都告诉大家,如果本文对您有所帮助,点赞+关注
鼓励一下呗~
相关文章
- 查漏补缺第一期(Redis相关)
- 查漏补缺第二期(synchronized & 锁升级)
- 查漏补缺第三期(分布式事务相关)
- 查漏补缺第四期(Mysql相关)
- 查漏补缺第五期(HashMap & ConcurrentHashMap)
- 查漏补缺第六期(京东一面)
- 查漏补缺第七期(美团到店一面)
- 查漏补缺第八期(阿里一面)
- 查漏补缺第九期(阿里二面)
- 查漏补缺第十期(网易实习一面)
- 查漏补缺第十一期(网易实习二面)
- 查漏补缺第十二期(网易实习三面)
- 查漏补缺第十三期(滴滴实习一面)
- 查漏补缺第十四期(滴滴实习二面)
项目源码(源码已更新 欢迎star⭐️)
往期设计模式相关文章
- 一起来学设计模式之认识设计模式
- 一起来学设计模式之单例模式
- 一起来学设计模式之工厂模式
- 一起来学设计模式之建造者模式
- 一起来学设计模式之原型模式
- 一起来学设计模式之适配器模式
- 一起来学设计模式之桥接模式
- 一起来学设计模式之组合模式
- 一起来学设计模式之装饰器模式
- 一起来学设计模式之外观模式
- 一起来学设计模式之享元模式
- 一起来学设计模式之代理模式
- 一起来学设计模式之责任链模式
- 一起来学设计模式之命令模式
- 一起来学设计模式之解释器模式
- 一起来学设计模式之迭代器模式
- 一起来学设计模式之中介者模式
- 一起来学设计模式之备忘录模式
- 一起来学设计模式之观察者模式
- 一起来学设计模式之状态模式
- 一起来学设计模式之策略模式
- 一起来学设计模式之模板方法模式
- 一起来学设计模式之访问者模式
- 一起来学设计模式之依赖注入模式
设计模式项目源码(源码已更新 欢迎star⭐️)
Kafka 专题学习
- 一起来学kafka之Kafka集群搭建
- 一起来学kafka之整合SpringBoot基本使用
- 一起来学kafka之整合SpringBoot深入使用(一)
- 一起来学kafka之整合SpringBoot深入使用(二)
- 一起来学kafka之整合SpringBoot深入使用(三)
项目源码(源码已更新 欢迎star⭐️)
ElasticSearch 专题学习
项目源码(源码已更新 欢迎star⭐️)
往期并发编程内容推荐
- Java多线程专题之线程与进程概述
- Java多线程专题之线程类和接口入门
- Java多线程专题之进阶学习Thread(含源码分析)
- Java多线程专题之Callable、Future与FutureTask(含源码分析)
- 面试官: 有了解过线程组和线程优先级吗
- 面试官: 说一下线程的生命周期过程
- 面试官: 说一下线程间的通信
- 面试官: 说一下Java的共享内存模型
- 面试官: 有了解过指令重排吗,什么是happens-before
- 面试官: 有了解过volatile关键字吗 说说看
- 面试官: 有了解过Synchronized吗 说说看
- Java多线程专题之Lock锁的使用
- 面试官: 有了解过ReentrantLock的底层实现吗?说说看
- 面试官: 有了解过CAS和原子操作吗?说说看
- Java多线程专题之线程池的基本使用
- 面试官: 有了解过线程池的工作原理吗?说说看
- 面试官: 线程池是如何做到线程复用的?有了解过吗,说说看
- 面试官: 阻塞队列有了解过吗?说说看
- 面试官: 阻塞队列的底层实现有了解过吗? 说说看
- 面试官: 同步容器和并发容器有用过吗? 说说看
- 面试官: CopyOnWrite容器有了解过吗? 说说看
- 面试官: Semaphore在项目中有使用过吗?说说看(源码剖析)
- 面试官: Exchanger在项目中有使用过吗?说说看(源码剖析)
- 面试官: CountDownLatch有了解过吗?说说看(源码剖析)
- 面试官: CyclicBarrier有了解过吗?说说看(源码剖析)
- 面试官: Phaser有了解过吗?说说看
- 面试官: Fork/Join 有了解过吗?说说看(含源码分析)
- 面试官: Stream并行流有了解过吗?说说看
推荐 SpringBoot & SpringCloud (源码已更新 欢迎star⭐️)
博客(阅读体验较佳)
转载自:https://juejin.cn/post/7249152627887882297