java多线程和并发编程面试题

多线程和并发编程

1) 什么是线程?

线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,可以使用多线程对运算密集型任务提速。比如,如果一个线程完成一个任务要100毫秒,那么用十个线程完成该任务只需10毫秒。

2) 线程和进程有什么区别?

线程是进程的子集,一个进程可以有很多线程,每条线程并发执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。

3)线程的实现方式?使用哪个更好?

有两种方式,一是继承Thread类,二是实现Runnable接口。

java不支持类的多继承,但是允许实现多个接口,所以继承了Thread类就不能继承其他类,而且Thread类实际上也是实现了Runnable接口,所以最好使用Runnable接口实现线程。

4)Thread类种的start()和run()方法有什么区别?

start()方法用来启动新创建的线程,而且start()方法内部调用了run()方法;当调用run()方法的时候,只会在原来的线程中调用,并没有启动新的线程。

5)什么是线程安全性?如何编写线程安全的代码?

定义:当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么这个类就可以称为是线程安全的。

编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的和可变的状态的访问。

无状态的变量一定是线程安全的,即没有共享变量

6)什么是竞态条件?举例说明

竞态条件就是,由于不正确的执行时序,而出现不正确的结果。

最常见的竞态条件类型就是“先检查后执行”操作,即通过一个可能失效的观测结果来决定下一步的动作。

比如,++count操作,看上去是一个操作,但其实这个操作并非是原子的,实际上它包含了3个独立的操作,读取count,将其+1,将计算结果写入count,这是一个“读取-修改-写入”的操作序列,并且其结果状态依赖于之前的状态。

7)什么是内置锁?

每个java对象都可以用作一个实现同步的锁,这些锁称为内置锁。

其使用方式就是使用synchronized关键字、synchronized 方法或者 synchronized 代码块。线程在进入同步代码块之前自动获得锁,在退出同步代码块时自动释放锁,获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。

8)什么是可见性问题?如何解决?

一个共享变量,如果线程A做了修改,线程B随后读取到的还是旧值,导致线程读取到脏数据,这就是可见性问题。

解决:

1. 加锁:内置锁可以保证可见性,确保某个线程以一种可预测的方式来查看另一个线程的执行结果。

2. 使用volatile变量:可以确保将变量的更新操作 通知到其他线程。

9)什么是volatile变量?什么时候使用volatile?

volatile是一种轻量级的锁,它能够保证可见性,但不能保证原子性。

 

volatile变量,可以确保将变量的更新操作通知到其他线程。把变量声明为volatile类型后,编译器和运行时都会注意到这个变量是共享的,因此不会将该变量上的操作和其他内存操作一起重排序。volatile变量不会被缓存在寄存器或其他处理器不可见的地方,因此读取volatile类型的变量总是返回最新写入的值。

 

什么时候应该使用volatile变量:

1. 对变量的写入操作不依赖变量的当前值(如++count)

2. 该变量不会与其他状态变量一起纳入不变性条件中(如(lower,upper)上下限,volatile只能保证lower和upper的写入能被其他线程看到,但是不能保证其中一个变量写入时,另一个变量的值不发生变化)

3. 在访问变量时不需要加锁。

 

(volatile变量通常用做某个操作完成、发生中断或者状态的标志)

 

volatile特性:

1. 保证此变量对所有线程都是可见的;

2. 禁止指令重排序

10)什么是ThreadLocal?

ThreadLocal是线程局部变量,为解决多线程的并发问题提供了一种新的思路。

ThreadLocal提供了set、get等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。

 

如何实现为每个线程维护变量副本?

在ThreadLocal类中有一个Map(ThreadLocalMap),用于存储每个线程的变量副本,Map中元素的key为线程对象,而value对应线程的变量副本。

11)ThreadLocal与同步机制的比较:

对于多线程资源共享的问题,

同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。

同步机制只提供了一份变量,让不同的线程排队访问;而ThreadLocal为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

12)什么是不可变对象?

如果某个对象在被创建后,其状态就不能被修改,就可以称这个对象为不可变对象。他们的不变性条件是由构造函数创建的,不可变对象一定是线程安全的。

不可变对象的条件:

1. 对象创建后,其状态就不能被修改。

2.对象的所有域都是final类型。

3.对象是被正确创建的。

13)Runnable和Callable的区别?

1. Runnable执行方法是run(),无返回值,有异常不能抛出,只能捕获;

2. Callable执行方法是call(),有返回值,可以抛出异常。

14)线程有哪些状态 或者 线程的生命周期?

线程从创建、运行到结束一共是5个状态,即:新建状态(New)、就绪状态(Runnable)、运行状态(Running)、阻塞状态(Blocked)、死亡状态(Dead)。

 

15)CountDownLatch(闭锁)、CyclicBarrier(栅栏) 、 Semaphore(信号量)有什么不同?

1. CountDownLatch和CyclicBarrier都能够实现线程之间的等待,不过侧重点不同:

CountDownLatch一般用于某个线程A等待若干个线程执行完任务之后,它才执行;

CyclicBarrier一般用于一组线程相互等待至某个状态,然后一组线程再同时执行;

2. Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。

 

可以通过CountdownLatch同时启动多个线程。

16)如何终止一个线程?

java中终止一个线程一共有3个方法,即:stop、interrupt、设置标志位。

 

1.Thread的stop()方法是一个被废弃的方法,因为stop会强行把执行一半的线程终止,导致线程资源不能被正确释放。

2. 使用Boolean类型的变量,设置标志位,来终止线程。

3. 使用线程中断机制,线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。

17) 一个线程发生运行时异常会怎么样?

Java中Throwable分为Exception和Error: 出现Error的情况下,程序会停止运行。 Exception分为RuntimeException和非运行时异常。 非运行时异常必须处理,比如thread中sleep()时,必须处理InterruptedException异常,才能通过编译。 而RuntimeException可以处理也可以不处理,因为编译并不能检测该类异常,比如NullPointerException、ArithmeticException、ArrayIndexOutOfBoundException等。

所以这里存在两种情形:

① 如果该异常被捕获或抛出,则程序继续运行。

② 如果异常没有被捕获该线程将会停止执行。

Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候JVM会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler,并将线程和异常作为参数传递给handler的uncaughtException()方法进行处理。

18)如何在多个线程间共享数据?

1,如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,卖票系统就可以这么做。

2,如果每个线程执行的代码不同,这时候需要用不同的Runnable对象,例如,设计4个线程。其中两个线程每次对j增加1,另外两个线程对j每次减1,银行存取款

19) notify和notifyAll有什么区别?

在调用notify时,JVM会从这个条件队列上等待的多个线程中选择一个来唤醒;

而调用notifyAll时,则会唤醒所有在这个条件队列上等待的线程。

由于多个线程可以基于不同的条件谓词在同一个条件队列上等待,如果使用notify,容易导致类似于信号丢失的问题,因此大多数情况下,应该优先选择notifyAll而不是单个的notify。

20)ConcurrentHashMap?

ConcurrentHashMap采用了分段锁的设计,只有在同一个分段内才存在竞态关系,不同的分段锁之间没有锁竞争。相比于对整个Map加锁的设计,分段锁大大的提高了高并发环境下的处理能力。但同时,由于不是对整个Map加锁,导致一些需要扫描整个Map的方法(如size())需要使用特殊的实现,另外一些方法(如clear())甚至放弃了对一致性的要求。

ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。ConcurrentHashMap中的HashEntry相对于HashMap中的Entry有一定的差异性:HashEntry中的value以及next都被volatile修饰,这样在多线程读写过程中能够保持它们的可见性。

并发度实际上就是ConcurrentHashMap中的分段锁个数,即Segment[]的数组长度。ConcurrentHashMap默认的并发度为16,但用户也可以在构造函数中设置并发度。

ConcurrentHashMap在JDK8中进行了巨大改动,摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。它沿用了与它同时期的HashMap版本的思想,底层依然由“数组+链表+红黑树“的思想。

在ConcurrentHashMap中并没有实现对Map加锁以提供独占访问,因此在大多数情况下,用ConcurrentHashMap来替代同步map能进一步提供代码的可伸缩性,只有当应用程序需要加锁map以进行独占访问时,才应该放弃使用ConcurrentHashMap。

21)什么是BlockingQueue(阻塞队列)?

阻塞队列指在队列的基础上增加了支持阻塞的插入和移除操作的队列。

提供了可阻塞的put和take方法,以及支持定时的offer和poll方法。如果队列已经满了,put时将阻塞直到有空间可用,如果队列为空,take时将阻塞直到有元素可用。队列可以是有界的,也可以是无界的。常用有界队列ArrayBlockingQueue、LinkedBlockingQueue 以及支持优先级的无界队列PriorityBlockingQueue

 

阻塞队列常用于生产者-消费者模式,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。简而言之,阻塞队列是生产者用来存放元素、消费者获取元素的容器。

 

22)什么是生产者-消费者模式?

某个模块负责产生数据,另一个模块负责处理数据,可以将产生数据的模块称为生产者,处理数据的模块称为消费者。在生产者与消费者之间在加个缓冲区,形象的称为仓库,生产者负责往仓库了进商品,而消费者负责从仓库里拿商品,这就构成了生产者消费者模式。

 

优点:

1.解耦

2.支持并发

3.支持忙闲不均

23)为什么wait, notify 和 notifyAll这些方法不在thread类里面?

由于wait,notify和notifyAll都是锁级别的操作,锁属于对象,所以把他们定义在Object类中。

24)wait()、notify()、notifyAll()、yield()、sleep()、join()、interrupt() 区别?

wait() -- 让当前线程处于“等待(阻塞)状态”,会立即释放它所持有对象的锁,直到其他线程调用此对象的notify() 或notifyAll()唤醒线程;

 

notify() -- 唤醒在此对象监视器上等待的单个线程。

notifyAll() -- 唤醒在此对象监视器上等待的所有线程。

 

yield() -- 让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权,不会释放锁;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行 。

 

sleep() -- 让当前线程休眠,即当前线程会从“运行状态”进入到“休眠(阻塞)状态”,不会释放锁。sleep()会指定休眠时间,当线程重新被唤醒时,它会由“阻塞状态”变成“就绪状态”。

 

join() -- 让“主线程”等待“子线程”结束之后才能继续运行。

 

interrupt() -- 中断线程。InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到wait()/sleep()/join()后,就会立刻抛出InterruptedException。

25)interrupt()、interrupted()的区别?

interrupt()是用来设置中断状态的。返回true说明中断状态被设置了而不是被清除了。 java的中断并不是真正的中断线程,而只设置标志位来通知用户。如果你捕获到中断异常,说明当前线程已经被中断,不需要继续保持中断位。调用sleep、wait等此类可中断方法时,一旦方法抛出InterruptedException,当前调用该方法的线程的中断状态就会被JVM自动清除了,就是说我们调用该线程的isInterrupted ()方法时是返回false。如果你想保持中断状态,可以再次调用interrupt方法设置中断状态。

interrupted是静态方法,返回的是当前线程的中断状态。

26)为什么应该在循环中检查等待条件?

处于等待状态的线程可能会收到错误警报和伪唤醒,在条件谓词不为真的情况下也可以反复醒来,因此必须在一个循环中调用wait,并在每次迭代中都测试条件谓词,如果条件谓词不为真,就继续等待或失败。

27)线程池的作用?如何创建线程池?

线程池就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。

 

作用:

1、可以限定线程的个数,以防由于线程过多导致系统运行缓慢或崩溃

2、线程池不需要每次都去创建或销毁线程,节约了资源、响应时间更快

 

创建:

可以使用Executors类中的静态工厂方法(newCachedThreadPool 、newFixedThreadPool、newScheduledThreadPool 、newSingleThreadExecutor ),也可以使用ThreadPoolExecutor类(提供了4个构造器)

28)什么是死锁?发生死锁的条件是什么?如何避免死锁?

  死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法执行下去。

 

死锁的发生必须满足以下四个条件:

  • 互斥条件:一个资源每次只能被一个线程使用。
  • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:线程已获得的资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

 

避免死锁:

首先找出在什么地方将获取多个锁,然后对所有这些事例进行全局分析,确保他们在整个程序中获取锁的顺序保持一致,尽可能的使用开放调用,从而避免死锁。

29) 死锁、活锁有什么区别?

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法执行下去。

活锁是另一种形式的活跃性问题,它虽然不会阻塞线程,但也不会继续执行,因为线程将不断重复执行相同的操作,而且总会失败。

30)如何减少锁的竞争?

1.减少锁的持有时间,比如可以将一些与锁无关的代码移出同步代码块。

2.降低锁的请求频率,可以通过锁分解和锁分段技术实现。

如果一个锁需要保护多个相互独立的状态变量,就可以将这个锁分解为多个锁,并且每个锁只能保护一个变量,从而提高可伸缩性,最终降低每个锁的请求频率。

某些情况下,可以将锁分解进一步扩展为锁分段,即对一组独立对象上的锁进行分解,如ConcurrentHashMap实现了锁分段技术。

3.使用带有协调机制的独占锁。比如并发容器、读-写锁、不可变对象、原子变量。

ReadWriteLock:实现了一种在多个读取操作以及单个写入操作情况下的加锁机制,如果多个读取操作都不会修改共享资源,那么这些读取操作可以同时访问该共享资源,但在执行写入操作时必须以独占方式获取锁。

原子变量类:提供了在整数或对象应用上的细粒度原子操作,并使用了现代处理器中提供的底层并发原语(如CAS)。

31)内置锁(synchronized) 和 显式锁(ReentrantLock)的区别?

1. 基本使用:

Java中通过Synchronized实现内置锁,内置锁获得锁和释放锁是隐式的,线程进入同步代码块或方法的时候会自动获得锁,在退出同步代码块或方法时会释放锁;

ReentrantLock是显示锁,需要显示的进行 lock 以及 unlock 操作。

2.通信:

与Synchronized配套使用的通信方法通常有wait()、notify()/notifyAll()。

与ReentrantLock搭配的通行方式是Condition,Condition是被绑定到Lock上的,必须使用lock.newCondition()才能创建一个Condition。Synchronized能实现的通信方式,Condition都可以实现,而Condition的优秀之处在于它可以为多个线程间建立不同的Condition,比如对象的读/写Condition。

3.编码:

Synchronized编码模式比较简单,不必显示的获得锁,释放锁。

ReentrantLock必须在finally块中释放锁,否则如果被保护的代码中抛出了异常,这个锁永远都无法释放。

4.灵活性:

内置锁在进入同步块时,采取的是无限等待的策略,一旦开始等待,就既不能中断也不能取消,容易产生饥饿与死锁的问题;

ReentrantLock支持可轮询的、可定时的、可中断的锁获取操作,lockInterruptibly()方法能够在获得锁的同时保持对中断的响应;tryLock()方法可以使得线程在等待一段时间后,如果还未获得锁,就停止等待而非一直等待,可以更好的解决饥饿与死锁的问题。另外ReentrantLock提供了两种公平性选择,即公平锁和非公平锁。在公平的锁中,如果有另一个线程持有这个锁或有其他线程在队列中等待这个锁,那么发出请求的线程将放入队列中,在非公平的锁中,只有当锁被某个线程持有时,发出请求的线程将放入队列中。

5.性能:

Synchronized是JVM的内置属性,它能执行一些优化,JVM可以通过线程转储来帮助识别死锁的发生,仅当内置锁不能满足需求时,才考虑使用显式锁。

32)读写锁?

涉及接口:ReadWriteLock

实现:ReentrantReadWriteLock

优势:提供程序可伸缩性

33)AbstractQueuedSynchronizer(AQS)?

AQS其实就是一个可以给我们实现锁的框架。可以说Lock的子类实现都是基于AQS的,在LOCK包中的相关锁(常用的有ReentrantLock、 ReadWriteLock)都是基于AQS来构建

34)什么是CAS?

比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。 CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。

优势:高效的解决了原子操作问题。

缺点:

1.循环时间长开销很大。

2.只能保证一个共享变量的原子操作。

3.ABA问题。(如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗? 如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题)

ABA问题可以使用JDK的并发包中的AtomicStampedReference和 AtomicMarkableReference来解决。

35)什么是原子变量类?

原子变量比锁的粒度更细,量级更轻,将发生竞争的范围缩小到单个变量上,能够支持原子的和有条件的读-改-写操作。

java.util.concurrent.atomic包下,常用:AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference

常用方法:incrementAndGet() 加一 、decrementAndGet() 减一,compareAndSet(expect, update) 比较并交换

 

36)乐观锁和悲观锁的区别?

乐观锁:

总是认为不会产生并发问题,每次读取数据的总认为不会有其他线程对数据进行修改,因此不会加锁,但是在更新时会判断其他线程是否对数据进行修改,一般会使用版本号机制和CAS操作实现。

version方式:一般是在数据表中加上一个version字段,表示数据被修改的次数,当数据被修改时,version加1。当线程A进行更新操作时,在读取数据的同时也会读取version值,在提交更新时,判断读取到的version值和当前version值是否相等,若相等则更新,否则重试更新操作,直到更新成功。

 

CAS操作方式:比较并替换,有3个操作数,内存值V,旧的预期值A,将要替换的新值B,只有当V和A相等时才替换,若不等,则重试(自旋操作)。

 

悲观锁:

总是认为会产生并发问题,每次读写数据时都认为其他线程会修改,因此加锁,当其他线程想要访问数据时,都要被堵塞。可以依靠数据库实现,如读锁、写锁、行锁等,都是在操作之前加锁,synchronized也是悲观锁。

乐观锁适合于读操作比较多的场景,悲观锁适合于写操作比较多的场景。

37)乐观锁如何保证数据一致性?

可以通过版本号机制实现。

一般是在数据表中加上一个version字段,表示数据被修改的次数,当数据被修改时,version加1。当线程A进行更新操作时,在读取数据的同时也会读取version值,在提交更新时,判断读取到的version值和当前version值是否相等,若相等则更新,否则重试更新操作,直到更新成功。

 

你可能感兴趣的:(多线程)