Java并发编程问题是面试过程中很容易遇到的问题,提前准备是解决问题的最好办法,将试题总结起来,时常查看会有奇效。
现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?
这个线程问题通常会在第一轮或电话面试阶段被问到,目的是检测你对”join”方法是否熟悉。这个多线程问题比较简单,可以用join方法实现。
核心:
thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。
比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
想要更深入了解,建议看一下join的源码,也很简单的,使用wait方法实现的。t.join(); //调用join方法,等待线程t执行完毕
t.join(1000); //等待 t 线程,等待时间是1000毫秒。
代码实现:
-
public
static
void
main
(String[] args) {
-
method01();
-
method02();
-
}
-
-
/**
-
* 第一种实现方式,顺序写死在线程代码的内部了,有时候不方便
-
*/
-
private
static
void
method01
() {
-
Thread t1 =
new Thread(
new Runnable() {
-
@Override
public
void
run
() {
-
System.
out.println(
"t1 is finished");
-
}
-
});
-
Thread t2 =
new Thread(
new Runnable() {
-
@Override
public
void
run
() {
-
try {
-
t1.
join();
-
}
catch (InterruptedException e) {
-
e.printStackTrace();
-
}
-
System.
out.println(
"t2 is finished");
-
}
-
});
-
Thread t3 =
new Thread(
new Runnable() {
-
@Override
public
void
run
() {
-
try {
-
t2.
join();
-
}
catch (InterruptedException e) {
-
e.printStackTrace();
-
}
-
System.
out.println(
"t3 is finished");
-
}
-
});
-
-
t3.start();
-
t2.start();
-
t1.start();
-
}
-
-
-
/**
-
* 第二种实现方式,线程执行顺序可以在方法中调换
-
*/
-
private
static
void
method02
(){
-
Runnable runnable =
new Runnable() {
-
@Override
public
void
run
() {
-
System.
out.println(Thread.currentThread().getName() +
"执行完成");
-
}
-
};
-
Thread t1 =
new Thread(runnable,
"t1");
-
Thread t2 =
new Thread(runnable,
"t2");
-
Thread t3 =
new Thread(runnable,
"t3");
-
try {
-
t1.start();
-
t1.
join();
-
t2.start();
-
t2.
join();
-
t3.start();
-
t3.
join();
-
}
catch (InterruptedException e) {
-
e.printStackTrace();
-
}
-
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
在Java中Lock接口比synchronized块的优势是什么?你需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写,以此来保持它的完整性,你会怎样去实现它?
这个题的原答案我认为不是很全面。
Lock接口 和 ReadWriteLock接口 如下:
-
public
interface Lock {
-
void
lock();
-
void
lockInterruptibly()
throws
InterruptedException;
-
boolean
tryLock();
-
boolean
tryLock(
long
time, TimeUnit unit)
throws
InterruptedException;
-
void
unlock();
-
Condition newCondition();
-
}
-
-
public
interface ReadWriteLock {
-
Lock readLock();
-
Lock writeLock();
-
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
整体上来说Lock是synchronized的扩展版,Lock提供了无条件的、可轮询的(tryLock方法)、定时的(tryLock带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition方法)锁操作。另外Lock的实现类基本都支持非公平锁(默认)和公平锁,synchronized只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。
ReadWriteLock是对Lock的运用,具体的实现类是 ReentrantReadWriteLock ,下面用这个类来实现读写类型的高效缓存:
-
import java.util.HashMap;
-
import java.util.Map;
-
import java.util.Random;
-
import java.util.concurrent.ExecutorService;
-
import java.util.concurrent.Executors;
-
import java.util.concurrent.locks.Lock;
-
import java.util.concurrent.locks.ReadWriteLock;
-
import java.util.concurrent.locks.ReentrantReadWriteLock;
-
-
/**
-
* 用ReadWriteLock读写锁来实现一个高效的Map缓存
-
* Created by LEO on 2017/10/30.
-
*/
-
public
class ReaderAndWriter<K, V> {
-
private
final
ReadWriteLock lock =
new
ReentrantReadWriteLock();
-
private
final
Lock readLock = lock.readLock();
-
private
final
Lock writeLock = lock.writeLock();
-
private
final
Map<
K,
V>
map;
-
-
public
ReaderAndWriter(
Map<
K,
V>
map) {
-
this.
map =
map;
-
}
-
-
/************* 这是用lock()方法写的 ********************/
-
// public V put(K key, V value){
-
// writeLock.lock();
-
// try {
-
// return map.put(key, value);
-
// }finally {
-
// writeLock.unlock();
-
// }
-
// }
-
// public V get(K key){
-
// readLock.lock();
-
// try {
-
// return map.get(key);
-
// }finally {
-
// readLock.unlock();
-
// }
-
// }
-
-
/************* 这是用tryLock()方法写的 ********************/
-
public
V put(
K key,
V value){
-
while (
true){
-
if(writeLock.tryLock()){
-
try {
-
System.out.
println(
"put "+ key +
" = " + value);
-
return
map.put(key, value);
-
}
finally {
-
writeLock.unlock();
-
}
-
}
-
}
-
}
-
public
V
get(
K key){
-
while (
true){
-
if (readLock.tryLock()) {
-
try {
-
V v =
map.
get(key);
-
System.out.
println(
"get "+ key +
" = " + v);
-
return v;
-
}
finally {
-
readLock.unlock();
-
}
-
}
-
}
-
}
-
-
-
/******************** 下面是测试区 *********************************/
-
public
static void main(
String[] args) {
-
final
ReaderAndWriter<
String,
Integer> rw =
new
ReaderAndWriter<>(
new
HashMap<>());
-
ExecutorService exec =
Executors.newCachedThreadPool();
-
for (int i =
0; i <
100; i++) {
-
exec.execute(
new
TestRunnable(rw));
-
}
-
exec.shutdown();
-
}
-
-
static
class TestRunnable implements Runnable{
-
private
final
ReaderAndWriter<
String,
Integer> rw;
-
private
final
String
KEY =
"x";
-
-
TestRunnable(
ReaderAndWriter<
String,
Integer> rw) {
-
this.rw = rw;
-
}
-
-
@Override
-
public void run() {
-
Random random =
new
Random();
-
int r = random.nextInt(
100);
-
//生成随机数,小于30的写入缓存,大于等于30则读取数字
-
if (r <
30){
-
rw.put(
KEY, r);
-
}
else {
-
rw.
get(
KEY);
-
}
-
}
-
}
-
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
在java中wait和sleep方法的不同?
通常会在电话面试中经常被问到的Java线程面试问题。
最大的不同是在等待时wait会释放锁,而sleep一直持有锁。Wait通常被用于线程间交互,sleep通常被用于暂停执行。
此处我想理一下Java多线程的基础知识:
- Java的多线程锁是挂在对象上的,并不是在方法上的。即每个对象都有一个锁,当遇到类似synchronized的同步需要时,就会监视(monitor)每个想使用本对象的线程按照一定的规则来访问,规则也就是在同一时间内只能有一个线程能访问此对象。
- Java中获取锁的单位是线程。当线程A获取了对象B的锁,也就是对象B的持有标记上写的是线程A的唯一标识,在需要同步的情况下的话,只有线程A能访问对象B。
- Thread常用方法有:start/stop/yield/sleep/interrupt/join等,他们是线程级别的方法,所以并不会太关心锁的具体逻辑。
- Object的线程有关方法是:wait/wait(事件参数)/notify/notifyAll,他们是对象的方法,所以使用的时候就有点憋屈了,必须当前线程获取了本对象的锁才能使用,否则会报异常。但他们能更细粒度的控制锁,可以释放锁。
用Java实现阻塞队列。
这是一个相对艰难的多线程面试问题,它能达到很多的目的。第一,它可以检测侯选者是否能实际的用Java线程写程序;第二,可以检测侯选者对并发场景的理解,并且你可以根据这个问很多问题。如果他用wait()和notify()方法来实现阻塞队列,你可以要求他用最新的Java 5中的并发类来再写一次。
下面是实现了阻塞的take和put方法的阻塞队列(分别用synchronized 和 wait/notify 实现):
-
import java.util.LinkedList;
-
import java.util.List;
-
import java.util.Random;
-
import java.util.concurrent.ExecutorService;
-
import java.util.concurrent.Executors;
-
-
/**
-
* 实现了阻塞的take和put方法的阻塞队列
-
* 分别用synchronized 和 wait/notify 实现
-
* @author xuexiaolei
-
* @version 2017年11月01日
-
*/
-
public
class
MyBlocingQueue<E> {
-
private
final List list;
-
private
final
int limit;
//有大小限制的
-
-
public
MyBlocingQueue
(
int
limit) {
-
this.limit = limit;
-
this.list =
new LinkedList<E>();
-
}
-
-
//这是用synchronized写的,在list空或者满的时候效率会低,因为会一直轮询
-
// public void put(E e){
-
// while(true){
-
// synchronized (list){
-
// if (list.size() < limit) {
-
// System.out.println("list : " + list.toString());
-
// System.out.println("put : " + e);
-
// list.add(e);
-
// return;
-
// }
-
// }
-
// }
-
// }
-
// public E take(){
-
// while (true) {
-
// synchronized (list) {
-
// if (list.size() > 0){
-
// System.out.println("list : " + list.toString());
-
// E remove = (E) list.remove(0);
-
// System.out.println("take : " + remove);
-
// return remove;
-
// }
-
// }
-
// }
-
// }
-
-
//用wait,notify写的,在list空或者满的时候效率会高一点,因为wait释放锁,然后等待唤醒
-
public
synchronized
void
put
(E e){
-
while (list.size() == limit){
-
try {
-
wait();
-
}
catch (InterruptedException e1) {
-
e1.printStackTrace();
-
}
-
}
-
System.
out.println(
"list : " + list.toString());
-
System.
out.println(
"put : " + e);
-
list.add(e);
-
notifyAll();
-
}
-
public
synchronized E
take
() {
-
while (list.size() ==
0){
-
try {
-
wait();
-
}
catch (InterruptedException e) {
-
e.printStackTrace();
-
}
-
}
-
System.
out.println(
"list : " + list.toString());
-
E remove = (E) list.remove(
0);
-
System.
out.println(
"take : " + remove);
-
notifyAll();
-
return remove;
-
}
-
-
-
/******************** 下面是测试区 *********************************/
-
public
static
void
main
(String[] args) {
-
final MyBlocingQueue<Integer> myBlocingQueue =
new MyBlocingQueue(
10);
-
ExecutorService exec = Executors.newCachedThreadPool();
-
for (
int i =
0; i <
100; i++) {
-
exec.execute(
new TestRunnable(myBlocingQueue));
-
}
-
exec.shutdown();
-
}
-
-
static
class TestRunnable implements Runnable{
-
private
final MyBlocingQueue<Integer> myBlocingQueue;
-
-
TestRunnable(MyBlocingQueue<Integer> myBlocingQueue) {
-
this.myBlocingQueue = myBlocingQueue;
-
}
-
-
@Override
-
public
void
run
() {
-
Random random =
new Random();
-
int r = random.nextInt(
100);
-
//生成随机数,按照一定比率读取或者放入,可以更改!!!
-
if (r <
30){
-
myBlocingQueue.put(r);
-
}
else {
-
myBlocingQueue.take();
-
}
-
}
-
}
-
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
BlockingQueue介绍:
Java5中提供了BlockingQueue的方法,并且有几个实现,在此介绍一下。
BlockingQueue 具有 4 组不同的方法用于插入、移除以及对队列中的元素进行检查。如果请求的操作不能得到立即执行的话,每个方法的表现也不同。这些方法如下:
Throws exception | Special value | Blocks | Times out |
---|---|---|---|
add(e) | offer(e) | put(e) | offer(Object, long, TimeUnit) |
remove() | poll() | take() | poll(long, TimeUnit) |
element() | peek() |
- Throws exception 抛异常:如果试图的操作无法立即执行,抛一个异常。
- Special value 特定值:如果试图的操作无法立即执行,返回一个特定的值(常常是 true / false)
- Blocks 阻塞:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
- Times out 超时:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功(典型的是true / false)。
BlockingQueue 的实现类
- ArrayBlockingQueue:ArrayBlockingQueue 是一个有界的阻塞队列,其内部实现是将对象放到一个数组里。有界也就意味着,它不能够存储无限多数量的元素。它有一个同一时间能够存储元素数量的上限。你可以在对其初始化的时候设定这个上限,但之后就无法对这个上限进行修改了(译者注:因为它是基于数组实现的,也就具有数组的特性:一旦初始化,大小就无法修改)。
- DelayQueue:DelayQueue 对元素进行持有直到一个特定的延迟到期。注入其中的元素必须实现 java.util.concurrent.Delayed 接口。
- LinkedBlockingQueue:LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。
- PriorityBlockingQueue:PriorityBlockingQueue 是一个无界的并发队列。它使用了和类 java.util.PriorityQueue 一样的排序规则。你无法向这个队列中插入 null 值。所有插入到 PriorityBlockingQueue 的元素必须实现 java.lang.Comparable 接口。因此该队列中元素的排序就取决于你自己的 Comparable 实现。
- SynchronousQueue:SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。据此,把这个类称作一个队列显然是夸大其词了。它更多像是一个汇合点。
BlocingQueue的实现大多是通过 lock锁的多条件(condition)阻塞控制,下面我们自己写一个简单版:
-
import java.util.LinkedList;
-
import java.util.List;
-
import java.util.concurrent.locks.Condition;
-
import java.util.concurrent.locks.Lock;
-
import java.util.concurrent.locks.ReentrantLock;
-
-
/**
-
* 模仿ArrayBlockingQueue实现阻塞队列
-
* @author xuexiaolei
-
* @version 2017年11月01日
-
*/
-
public
class
MyBlocingQueue2<E> {
-
private
final List list;
-
private
final
int limit;
//有大小限制的
-
private
final Lock
lock =
new ReentrantLock();
-
private
final Condition notFull =
lock.newCondition();
-
private
final Condition notEmpty =
lock.newCondition();
-
-
public
MyBlocingQueue2
(
int
limit) {
-
this.limit = limit;
-
this.list =
new LinkedList<E>();
-
}
-
-
public
void
put
(E e) throws InterruptedException {
-
lock.
lock();
-
try {
-
while (list.size() == limit){
-
notFull.
await();
-
}
-
list.add(e);
-
notEmpty.signalAll();
-
}
finally {
-
lock.unlock();
-
}
-
}
-
public
E
take
() throws InterruptedException {
-
lock.
lock();
-
try {
-
while (list.size() ==
0){
-
notEmpty.
await();
-
}
-
E remove = (E) list.remove(
0);
-
notFull.signalAll();
-
return remove;
-
}
finally {
-
lock.unlock();
-
}
-
}
-
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
用Java写代码来解决生产者——消费者问题。
与上面的问题很类似,但这个问题更经典,有些时候面试都会问下面的问题。在Java中怎么解决生产者——消费者问题,当然有很多解决方法,我已经分享了一种用阻塞队列实现的方法。有些时候他们甚至会问怎么实现哲学家进餐问题。
生产者、消费者有很多的实现方法:
- 用wait() / notify()方法
- 用Lock的多Condition方法
- BlockingQueue阻塞队列方法
可以发现在上面实现阻塞队列题中,BlockingQueue的实现基本都用到了类似的实现,将BlockingQueue的实现方式稍微包装一下就成了一个生产者-消费者模式了。
-
import java.util.Random;
-
import java.util.concurrent.ArrayBlockingQueue;
-
import java.util.concurrent.BlockingQueue;
-
-
/**
-
* 用阻塞队列快速实现生产者-消费者
-
*
@author
xuexiaolei
-
*
@version
2017年11月01日
-
*/
-
public
class ProduceAndConsumer {
-
public
static
void main(String[] args) {
-
final BlockingQueue<Integer>
list =
new ArrayBlockingQueue<Integer>(
10);
-
Procude procude =
new Procude(
list);
-
Consumer consumer =
new Consumer(
list);
-
procude.start();
-
consumer.start();
-
}
-
-
static
class Procude extends Thread{
-
private
final BlockingQueue<Integer>
list;
-
Procude(BlockingQueue<Integer>
list) {
-
this.
list =
list;
-
}
-
@
Override
public
void run() {
-
while(
true){
-
try {
-
Integer take =
list.take();
-
System.out.println(
"消费数据:" + take);
-
// Thread.sleep(1000);
-
}
catch (InterruptedException e) {
-
e.printStackTrace();
-
}
-
}
-
}
-
}
-
-
static
class Consumer extends Thread{
-
private
final BlockingQueue<Integer>
list;
-
Consumer(BlockingQueue<Integer>
list) {
-
this.
list =
list;
-
}
-
@
Override
public
void run() {
-
while (
true){
-
try {
-
int i =
new Random().nextInt(
100);
-
list.put(i);
-
System.out.println(
"生产数据:" + i);
-
Thread.sleep(
1000);
-
}
catch (InterruptedException e) {
-
e.printStackTrace();
-
}
-
}
-
}
-
}
-
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
此处不再详细地写另外几种实现方式了:wait() / notify()方法、Lock的多Condition方法、信号量等,甚至可以考虑用CyclicBarrier、CountDownLatch也可以实现生产者-消费者的,难易程度、效率不一样罢了。
用Java写一个会导致死锁的程序,你将怎么解决?
这是我最喜欢的Java线程面试问题,因为即使死锁问题在写多线程并发程序时非常普遍,但是很多侯选者并不能写deadlock free code(无死锁代码?),他们很挣扎。只要告诉他们,你有N个资源和N个线程,并且你需要所有的资源来完成一个操作。为了简单这里的n可以替换为2,越大的数据会使问题看起来更复杂。通过避免Java中的死锁来得到关于死锁的更多信息。
-
/**
-
* 简单死锁程序
-
* lockA、lockB分别是两个资源,线程A、B必须同是拿到才能工作
-
* 但A线程先拿lockA、再拿lockB
-
* 线程先拿lockB、再拿lockA
-
*
@author
xuexiaolei
-
*
@version
2017年11月01日
-
*/
-
public
class SimpleDeadLock {
-
public static void main(String[] args) {
-
Object lockA =
new Object();
-
Object lockB =
new Object();
-
A a =
new A(lockA, lockB);
-
B b =
new B(lockA, lockB);
-
a.start();
-
b.start();
-
}
-
-
static
class A extends Thread{
-
private
final Object lockA;
-
private
final Object lockB;
-
A(Object lockA, Object lockB) {
-
this.lockA = lockA;
-
this.lockB = lockB;
-
}
-
-
@Override
public void run() {
-
synchronized (lockA){
-
try {
-
Thread.sleep(
1000);
-
synchronized (lockB){
-
System.out.println(
"Hello A");
-
}
-
}
catch (InterruptedException e) {
-
e.printStackTrace();
-
}
-
}
-
}
-
}
-
-
static
class B extends Thread{
-
private
final Object lockA;
-
private
final Object lockB;
-
B(Object lockA, Object lockB) {
-
this.lockA = lockA;
-
this.lockB = lockB;
-
}
-
-
@Override
public void run() {
-
synchronized (lockB){
-
try {
-
Thread.sleep(
1000);
-
synchronized (lockA){
-
System.out.println(
"Hello B");
-
}
-
}
catch (InterruptedException e) {
-
e.printStackTrace();
-
}
-
}
-
}
-
}
-
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
如何避免死锁?
- 从死锁的四个必要条件来看,破坏其中的任意一个条件就可以避免死锁。但互斥条件是由资源本身决定的,不剥夺条件一般无法破坏,要实现的话得自己写更多的逻辑。
- 避免无限期的等待:用Lock.tryLock(),wait/notify等方法写出请求一定时间后,放弃已经拥有的锁的程序。
- 注意锁的顺序:以固定的顺序获取锁,可以避免死锁。
- 开放调用:即只对有请求的进行封锁。你应当只想你要运行的资源获取封锁,比如在上述程序中我在封锁的完全的对象资源。但是如果我们只对它所属领域中的一个感兴趣,那我们应当封锁住那个特殊的领域而并非完全的对象。
- 最后,如果能避免使用多个锁,甚至写出无锁的线程安全程序是再好不过了。
什么是原子操作,Java中的原子操作是什么?
非常简单的java线程面试问题,接下来的问题是你是否需要同步一个原子操作。
原子操作是不可分割的操作,一个原子操作中间是不会被其他线程打断的,所以不需要同步一个原子操作。
多个原子操作合并起来后就不是一个原子操作了,就需要同步了。
i++不是一个原子操作,它包含 读取-修改-写入 操作,在多线程状态下是不安全的。
另外,java内存模型允许将64位的读操作或写操作分解为2个32位的操作,所以对long和double类型的单次读写操作并不是原子的,注意使用volitile使他们成为原子操作。
Java中的volatile关键是什么作用?怎样使用它?在Java中它跟synchronized方法有什么不同?
自从Java 5和Java内存模型改变以后,基于volatile关键字的线程问题越来越流行。应该准备好回答关于volatile变量怎样在并发环境中确保可见性。
volatile关键字的作用是:保证变量的可见性。
在java内存结构中,每个线程都是有自己独立的内存空间(此处指的线程栈)。当需要对一个共享变量操作时,线程会将这个数据从主存空间复制到自己的独立空间内进行操作,然后在某个时刻将修改后的值刷新到主存空间。这个中间时间就会发生许多奇奇怪怪的线程安全问题了,volatile就出来了,它保证读取数据时只从主存空间读取,修改数据直接修改到主存空间中去,这样就保证了这个变量对多个操作线程的可见性了。换句话说,被volatile修饰的变量,能保证该变量的 单次读或者单次写 操作是原子的。
但是线程安全是两方面需要的 原子性(指的是多条操作)和可见性。volatile只能保证可见性,synchronized是两个均保证的。
volatile轻量级,只能修饰变量;synchronized重量级,还可修饰方法。
volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞。
什么是竞争条件(race condition)?你怎样发现和解决的?
这是一道出现在多线程面试的高级阶段的问题。大多数的面试官会问最近你遇到的竞争条件,以及你是怎么解决的。有些时间他们会写简单的代码,然后让你检测出代码的竞争条件。可以参考我之前发布的关于Java竞争条件的文章。在我看来这是最好的java线程面试问题之一,它可以确切的检测候选者解决竞争条件的经验。关于这方面最好的书是《java并发编程实战》。
竞争条件,在《java并发编程实战》叫做竞态条件:指设备或系统出现不恰当的执行时序,而得到不正确的结果。
下面是个最简单的例子,是一个单例模式实现的错误示范:
-
@NotThreadSafe
-
public
class
LazyInitRace {
-
private ExpensiveObject instance =
null;
-
-
public
ExpensiveObject
getInstance
() {
-
if (instance ==
null)
-
instance =
new ExpensiveObject();
-
return instance;
-
}
-
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
在上述例子中,表现一种很常见的竞态条件类型:“先检查后执行”。根据某个检查结果来执行进一步的操作,但很有可能这个检查结果是失效的!还有很常见的竞态条件“读取-修改-写入”三连,在多线程条件下,三个小操作并不一定会放在一起执行的。
如何对待竞态条件?
首先,警惕复合操作,当多个原子操作合在一起的时候,并不一定仍然是一个原子操作,此时需要用同步的手段来保证原子性。
另外,使用本身是线程安全的类,这样在很大程度上避免了未知的风险。
你将如何使用thread dump?你将如何分析Thread dump?
在UNIX中你可以使用kill -3,然后thread dump将会打印日志,在windows中你可以使用”CTRL+Break”。非常简单和专业的线程面试问题,但是如果他问你怎样分析它,就会很棘手。
SIGQUIT(kill -3 pid)用来打印Java进程trace,并不会影响程序运行,不用担心他把程序杀死了;SIGUSR1(kill -10 pid)可触发进程进行一次强制GC。
java线程的状态转换介绍
后续分析要用到,所以此处穿插一下这个点:
- 新建状态(New)
用new语句创建的线程处于新建状态,此时它和其他Java对象一样,仅仅在堆区中被分配了内存。 - 就绪状态(Runnable)
当一个线程对象创建后,其他线程调用它的start()方法,该线程就进入就绪状态,Java虚拟机会为它创建方法调用栈和程序计数器。处于这个状态的线程位于可运行池中,等待获得CPU的使用权。 - 运行状态(Running)
处于这个状态的线程占用CPU,执行程序代码。只有处于就绪状态的线程才有机会转到运行状态。 - 阻塞状态(Blocked)
阻塞状态是指线程因为某些原因放弃CPU,暂时停止运行。当线程处于阻塞状态时,Java虚拟机不会给线程分配CPU。直到线程重新进入就绪状态,它才有机会转到运行状态。
阻塞状态可分为以下3种:
- 位于对象等待池中的阻塞状态(Blocked in object’s wait pool):当线程处于运行状态时,如果执行了某个对象的wait()方法,Java虚拟机就会把线程放到这个对象的等待池中,这涉及到“线程通信”的内容。
- 位于对象锁池中的阻塞状态(Blocked in object’s lock pool):当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,Java虚拟机就会把这个线程放到这个对象的锁池中,这涉及到“线程同步”的内容。
- 其他阻塞状态(Otherwise Blocked):当前线程执行了sleep()方法,或者调用了其他线程的join()方法,或者发出了I/O请求时,就会进入这个状态。
- 死亡状态(Dead)
当线程退出run()方法时,就进入死亡状态,该线程结束生命周期。
我们运行之前的那个死锁代码SimpleDeadLock.java,然后尝试输出信息(/*这是注释,作者自己加的*/):
-
/* 时间,jvm信息 */
-
2017
-
11
-
01
17:
36:
28
-
Full thread dump Java HotSpot(TM)
64-Bit Server VM (
25.144-b01 mixed mode):
-
-
/* 线程名称:DestroyJavaVM
-
编号:#13
-
优先级:5
-
系统优先级:0
-
jvm内部线程id:0x0000000001c88800
-
对应系统线程id(NativeThread ID):0x1c18
-
线程状态: waiting on condition [0x0000000000000000] (等待某个条件)
-
线程详细状态:java.lang.Thread.State: RUNNABLE 及之后所有*/
-
"DestroyJavaVM"
#13 prio=5 os_prio=0 tid=0x0000000001c88800 nid=0x1c18 waiting on condition [0x0000000000000000]
-
java
.lang
.Thread
.State: RUNNABLE
-
-
"Thread-1"
#12 prio=5 os_prio=0 tid=0x0000000018d49000 nid=0x17b8 waiting for monitor entry [0x0000000019d7f000]
-
/* 线程状态:阻塞(在对象同步上)
-
代码位置:at com.leo.interview.SimpleDeadLock$B.run(SimpleDeadLock.java:56)
-
等待锁:0x00000000d629b4d8
-
已经获得锁:0x00000000d629b4e8*/
-
java
.lang
.Thread
.State: BLOCKED (
on
object monitor)
-
at
com
.leo
.interview
.SimpleDeadLock$B
.run(SimpleDeadLock
.java:
56)
-
- waiting
to lock <
0x00000000d629b4d8> (a java
.lang
.Object)
-
- locked <
0x00000000d629b4e8> (a java
.lang
.Object)
-
-
"Thread-0"
#11 prio=5 os_prio=0 tid=0x0000000018d44000 nid=0x1ebc waiting for monitor entry [0x000000001907f000]
-
java
.lang
.Thread
.State: BLOCKED (
on
object monitor)
-
at
com
.leo
.interview
.SimpleDeadLock$A
.run(SimpleDeadLock
.java:
34)
-
- waiting
to lock <
0x00000000d629b4e8> (a java
.lang
.Object)
-
- locked <
0x00000000d629b4d8> (a java
.lang
.Object)
-
-
"Service Thread"
#10 daemon prio=9 os_prio=0 tid=0x0000000018ca5000 nid=0x1264 runnable [0x0000000000000000]
-
java
.lang
.Thread
.State: RUNNABLE
-
-
"C1 CompilerThread2"
#9 daemon prio=9 os_prio=2 tid=0x0000000018c46000 nid=0xb8c waiting on condition [0x0000000000000000]
-
java
.lang
.Thread
.State: RUNNABLE
-
-
"C2 CompilerThread1"
#8 daemon prio=9 os_prio=2 tid=0x0000000018be4800 nid=0x1db4 waiting on condition [0x0000000000000000]
-
java
.lang
.Thread
.State: RUNNABLE
-
-
"C2 CompilerThread0"
#7 daemon prio=9 os_prio=2 tid=0x0000000018be3800 nid=0x810 waiting on condition [0x0000000000000000]
-
java
.lang
.Thread
.State: RUNNABLE
-
-
"Monitor Ctrl-Break"
#6 daemon prio=5 os_prio=0 tid=0x0000000018bcc800 nid=0x1c24 runnable [0x00000000193ce000]
-
java
.lang
.Thread
.State: RUNNABLE
-
at java
.net
.SocketInputStream
.socketRead0(Native Method)
-
at java
.net
.SocketInputStream
.socketRead(SocketInputStream
.java:
116)
-
at java
.net
.SocketInputStream
.read(SocketInputStream
.java:
171)
-
at java
.net
.SocketInputStream
.read(SocketInputStream
.java: