线程&同步 - 练习面试题
文章目录
线程概念
进程(Process)
程序的一次运行。操作系统会给这个进程分配资源(例如:内存),进程与进程之间的内存是独立,无法直接共享。最早的DOS操作系统是单任务的,同一时间只能运行一个进程。后来现在的操作系统都是支持多任务的,可以同时运行多个进程。两个进程之间进行来回切换,通信(交换数据)等操作时,成本比较高。
总的来说进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。
什么是进程?
进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体 程序的一次运行 OS进行资源分配和调度的单位 应用程序运行的载体
什么是线程?
-
线程是操作系统能够进行运算调度的最小单位。
-
它被包含在进程之中,是进程中的实际运作单位。
-
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程是进程中的其中一条执行路径。一个进程中至少有一个线程,也可以有多个线程。有的时候也把线程称为轻量级的进程。同一个进程的多个线程之间有些内存是可以共享的(方法区、堆),也有些内存是独立的(栈(包括虚拟机栈和本地方法栈)、程序计数器)。 因为线程之间可能使用共享内存,那么在数据交换成本上就比较低。而且线程之间的切换相对进程对于CPU和操作系统来说,成本比较低。
进程和线程的区别?
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。
进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响。
线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。
但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
java 中线程有哪些状态?
共 6 种状态:
- 初始状态 (NEW) :尚未启动的线程处于此状态。通常是新创建了线程,但还没有调用 start () 方法;
- 运行状态 (RUNNABLE):Java 线程中将就绪(ready)和运行中(running)两种状态笼统的称为 “运行中”。比如说线程可运行线程池中,等待被调度选中,获取 CPU 的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得 CPU 时间片后变为运行中状态(running)。
- 阻塞状态 (BLOCKED):表示线程阻塞于锁;
- 等待状态 (WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断);
- 超时等待状态 (TIMED_WAITING):进入该状态的线程需要等待其他线程在指定时间内做出一些特定动作(通知或中断),可以在指定的时间自行返回;
- 终止状态 (TERMINATED):表示该线程已经执行完毕,已退出的线程处理此状态。
新建/出生、就绪、运行、阻塞、死亡
多线程和单线程有什么区别?
单线程程序:程序执行过程中只有一个有效操作的序列,不同操作之间都有明确的执行先后顺序,容易出现代码阻塞
多线程程序:有多个线程,线程间独立运行,能有效地避免代码阻塞,并且提高程序的运行性能
为什么要使用多线程?
1、使用多线程可以减少程序的响应时间。 在单线程的情况下,如果某个程序很耗时或者陷入长时间等待(如等待网络响应),此时程序将不会相应鼠标和键盘等操作,使用多线程后,可以把这个耗时的线程分配到一个单独的线程去执行,从而是程序具备了更好的交互性。
2、与进程相比,线程的创建和切换开销更小。 由于启动一个新的线程必须给这个线程分配独立的地址空间,建立许多数据结构来维护线程代码段、数据段等信息,而运行于同一个进程内的线程共享代码段、数据段,线程的启动或切换的开销就比进程要少很多。同时多线程在数据共享方面效率非常高。
3、多CPU或多核心计算机本身就具有执行多线程的能力。 如果使用单个线程,将无法重复利用计算机资源,造成资源的巨大浪费。因此在多CPU计算机上使用多线程能提高CPU的利用率。
4、使用多线程能简化程序的结构,使用程序便于理解和维护。 一个非常复杂的进程可以分成多个线程来执行。
什么是线程安全?
当多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替运行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获取正确的结果,那这个对象是线程安全的。
为何要使用线程同步?
Java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突。
因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。
如何确保线程安全?
1、对非安全的代码进行加锁控制;
2、使用线程安全的类;
3、多线程并发情况下,线程共享的变量改为方法级的局部变量
线程安全的级别?
1、不可变。不可变的对象一定是线程安全的,并且永远也不需要额外的同步。
Java类库中大多数基本数值类如Integer、String和BigInteger都是不可变的。
2、无条件的线程安全。由类的规格说明所规定的约束在对象被多个线程访问时仍然有效,不管运行时环境如何排列,线程都不需要任何额外的同步。
如 Random 、ConcurrentHashMap、Concurrent集合、atomic
3、有条件的线程安全。有条件的线程安全类对于单独的操作可以是线程安全的,但是某些操作序列可能需要外部同步。
有条件线程安全的最常见的例子是遍历由 Hashtable 或者 Vector 或者返回的迭代器
4、非线程安全(线程兼容)。线程兼容类不是线程安全的,但是可以通过正确使用同步而在并发环境中安全地使用。
如ArrayList HashMap
5、线程对立。线程对立是那些不管是否采用了同步措施,都不能在多线程环境中并发使用的代码。
如如System.setOut()、System.runFinalizersOnExit()
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
new 一个 Thread,线程进入了新建状态(NEW);调用 start() 方法,会启动一个线程并使线程进入了就绪状态(READY),当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
使用多线程可能带来什么问题?
多线程的目的就是为了能提高程序的执行效率提高程序运行速度,但也可能会遇到很多问题:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。
实现Runnable接口和Callable接口的区别?
两者的区别在于 Runnable 接口不会返回结果但是 Callable 接口可以返回结果。
备注: 工具类Executors可以实现Runnable对象和Callable对象之间的相互转换。(Executors.callable(Runnable task)或Executors.callable(Runnable task,Object resule))。
创建线程有哪些方式?
1、继承 Thread 类创建线程类;
2、通过 Runnable 接口创建线程类。
3、通过 Callable 和 Future 创建线程。
4、线程池
练习一:多线程开启
问题:
请描述Thread类中的start()方法与run()方法的区别。
答:
线程对象调用run()方法不开启线程,仅是对象调用方法。
线程对象调用start()方法开启线程,并让jvm调用run()方法在开启的线程中执行。
启动线程的就是start()方法异步执行,run()方法就是普通方法调用同步执行。
练习二:创建多线程
问题:
请描述创建线程的两种方式。
答:
第一种方式是将类声明为 Thread 的子类。
-
定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
-
创建Thread子类的实例,即创建了线程对象。
-
调用线程对象的start()方法来启动该线程。
start()方法
第二种方式是声明一个类实现Runnable 接口。
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动线程。
练习三:多线程练习
问题:
请编写程序,分别打印主线程的名称和子线程的名称。
要求使用两种方式实现:
第一种方式:继承Thread类。
第二种方法:实现Runnable接口。
答:
操作步骤描述
l 第一种方式:继承Thread类
1.定义一个子线程的类,继承Thread类;
2.在子线程类中重写run方法,在run方法中打印子线程的名称;
3.定义一个测试类;
4.在main方法中打印主线程的名称;
5.在main方法中创建子线程对象;
6.调用子线程对象的start方法,开启子线程;
l 第二种方式:实现Runnable接口
1.定义一个子任务类,实现Runnable接口;
2.在子任务类中重写run方法,在run方法中打印子线程的名称;
3.定义一个测试类;
4.在main方法中打印主线程的名称;
5.在main方法中创建一个子任务对象;
6.在main方法中创建一个Thread类的对象,并把子任务对象传递给Thread类的构造方法;
7.调用Thread类对象的start方法开启子线程;
操作步骤答案
l 第一种方式:继承Thread类
SubThread.java
/*
* 1.定义一个子线程的类,继承Thread类;
*/
public class SubThread extends Thread {
/*
*2.在子线程类中重写run方法,在run方法中打印子线程的名称;
*/
public void run() {
// 打印子线程的名称
System.out.println("subThread:" + Thread.currentThread().getName());
}
}
ThreadDemo.java
/*
* 3.定义一个测试类
*/
public class ThreadDemo {
public static void main(String[] args) {
// 4.在main方法中打印主线程的名称;
System.out.println("main:" + Thread.currentThread().getName());
// 5.在main方法中创建子线程对象;
SubThread st = new SubThread();
// 6.调用子线程对象的start方法,开启子线程。
st.start();
}
}
l 第二种方式:实现Runnable接口
SubRunnable.java
/*
* 1.定义一个子任务类,实现Runnable接口。
*/
public class SubRunnable implements Runnable {
@Override
public void run() {
// 2.在子任务类中重写run方法,在run方法中打印子线程的名称。
System.out.println("SubRunnable:" + Thread.currentThread().getName());
}
}
RunnableDemo
/*
* 3.定义一个测试类。
*/
public class RunnableDemo {
public static void main(String[] args) {
// 4.在main方法中打印主线程的名称。
System.out.println("RunnableDemo:" + Thread.currentThread().getName());
// 5.在main方法中创建一个子任务对象。
SubRunnable r = new SubRunnable();
// 6.在main方法中创建一个Thread类的对象,并把子任务对象传递给Thread类的 构造方法。
Thread t = new Thread(r);
// 7.调用Thread类对象的start方法开启子线程。
t.start();
//RunnableDemo:main
//SubRunnable:Thread-0
}
}
练习四:实现Runnable接口的优势
问题:
请描述实现Runnable接口比继承Thread类所具有的优势:
答:
-
适合多个相同的程序代码的线程去共享同一个资源。
-
可以避免java中的单继承的局限性。
-
增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和数据独立。
-
线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类。
练习五:线程状态
问题:请描述在线程的生命周期中, 有几种状态呢 ?
答:
-
NEW(新建) 线程刚被创建,但是并未启动。
-
Runnable(可运行)
线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。
- Blocked(锁阻塞)
当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
- Waiting(无限等待)
一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
- Timed Waiting(计时等待)
同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
- Teminated(被终止)
因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。
问题 1:java 中线程有哪些状态?
参考答案:
共 6 种状态:
- 初始状态 (NEW) :尚未启动的线程处于此状态。通常是新创建了线程,但还没有调用 start () 方法;
- 运行状态 (RUNNABLE):Java 线程中将就绪(ready)和运行中(running)两种状态笼统的称为 “运行中”。比如说线程可运行线程池中,等待被调度选中,获取 CPU 的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得 CPU 时间片后变为运行中状态(running)。
- 阻塞状态 (BLOCKED):表示线程阻塞于锁;
- 等待状态 (WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断);
- 超时等待状态 (TIMED_WAITING):进入该状态的线程需要等待其他线程在指定时间内做出一些特定动作(通知或中断),可以在指定的时间自行返回;
- 终止状态 (TERMINATED):表示该线程已经执行完毕,已退出的线程处理此状态。
记录
1、 搞定Java核心技术
3、【Java】Java->JavaWeb->Spring全家桶->社区、教育、电商项目等等
从Hello到goodbye