当前位置:   article > 正文

Java经典面试题三:多线程_java开三个线程分别处理任务

java开三个线程分别处理任务

问题一:并行和并发的区别?

  • 并行是两个或多个事件同一时刻发生
  • 是在不同实体上的多个事件
  • 在多台处理器同时处理多个任务
  • 并发是两个或多个事件在同一时刻间隔发生。
  • 是同一实体上的多个事件。
  • 在一台处理器同时处理多个任务
    所以并发编程的目的是充分利用处理器的每一个核。
    举个栗子:并行就是多个处理器共同工作,互不影响,并行么,并肩行走;并发呢就是一个处理器处理好多工作,并发么,同时发生,但是呢就好像只有一个厕所,并发要等另一个出来之后才可以进去。

问题二:线程和进程的区别?
答:先写一个式子:
一个程序=n个进程;一个进程=n个线程;

  • 进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。
  • 进程执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。
  • 线程是进程的一个实体,是cpu调度和分派的基本单位,比程序更小的能独立运行的基本单位,同一进程中多个线程可以并发执行。

问题三:守护线程?
答:即daemon thread,是服务线程,服务其他线程。GC就是一个。
问题四:创建线程的方式?
答:有三种方式

  • 继承Thread类创建:
    定义Thread类的子类,重写run方法,创建Thread子类的实例,即创建了线程对象,调用start方法启动线程。
  • 通过Runnable接口创建:
    定义Runnable接口实现类,重写run方法,创建Runnable实现类的实例,然后new一个thread,把该实例作为thread的taget来创建thread对象,这个thread对象才是真正的线程对象,然后调用start方法。
  • 通过Callable和Future创建:
    定义Callable接口实现类,并实现它的call方法,call方法就相当于上面的run方法,用来作为线程执行体,并且call方法是有返回值;创建Callable实现类的实例,使用FutureTask类包装Callable对象,生成的FuturnTask对象封装了Callable对象的call方法的返回值,调用get方法可以获得返回值;再把FutureTask对象作为Thread对象的target创建新线程。

问题五:runnable和callable有什么区别?
答:Runnable的run方法没有返回值,只是执行了一下run方法;
Callable的call方法有返回值,是一个泛型,和Future和FutureTask配合可以获取异步执行结果。
Callable接口的call方法允许抛出异常,run方法只能内部消化,不能继续向上抛。
其中FutureTask的get方法会阻塞主进程向下执行,不调用不会
问题六:线程的状态?
答:创建、就绪、运行、阻塞、死亡。

  • 创建状态:只是生成线程对象,但并没调用start方法,此时线程处于创建状态;
  • 就绪状态:线程调用了strat方法,线程就进入了就绪状态,但此时线程调度程序还没有把该线程设置为当前线程,此时就处于就绪状态;还有在线程运行后从等待或睡眠中回来之后,也会处于就绪状态;
  • 运行状态:线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run方法 ;
  • 阻塞状态:是线程因为某种原因放弃CPU使用权,暂时停止运行;直到线程进入就绪状态,才有机会运行;阻塞分为三种:01:等待阻塞(调用了wait方法)02:同步阻塞(获取synchronized同步锁失败,可能锁被其他线程占用)03:其他阻塞(调用线程的sleep或join方法或发出了I/O请求时,线程会进入阻塞状态,当sleep超时、join等待线程终止或超时、i/o处理完毕,线程进入就绪状态)
  • 死亡状态:run方法结束或因异常退出了run方法。

我的理解就像接力赛跑一样,当运动员进场,线程创建;准备声响起,线程就绪;发令声响起,线程运行;如果不小心接力棒掉了或没接力好,线程就会阻塞;当跑完全程,线程死亡。

问题七:sleep和wait的区别?

  • sleep()是线性类Thread类的静态方法,让线程进入睡眠状态,给其他线程执行机会,等睡眠时间结束,线程进入就绪状态和其他线程竞争CPU执行。因为是静态方法,不能改变对象的机锁,当一个synchronized块中调用sleep方法,线程虽然进入休眠,但是对象机锁没有释放,就是该线程睡眠,但是还在占用这个锁空间,其他线程依然无法访问这个对象。
  • wait()是Object类的方法,当线程执行wait方法,它就会进入一个和该对象相关的线程池,同时释放机锁,使得其他线程可以访问,用notify、notifyAll方法可以唤醒该线程。

问题八:notify()和notifyAll()的区别?

  • 如果线程调用了wait()方法,那么线程就会处于该对象的等待池中,等待池的线程不会去竞争该对象的锁。
  • 当线程调用notifyAll()方法(唤醒所有wait线程),或notify()方法(只随机唤醒一个wait线程),被唤醒的线程会进入该对象的锁池中,锁池中的对象会去竞争对象锁。
  • 优先级高的线程竞争到对象锁的概率大,没有竞争到对象锁的线程还会留在锁池中,只有再次调用wait()方法才会重新回到等待池中。而竞争到对象锁的线程还会继续向下执行,直到执行完synchronized代码块,就会释放掉对象锁,这时锁池的其他线程会继续竞争对象锁。

问题九:线程run()和start()的区别

  • 每个线程都是通过Thread对象的run方法执行的,run方法称为线程体,然后再调用start方法启动一个线程。
  • start方法启动一个线程,真正实现了多线程运行。此时无需等待线程的run方法体代码执行完毕,可以继续执行下面的代码;此时线程处于就绪状态,并没有运行。然后通过此Thread类调用run方法来完成运行状态,这里run方法称为线程体,它包含线程执行的内容,run方法运行结束,线程终止,然后CPU再调度其他线程。
  • run方法是在线程里的,只是线程里的一个函数,并不是多线程。如果直接调用run就相当于调用一个普通函数,直接调用run方法,必须等待run方法执行完毕才能执行下面的代码,执行路径还是只有一条,没有线程特征,所以多线程执行要用start方法而不是run方法。

问题十:创建线程池有几种方式?
1.newFixedThreadPool(int),创建一个固定长度的线程池,美当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
2.newCachedThredaPool,创建一个可缓存的线程池,如果线程池规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。
3.newScheduledThreadPool,创建一个固定长度的线程池,可执行周期性的任务。
4.newSingleThreadExecutor,创建单线程的线程池,线程异常结束,会创建一个新的线程,确保任务提交按顺序执行。
还有其他的创建线程池的方式
问题十一:线程池有哪些状态?
Running、ShutDown、Stop、Tidying、Terminated。
在这里插入图片描述
RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。线程池的初始状态就是Running。线程池一旦创建,就处于Running状态,并且其中任务数为0
SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。调用shutdown方法,线程池由RUNNING进入SHUTDOWN状态。
STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。调用线程池shutdownNow方法,线程池由running 或shutdown进入STOP状态。
TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。因为terminated()在ThreadPoolExecutor类中是空的,所以用户想在线程池变为TIDYING时进行相应的处理;可以通过重载terminated()函数来实现。当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。
问题十二:线程池中 submit()和 execute()方法有什么区别?

  • 参数不同;execute()参数是Runnable,submit()参数Runnable、(Runnable,T)、Callable;
  • execute()没有返回值,submit()有返回值Future;
  • submit的返回值Future.get方法,方便异常处理。

问题十三:再Java中怎么保证多线程运行安全?

  • 原子性:提供互斥访问,同一时刻只能有一个线程对数据操作(atomic、synchronized);
  • 可见性:一个线程对共享变量的修改可以被其他线程看到(synchronized、volatile);
  • 有序性:程序执行的顺序按照代码先后顺序执行(happens-before原则)。

缓存会导致可见性问题
线程切换会导致原子性问题
编译优化会带来有序性问题。

使用安全类,Java.util.concurrent下的类、synchronized、LOCK可以解决原子性问题
synchronized、volatile、LOCK可以解决可见性问题
Happens-Before规则可以解决有序性问题

Happens-Before规则如下:

  • 传递规则:如果操作1在操作2前,而操作2在操作3前,那操作1一定在3前发生
  • 锁定规则:一个unlock操作肯定会在后面对同一个锁的lock操作前发生,也就是锁只有被释放才会被再次获取
  • volatile变量规则:对一个被volatile修饰的写操作先发生于后面对该变量的读操作
  • 程序次序规则:在一个线程内,按照代码顺序执行
  • 线程启动规则:Thread对象的start方法先发生于此线程的其他动作
  • 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测
  • 线程中断规则:对线程interrupt(中断、打断)方法的调用先发生于对该中断异常的获取
  • 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize(垃圾回收器回收对象会调用该方法)方法的开始。

问题十四:多线程锁升级的原理是什么?
锁共有四种状态:无状态锁、偏向锁、轻量级锁、重量级锁;这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。
锁分级别的原因:没有优化以前,synchronized是重量级锁(悲观锁),使用wait、notify、notifyall来切换线程非常消耗系统资源,线程的挂起和唤醒间隔很短暂,这样很浪费资源,影响性能。所有JVM对关键字synchronized进行了优化,分为无锁、偏向锁、轻量级锁、重量级锁。

  • 无锁:没有对资源进行锁定,所有线程都能访问并修改同一资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到成功。
  • 偏向锁:当前对象的代码一直被同一个线程访问,即不存在多个线程竞争,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,提高性能。偏向锁指的是偏向第一个加锁线程,该线程不会主动释放锁,只有其他线程竞争偏向锁时才会被释放。

偏向锁的撤销,需要某个时间没有字节码正在执行,先暂停偏向锁的线程,然后判断锁对象是否处于被锁定状态,如果线程不处于活动状态,则将对象头设置为无锁状态,并撤销偏向锁。如果处于活动状态,则升级为轻量级锁。

  • 轻量级锁:当锁是偏向锁时,被第二个线程B访问,此时偏向锁会升级为轻量级锁,线程B会通过自旋的形式获取锁,线程不会阻塞,从而提高性能。若当前只有一个线程等待,则该线程通过自旋进行等待,但自旋超过一定次数后,轻量级锁会升级为重量级锁。另外如果一个线程已持有锁,另一个线程在自旋,而此时有第三个线程来访,轻量级锁也会升级为重量级锁。
    注:自旋是指当一个线程获取锁的时候,如果锁被其他线程获取,那该线程将循环等待,然后不断的判断锁是否能够成功获取,直到获取到锁才退出循环。
  • 重量级锁:当有一个线程获取锁后,其余所有等待获取该锁的线程都会处于阻塞状态。重量级锁是通过对象内部的监听器(monitor)实现,而其中monitor的本质是依赖于底层操作系统的MutexLock实现,操作系统实现线程之间的切换需从用户状态切换到内核态,成本非常高。简而言之,就是所有控制权都交给操作系统,由操作系统来负责线程间的调度和线程状态的改变。而这样频繁的对现场状态切换,会消耗大量系统资源,导致性能低下。
    在这里插入图片描述

问题十五:什么是死锁?
两个或两个以上的进程在执行过程中,由于竞争资源或彼此通信而造成的一种阻塞现象,若无外力作用,则永久阻塞下去。死锁是操作系统层面的一个错误,是进程死锁的简称,是整个计算机操作系统乃至整个并发程序设计领域最难处理的问题之一。

问题十六:怎么防止死锁?
死锁的四个必要条件:

  • 互斥条件:一个资源每次只能被一个进程使用;
  • 请求和保持条件:进程获得一定资源后,又请求其他资源,但该资源被其他进程占有,此时请求阻塞,但又对自己获得的资源保持不放;
  • 不可剥夺条件:进程已获得的资源,在未完成使用之前,不可剥夺
  • 循环等待条件:指发生死锁后,若干个进程形成头尾相接的循环等待资源关系。

只要上述四个条件之一不满足,就不会死锁;
减少同步代码块嵌套;降低锁的使用粒度,不要几个功能共用一把锁;尽量采用tryLock(timeout)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间;使用concurrent并发类代替手写锁。
问题十七:ThreadLocal是什么?有哪些使用场景?
ThreadLocal类支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如web服务器)使用线程局部变量,ThreadLocal的生命周期比线程的生命周期都长;如果ThreadLocal在工作完成后没有释放,就存在内存泄漏的风险。
ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立的改变自己的副本而不影响其他线程对应的副本。
经典使用场景:数据库连接,这样每个线程都在各自的Connection进行数据库操作和session管理等。
问题十八:说一下synchronized底层实现原理?
synchronized可以保证方法或代码块在运行时,只有一个方法可以进入,同时还可以保证共享变量的可见性。Java中每一个对象都可以作为锁,这是synchronized实现同步的基础;作用在方法上,锁的是当前实例对象;作用在静态方法上,锁的是当前类的class对象;作用在方法块,锁的是括号里面的对象。

synchronized是由一对monitorenter/monitorexit指令实现,monitor对象是同步的基本实现单元。在Java6之前,monitor实现完全依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步是一个无差别的重量级操作,性能低;Java6时,JVM提供了monitor三种不同的实现,也就是偏向锁,轻量级锁,重量级锁,改进了性能。
问题十九:synchronized和volatile的区别是什么?

  • volatile本质是告诉JVM当前变量在内存中的值是不确定的,需要从主存中读取;仅能作用在变量上;变量修改可见性,不保证原子性;不会造成线程堵塞;标记的变量不会被编译器优化。
  • synchronized是锁定当前变量,只有当前线程可以访问该变量,其他线程阻塞;可以作用在变量、方法、类上;保证变量修改可见性和原子性;可能会造成线程阻塞;可以被编译器优化。
    问题二十:synchronized和lock有什么区别?
  • synchronized是Java内置关键字,在JVM层面;synchronized无法判断是否获取锁的状态;synchronized会自动释放锁;使用synchronized关键字的两个线程1和线程2,如果线程1获得锁,线程2等待,如果线程1阻塞,线程2会一直等待;synchronized的锁可重入、不可中断、非公平;适合代码少量的同步问题。
  • lock是Java类;可以判断是否获取到锁;需要在finally中手动释放锁,调用unlock方法,否则容易死锁;lock锁如果尝试获取不到锁,线程可以不用等待就结束;lock锁可重入、可判断、可公平;适合大量代码同步问题。
    注:可重入锁:允许同一个线程多次获取同一把锁,也叫递归锁;
    公平锁:如果多个线程申请一把公平锁,那么锁释放时,先申请的先得到;反之,非公平锁,后申请的可能先获取锁,是随机或者其他方式排序的;非公平锁的吞吐量比公平锁大,默认情况下是非公平锁;
    可中断锁:可以响应中断的锁;Java并没有提供中断某线程的方法,只提供了中断机制,即线程a向线程b发送中断请求,也可以自己向自己发送中断请求,线程b不会立刻停止,而是自行选择合适时机响应中断,也可以忽略此中断。也就是Java中的中断不能之间终止线程,而是需要被中断的线程自己决定是否中断。好比父母叮嘱在外的子女照顾好身体,但主动权完全取决于子女自己。

问题二十一:synchronized和ReentrantLock有什么区别?

  • synchronized是关键字,ReentrantLock是类;
  • synchronized竞争锁时会一直等待,ReentrantLock可以得到获取锁的结果
  • synchronized无法实现公平锁,ReentrantLock可以实现公平锁;
  • synchronized代码块执行完或有异常,自动释放锁,ReentrantLock不会自动释放,需在finally中释放;
  • ReentrantLock比synchronized扩展性更强;可以对获取锁的等待时间进行设置,避免死锁;

问题二十二:说一下atomic的原理?
Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个变量进行操作时,具有排他性。atomic主要利用CAS和volatile和native方法保证原子操作,从而避免synchronized的高开销,提高执行效率。

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

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号