一、什么是线程和进程?

进程:

是程序的一次执行过程,是系统运行程序的基本单元(就比如打开某个应用,就是开启了一个进程),因此进程是动态的。系统运行一个程序即是一个程序从创建、运行到消亡的过程。

在 Java 中,当我们启动 main 函数时其实就是启动了 JVM 进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。

线程:

线程与就进程相似,但线程是一个比进程更小的执行单位。一个进程在执行过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个进程,或是在各个进程之间做切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

二、线程与进程的关系,区别及优缺点?

从 JVM 角度说进程和线程之间的关系

下图是 Java 内存区域,通过下图我们从 JVM 的角度说明线程与进程之间的关系。

可以看出,一个进程可以有多个线程,多个线程共享进程的堆和方法区(JDK 1.8 之后的元空间)资源。但是每个线程有自己的程序计数器、虚拟机栈和本地方法栈。

综上:线程是进程划分成的更小的运行单位。线程与进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程则相反。

为什么程序计数器、虚拟机栈和本地方法栈是线程私有的呢?为什么堆和方法区是线程共享的呢?

(1) 程序计数器为什么是私有的?

首先明确程序计数器的作用:

  • 字节码解释器通过改变程序计数器来一次读取指令,从而实现代码的流程控制。如:顺序执行、选择、循环、异常处理。
  • 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程运行到哪了。

需要注意的是:如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。

所以,程序计数器私有主要是为了线程切换后能够恢复到正确的执行位置。

(2) 虚拟机栈和本地方法栈为什么是私有的?

  • 虚拟机栈:每个Java 方法在执行的同时会创建一个帧栈用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至完成的过程,就对应一个帧栈在 Java 虚拟机中入栈和出栈的过程。
  • 本地方法栈:和虚拟机的作用非常相似。区别是:虚拟机为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 native 方法服务。在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

(3)  堆和方法区

堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用来存放新创建的对象(所有的对象都在这里分配内存);方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。

参考:JavaGuide 公众号及其相应的 Github

三、并发和并行有什么区别?

  • 并发:同一时间段,多个任务都在执行(单位时间内不一定同时执行);
  • 并行:单位时间内,多个任务同时执行。

并发的关键是你有处理多个任务的能力,不一定要同时。 而并行的关键是你有同时处理多个任务的能力。  

四、为什么要使用多线程?

先总体上:

  • 从计算机底层来说:线程可以比作是轻量级的进程,是程序执行的最小单元,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
  • 从当代互联网发展趋势来说:现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正式开发高并发系统的基础,利用好多线程机制可以大大提高系统的并发能力以及性能。

再深入到计算机底层:

  • 单核时代:在单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率。
  • 多核时代:多核时代主要是为了提高 CPU 的利用率。

五、使用多线程可能会带来什么问题?

并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁等,还有受限于硬件和软件和资源闲置问题。

六、说说线程的生命周期和状态。

Java 线程在运行的生命周期中的指定时刻只可能指定处于下面几种不同状态的其中一个状态:

  1. 新建状态(NEW):新创建了一个线程对象;
  2. 就绪状态(RUNNABLE):线程创建后,其他线程调用了该对象的 start() 方法。该方法状态的线程位于可运行线程池中,变得可运行,等待获取 CPU 的使用权;
  3. 运行状态(RUNNING):就绪状态的线程获取了 CPU,执行程序代码;
  4. 阻塞状态(BLOCKED):阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止运行。知道线程进入就绪状态,才有机会转到运行状态。阻塞的情况分为三种:
    • 等待阻塞:运行的线程执行 wait() 方法,JVM 会把该线程放入线程池中。
    • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池中。
    • 其他阻塞:运行的线程执行 sleep() 或 join() 方法,或者发出了 I/O 请求时,JVM 会把该线程设置为阻塞状态。当 sleep() 超时、join() 等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
  5. 死亡状态(DEAD):线程执行完了或者因异常退出了 run() 方法,该线程结束生命周期。

线程在生命周期中并不是固定处于一个状态,而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图(图为《Java 并发编程的艺术》)

可以看出:线程创建之初处于 NEW (新建) 状态。调用 start() 方法后开始运行,线程这时候处于 READY (可运行) 状态。可运行状态的线程获得了 CPU 时间片 (timeslice) 后就处于 RUNNING (运行)状态。线程执行了 wait() 方法后,线程进入 WAITING (超时等待) 状态相当于等待状态的基础上增加了超时限制,比如 sleep(long millis) 方法或 waiting(long millis) 方法可以将 Java 线程置于 TIME WAITING 状态。当超时时间达到后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED (阻塞)状态。线程在执行 Runnable 的 run() 方法之后将进入到 TERMINATED (终止) 状态。

七、java 中如何创建线程?

Java 中创建线程有四种方式:① 继承 Thread;② 实现 Runnable 接口;③ 线程池;④ 实现 Callable 接口。

关于 Thread 或者 Runnable 接口,首先 Runnable 是接口,实现了改接口的类还可以继承其他类,更灵活;其次,Runnable 任务可以在 Executors 中或者 ExecutorService 提交运行。

Future 和 Callable:Callable 与 Runnable 一样都是代表抽象的计算任务,其中的 call 方法做用与 run 一样,但是会返回一个值。Future 表示一个任务的生命周期,并提供了相应的方法来判断是否已经完成或者取消,以及获取任务的结果。ExecutorService 中所有的 submit 方法都会返回一个 future。

Callable 和 Runnable 的区别:

  • Callable 定义的方法是 call,而 Runnable 定义的方法是 run;
  • Callable 的 call 方法可以有返回值,而 Runnable 的 run 方法不能有返回值;
  • Callable 的 call 方法可以抛出异常,而 Runnable 的 run 方法不能抛出异常。

八、什么是上下文切换?

多线程编程中一般线程的个数都大于 CPU 核的个数,而一个 CPU 核在任意时刻只能被一个线程使用,为了让这些县城都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程是时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。也就是:当任务执行完, CPU 时间片切换到另一个任务之前会先保存自己的状态,以便于再切换回这个任务时,可以加载这个任务的状态。任务从保持到再加载的过程就是一个上下文切换。

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。

九、什么是线程死锁?怎么避免?

死锁:

两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待的现象。

为什么会出现死锁?

Java 运行多线程并发控制,当多个线程同时操作一个共享的资源变量时(如数据的增删改查),将会导致数据出现不正确的结果,相互之间产生冲突,因此加入锁保证了该变量的唯一性和准确性。

如下代码(代码源自《Java多线程编程核心技术》):

public class DeadThreadDemo implements Runnable{
public String username;
public Object lock1 = new Object();
public Object lock2 = new Object();
public void setFlag(String username) {
this.username = username;
}
@Override
public void run(){
if(username.equals("a")) {
synchronized (lock1) {
try {
System.out.println("username = " + username);
Thread.sleep(3000);
}catch(InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("按 lock1->lock2代码 顺序执行了");
}
}
}
if(username.equals("b")) {
synchronized (lock2) {
try {
System.out.println("username = " + username);
Thread.sleep(3000);
}catch(InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("按lock2->lock1代码顺序执行了");
}
}
}
}
}

测试类:

public class DeadThreadTest {

    public static void main(String[] args) {
try {
DeadThreadDemo dtd1 = new DeadThreadDemo();
dtd1.setFlag("a");
Thread thread1 = new Thread(dtd1);
thread1.start();
Thread.sleep(100);
dtd1.setFlag("b");
Thread thread2 = new Thread(dtd1);
thread2.start();
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}

输出:

username = a
username = b

线程 a 通过 synchronized (lock1) 获得 lock1 的监视器锁,然后通过thread.sleap(3000); 让线程 a 休眠 3s 为的是让线程 b 得到执行然后获取到 lock2 的监视器锁。线程 a 和线程 b 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。

死锁产生的四个条件:

  1. 互斥条件: 该资源任意一个时刻只由一个线程占用;
  2. 请求与保持条件:一个线程因请求资源而阻塞,对已获得的资源保持不放;
  3. 不剥夺条件:线程已经获得的资源在未使用完之前不能被其他线程强行剥夺,只由自己使用完毕后才释放资源;
  4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

怎么避免线程死锁?

只需要破坏产生死锁的四个条件之一即可。

  • 破坏互斥条件:这个条件我们没有办法破坏,因为我们用锁本身就是想让他们互斥的(临界资源需要互斥访问)。
  • 破坏请求与保持条件:一次性申请所有的资源
  • 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  • 破坏循环等待条件:靠按顺序申请资源来预防。按照某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

十、sleep() 方法和 wait() 方法区别和共同点

  • 两者最主要的区别在于:sleep() 方法没有释放锁,而 wait() 方法释放了锁;
  • 两者都可以暂停多线程;
  • wait() 通常被用于线程间交互/通信,sleep() 通常被用于暂停执行;
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep 执行完后,会自动苏醒。

十一、为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入就绪状态,当分配到时间片后就可以开始运行了。start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。而直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这不是多线程工作。

总之:调用 start() 方法可启动线程并使线程进入就绪状态,而 run() 方法只是 thread 的一个普通方法,还是在主线程里执行的。

Java 并发基础知识的更多相关文章

  1. java并发基础知识

    这几天全国都是关键时候,放假了,还是要学习啊!很久没有写博客了,最近看了一本书,有关于java并发编程的,书名叫做“java并发编程之美”,讲的很有意思,这里就做一个笔记吧! 有需要openjdk8源 ...

  2. Java并发基础知识你知道多少?

    并发 https://blog.csdn.net/justloveyou_/article/details/53672005 并发的三个特性是什么? 什么是指令重排序? 单线程的指令重排序靠什么保证正 ...

  3. Java并发--基础知识

    一.为什么要用到并发 充分利用多核CPU的计算能力 方便进行业务拆分,提升应用性能 二.并发编程有哪些缺点 频繁的上下文切换 时间片是CPU分配给各个线程的时间,因为时间非常短,所以CPU不断通过切换 ...

  4. 目录-java并发基础知识

    ====================== 1.volatile原理 2.ThreadLocal的实现原理(源码级) 3.线程池模型以及核心参数 4.HashMap的实现以及jdk8的改进(源码级) ...

  5. java并发基础及原理

    java并发基础知识导图   一 java线程用法 1.1 线程使用方式 1.1.1 继承Thread类 继承Thread类的方式,无返回值,且由于java不支持多继承,继承Thread类后,无法再继 ...

  6. Java笔记(十四) 并发基础知识

    并发基础知识 一.线程的基本概念 线程表示一条单独的执行流,它有自己的程序计数器,有自己的栈. 1.创建线程 1)继承Thread Java中java.lang.Thread这个类表示线程,一个类可以 ...

  7. Java 多线程——基础知识

    java 多线程 目录: Java 多线程——基础知识 Java 多线程 —— synchronized关键字 java 多线程——一个定时调度的例子 java 多线程——quartz 定时调度的例子 ...

  8. Java并发基础:进程和线程之由来

    转载自:http://www.cnblogs.com/dolphin0520/p/3910667.html 在前面,已经介绍了Java的基础知识,现在我们来讨论一点稍微难一点的问题:Java并发编程. ...

  9. 什么才是java的基础知识?

    近日里,很多人邀请我回答各种j2ee开发的初级问题,我无一都强调java初学者要先扎实自己的基础知识,那什么才是java的基础知识?又怎么样才算掌握了java的基础知识呢?这个问题还真值得仔细思考. ...

随机推荐

  1. Win64 驱动内核编程-11.回调监控进线程句柄操作

    无HOOK监控进线程句柄操作 在 NT5 平台下,要监控进线程句柄的操作. 通常要挂钩三个API:NtOpenProcess.NtOpenThread.NtDuplicateObject.但是在 VI ...

  2. 双非硕士的辛酸求职回忆录:第 2 篇 谈谈我是如何同时找到Java、Python、Go等开发岗和国企银行的科技岗位Offer(上篇)

    1. 双非硕士的辛酸求职之旅--谈谈我是如何同时找到Java.Python.Go等开发岗和国企银行的offer 1.1. 秋招最终情况 本人情况:双非硕,意向工作城市广深,Java和Python技术栈 ...

  3. Redis数据结构—链表与字典的结构

    目录 Redis数据结构-链表与字典的结构 链表 Redis链表节点的结构 Redis链表的表示 Redis链表用在哪 字典 Redis字典结构总览 Redis字典结构分解 Redis字典的使用 Re ...

  4. 本地jar包安装Maven本地仓库

    下载jar包 如果本地存在,可以忽略. 本地安装命令 mvn install:install-file -Dfile=D:\Environment\Java\JDK\lib\tools.jar -Dg ...

  5. QFNU-11.08training

    7-1  阅览室 题目: 天梯图书阅览室请你编写一个简单的图书借阅统计程序.当读者借书时,管理员输入书号并按下S键,程序开始计时:当读者还书时,管理员输入书号并按下E键,程序结束计时.书号为不超过10 ...

  6. LightningChart JS 3.0 新功能上线

    在这次的LC JS更新中,首次将极坐标图引入图表库. 这种全新的图表类型可以通过API轻松地进行样式设置.极坐标可以用作独立图表或在仪表板中使用. 另外,用于 XY图表的对数轴也添加到了这次的更新,L ...

  7. 有哪些适合中小企业使用的PaaS平台?

    对于中小企业来说,在业务上同样需要工作流.应用平台来进行支持,但是,面对诸如ERP等动辄好几十万的费用来说,完全是在增加运营成本.如何解决中小企业对于业务应用.工作流管理的需求问题呢?使用PaaS低代 ...

  8. EFCore之增删改查

    1. 连接数据库 通过依赖注入配置应用程序,通过startup类的ConfigureService方法中的AddDbContext将EFCore添加到依赖注入容器 public void Config ...

  9. [刷题] 102 Binary Tree Level Order Traversal

    要求 对二叉树进行层序遍历 实现 返回结果为双重向量,对应树的每层元素 队列的每个元素是一个pair对,存树节点和其所在的层信息 1 Definition for a binary tree node ...

  10. python实现UDP通讯

    Environment Client:Windows Server:KaLi Linux(VM_virtul) Network:Same LAN Client UDPClient.py #-*- co ...