java面试下_Java面试宝典(下)

原标题:Java面试宝典(下)

9c845eb5cf56bccf10ca5bdfc8098c29.png

简单的解释一下垃圾回收

Java 垃圾回收机制最基本的做法是分代回收。

内存中的区域被划分成不同的世代,对象根据其存活的时间被保存在对应世代的区域中。

一般的实现是划分成3个世代:年轻、年老和永久。

内存的分配是发生在年轻世代中的。

当一个对象存活时间足够长的时候,它就会被复制到年老世代中。

对于不同的世代可以使用不同的垃圾回收算法。

进行世代划分的出发点是对应用中对象存活时间进行研究之后得出的统计规律。

一般来说,一个应用中的大部分对象的存活时间都很短。

比如局部变量的存活时间就只在方法的执行过程中。

基于这一点,对于年轻世代的垃圾回收算法就可以很有针对性。

43调用System.gc()会发生什么?

通知GC开始工作,但是GC真正开始的时间不确定。

进程,线程相关

44说说进程,线程,协程之间的区别

简而言之,进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。

进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。

线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。

同一进程中的多个线程之间可以并发执行。

45你了解守护线程吗?它和非守护线程有什么区别

程序运行完毕,jvm会等待非守护线程完成后关闭,但是jvm不会等待守护线程。

守护线程最典型的例子就是GC线程。

46什么是多线程上下文切换

多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。

47创建两种线程的方式?他们有什么区别?

通过实现java.lang.Runnable或者通过扩展java.lang.Thread类。

相比扩展Thread,实现Runnable接口可能更优。

原因有二:

1). Java不支持多继承.因此扩展Thread类就代表这个子类不能扩展其他类。

而实现Runnable接口的类还可能扩展另一个类。

2). 类可能只要求可执行即可,因此集成整个Thread类的开销过大。

48Runnable和Callable的区别

Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;

Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。

这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,

某条线程是否执行了?

某条线程执行了多久?

某条线程执行的时候我们期望的数据是否已经赋值完毕?

无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。

而Callable+Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用。

49什么导致线程阻塞

阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪),学过操作系统的同学对它一定已经很熟悉了。

Java 提供了大量方法来支持阻塞,下面让我们逐一分析。

方法

说明

sleep()

sleep() 允许 指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU 时间,指定的时间一过,线程重新进入可执行状态。 典型地,sleep() 被用在等待某个资源就绪的情形:测试发现条件不满足后,让线程阻塞一段时间后重新测试,直到条件满足为止

suspend()

resume()

两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume() 被调用,才能使得线程重新进入可执行状态。典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。

yield()

yield() 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程

wait()

notify()

两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式,一种允许 指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用.

50wait(),notify()和suspend(),resume()之间的区别

初看起来它们与 suspend() 和 resume() 方法对没有什么分别,但是事实上它们是截然不同的。

区别的核心在于,前面叙述的所有方法,阻塞时都不会释放占用的锁(如果占用了的话),而这一对方法则相反。

上述的核心区别导致了一系列的细节上的区别。

首先,

前面叙述的所有方法都隶属于 Thread 类,但是这一对却直接隶属于 Object 类,也就是说,所有对象都拥有这一对方法。

初看起来这十分不可思议,但是实际上却是很自然的,因为这一对方法阻塞时要释放占用的锁,

而锁是任何对象都具有的,调用任意对象的 wait() 方法导致线程阻塞,并且该对象上的锁被释放。

而调用 任意对象的notify()方法则导致因调用该对象的 wait() 方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。

其次,

前面叙述的所有方法都可在任何位置调用,但是这一对方法却必须在 synchronized 方法或块中调用,理由也很简单,只有在synchronized 方法或块中当前线程才占有锁,才有锁可以释放。

同样的道理,调用这一对方法的对象上的锁必须为当前线程所拥有,这样才有锁可以释放。

因此,这一对方法调用必须放置在这样的 synchronized 方法或块中,该方法或块的上锁对象就是调用这一对方法的对象。

若不满足这一条件,则程序虽然仍能编译,但在运行时会出现IllegalMonitorStateException 异常。

wait() 和 notify() 方法的上述特性决定了它们经常和synchronized 方法或块一起使用,

将它们和操作系统的进程间通信机制作一个比较就会发现它们的相似性:

synchronized方法或块提供了类似于操作系统原语的功能,它们的执行不会受到多线程机制的干扰,

而这一对方法则相当于 block 和wakeup 原语(这一对方法均声明为 synchronized)。

它们的结合使得我们可以实现操作系统上一系列精妙的进程间通信的算法(如信号量算法),并用于解决各种复杂的线程间通信问题。

关于 wait() 和 notify() 方法最后再说明两点:

1). 调用 notify() 方法导致解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中随机选取的,

我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。

2). 除了 notify(),还有一个方法 notifyAll() 也可起到类似作用,唯一的区别在于,调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程一次性全部解除阻塞。

当然,只有获得锁的那一个线程才能进入可执行状态。

谈到阻塞,就不能不谈一谈死锁,略一分析就能发现,suspend() 方法和不指定超时期限的 wait() 方法的调用都可能产生死锁。

遗憾的是,Java 并不在语言级别上支持死锁的避免,我们在编程中必须小心地避免死锁。

以上我们对 Java 中实现线程阻塞的各种方法作了一番分析,我们重点分析了 wait() 和 notify() 方法,

因为它们的功能最强大,使用也最灵活,但是这也导致了它们的效率较低,较容易出错。

实际使用中我们应该灵活使用各种方法,以便更好地达到我们的目的。

50为什么 wait() 方法和 notify()/notifyAll() 方法要在同步块中被调用

这是 JDK强制的,wait() 方法和 notify()/notifyAll() 方法在调用前都必须先获得对象的锁

51wait() 方法和 notify()/notifyAll() 方法在放弃对象监视器时有什么区别

wait() 方法和 notify()/notifyAll()方法在放弃对象监视器的时候的区别在于:

wait()方法立即释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。

52wait()与sleep()的区别

关于这两者已经在上面进行详细的说明,这里就做个概括好了:

sleep()来自Thread类,和wait()来自Object类.调用sleep()方法的过程中,线程不会释放对象锁。而 调用 wait 方法线程会释放对象锁。

sleep()睡眠后不出让系统资源,wait让其他线程可以占用CPU

sleep(milliseconds)需要指定一个睡眠时间,时间一到会自动唤醒。而wait()需要配合notify()或者notifyAll()使用。

53synchronized和ReentrantLock的区别

synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。

既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:

1). ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁

2). ReentrantLock可以获取各种锁的信息

3). ReentrantLock可以灵活地实现多路通知

另外,二者的锁机制其实也是不一样的:

ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word。

54FutureTask是什么

这个其实前面有提到过,FutureTask表示一个异步运算的任务。

FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。

当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。

55一个线程如果出现了运行时异常怎么办?

如果这个异常没有被捕获的话,这个线程就停止执行了。

另外重要的一点是:

如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放。

56如何在两个线程间共享数据

通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进行唤起和等待,

比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的。

57如何正确的使用wait()?使用if还是while?

wait() 方法应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。

下面是一段标准的使用 wait 和 notify 方法的代码:

synchronized (obj) { while (condition does not hold) obj.wait(); // (Releases lock, and reacquires on wakeup) ... // Perform action appropriate to condition }

57什么是线程局部变量

线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。

Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。

但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。

任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。

58ThreadLoal的作用是什么?

简单说ThreadLocal就是一种以空间换时间的做法在每个Thread里面维护了一个ThreadLocal。

ThreadLocalMap把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。

59生产者消费者模型的作用是什么?

1). 通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用 。

2). 解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约。

60写一个生产者-消费者队列

可以通过阻塞队列实现,也可以通过wait-notify来实现。

使用阻塞队列来实现

//消费者public class Producer implements Runnable{ private final BlockingQueue queue; public Producer(BlockingQueue q){ this.queue=q; } @Override public void run() { try { while (true){ Thread.sleep(1000);//模拟耗时 queue.put(produce()); } }catch (InterruptedException e){ } } private int produce() { int n=new Random().nextInt({{10000:0}}); System.out.println("Thread:" + Thread.currentThread().getId() + " produce:" + n); return n; }

}//消费者public class Consumer implements Runnable { private final BlockingQueue queue; public Consumer(BlockingQueue q){ this.queue=q; } @Override public void run() { while (true){ try { Thread.sleep(2000);//模拟耗时 consume(queue.take()); }catch (InterruptedException e){ } } } private void consume(Integer n) { System.out.println("Thread:" + Thread.currentThread().getId() + " consume:" + n); }

}//测试public class Main { public static void main(String[] args) { BlockingQueue queue=new ArrayBlockingQueue(100); Producer p=new Producer(queue); Consumer c1=new Consumer(queue); Consumer c2=new Consumer(queue); new Thread(p).start(); new Thread(c1).start(); new Thread(c2).start(); }}

使用wait-notify来实现

该种方式应该最经典,这里就不做说明了。

61ConcurrentHashMap的并发度是什么?

ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,

这也是ConcurrentHashMap对Hashtable的最大优势,

任何情况下,Hashtable能同时有两条线程获取Hashtable中的数据吗?

62CyclicBarrier和CountDownLatch区别

这两个类非常类似,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,

二者的区别在于:

1).CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;

CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行。

2).CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务。

3).CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了。

63java中的++操作符线程安全么?

不是线程安全的操作。

它涉及到多个指令,如读取变量值,增加,然后存储回内存,这个过程可能会出现多个线程交差。

64你有哪些多线程开发良好的实践?

1). 给线程命名

2). 最小化同步范围

3). 优先使用volatile

4). 尽可能使用更高层次的并发工具而非wait和notify()来实现线程通信,如BlockingQueue,Semeaphore

5). 优先使用并发容器而非同步容器.

6). 考虑使用线程池

关于volatile关键字

65可以创建Volatile数组吗?

Java 中可以创建 volatile类型数组,不过只是一个指向数组的引用,而不是整个数组。

如果改变引用指向的数组,将会受到volatile 的保护,但是如果多个线程同时改变数组的元素,volatile标示符就不能起到之前的保护作用了。

66volatile能使得一个非原子操作变成原子操作吗?

一个典型的例子是在类中有一个 long 类型的成员变量。

如果你知道该成员变量会被多个线程访问,如计数器、价格等,你最好是将其设置为 volatile。

为什么?

因为 Java 中读取 long 类型变量不是原子的,需要分成两步,如果一个线程正在修改该 long 变量的值,另一个线程可能只能看到该值的一半(前 32 位)。

但是对一个 volatile 型的 long 或 double 变量的读写是原子。

一种实践是用 volatile 修饰 long 和 double 变量,使其能按原子类型来读写。

double 和 long 都是64位宽,因此对这两种类型的读是分为两部分的,第一次读取第一个 32 位,然后再读剩下的 32 位,这个过程不是原子的,但 Java 中 volatile 型的 long 或 double 变量的读写是原子的。

volatile 修复符的另一个作用是提供内存屏障(memory barrier),例如在分布式框架中的应用。

简单的说,就是当你写一个 volatile 变量之前,Java 内存模型会插入一个写屏障(write barrier),读一个 volatile 变量之前,会插入一个读屏障(read barrier)。

意思就是说,在你写一个 volatile 域时,能保证任何线程都能看到你写的值,同时,在写之前,也能保证任何数值的更新对所有线程是可见的,因为内存屏障会将其他所有写的值更新到缓存。

67volatile类型变量提供什么保证?

volatile 主要有两方面的作用:

1). 避免指令重排

2). 可见性保证.

例如,JVM 或者 JIT为了获得更好的性能会对语句重排序,但是 volatile 类型变量即使在没有同步块的情况下赋值也不会与其他语句重排序。

volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。

某些情况下,volatile 还能提供原子性,如读 64 位数据类型,像 long 和 double 都不是原子的(低32位和高32位),但 volatile 类型的 double 和 long 就是原子的。

转发分享是一种美德返回搜狐,查看更多

责任编辑:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值