面试也要说人话

Posted JavaPub

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试也要说人话相关的知识,希望对你有一定的参考价值。

什么是《面试1v1》?

《面试1v1》是一个以对话形式讲解知识点的文章合集,是由 JavaPub 编写的真人1对1面试对话教程,通过真实案例编写,生动、有趣、干货满满。

为什么要写《面试1v1》这个专题?

我在后台收到很多读者的描述,说自己在面试准备过程中感觉抓不住重点,总是复习的没考、考的没复习。面试过后导致自己自信心受挫,不知道

再卷我也要说,多线程面试答案都在这里了~

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

做并发编程之前,必须首先理解什么是并发,什么是并行,什么是并发编程,什么是并行编程。

并发(concurrency)和并行(parallellism)是:

  • 解释一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
  • 解释二:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
  • 解释三:在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群

所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。

二、线程和进程的区别?

进程是指运行中的应用程序,每一个进程都有自己独立的内存空间。一个应用程序可以同时启动多个进程。例如对于IE浏览器程序,每打开一个IE浏览器窗口,就启动了一个新的进程。同样,每次执行JDK的java.exe程序,就启动了一个独立的Java虚拟机进程,该进程的任务是解析并执行Java程序代码。

线程是指进程中的一个执行流程,有时也称为执行情景。一个进程可以由多个线程组成,即在一个进程中可以同时运行多个不同的线程,它们分别执行不同的任务。当进程内的多个线程同时运行时,这种运行方式称为并发运行。许多服务器程序,如数据库服务器和Web服务器,都支持并发运行,这些服务器能同时响应来自不同客户的请求。

进程和线程的主要区别在于:每个进程都需要操作系统为其分配独立的内存地址空间,而同一进程中的所有线程在同一块地址空间中工作,这些线程可以共享同一块内存和系统资源,比如共享一个对象或者共享已经打开的一个文件。

三、守护线程是什么?

Linux守护进程的编程方法  守护进程是一种很有用的进程。Linux的大多数服务器就是用守护进程实现的。比如,Internet服务器inetd,Web服务器等。同时,守护进程完成许多系统任务。比如,作业规划进程crond,打印进程lpd等。

守护进程的编程本身并不复杂,复杂的是各种版本的Unix的实现机制不尽相同,造成不同Unix环境下守护进程的编程规则并不一致。这需要读者注意,照搬某些书上的规则(特别是BSD4.3和低版本的System V)到Linux会出现错误的。下面将全面介绍Linux下守护进程的编程要点并给出详细实例。

守护进程及其特性

守护进程最重要的特性是后台运行。在这一点上DOS下的常驻内存程序TSR与之相似。其次,守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩模等。这些环境通常是守护进程从执行它的父进程(特别是shell)中继承下来的。最后,守护进程的启动方式有其特殊之处。它可以在Linux系统启动时从启动脚本/etc/rc.d中启动,可以由作业规划进程crond启动,还可以由用户终端(通常是shell)执行。

总之,除开这些特殊性以外,守护进程与普通进程基本上没有什么区别。因此,编写守护进程实际上是把一个普通进程按照上述的守护进程的特性改造成为守护进程。如果读者对进程有比较深入的认识就更容易理解和编程了。

守护进程的编程要点

前面讲过,不同Unix环境下守护进程的编程规则并不一致。所幸的是守护进程的编程原则其实都一样,区别在于具体的实现细节不同。这个原则就是要满足守护进程的特性。同时,Linux是基于Syetem V的SVR4并遵循Posix标准,实现起来与BSD4相比更方便。编程要点如下;

在后台运行

为避免挂起控制终端将Daemon放入后台执行。方法是在进程中调用fork使父进程终止,让Daemon在子进程中后台执行。

if(pid=fork())

exit(0);//是父进程,结束父进程,子进程继续

脱离控制终端,登录会话和进程组

有必要先介绍一下Linux中的进程与控制终端,登录会话和进程组之间的关系:进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。

控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。方法是在第1点的基础上,调用setsid()使进程成为会话组长:

setsid();

说明:当进程是会话组长时setsid()调用失败。但第一点已经保证进程不是会话组长。setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。

禁止进程重新打开控制终端

现在,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端:

if(pid=fork())

  exit(0);//结束第一子进程,第二子进程继续(第二子进程不再是会话组长)

关闭打开的文件描述符

进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。按如下方法关闭它们:

for(i=0;i 关闭打开的文件描述符close(i);>

改变当前工作目录

进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如/tmpchdir("/")

重设文件创建掩模

进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:umask(0);

处理SIGCHLD信号

处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将 SIGCHLD信号的操作设为SIG_IGN。

signal(SIGCHLD,SIG_IGN);

这样,内核在子进程结束时不会产生僵尸进程。这一点与BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程。

守护进程实例

守护进程实例包括两部分:主程序test.c和初始化程序init.c。主程序每隔一分钟向/tmp目录中的日志test.log报告运行状态。初始化程序中的init_daemon函数负责生成守护进程。读者可以利用init_daemon函数生成自己的守护进

四、创建线程有哪几种方式?

第一种方式:使用runnable接口创建线程

第二种方式:直接继承thread类创建对象

使用runnable接口创建线程

1.可以将cpu,代码和数据分开,形成清晰的模型

2.线程体run()方法所在的类可以从其它类中继承一些有用的属性和方法

3.有利于保持程序的设计风格一致

直接继承thread类创建对象

1.thread子类无法再从其它类继承(java语言单继承)。

2.编写简单,run()方法的当前对象就是线程对象,可直接操作。

在实际应用中,几乎都采取第一种方式

五、说一下 runnablecallable 有什么区别?

相同点

1、两者都是接口;(废话)
2、两者都可用来编写多线程程序;
3、两者都需要调用Thread.start()启动线程;

不同点

1、两者最大的不同点是:实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果;
2、Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;

注意点

Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!

六、线程有哪些状态?

一般说有3种,但也有说4种的

3种:

  • 就绪:线程分配了CPU以外的全部资源,等待获得CPU调度
  • 执行:线程获得CPU,正在执行
  • 阻塞:线程由于发生I/O或者其他的操作导致无法继续执行,就放弃处理机,转入线程就绪队列

第四种:

  • 挂起:由于终端请求,操作系统的要求等原因,导致挂起。

七、sleep() 和 wait() 有什么区别?

1、这两个方法来自不同的类分别是Thread和Object

2、最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。

3、wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在。

4、sleep是让某个线程暂停运行一段时间,其控制范围是由当前线程决定,也就是说,在线程里面决定.好比如说,我要做的事情是 "点火->烧水->煮面,而当我点完火之后我不立即烧水,我要休息一段时间再烧.对于运行的主动权是由我的流程来控制.

5、而wait,首先,这是由某个确定的对象来调用的,将这个对象理解成一个传话的人,当这个人在某个线程里面说"暂停!",也是 thisOBJ.wait(),这里的暂停是阻塞,还是"点火->烧水->煮饭"。

八、notify()和 notifyAll()有什么区别?

public void notifyDataSetChanged ()

该方法内部实现了在每个观察者上面调用onChanged事件。每当发现数据集有改变的情况,或者读取到数据的新状态时,就会调用此方法。

public void notifyDataSetInvalidated ()

该方法内部实现了在每个观察者上面调用onInvalidated事件。每当发现数据集监控有改变的情况,比如该数据集不再有效,就会调用此方法。

九、线程的 run()start()有什么区别?

两种方法的区别

1) start:

  • 用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的
  • start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里方法
  • run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。

2) run:

  • run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待
  • run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。总结:调用start方法方可启动线程,而run方法只是thread的一
  • 个普通方法调用,还是在主线程里执行。这两个方法应该都比较熟悉,把需要并行处理的代码放在run()方法中,start()方法启动线程将自动调用
  • run()方法,这是由jvm的内存机制规定的。并且run()方法必须是public访问权限,返回值类型为void.。

两种方式的比较 :

实际中往往采用实现Runable接口,一方面因为java只支持单继承,继承了Thread类就无法再继续继承其它类,而且Runable接口只有一个run方法;另一方面通过结果可以看出实现Runable接口才是真正的多线程……

十、创建线程池有哪几种方式?

一:创建大小不固定的线程池
二:创建固定数量线程的线程池
三:创建单线程的线程池
四:创建定时线程

1.创建大小不固定的线程池

java view plain copy
package com.peace.pms.Test;  
import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
 /** 
 * @Author: cxx 
* @Date: 2018/3/3 17:16 
*/  
public class ThreadPoolDemo {  
public static class Taskdemo implements Runnable{  
@Override  
public void run() {  
for(int i=0;i<10;i++){  
System.out.println(Thread.currentThread().getName()+":"+i);  
}  
}  
}  
public static void main(String[] args) {  
ExecutorService es=Executors.newFixedThreadPool(2);  
for(int i=0;i<10;i++){  
Taskdemo tc=new Taskdemo();  
es.execute(tc);  
}  
es.shutdown();  
}  
}  </pre>

2.创建固定数量线程的线程池

java view plain copy
public static void main(String[] args) {  
ExecutorService es=Executors.newFixedThreadPool(2);  
for(int i=0;i<10;i++){  
Taskdemo tc=new Taskdemo();  
es.execute(tc);  
}  
es.shutdown();  
}  

3.创建单线程的线程池

java view plain copy
public static void main(String[] args) {  
ExecutorService es=Executors.newSingleThreadExecutor();  
for(int i=0;i<10;i++){  
Taskdemo tc=new Taskdemo();  
es.execute(tc);  
}  
es.shutdown();  
}  

4.创建定时线程

java view plain copy
public static void main(String[] args) {  
ScheduledExecutorService es=Executors.newScheduledThreadPool(2);  
for(int i=0;i<10;i++){  
Taskdemo tc=new Taskdemo();  
//参数1:目标对象  
//参数2:隔多长时间开始执行线程,  
//参数3:执行周期  
//参数4:时间单位
es.scheduleAtFixedRate(tc, 30, 10, TimeUnit.MILLISECONDS);  
}  
es.shutdown();  
}</pre>

十一、线程池都有哪些状态?

  • RUNNING :能接受新提交的任务,并且也能处理阻塞队列中的任务。
  • SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown() 方法会使线程池进入到该状态。(finalize() 方法在执行过程中也会调用 shutdown() 方法进入该状态)。
  • STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态。
  • TIDYING:如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入 TERMINATED 状态。
  • TERMINATED:在 terminated() 方法执行完后进入该状态,默认 terminated() 方法中什么也没有做。

十二、线程池中 submit()execute()方法有什么区别?

在java5之后,并发线程这块发生了根本的变化,最重要的莫过于新的启动、调度、管理线程的一大堆api了。在java5以后,通过 executor来启动线程比用thread的start()更好。在新特征中,可以很容易控制线程的启动、执行和关闭过程,还可以很容易使用线程池的特 性。

一、创建任务

  • 任务就是一个实现了runnable接口的类。
  • 创建的时候实run方法即可。

二、执行任务

通过java.util.concurrent.executorservice接口对象来执行任务,该接口对象通过工具类java.util.concurrent.executors的静态方法来创建。

executors此包中所定义的executor、executorservice、scheduledexecutorservice、threadfactory 和callable 类的工厂和实用方法。

executorservice提供了管理终止的方法,以及可为跟踪一个或多个异步任务执行状况而生成 future 的方法。 可以关闭 executorservice,这将导致其停止接受新任务。关闭后,执行程序将最后终止,这时没有任务在执行,也没有任务在等待执行,并且无法提交新任务。

executorservice.execute(new testrunnable());

十三、在 java 程序中怎么保证多线程的运行安全?

可以在方法变为同步方法,或将共享对象变为同步块。这样就可以了。然而缺点也带来了:那就是效率。java里面有很多有关同步的类和非同步的类:HashTable,HashMap,StringBuffer,StringBuilder等。如果你的项目需要安全,就同步,如果你的项目需要效率,就是用非同步的类。

十四、多线程锁的升级原理是什么?

在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。

锁升级的图示过程:

十五、什么是死锁

死锁是指在执行并发计算时,一组进程中的每个进程都在等待包括自身在内的其他进程释放资源的一种现象,例如发送消息或更常见的是释放锁;在多处理系统、并行计算和分布式系统中,死锁是一个常见的问题,多处理系统、并行计算和分布式系统的软件和硬件锁用于仲裁共享资源和实现进程同步。

操作系统中的进程或线程互相等待时,发生死锁。因为其他的等待进程占有了该进程所请求的系统资源,相应地,如果一个进程因为请求的资源正被另一个等待进程使用,而永远地改变其状态,那么系统就被称为处于死锁状态。

在通信系统中,死锁主要是由信号丢失或损坏造成的,而不是资源争用

十六、怎么防止死锁?

死锁是不应该在程序中出现的,在编写程序时应该尽量避免出现死锁。下面有几种常见的方式用来解决死锁问题:

  • 避免多次锁定。尽量避免同一个线程对多个 Lock 进行锁定。例如上面的死锁程序,主线程要对 A、B 两个对象的 Lock 进行锁定,副线程也要对 A、B 两个对象的 Lock 进行锁定,这就埋下了导致死锁的隐患。
  • 具有相同的加锁顺序。如果多个线程需要对多个 Lock 进行锁定,则应该保证它们以相同的顺序请求加锁。比如上面的死锁程序,主线程先对 A 对象的 Lock 加锁,再对 B 对象的 Lock 加锁;而副线程则先对 B 对象的 Lock 加锁,再对 A 对象的 Lock 加锁。这种加锁顺序很容易形成嵌套锁定,进而导致死锁。如果让主线程、副线程按照相同的顺序加锁,就可以避免这个问题。
  • 使用定时锁。程序在调用 acquire() 方法加锁时可指定 timeout 参数,该参数指定超过 timeout 秒后会自动释放对 Lock 的锁定,这样就可以解开死锁了。
  • 死锁检测。死锁检测是一种依靠算法机制来实现的死锁预防机制,它主要是针对那些不可能实现按序加锁,也不能使用定时锁的场景的。

十七、ThreadLocal 是什么?有哪些使用场景?

什么是ThreadLocal?

ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

测试代码:

package com.javaBase.LineDistance;

/**
 * 〈一句话功能简述〉;
 * 〈功能详细描述〉
 *
 * @author jxx
 * @see [相关类/方法](可选)
 * @since [产品/模块版本] (可选)
 */
public class TestThreadLocal {

    public static void main(String[] args) {

        ThreadLocal<Integer> threadLocal = new MyThreadLocal();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    threadLocal.set(threadLocal.get() + 1);
                    System.out.println("线程1:" + threadLocal.get());
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    threadLocal.set(threadLocal.get() + 1);
                    System.out.println("线程2:" + threadLocal.get());
                }
            }
        });

        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    threadLocal.set(threadLocal.get() + 1);
                    System.out.println("线程3:" + threadLocal.get());
                }
            }
        });

        t1.start();
        t2.start();
        t3.start();
    }

    private static class MyThreadLocal extends ThreadLocal<Integer> {

        @Override
        protected Integer initialValue() {
            return 0;
        }
    }

}

执行结果:

线程21
线程11
线程22
线程31
线程12
线程32
线程23
线程33
线程13

有结果可知个线程之间对ThreadLocal的操作互不影响。

ThreadLocal的应用场景

1、方便同一个线程使用某一对象,避免不必要的参数传递;
2、线程间数据隔离(每个线程在自己线程里使用自己的局部变量,各线程间的ThreadLocal对象互不影响);
3、获取数据库连接、Session、关联ID(比如日志的uniqueID,方便串起多个日志);其中spring中的事务管理器就是使用的ThreadLocal:

Spring的事务管理器通过AOP切入业务代码,在进入业务代码前,会依据相应的事务管理器提取出相应的事务对象,假如事务管理器是DataSourceTransactionManager,就会从DataSource中获取一个连接对象,通过一定的包装后将其保存在ThreadLocal中。而且Spring也将DataSource进行了包装,重写了当中的getConnection()方法,或者说该方法的返回将由Spring来控制,这样Spring就能让线程内多次获取到的Connection对象是同一个。

十八、说一下 synchronized 底层实现原理?

synchronized 是由一对 monitorenter/monitorexit 指令实现的,monitor 对象是同步的基本实现单元。在 Java 6 之前,monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。但在 Java 6 的时候,Java 虚拟机 对此进行了大刀阔斧地改进,提供了三种不同的 monitor 实现,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。

十九、synchronizedvolatile 的区别是什么?

1.volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
2.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
3.volatile仅能实现变量的修改可见性,并能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
4.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
5.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

二十、synchronizedLock 有什么区别?

总的来说,lock更加灵活。

主要相同点:Lock能完成synchronized所实现的所有功能

不同:

1.ReentrantLock功能性方面更全面,比如时间锁等候,可中断锁等候,锁投票等,因此更有扩展性。在多个条件变量和高度竞争锁的地方,用ReentrantLock更合适,ReentrantLock还提供了Condition,对线程的等待和唤醒等操作更加灵活,一个ReentrantLock可以有多个Condition实例,所以更有扩展性。
2.ReentrantLock必须在finally中释放锁,否则后果很严重,编码角度来说使用synchronized更加简单,不容易遗漏或者出错。
3.ReentrantLock 的性能比synchronized会好点。
4.ReentrantLock提供了可轮询的锁请求,他可以尝试的去取得锁,如果取得成功则继续处理,取得不成功,可以等下次运行的时候处理,所以不容易产生死锁,而synchronized则一旦进入锁请求要么成功,要么一直阻塞,所以更容易产生死锁。

二十一、synchronizedReentrantLock 区别是什么?

一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。

ReentrantLock 将由最近成功获得锁,并且还没有释放该锁的线程所拥有。当锁没有被另一个线程所拥有时,调用 lock 的线程将成功获取该锁并返回。如果当前线程已经拥有该锁,此方法将立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法来检查此情况是否发生。

二十二、说一下 atomic 的原理?

Atomic 包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。

Atomic 系列的类中的核心方法都会调用 unsafe 类中的几个本地方法。我们需要先知道一个东西就是 Unsafe 类,全名为:sun.misc.Unsafe,这个类包含了大量的对 C 代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是告诉你这个里面大量的方法调用都会存在安全隐患,需要小心使用,否则会导致严重的后果,例如在通过 unsafe 分配内存的时候,如果自己指定某些区域可能会导致一些类似 C++ 一样的指针越界到其他进程的问题。

Java学习资料/路线+面试资料

以上是关于面试也要说人话的主要内容,如果未能解决你的问题,请参考以下文章

几百页元宇宙报告没句人话,不如这家公司的布局实在

震惊,居然有人说学习python不好找工作?这些话你打我我也要说!

有哪些事情虽然是事实,但也不能在面试的时候说?

面试时这一点,千万不要说,“不喜欢开发,所以选择测试”

云计算也不过如此,哼~IT男,请你说人话!

99% 会碰到的 Java 面试题!