面试-多线程
面试-多线程
谈谈线程和进程
问:谈谈线程和进程
答:进程是操作系统分配资源的最小独立单位,相当于一个独立运行的程序(比如打开的微信、IDEA),每个进程都有自己专属的内存空间,进程之间相互隔离、互不干扰。而线程是操作系统调度执行的最小单位,它不能独立存在,必须依附于进程,一个进程可以包含多个线程,这些线程共享所属进程的全部资源。
Java实现多线程的方式
问:Java实现多线程的方式
答:
1. 继承Thread类重写run()方法
2. 实现Runnable接口重写run()方法
3. 实现Callable接口
4. 线程池(通过管理线程的方式)
package com.tensoflow;
class MyThread extends Thread {
@Override
public void run() {
while (true) {
try {
Thread.sleep(200);
System.out.println(Thread.currentThread().getName() + "执行中...");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class ThreadCreateDemo {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
// 必须调用start(),不能直接调用run()(直接调是普通方法,不启动新线程)
thread1.start();
thread2.start();
}
}
package com.tensoflow;
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("实现Runnable的线程:" + Thread.currentThread().getName());
}
}
public class RunnableCreateDemo {
public static void main(String[] args) {
// 创建Runnable实例,传给Thread
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
// 简化写法:匿名内部类
new Thread(() -> {
System.out.println("匿名线程:" + Thread.currentThread().getName());
}).start();
}
}
创建线程池的四种方式
问:创建线程池的四种方式
答:
1. newSingleThreadExecutor():只有线程,任务顺序执行
2. newFixedThreadPool(n):固定n个核心线程
3. newCachedThreadPool():线程数量动态变化,空闲 60 秒回收,适合短任务
4. newScheduledThreadPool(n):支持延迟、定时、周期执行任务
线程中start()和run()有哪些区别
问:线程中start()和run()有哪些区别
答:
1. start()方法用于启动线程
2. run()方法用于执行线程的运行时代码
3. run()可以重复调用而start()只能调用一次
Thread类中的常用方法
问:Thread类中的常用方法
答:
1. start() 启动线程,并执行线程的run()方法
2. run() 线程的执行体
3. sleep() 让当前线程睡眠指定的毫秒数
4. setName() 设置线程名称
5. getName() 获取线程名称
6. setPriority(int priority) 设置线程的优先级
7. getPriority() 获取线程的优先级
线程编排
问:线程编排
答:顺序执行:使用join()方法
package com.tensoflow;
public class CccServiceApplication {
public static void main(String[] args) throws Exception {
Thread a = new Thread(() -> {
for(int i = 0; i < 10000; i++) {
System.out.println("aaa...");
}
});
Thread b = new Thread(() -> {
try {
a.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i = 0; i < 10000; i++) {
System.out.println("bbb...");
}
});
b.start();
a.start();
System.out.println("主程序");
}
}
CompletableFuture:Java 8 提供的异步编程工具类
public class CompletableFutureDemo {
public static void main(String[] args) {
// 先定义两个线程任务
Runnable task1 = () -> System.out.println(Thread.currentThread().getName() + " → 线程1执行");
Runnable task2 = () -> System.out.println(Thread.currentThread().getName() + " → 线程2执行");
// 1. 异步执行(无返回值)
CompletableFuture.runAsync(task1);
// 2. 串行执行:先1后2
CompletableFuture.runAsync(task1).thenRun(task2);
// 3. 并行执行 + 全部完成后聚合
CompletableFuture<Void> f1 = CompletableFuture.runAsync(task1);
CompletableFuture<Void> f2 = CompletableFuture.runAsync(task2);
CompletableFuture.allOf(f1, f2).thenRun(() -> {
System.out.println("两个线程全部执行完毕");
});
// 4. 任意一个完成就继续
CompletableFuture.anyOf(f1, f2).thenRun(() -> {
System.out.println("有一个线程已完成");
});
// 5. 带返回值的线程(定义两个带结果任务)
CompletableFuture<String> supply1 = CompletableFuture.supplyAsync(() -> {
System.out.println("supply1 线程执行");
return "结果A";
});
CompletableFuture<String> supply2 = CompletableFuture.supplyAsync(() -> {
System.out.println("supply2 线程执行");
return "结果B";
});
// 6. 合并两个线程结果
supply1.thenCombine(supply2, (r1, r2) -> r1 + " + " + r2)
.thenAccept(System.out::println);
// 7. 异常处理
CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("出错");
}).exceptionally(e -> "异常已处理").thenAccept(System.out::println);
}
}
wait()和sleep()有什么区别
问:wait()和sleep()有什么区别
答:
1. 从来源看:sleep() 来自Thread ,wait() 来自Object 。
2. 从线程状态看:sleep() 导致当前线程进入timed_waiting状态,wait() 导致当前线程进入waiting状态。
3. 从恢复执行看:sleep() 在指定时间之后,线程会恢复执行,wait() 则需要等待别的线程使用 notify()/notifyAll() 来唤醒它
notify()和notifyAll() 区别
问:notify()和notifyAll() 区别
答:
1. notify():随机唤醒一个wait线程
2. notifyAll() :唤醒所有wait的线程
如何停止一个线程
问:如何停止一个线程
答:
1. 使用退出标志,使线程正常退出
2. 使用interrupt方法中断线程
谈谈JMM
问:谈谈JMM
答:JMM(Java Memory Model)是Java内存模型,JMM把内存非为两块,一块是私有线程的工作区域,一块是所有线程的共享区域(主内存)。线程跟线程之间是相互隔离的,线程跟线程交互需要通过主内存。
谈谈volatile
问:谈谈volatile
答:让一个线程对共享变量的修改对一个线程可见,即一个线程修改了主内存中的共享变量则立即让其它线程都同步。其也能防止指令重排序。
守护线程是什么
问:守护线程是什么
答:守护线程是一种比较低级别的线程,一般用于为其他类别线程提供服务。因此当其它线程都退出时,它也就没有存在的必要了。例如JVM中的垃圾回收线程。主要应用有后台日志输出线程、后台监控线程(监控系统CPU、内存使用率、......)等。好处是仅在程序运行时需要,程序退出后自动终止无需手动关闭。
package com.tensoflow;
public class DaemonThreadDemo {
public static void main(String[] args) {
// 定义守护线程 b
Thread b = new Thread(() -> {
for (int i = 0; true; ++i) {
try {
// 放慢打印速度,便于观察
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i + " " + Thread.currentThread().getName() + " is running.");
}
}, "Daemon-Thread");
// 标记b为守护线程,必须写在start()方法之前
b.setDaemon(true);
// 启动线程
b.start();
// 主线程(用户线程)执行500ms后结束
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程执行完毕,JVM 即将退出!");
// 主线程结束后,守护线程b会被强制终止
}
}
线程有哪些状态
问:线程有哪些状态
new:尚未启动
runnable:正在执行中
blocked:阻塞(被同步锁或者IO锁阻塞)
waiting:永久等待状态
timed_waiting:等待指定的时间重新被唤醒的状态
terminated:执行完成
谈谈ThreadLocal
问:谈谈ThreadLocal
答:可以把ThreadLocal视为一个普通变量,其与普通的变量之间的区别在于ThreadLocal变量只属于某一个线程。单个ThreadLocal变量默认只存一个值。需要存多个值可以在ThreadLocal泛型中写上一个对象,使用对象存储多个值。其能解决线程内跨方法传递私有数据等问题。另外使用线程池时每次使用完都需要remove,不然会导致内存泄露与数据错乱。
package com.tensoflow;
class Person{
String name = "zhangsan";
}
public class ThreadLocalTest {
static ThreadLocal<Person> t1 = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(()->{
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// null 即使第二个线程设置了new Person()
System.out.println(t1.get());
}).start();
new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t1.set(new Person());
// zhangsan 只有自己的线程能拿到这个Person对象
System.out.println("线程二: " + t1.get().name);
}).start();
}
}
ThreadLocal父获取子线程数据
问:ThreadLocal父获取子线程数据
答:使用CompletableFuture接收返回值
public class Test {
public static void main(String[] args) throws Exception {
ThreadLocal<String> tl = new ThreadLocal<>();
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 子线程 set 值
tl.set("子线程的值");
return tl.get(); // 把值返回
});
// 父线程获取
String val = future.get();
System.out.println(val);
}
}
谈谈你知道的锁
问:谈谈你知道的锁
答:锁本质就是控制多线程并发访问共享资源的规则从而保证线程安全。
synchronized(JVM层面的锁):同一时间只有一个线程能够进入被锁住的代码块或方法。修饰实例方法时锁的是当前对象this、修饰静态方法时锁的是当前类的Class对象、修饰方法块时锁的是你指定的对象。
谈谈CAS
问:谈谈CAS
答:CAS(Compare And Swap)比较再交换,它体现的是一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。原理:包含预期值 A、内存原值 V、新值 B,只有当内存原值 V == 预期值 A 时,才把 V 更新为 B。否则更新失败并开始自旋,即把内存原值赋值给预期值,然后再比较内存原值与预期值。
谈谈AQS
问:谈谈AQS
答:AQS(AbstractQueuedSynchronizer),抽象队列同步器,是JUC并发包所有锁和同步工具的底层核心。
state:同步状态,0 无锁,>0 代表持有锁,可自定义含义
CLH 阻塞队列:抢锁失败线程入队阻塞,唤醒后再竞争
悲观锁和乐观锁
问:悲观锁和乐观锁
答:悲观锁和乐观锁是两种锁思想不是具体的锁。
1. 悲观锁:认为一定会发生并发冲突所以先加锁再操作,代表synchronized
2. 乐观锁:认为一般不会发生并发冲突,不加锁直接操作,最后用版本号或CAS检查是否被修改
如何保证幂等性
问:如何保证幂等性
答:幂等指的是方法被重复执行的时候所产生的影响和第一次执行的影响是相同的。网络抖动请求超时后客户端重试以及用户快速点击按钮等都有可能会导致接口重复执行。对于插入性重复操作可以利用数据库唯一索引来防止重复插入。通过分布式锁锁定操作唯一标识(如用户ID+订单类型),同一时间只有一个请求能执行。对于一些有状态的业务可以基于状态机实现,每个状态只能按固定流程切换,重复操作时校验当前状态,不符合则拒绝。对于并发更新可以采用乐观锁的方式,为数据增加版本号,更新时校验版本号。
线程池的核心参数
问:线程池的核心参数
答:
1. 核心线程数据
2. 最大线程数目:核心线程 + 救急线程最大数目
3. 生存时间
4. 时间单位
5. 阻塞队列
6. 线程工厂
7. 拒绝策略
线程池拒绝策略有哪些
问:线程池拒绝策略有哪些
答:
1. 抛异常(默认)
2. 直接丢弃新来任务,不抛异常
3. 丢到队列里面最旧的未执行任务
4. 谁提交任务谁自己跑
另外也可重写方法自定义拒绝策略
Java并发的三大特性
问:Java并发的三大特性
答:
1. 原子性:一个操作要么全部执行成功,要么全部不执行
2. 可见性:一个线程修改了共享变量,其他线程能立刻看到最新值
3. 有序性:程序执行顺序按代码编写顺序,禁止指令重排序
