likes
comments
collection
share

查漏补缺第十五期(华为一面)

作者站长头像
站长
· 阅读数 12

前言

目前正在出一个查漏补缺专题系列教程, 篇幅会较多, 喜欢的话,给个关注❤️ ~

本专题主要以Java语言为主, 好了, 废话不多说直接开整吧~

volatile和synchronized的区别

在Java中,volatilesynchronized都是用于实现多线程编程时的同步机制,但它们有着不同的特性和应用场景。

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)的根节点,需要进行堆的调整以维持堆的性质。

删除大顶堆的根节点的步骤如下:

  1. 将堆的根节点与最后一个叶子节点交换位置。
  2. 删除最后一个叶子节点(即原来的根节点)。
  3. 将新的根节点与其子节点进行比较,将其与较大的子节点交换位置,以满足大顶堆的性质。
  4. 重复步骤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攻击,可以采取以下几种常用的防御措施:

  1. 随机化请求参数:为每个请求添加一个随机生成的参数(称为CSRF令牌或同步令牌),并将其与用户会话关联。服务器验证请求参数中的令牌与用户会话中的令牌是否匹配,只有在匹配的情况下才接受请求。这样可以防止攻击者伪造有效请求。

  2. 检查Referer头部:在服务器端验证请求的Referer头部,确保请求来自可信任的源。但需要注意的是,Referer头部并不是100%可靠,因为它可能会被篡改或被一些浏览器禁用。

  3. 使用验证码:在进行敏感操作时,要求用户输入验证码,以防止自动化的CSRF攻击。

  4. 同源检测:在服务器端验证请求的来源是否与目标网站具有相同的源(协议、域名和端口),以确保请求来自同一个源。可以通过检查OriginReferer头部来实现。

  5. 限制敏感操作的HTTP方法:将敏感操作限制为POST、PUT、DELETE等需要明确用户意图的HTTP方法,而不是GET请求。这样可以减少被CSRF攻击的风险。

  6. 使用双重身份验证:要求用户进行双重身份验证,例如使用短信验证码、二次密码等。这样即使攻击者成功伪造了请求,也无法通过第二层验证。

  7. 定期更改会话标识符:在用户登录时,为其分配一个新的会话标识符,并在每次会话过期或重要操作后重新生成。这样即使攻击者窃取了会话标识符,也很快失效。

线程通信方式

在Java中,有以下几种常见的线程通信方式:

  1. 共享变量:线程之间通过读写共享变量来进行通信。多个线程可以共享同一个变量,并在读写该变量时进行同步操作,如使用volatile关键字或synchronized关键字来保证可见性和原子性。

  2. 对象锁(wait()notify()):线程通过等待和通知机制来进行通信。在对象锁机制中,线程可以调用对象的wait()方法将自己阻塞,等待其他线程调用相同对象的notify()notifyAll()方法来唤醒它们。这种方式常用于生产者-消费者模式等场景。

  3. 管道(PipedInputStreamPipedOutputStream):线程通过管道进行通信。一个线程将数据写入管道的输出流,另一个线程从管道的输入流中读取数据。这种方式可以用于实现线程间的单向数据传输。

  4. 信号量(Semaphore):线程通过信号量来进行通信。信号量维护了一个计数器,线程可以通过调用acquire()方法来获取信号量,如果计数器为0,则线程将被阻塞。其他线程可以通过调用release()方法释放信号量,增加计数器的值,从而允许其他线程获取信号量。

  5. 栅栏(CyclicBarrier):线程通过栅栏进行同步。栅栏是一个同步辅助类,它允许一组线程互相等待,直到所有线程都达到某个共同的状态。当所有线程都到达栅栏时,它们会被释放继续执行。

  6. 阻塞队列(BlockingQueue):线程通过阻塞队列进行数据交换。阻塞队列提供了线程安全的生产者-消费者模型,生产者线程可以将数据放入队列,消费者线程可以从队列中取出数据。当队列为空时,消费者线程会被阻塞等待,当队列满时,生产者线程会被阻塞等待。

如何高效拷贝数组

在Java中,有几种高效拷贝数组的方式:

  1. 使用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);
  1. 使用Arrays.copyOf()方法:Arrays.copyOf()方法也是一种方便的数组拷贝方式。它会创建一个新的数组,并将指定长度的元素拷贝到新数组中。下面是使用Arrays.copyOf()方法进行数组拷贝的示例代码:
int[] sourceArray = {1, 2, 3, 4, 5};
int[] destinationArray = Arrays.copyOf(sourceArray, sourceArray.length);
  1. 使用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)

结束语

大家可以针对自己薄弱的地方进行复习, 然后多总结,形成自己的理解,不要去背~

本着把自己知道的都告诉大家,如果本文对您有所帮助,点赞+关注鼓励一下呗~

相关文章

项目源码(源码已更新 欢迎star⭐️)

往期设计模式相关文章

设计模式项目源码(源码已更新 欢迎star⭐️)

Kafka 专题学习

项目源码(源码已更新 欢迎star⭐️)

ElasticSearch 专题学习

项目源码(源码已更新 欢迎star⭐️)

往期并发编程内容推荐

推荐 SpringBoot & SpringCloud (源码已更新 欢迎star⭐️)

博客(阅读体验较佳)