二本26届大三Java实习准备 Day4

2025 年 2 月 27 日 Day 4

今天初步复习一下 java 并发的内容; 明天休息一天 推荐一个优质 b 站 up 主是支付宝的工程师, 虽然已经停更好久了但是他的视频质量很高通熟易懂 up :寒食君

今日算法题

  • leetCode 11 盛水最多的容器
  • leetCode 15 三数之和

leetCode 11 盛水最多的容器

class Solution {
    public int maxArea(int[] height) {
        int res = Integer.MIN_VALUE;

        int left = 0, right = height.length - 1;
        while (left < right) {
//            计算当前双指针下的容量
            int tmp = (right - left) * Math.min(height[left], height[right]);
            res = Math.max(res, tmp);
//            移动短板尝试优化最优解
            if (height[left] < height[right])
                left++;
            else
                right--;
        }

        return res;
    }
}

使用双指针从两边向中间移动; 每次都移动较短的指针, 这样能确保在宽度变小的情况下得到更大的面积; 在移动过程中不断记录当前双指针作为容器的面积; 因为每次移动都是当前状况下的最优解, 那么整体的最优解肯定在这些解当中

leetCode 15 三数之和

排序+双指针时间复杂度 O(n^2)

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> res = new ArrayList<List<Integer>>();
        int len = nums.length;
        Arrays.sort(nums);

        for (int first = 0; first < len; first++) {
//            判断当前位置是否和前一个位置相同  如果相同那么说明解已经被记录过  直接跳过
            if (first - 1 >= 0 && nums[first - 1] == nums[first])
                continue;

//            从剩下的有序部分通过双指针查找 -nums[first] 的两数和
            int second = first + 1;
            int third = len - 1;
            while (second < third) {

                if (nums[second] + nums[third] + nums[first] == 0 ) {
//                    记录当前序列
                    List<Integer> list = new ArrayList<>();
                    list.add(nums[first]);
                    list.add(nums[second]);
                    list.add(nums[third]);
                    res.add(list);
//                            同时移动左右两侧指针 且需要移动到有效位置
                    do {
                        second++;
                        third--;
                    } while (second < third && nums[second] == nums[second - 1] && nums[third] == nums[third + 1]);

                } else if (nums[second] + nums[third] > -nums[first]) {
//                    过大移动右指针让和变小
                    third--;
                } else {
//                    过小移动做指针让和变大
                    second++;
                }
            }
        }
        return res;
    }
}

介绍一下开启多线程的方法

继承 Thread 类: 创建类继承 Thread 类然后重学 run 方法; 然后实例化类对象调用 .start 方法即可实现

  • 注意的是 run 方法并不会直接创建新线程, 而是 start 方法会开辟一个新线程来运行 run 方法
  • 这种方式的缺点就是单继承, 在类本身有其他需要继承类的时候不推荐
public class NumberThread extends Thread{  
    @Override  
    public void run() {  
        for (int i = 0; i < 20; i++) {  
            System.out.println("----:"+i);  
            try {  
                Thread.sleep(100);  
            } catch (InterruptedException e) {  
                throw new RuntimeException(e);  
            }        }    }  
    public static void main(String[] args) throws InterruptedException {  
        new NumberThread().start();  
        for (int i = 0; i < 26; i++) {  
            System.out.println("=====:"+(char)('a'+i));  
            sleep(100);  
        }  
    }}

实现 Runnable 接口: 在类中实现 Runnable 方法实例化类对象作为参数删除 Thread 的构造方法中; 一般我们会使用匿名内部类和 lambda 表达式的方式实现这种方法;

  • 优点就是解决了继承 Thread 类方式的单继承问题; 缺点就是 run 方法执行完成后得不到返回值
public class MyRunnable implements Runnable {
    public void run() {
        System.out.println("Hello from MyRunnable");
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
    }
}

  • 用 lambda 表达式简化:
new NumberThread().start();
new Thread(()->{
	for (int i = 0; i < 20; i++) {
		System.out.println("****:"+i);
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		}
	}
}).start();

复习: 如果一个接口只有一个方法那么就是函数式接口; 函数式接口可以通过 lambda 进行简化;

可以让多个线程操作同一个对象

如果需要得到返回值那么就可以通过类继承 Callable 接口的方式, 类实现 Callable 接口重写 call 方法, call 方法中可以有返回值并且可以抛出异常; 然后通过 FutureTask 对这个类的实例化对象进行封装传入 Thread 中; 也可以直接将类对象提交到线程池; call 的返回值会通过 FutureTask 的方式发封装返回

  • FutureTask 封装
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "线程返回值:" + Thread.currentThread().getName();
    }

    public static void main(String[] args) throws Exception {
        FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
        Thread thread = new Thread(futureTask);
        thread.start();
        System.out.println(futureTask.get()); // 获取返回值(会阻塞直到线程完成)
    }
}
  • 线程池方式
public class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        int result = 0;
        for (int i = 1; i <= 10; i++) {
            result += i;
        }
        return result;
    }
	public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        MyCallable myCallable = new MyCallable();

        Future<Integer> future = executor.submit(myCallable);

        try {
            Integer result = future.get();
            System.out.println("线程返回结果:" + result);
        } catch (Exception e) {
            e.printStackTrace();
        }

        executor.shutdown();
    }
}

线程的状态有哪些

线程的状态有 6 种; 这 6 种已经通过枚举的方式定义好了;分别是 new - runnable - blocked - waiting - timed_waiting - terminated

当线程被创建的时候状态为 new; 调用 strat 方法后开始运行线程执行任务, 状态从 new 转为 runnable; 如果在执行过程中需要等待锁那么就会转为 Blocked 阻塞状态, 获取到锁后继续执行; 程序过程中还可能触发等待就会变成 waiting 或者是 timed_waiting 区别就是等待是定时唤醒还是等待其他唤醒; 最后执行完成后 run 就会变成 terminated;

wait 是将当前线程挂起并释放锁等待其他线程唤醒; join 是不释放锁阻塞, 一般是主线程等待子线程完成任务时使用;

状态 说明
NEW(新建) 线程被创建,但尚未调用 start() 方法。
RUNNABLE(可运行) 线程已启动(调用了 start()),可能正在执行或等待 CPU 资源(就绪/运行)。
BLOCKED(阻塞) 线程等待获取锁(如进入 synchronized 代码块时锁被其他线程占用)。
WAITING(无限等待) 线程主动进入等待状态,需其他线程显式唤醒(如调用 wait() 或 join())。
TIMED_WAITING(限时等待) 线程在指定时间内等待(如 sleep(ms)wait(timeout)join(timeout))。
TERMINATED(终止) 线程执行完毕(run() 方法结束)或异常退出。

run/start、wait/sleep、notify/notifyAll 区别?

run 方法可以作为一个普通方法被调用, 他的执行不会开启新的线程; start 方法则会开启新线程, start 让线程状态从 new 变成 runnable 的方法

wait 方法上属于 Object 类的; sleep 是属于 Thread 类的静态方法; wait 必须在同步方法中使用, 也就是必须要在持有锁的前提下使用; 而 sleep 方法则可以在任意上下文环境下使用; wait 会让线程状态从 runnable 变成 waiting 并且释放锁对象, 而 sleep 是将状态变为 timed_waiting 不会释放锁

notify 是从所有希望竞争当前锁的线程中唤醒一个线程, notifyAll 则是唤醒所有希望获取当前锁的线程; 被唤醒的线程会进行队列中竞争锁

什么用户线程和守护线程?

java 中的线程一般可以分为两类: 用户线程 UserThread 和守护线程 DaemonThread; 用户线程是执行用户业务任务的线程也是我们日常工作使用的线程; 守护线程一般是后台默默运行目的是为了用户线程能够健康运行的线程, 最经典的就是 JVM 中的 GC 垃圾回收线程 如果 JVM 所有的用户线程都执行结束了那么就会关闭, 因为所有用户线程都结束了那么守护线程要守护的对象都没有了所以就没有运行的必要了

可以通过 .setDaemon (true) 和 .isDaemon ()来切换和判断线程状态

并发和并行的区别是什么?

简单来说并发就是一对多, 并行就是多对多; 并发和并行从体验上来说, 都说同时执行多个线程任务; 但是在并发的环境下这个同时上要打引号的因为并发环境下实际上处理的 CPU 资源只有一个, 只是通过线程执行权切换实现似乎同时执行多个线程的效果, 但是实际上同一时刻至于一个线程拥有 CPU 执行权; 而并行则是多个 CPU 同时相互独立的执行多个线程; 并发就相当于一个银行窗口前很多人排队轮流办理业务, 而并行则是多个窗口同时办理业务

Synchronized 锁的是什么?

主要看他被加在什么位置; 如果是加在一个类实例对象的方法上那么他锁的就是这个实例对象; 如果是所在一个静态方法中, 那么锁的就是这个类的 class 对象; 如果是通过代码快的方式锁一部分代码那么就锁的是括号内显示指定的引用对象;

Synchronized 直接加载方法上锁的是当前的实例对象, 如果方法上同时有 static 那么这个静态同步方法锁的就是当前类对象; 对应的静态同步代码块为 sychronized (this)锁当前对象, synchronized (Xxx. class)锁类对象; 从 JVM 的角度区别, 类锁类对象存储在的是方法区, 而实例对象是存储在堆中

Synchronized 可以锁字符串吗?

理论上可以但是相当不建议这么做 ; 因为字符串在 JVM 中的存储时存在字符串常量池中的也就是共享的; 如果将一个字符串当做锁, 那么会出现极其严重的错误;

问题

1String s1 = "lock";
2String s2 = new String("lock");
3synchronized(s1) { ... }
4synchronized(s2) { ... }
5// 两个代码块会互斥吗?

答案

  • 如果 s2 未调用 intern() → 不会互斥(s 1 指向常量池,s 2 指向堆内新对象)
  • 如果 s2 = new String("lock").intern() → 会互斥(s 1 和 s 2 指向常量池同一对象)
#java##面试#
全部评论
如果不是为了面试,感觉很多场景在实际生产中都不可能会出现,比如用字符串去当锁; 有的时候看八股是真的能让我提高对java的理解,而有的时候真的让我很无语,生怕以后面试面试官突然问一个很刁钻的类似问题
点赞 回复 分享
发布于 今天 00:29 重庆

相关推荐

就是说这不对口的实习还有必要加么,不加就是纯纯三无
Java抽象小篮子:实习经历得好好包装一下,可以看看我发过的包装帖子
点赞 评论 收藏
分享
牛客765689665号:没有实习是硬伤,央国企看学历
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务