多线程基础知识
进程和线程的区别?
进程是操作系统资源分配、调度和管理的最小单位。进程是处于运行过程的程序。
线程是CPU执行和调度的最小单元。
一个程序运行至少有一个进程,一个进程中可以包含多个线程,但至少要包含一个线程。
多个线程可以共享进程中堆和方法区的资源,但是每个线程又可以有自己的程序计数器、虚拟机栈和本地方法栈。
堆和方法区
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存)。
方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
并行和并发的区别?
并行是指在同一时刻,有多条指令在多个处理器上同时执行。
并发是指在同一时刻只能有一条指令执行,但多个进程的指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。
同步和异步
- 同步:发出一个调用之后,在没有得到结果之前,该调用就不可以返回,一直等待。
- 异步:调用在发出之后,不用等待返回结果,该调用直接返回。
线程安全和线程不安全
线程安全和不安全是在多线程环境下对于同一份数据的访问是否能够保证其正确性和一致性的描述。
- 线程安全指的是在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性。
- 线程不安全则表示在多线程环境下,对于同一份数据,多个线程同时访问时可能会导致数据混乱、错误或者丢失。
线程的生命周期和状态
线程状态划分并不唯一,下面参考《Java并发编程的艺术》
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。线程转化过程如下:
图片来源:菜鸟教程
死锁
死锁指的是两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
线程A持有资源2,线程B持有资源1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
产生死锁:
csharp
public class DeadLockDemo {
private static Object resource1=new Object();
private static Object resource2=new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (resource1){
System.out.println(Thread.currentThread()+"get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("waiting get resource2");
synchronized (resource2){
System.out.println(Thread.currentThread()+"get resource2");
}
}
}).start();
new Thread(()->{
synchronized (resource2){
System.out.println(Thread.currentThread()+"get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("waiting get resource1");
synchronized (resource1){
System.out.println(Thread.currentThread()+"get resource1");
}
}
}).start();
}
}
上面代码产生死锁的主要原因是线程1获取到了资源1,线程2获取到了资源2,线程1继续获取资源2而产生阻塞,线程2继续获取资源1而产生阻塞。
解决该问题最简单的方式就是两个线程按顺序获取资源,线程1和线程2都先获取资源1再获取资源2,无论哪个线程先获取到资源1,另一个线程都会因无法获取线程1而阻塞,等到先获取线程1的线程释放资源1,另一个线程获取资源1,这样两个线程可以轮流获取资源1和资源2,代码如下:
csharp
private static Object resource1=new Object();
private static Object resource2=new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (resource1){
System.out.println(Thread.currentThread()+"get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("waiting get resource2");
synchronized (resource2){
System.out.println(Thread.currentThread()+"get resource2");
}
}
}).start();
new Thread(()->{
synchronized (resource1){
System.out.println(Thread.currentThread()+"get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("waiting get resource2");
synchronized (resource2){
System.out.println(Thread.currentThread()+"get resource2");
}
}
}).start();
}
Java中守护线程和用户线程
守护线程是指在程序运行的时候在后台提供一种通用服务的线程,这种线程并不属于程序中不可或缺的部分。
通俗的讲,任何一个守护线程都是整个JVM中所有非守护线程的"保姆"。
守护线程的一个典型例子就是垃圾回收器,只要JVM启动,它始终在运行,实时监控和管理系统中可以被回收的资源。
将一个用户线程设置为守护线程的方法就是在调用start启动线程之前调用对象的
setDaemon(true)
方法,如果将以上参数设置为false,则表示的是用户线程模式。
临界区资源与临界区代码段
临界区资源表示一种可以被多个线程使用的公共资源或共享数据,但是每一次只能有一个线程使用它。一旦临界区资源被占用,想使用该资源的其他线程则必须等待。
在并发情况下,临界区资源是受保护的对象。临界区代码段是每个线程中访问临界资源的那段代码,多个线程必须互斥地对临界区资源进行访问。线程进入临界区代码段之前,必须在进入区申请资源,申请成功之后执行临界区代码段,执行完成之后释放资源。临界区代码段的进入和退出如下图所示。
竞态条件可能是由于在访问临界区代码段时没有互斥地访问而导致的特殊情况。如果多个线程在临界区代码段的并发执行结果可能因为代码的执行顺序不同而不同,我们就说这时在临界区出现了竞态条件问题。
比如,amount为临界区资源,selfPlus()可以理解为临界区代码段:
csharp
public class NotSafePlus{
private Integer amount=0;//临界区资源
//临界区代码段
public void selfPlus(){
amount++;
}
}
当多个线程访问临界区的selfPlus()方法时,就会出现竞态条件问题。
为了避免静态条件的问题,我们必须保证临界区代码段操作具备排他性。这就意味着当一个线程进入临界区代码段执行时,其他线程不能进入临界区代码段执行。
在Java中,我们除了使用synchronized
关键字还可以使用Lock
显式锁实例,或者使用原子变量(Atomic Variables)对临界区代码段进行排他性保护。