Java多线程面试题

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

 

答:sleep()方法是线程类(Thread)的静态方法,导致此线程暂停执行指定时间,将执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复(线程回到就绪(ready)状态),因为调用sleep 不会释放对象锁。wait()是Object 类的方法,对此对象调用wait()方法导致本线程放弃对象锁(线程暂停执行),进入等待此对象的等待锁定池,只有针对此对象发出notify 方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入就绪状态。

补充:这里似乎漏掉了一个作为先决条件的问题,就是什么是进程,什么是线程?为什么需要多线程编程?答案如下所示:

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位;线程是进程的一个实体,是CPU调度和分派的基本单位,是比进程更小的能独立运行的基本单位。线程的划分尺度小于进程,这使得多线程程序的并发性高;进程在执行时通常拥有独立的内存单元,而线程之间可以共享内存。使用多线程的编程通常能够带来更好的性能和用户体验,但是多线程的程序对于其他程序是不友好的,因为它占用了更多的CPU资源。

 

58、sleep()和yield()有什么区别?

答:

① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;

② 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;

③ sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;

④ sleep()方法比yield()方法(跟操作系统相关)具有更好的可移植性。

 

59、当一个线程进入一个对象的synchronized方法A之后,其它线程是否可进入此对象的synchronized方法?

答:不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。

 

60、请说出与线程同步相关的方法。

答:

  1. wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
  2. sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException 异常;
  3. notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
  4. notityAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争;
  5. JDK 1.5通过Lock接口提供了显式(explicit)的锁机制,增强了灵活性以及对线程的协调。Lock接口中定义了加锁(lock())和解锁(unlock())的方法,同时还提供了newCondition()方法来产生用于线程之间通信的Condition对象;
  6. JDK 1.5还提供了信号量(semaphore)机制,信号量可以用来限制对某个共享资源进行访问的线程的数量。在对资源进行访问之前,线程必须得到信号量的许可(调用Semaphore对象的acquire()方法);在完成对资源的访问后,线程必须向信号量归还许可(调用Semaphore对象的release()方法)。

下面的例子演示了100个线程同时向一个银行账户中存入1元钱,在没有使用同步机制和使用同步机制情况下的执行情况。

银行账户类:

 
  1. package com.lovo;

  2.  
  3. /**

  4. * 银行账户

  5. * @author 骆昊

  6. *

  7. */

  8. public class Account {

  9. private double balance; // 账户余额

  10.  
  11. /**

  12. * 存款

  13. * @param money 存入金额

  14. */

  15. public void deposit(double money) {

  16. double newBalance = balance + money;

  17. try {

  18. Thread.sleep(10); // 模拟此业务需要一段处理时间

  19. }

  20. catch(InterruptedException ex) {

  21. ex.printStackTrace();

  22. }

  23. balance = newBalance;

  24. }

  25.  
  26. /**

  27. * 获得账户余额

  28. */

  29. public double getBalance() {

  30. return balance;

  31. }

  32. }

存钱线程类:

 
  1. package com.lovo;

  2.  
  3. /**

  4. * 存钱线程

  5. * @author 骆昊

  6. *

  7. */

  8. public class AddMoneyThread implements Runnable {

  9. private Account account; // 存入账户

  10. private double money; // 存入金额

  11.  
  12. public AddMoneyThread(Account account, double money) {

  13. this.account = account;

  14. this.money = money;

  15. }

  16.  
  17. @Override

  18. public void run() {

  19. account.deposit(money);

  20. }

  21.  
  22. }

测试类:

 
  1. package com.lovo;

  2.  
  3. import java.util.concurrent.ExecutorService;

  4. import java.util.concurrent.Executors;

  5.  
  6. public class Test01 {

  7.  
  8. public static void main(String[] args) {

  9. Account account = new Account();

  10. ExecutorService service = Executors.newFixedThreadPool(100);

  11.  
  12. for(int i = 1; i <= 100; i++) {

  13. service.execute(new AddMoneyThread(account, 1));

  14. }

  15.  
  16. service.shutdown();

  17.  
  18. while(!service.isTerminated()) {}

  19.  
  20. System.out.println("账户余额: " + account.getBalance());

  21. }

  22. }

在没有同步的情况下,执行结果通常是显示账户余额在10元以下,出现这种状况的原因是,当一个线程A试图存入1元的时候,另外一个线程B也能够进入存款的方法中,线程B读取到的账户余额仍然是线程A存入1元钱之前的账户余额,因此也是在原来的余额0上面做了加1元的操作,同理线程C也会做类似的事情,所以最后100个线程执行结束时,本来期望账户余额为100元,但实际得到的通常在10元以下。解决这个问题的办法就是同步,当一个线程对银行账户存钱时,需要将此账户锁定,待其操作完成后才允许其他的线程进行操作,代码有如下几种调整方案:

1. 在银行账户的存款(deposit)方法上同步(synchronized)关键字

 
  1. package com.lovo;

  2.  
  3. /**

  4. * 银行账户

  5. * @author 骆昊

  6. *

  7. */

  8. public class Account {

  9. private double balance; // 账户余额

  10.  
  11. /**

  12. * 存款

  13. * @param money 存入金额

  14. */

  15. public synchronized void deposit(double money) {

  16. double newBalance = balance + money;

  17. try {

  18. Thread.sleep(10); // 模拟此业务需要一段处理时间

  19. }

  20. catch(InterruptedException ex) {

  21. ex.printStackTrace();

  22. }

  23. balance = newBalance;

  24. }

  25.  
  26. /**

  27. * 获得账户余额

  28. */

  29. public double getBalance() {

  30. return balance;

  31. }

  32. }

2. 在线程调用存款方法时对银行账户进行同步

 
  1. package com.lovo;

  2.  
  3. /**

  4. * 存钱线程

  5. * @author 骆昊

  6. *

  7. */

  8. public class AddMoneyThread implements Runnable {

  9. private Account account; // 存入账户

  10. private double money; // 存入金额

  11.  
  12. public AddMoneyThread(Account account, double money) {

  13. this.account = account;

  14. this.money = money;

  15. }

  16.  
  17. @Override

  18. public void run() {

  19. synchronized (account) {

  20. account.deposit(money);

  21. }

  22. }

  23.  
  24. }

3. 通过JDK 1.5显示的锁机制,为每个银行账户创建一个锁对象,在存款操作进行加锁和解锁的操作

 
  1. package com.lovo;

  2.  
  3. import java.util.concurrent.locks.Lock;

  4. import java.util.concurrent.locks.ReentrantLock;

  5.  
  6. /**

  7. * 银行账户

  8. *

  9. * @author 骆昊

  10. *

  11. */

  12. public class Account {

  13. private Lock accountLock = new ReentrantLock();

  14. private double balance; // 账户余额

  15.  
  16. /**

  17. * 存款

  18. *

  19. * @param money

  20. * 存入金额

  21. */

  22. public void deposit(double money) {

  23. accountLock.lock();

  24. try {

  25. double newBalance = balance + money;

  26. try {

  27. Thread.sleep(10); // 模拟此业务需要一段处理时间

  28. }

  29. catch (InterruptedException ex) {

  30. ex.printStackTrace();

  31. }

  32. balance = newBalance;

  33. }

  34. finally {

  35. accountLock.unlock();

  36. }

  37. }

  38.  
  39. /**

  40. * 获得账户余额

  41. */

  42. public double getBalance() {

  43. return balance;

  44. }

  45. }

按照上述三种方式对代码进行修改后,重写执行测试代码Test01,将看到最终的账户余额为100元。
 

61、编写多线程程序有几种实现方式?

答:Java 5以前实现多线程有两种实现方法:一种是继承Thread类;另一种是实现Runnable接口。两种方式都要通过重写run()方法来定义线程的行为,推荐使用后者,因为Java中的继承是单继承,一个类有一个父类,如果继承了Thread类就无法再继承其他类了,显然使用Runnable接口更为灵活。

补充:Java 5以后创建线程还有第三种方式:实现Callable接口,该接口中的call方法可以在线程执行结束时产生一个返回值,代码如下所示:

 

 
  1. package com.lovo.demo;

  2.  
  3. import java.util.ArrayList;

  4. import java.util.List;

  5. import java.util.concurrent.Callable;

  6. import java.util.concurrent.ExecutorService;

  7. import java.util.concurrent.Executors;

  8. import java.util.concurrent.Future;

  9.  
  10.  
  11. class MyTask implements Callable<Integer> {

  12. private int upperBounds;

  13.  
  14. public MyTask(int upperBounds) {

  15. this.upperBounds = upperBounds;

  16. }

  17.  
  18. @Override

  19. public Integer call() throws Exception {

  20. int sum = 0;

  21. for(int i = 1; i <= upperBounds; i++) {

  22. sum += i;

  23. }

  24. return sum;

  25. }

  26.  
  27. }

  28.  
  29. public class Test {

  30.  
  31. public static void main(String[] args) throws Exception {

  32. List<Future<Integer>> list = new ArrayList<>();

  33. ExecutorService service = Executors.newFixedThreadPool(10);

  34. for(int i = 0; i < 10; i++) {

  35. list.add(service.submit(new MyTask((int) (Math.random() * 100))));

  36. }

  37.  
  38. int sum = 0;

  39. for(Future<Integer> future : list) {

  40. while(!future.isDone()) ;

  41. sum += future.get();

  42. }

  43.  
  44. System.out.println(sum);

  45. }

  46. }

 

 

62、synchronized关键字的用法?

答:synchronized关键字可以将对象或者方法标记为同步,以实现对对象和方法的互斥访问,可以用synchronized(对象) { … }定义同步代码块,或者在声明方法时将synchronized作为方法的修饰符。在第60题的例子中已经展示了synchronized关键字的用法。

 

63、举例说明同步和异步。

答:如果系统中存在临界资源(资源数量少于竞争资源的线程数量的资源),例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就必须进行同步存取(数据库操作中的悲观锁就是最好的例子)。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。事实上,所谓的同步就是指阻塞式操作,而异步就是非阻塞式操作。

 

64、启动一个线程是用run()还是start()方法?

答:启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM 调度并执行,这并不意味着线程就会立即运行。run()方法是线程启动后要进行回调(callback)的方法。

 

65、什么是线程池(thread pool)?

答:在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是"池化资源"技术产生的原因。线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。

Java 5+中的Executor接口定义一个执行线程的工具。它的子类型即线程池接口是ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类Executors面提供了一些静态工厂方法,生成一些常用的线程池,如下所示:

 

  • newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
  • newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
  • newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
  • newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
  • newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。

第60题的例子中有通过Executors工具类创建线程池并使用线程池执行线程的代码。如果希望在服务器上使用线程池,强烈建议使用newFixedThreadPool方法来创建线程池,这样能获得更好的性能

 

 

66、线程的基本状态以及状态之间的关系?

答:

 

 

除去起始(new)状态和结束(finished)状态,线程有三种状态,分别是:就绪(ready)、运行(running)和阻塞(blocked)。其中就绪状态代表线程具备了运行的所有条件,只等待CPU调度(万事俱备,只欠东风);处于运行状态的线程可能因为CPU调度(时间片用完了)的原因回到就绪状态,也有可能因为调用了线程的yield方法回到就绪状态,此时线程不会释放它占有的资源的锁,坐等CPU以继续执行;运行状态的线程可能因为I/O中断、线程休眠、调用了对象的wait方法而进入阻塞状态(有的地方也称之为等待状态);而进入阻塞状态的线程会因为休眠结束、调用了对象的notify方法或notifyAll方法或其他线程执行结束而进入就绪状态。注意:调用wait方法会让线程进入等待池中等待被唤醒,notify方法或notifyAll方法会让等待锁中的线程从等待池进入等锁池,在没有得到对象的锁之前,线程仍然无法获得CPU的调度和执行。

 

67、简述synchronized 和java.util.concurrent.locks.Lock的异同?

答:Lock是Java 5以后引入的新的API,和关键字synchronized相比主要相同点:Lock 能完成synchronized所实现的所有功能;主要不同点:Lock 有比synchronized 更精确的线程语义和更好的性能。synchronized 会自动释放锁,而Lock 一定要求程序员手工释放,并且必须在finally 块中释放(这是释放外部资源的最好的地方)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值