1、并行与并发的区别是什么?
并行
指两个或两个以上事件或活动在同一时刻发生。如多个任务在多个 CPU 或 CPU 的多个核上同时执行,不存在 CPU 资源的竞争、等待行为。
并行与并发的区别
并行指多个事件在同一个时刻发生;并发指在某时刻只有一个事件在发生,某个时间段内由于 CPU 交替执行,可以发生多个事件。
并行没有对 CPU 资源的抢占;并发执行的线程需要对CPU 资源进行抢占。
并行执行的线程之间不存在切换;并发操作系统会根据任务调度系统给线程分配线程的 CPU 执行时间,线程的执行会进行切换。
Java 中的多线程
通过 JDK 中的 java.lang.Thread 可以实现多线程。
Java 中多线程运行的程序可能是并发也可能是并行,取决于操作系统对线程的调度和计算机硬件资源( CPU 的个数和 CPU 的核数)。
CPU 资源比较充足时,多线程被分配到不同的 CPU 资源上,即并行;CPU 资源比较紧缺时,多线程可能被分配到同个 CPU 的某个核上去执行,即并发。
不管多线程是并行还是并发,都是为了提高程序的性能。
2、线程的run()方法和start()区别?
启动一个线程需要调用 Thread 对象的 start() 方法
调用线程的 start() 方法后,线程处于可运行状态,此时它可以由 JVM 调度并执行,这并不意味着线程就会立即运行
run() 方法是线程运行时由 JVM 回调的方法,无需手动写代码调用
直接调用线程的 run() 方法,相当于在调用线程里继续调用方法,并未启动一个新的线程
3、线程?进程?为什么要有线程?
进程:
程序执行时的一个实例
每个进程都有独立的内存地址空间
系统进行资源分配和调度的基本单位
进程里的堆,是一个进程中最大的一块内存,被进程中的所有线程共享的,进程创建时分配,主要存放 new 创建的对象实例
进程里的方法区,是用来存放进程中的代码片段的,是线程共享的
在多线程 OS 中,进程不是一个可执行的实体,即一个进程至少创建一个线程去执行代码
为什么要有线程?
每个进程都有自己的地址空间,即进程空间。一个服务器通常需要接收大量并发请求,为每一个请求都创建一个进程系统开销大、请求响应效率低,因此操作系统引进线程。
线程:
进程中的一个实体
进程的一个执行路径
CPU 调度和分派的基本单位
线程本身是不会独立存在当前线程 CPU 时间片用完后,会让出 CPU 等下次轮到自己时候在执行,系统不会为线程分配内存,线程组之间只能共享所属进程的资源
线程只拥有在运行中必不可少的资源(如程序计数器、栈)
线程里的程序计数器就是为了记录该线程让出 CPU 时候的执行地址,待再次分配到时间片时候就可以从自己私有的计数器指定地址继续执行
每个线程有自己的栈资源,用于存储该线程的局部变量和调用栈帧,其它线程无权访问
关系:
一个程序至少一个进程,一个进程至少一个线程,进程中的多个线程是共享进程的资源
Java 中当我们启动 main 函数时候就启动了一个 JVM 的进程,而 main 函数所在线程就是这个进程中的一个线程,也叫做主线程
一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器,栈区域
如下图
区别:
本质:进程是操作系统资源分配的基本单位;线程是任务调度和执行的基本单位
内存分配
系统在运行的时候会为每个进程分配不同的内存空间,建立数据表来维护代码段、堆栈段和数据段;除了 CPU 外,系统不会为线程分配内存,线程所使用的资源来自其所属进程的资源
资源拥有
进程之间的资源是独立的,无法共享;同一进程的所有线程共享本进程的资源,如内存,CPU,IO 等
开销
每个进程都有独立的代码和数据空间,程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行程序计数器和栈,线程之间切换的开销小
通信
进程间 以IPC(管道,信号量,共享内存,消息队列,文件,套接字等)方式通信 ;同一个进程下,线程间可以共享全局变量、静态变量等数据进行通信,做到同步和互斥,以保证数据的一致性
调度和切换:
线程上下文切换比进程上下文切换快,代价小
执行过程
每个进程都有一个程序执行的入口,顺序执行序列;线程不能够独立执行,必须依存在应用程序中,由程序的多线程控制机制控制
健壮性
每个进程之间的资源是独立的,当一个进程崩溃时,不会影响其他进程;同一进程的线程共享此线程的资源,当一个线程发生崩溃时,此进程也会发生崩溃,稳定性差,容易出现共享与资源竞争产生的各种问题,如死锁等
可维护性:
线程的可维护性,代码也较难调试,bug 难排查
进程与线程的选择:
需要频繁创建销毁的优先使用线程。因为进程创建、销毁一个进程代价很大,需要不停的分配资源;线程频繁的调用只改变 CPU 的执行
线程的切换速度快,需要大量计算,切换频繁时,用线程
耗时的操作使用线程可提高应用程序的响应
线程对 CPU 的使用效率更优,多机器分布的用进程,多核分布用线程
需要跨机器移植,优先考虑用进程
需要更稳定、安全时,优先考虑用进程
需要速度时,优先考虑用线程
并行性要求很高时,优先考虑用线程
Java 编程语言中线程是通过 java.lang.Thread 类实现的。
Thread 类中包含 tid(线程id)、name(线程名称)、group(线程组)、daemon(是否守护线程)、priority(优先级) 等重要属性。
4、什么是守护线程?
Java线程分为用户线程和守护线程
守护线程是程序运行的时候在后台提供一种通用服务的线程。所有用户线程停止,进程会停掉所有守护线程,退出程序。
Java中把线程设置为守护线程的方法:在 start 线程之前调用线程的 setDaemon(true) 方法
注意:
setDaemon(true) 必须在 start() 之前设置,否则会抛出IllegalThreadStateException异常,该线程仍默认为用户线程,继续执行
守护线程创建的线程也是守护线程
守护线程不应该访问、写入持久化资源,如文件、数据库,因为它会在任何时间被停止,导致资源未释放、数据写入中断等问题
/**
* 测试守护线程
* @author ConstXiong
* @date 2019-09-03 12:15:59
*/
public class TestDaemonThread {
public static void main(String[] args) {
testDaemonThread();
}
//
public static void testDaemonThread() {
Thread t = new Thread(() -> {
//创建线程,校验守护线程内创建线程是否为守护线程
Thread t2 = new Thread(() -> {
System.out.println("t2 : " +
(Thread.currentThread().isDaemon() ? "守护线程"
: "非守护线程"));
});
t2.start();
//当所有用户线程执行完,守护线程会被直接杀掉,程序停止运
行
int i = 1;
while(true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t : " +
(Thread.currentThread().isDaemon() ? "守护线
程" : "非守护线程") + " , 执行次数 : " + i);
if (i++ >= 10) {
break;
}
}
});
//setDaemon(true) 必须在 start() 之前设置,否则会抛出
IllegalThreadStateException异常,该线程仍默认为用户线程,
继续执行
t.setDaemon(true);
t.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//主线程结束
System.out.println("主线程结束");
}
}
执行结果
t2 : 守护线程
t : 守护线程 , 执行次数 : 1
主线程结束
t : 守护线程 , 执行次数 : 2
结论:
上述代码线程t,未打印到 t : t : 守护线程 , 执行次数 : 3,说明所有用户线程停止,进程会停掉所有守护线程,退出程序
当 t.start(); 放到 t.setDaemon(true); 之前,程序抛出IllegalThreadStateException,t 仍然是用户线程,打印如下
Exception in thread "main" t2 :
非守护线程java.lang.IllegalThreadStateException at java.lang.Thread.setDaemon(Thread.java:1359)at constxiong.concurrency.a008.TestDaemonThread.testDaemonThread(TestDaemonThread.java:39)at constxiong.concurrency.a008.TestDaemonThread.main(TestDaemonThread.java:11)
t : 非守护线程 , 执行次数 : 1
t : 非守护线程 , 执行次数 : 2
t : 非守护线程 , 执行次数 : 3
t : 非守护线程 , 执行次数 : 4
t : 非守护线程 , 执行次数 : 5
t : 非守护线程 , 执行次数 : 6
t : 非守护线程 , 执行次数 : 7
t : 非守护线程 , 执行次数 : 8
t : 非守护线程 , 执行次数 : 9
t : 非守护线程 , 执行次数 : 10
5、如何创建、启动 Java 线程?
Java 中有 4 种常见的创建线程的方式。
1) 重写 Thread 类的 run() 方法。表现形式有两种:new Thread 对象匿名重写 run() 方法
public static void main(String[] args) {
new Thread("t"){
@Override
public void run() {
System.out.println("线程启动开始......");
}
}.start();
}
执行结果
thread t > 0
thread t > 1
thread t > 2
2) 1继承 Thread 对象,重写 run() 方法
public class Test2 {
public static void main(String[] args) {
new ThreadExtend().start();
}
}
class ThreadExtend extends Thread {
@Override
public void run() {
System.out.println("线程启动开始......");
}
}
执行结果
thread t > 0
thread t > 1
thread t > 2
3)实现 Runnable 接口,重写 run() 方法。
表现形式有两种:new Runnable 对象,匿名重写 run() 方法
public class Test3 {
public static void main(String[] args) {
}
public static void newRunable(){
new Thread(new Runnable() {
@Override
public void run() {
}
}).start();
new Thread(()->{
System.out.println("线程启动开始");
}).start();
}
}
执行结果
thread t1 > 0
thread t2 > 0
thread t1 > 1
thread t2 > 1
thread t1 > 2
thread t2 > 2
4)实现 Runnable 接口,重写 run() 方法
public class Test4 {
public static void main(String[] args) {
new Thread(new RunnableExtend()).start();
}
}
class RunnableExtend implements Runnable {
@Override
public void run() {
}
}
执行结果
thread t > 0
thread t > 1
thread t > 2
5)实现 Callable 接口,使用 FutureTask 类创建线程
/**
* @program: Test
* @description:
* @author: chuige
* @create: 2021-10-09 15:23
**/
public class Test5 {
public static void main(String[] args) {
FutureTask<String> ft = new FutureTask<String>(new
Callable<String>() {
@Override
public String call() throws Exception {
return "1213";
}
});
new Thread(ft).start();
}
}
执行结果
执行结果:ConstXiong
6)使用线程池创建、启动线程
public class Test6 {
public static void main(String[] args) {
ExecutorService singleService =
Executors.newSingleThreadExecutor();
singleService.submit(()->{
System.out.println("线程开始运行");
});
singleService.shutdown();
}
}
执行结果
单线程线程池执行任务
6 什么是并发编程?
并发:
在程序设计的角度,希望通过某些机制让计算机可以在一个时间段内,执行多个任务。
一个或多个物理 CPU 在多个程序之间多路复用,提高对计算机资源的利用率。
任务数多余 CPU 的核数,通过操作系统的任务调度算法,实现多个任务一起执行。
有多个线程在执行,计算机只有一个 CPU,不可能真正同时运行多个线程,操作系统只能把 CPU 运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。
并发编程:
用编程语言编写让计算机可以在一个时间段内执行多个任务的程序。
7 为什么要用并发编程?
“摩尔定律” 失效,硬件的单元计算能力提升受限;硬件上提高了 CPU 的核数和个数。并发编程可以提升 CPU 的计算能力的利用率。
提升程序的性能,如:响应时间、吞吐量、计算机资源使用率等。
并发程序可以更好地处理复杂业务,对复杂业务进行多任务拆分,简化任务调度,同步执行任务。
8 并发编程的缺点?
Java 中的线程对应是操作系统级别的线程,线程数量控制不好,频繁的创建、销毁线程和线程间的切换,比较消耗内存和时间。
容易带来线程安全问题。如线程的可见性、有序性、原子性问题,会导致程序出现的结果与预期结果不一致。
多线程容易造成死锁、活锁、线程饥饿等问题。此类问题往往只能通过手动停止线程、甚至是进程才能解决,影响严重。
对编程人员的技术要求较高,编写出正确的并发程序并不容易。
并发程序易出问题,且难调试和排查;问题常常诡异地出现,又诡异地消失。
46 乐观锁与悲观锁是什么?
悲观锁(Pessimistic Lock)
线程每次在处理共享数据时都会上锁,其他线程想处理数据就会阻塞直到获得锁。
乐观锁(Optimistic Lock):线程每次在处理共享数据时都不会上锁,在更新时会通过数据的版本号等机制判断其他线程有没有更新数据。乐观锁适合读多写少的应用场景
两种锁各有优缺点
乐观锁适用于读多写少的场景,可以省去频繁加锁、释放锁的开销,提高吞吐量
在写比较多的场景下,乐观锁会因为版本不一致,不断重试更新,产生大量自旋,消耗 CPU,影响性能。这种情况下,适合悲观锁