架构面试之并发编程篇

1)现在有 T1、T2、T3 三个线程,你怎样保证 T2 在 T1 执行完后执行,T3 在 T2 执行完后执行?

这个线程问题通常会在第一轮或电话面试阶段被问到,目的是检测你对”join”方法是否熟悉。这个多线程问题比较简单,可以用 join 方法实现。
详解:https://www.cnblogs.com/zhuyeshen/p/11005222.html

2)在 Java 中 Lock 接口比 synchronized 块的优势是什么?你需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写,以此来保持它的完整性,你会怎样去实现它?

lock 接口在多线程和并发编程中最大的优势是它们为读和写分别提供了锁,它能满足你写像 ConcurrentHashMap这样的高性能数据结构和有条件的阻塞。
使用Lock读写锁机制实现代码如下:

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class JoinTest2 {
    public static void main(String[] args) {
        final TheData theData = new TheData();
        for(int i=0;i<4;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    theData.get();
                }
            }).start();
        }
        for(int i=0;i<4;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    theData.put(new Random().nextInt(1000));
                }
            }).start();
        }
    }
}

class TheData{
    private Integer data = 0;
    private ReadWriteLock rwLock = new ReentrantReadWriteLock();
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    public void get(){
        rwLock.readLock().lock();//读锁开启,读进程均可进入
        try{//用try finally来防止因异常而造成的死锁
            System.out.println(Thread.currentThread().getName()+"read lock is ready.."+sdf.format(new Date()));
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName()+"read data is"+data);
        }catch (InterruptedException e) {
            e.printStackTrace();
        }finally{
            rwLock.readLock().unlock();//读锁解锁
        }
    }

    public void put(Integer data){
        rwLock.writeLock().lock();//写锁开启,这时只有一个写线程进入
        try{//用try finally来防止因异常而造成的死锁
            System.out.println(Thread.currentThread().getName()+"write lock is ready.."+sdf.format(new Date()));
            Thread.sleep(1000);
            this.data = data;
            System.out.println(Thread.currentThread().getName()+"write data is"+data);

        }catch (InterruptedException e) {
            e.printStackTrace();
        }finally{
            rwLock.writeLock().unlock();//写锁解锁
        }
    }
}

运行结果
Java 线程面试的问题越来越会根据面试者的回答来提问。我强烈建议在你去参加多线程的面试之前认真读一下Locks,因为当前其大量用于构建电子交易中的客户端缓存和交易连接空间。

3)在 java 中 wait 和 sleep 方法的不同?

通常会在电话面试中经常被问到的 Java 线程面试问题。最大的不同是在等待时 wait 会释放锁,而 sleep 一直持有锁。Wait 通常被用于线程间交互,sleep 通常被用于暂停执行。

4)用 Java 实现阻塞队列。

这是一个相对艰难的多线程面试问题,它能达到很多的目的。
第一,它可以检测侯选者是否能实际的用 Java 线程写程序;
第二,可以检测侯选者对并发场景的理解,并且你可以根据这个问很多问题。如果他用 wait()和 notify()方法来实现阻塞队列,你可以要求他用最新的 Java 5 中的并发类来再写一次。

package test1;

import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 使用wait和notify实现Queue BlockingQueue: 顾名思义,首先它是一个队列,并且支持阻塞机制,阻塞的放入和阻塞的得到数据,
 * 我们要实现LinkedBlockingQueue下面下面两个简单的方法put和take
 * put(anObject):把把对象加到BlockingQueue里面,如果BlockQueue没有空间,则调用此方法的线程被阻断
 * 直到BlockingQueue里面没有空间在继续。
 * take():取走blockingQueue里面该在首位的对象,若blockingQueue为空,阻塞进入等待状态,直到BlockingQueue有新的数据被加入
 * 
 * wait 和notify 结合synchronized使用
 * 
 */
public class QueueTest {

	// 1.需要装元素的集合
	private final LinkedList<Object> list = new LinkedList<Object>();

	/**
	 * 2.需要一个计数器,统计加入list几个的个数 AtomicInteger,一个提供原子操作的Integer的类。在Java语言中,
	 * ++i和i++操作并不是线程安全的,在使用的时候,不可避免的会用到
	 * synchronized关键字。而AtomicInteger则通过一种线程安全的加减操作接口。
	 */
	private AtomicInteger count = new AtomicInteger(0);

	// 指定上限上限和下限
	private final int minSize = 0;
	private final int maxSize;

	// 构造方法,构造容器最大长度
	public QueueTest(int size) {
		this.maxSize = size;
	}

	public QueueTest() {
		// 最大长度默认为10
		this(10);
	}

	/**
	 * 初始化一个对象用于加锁
	 */
	private final Object lock = new Object();

	// 返回长度
	public int size() {
		return count.get();
	}

	/**
	 * 添加一个对象,如果队列满了,则阻塞
	 */
	public void put(Object obj) {
		synchronized (lock) {
			// 如果容器大小刚好等于最大长度,则阻塞
			while (size() == maxSize) {
				try {
					lock.wait();// 阻塞
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			list.add(obj);
			count.incrementAndGet();// 相当于i++
			System.out.println("新加入的元素为:" + obj);
			lock.notify();// 通知另外一个线程去取元素
		}
	}

	/**
	 * 取出一个元素,如果队列为空,则阻塞
	 */
	public Object take() {
		synchronized (lock) {
			// 如果容器的大小刚好等于队列最小长度,则阻塞
			while (minSize == size()) {
				try {
					lock.wait();
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			// 取出第一个元素
			Object obj = list.removeFirst();
			// 计数器递减
			count.decrementAndGet();
			lock.notify();// 通知另外一个线程进行添加元素
			return obj;// 返回结果

		}
	}

	public static void main(String[] args) {
		final QueueTest m = new QueueTest(5);

		Thread t1 = new Thread(new Runnable() {

			@Override
			public void run() {
				m.put("a");
				m.put("b");
				m.put("c");
				m.put("d");
				m.put("e");
				m.put("f");

			}
		});
		t1.start();

		try {
			Thread.sleep(1000);
			System.out.println("当前容器的大小:" + m.size());
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		Thread t2 = new Thread(new Runnable() {

			@Override
			public void run() {
				Object o = m.take();
				System.out.println("取出的元素为:" + o);
			}
		});
		t2.start();
		try {
			Thread.sleep(10);
			System.out.println(m.size());
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

输出结果:
在这里插入图片描述
当前一个线程达到最大限制时,阻塞并通知另一个线程去取元素,另一个线程取出元素后再通知前一个线程继续添加元素。

5)用 Java 写代码来解决生产者—消费者问题。

与上面的问题很类似,但这个问题更经典,有些时候面试都会问下面的问题。在 Java 中怎么解决生产者—消费者问题,当然有很多解决方法。

synchronized, wait, notify阻塞队列实现

  1. 创建资源对象
//资源
package test1;

public class Resource {
   //当前资源的数量
   int num = 0;
   //当前资源的上限
   int size = 10;

   //消费资源
   public synchronized void remove() {
       //如果num为0,没有资源了,需要等待
       while (num == 0) {//这里jdk源码里推荐用while,因为有可能出现虚假唤醒,所以要再次确认
           try {
               System.out.println("消费者进入等待");
               this.wait();//线程等待,并释放锁
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
       //如果线程可以执行到这里,说明资源里有资源可以消费
       num--;
       System.out.println("消费者线程为:" + Thread.currentThread().getName() + "--资源数量:" + num);
       this.notify();//唤醒其他正在等待的线程
   }

   //生产资源
   public synchronized void put() {
       //如果资源满了,就进入阻塞状态
       while (num == size) {//这里jdk源码里推荐用while,因为有可能出现虚假唤醒,所以要再次确认
           try {
               System.out.println("生产者进入等待");
               this.wait();//线程进入阻塞状态,并释放锁

           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }

       num++;
       System.out.println("生产者线程为:" + Thread.currentThread().getName() + "--资源数量:" + num);
       this.notify();//唤醒其他正在等待的线程
   }
}
  1. 创建生产者对象
package test1;


public class Producer implements Runnable {

   private Resource resource;

   public Producer(Resource resource){
       this.resource=resource;
   }

   @Override
   public void run() {
       while (true){
           resource.put();
       }
   }
}
  1. 创建消费者对象
package test1;

public class Consumer implements Runnable {
	 
    private Resource resource;
 
    public Consumer(Resource resource) {
        this.resource = resource;
    } 
    @Override
    public void run() {
        while (true){
            resource.remove();
        } 
    }
}
  1. 测试类
package test1;

public class ConsumerAndProducerTest {
	 
    public static void main(String[] args) {
        Resource resource = new Resource();
        //生产线程
        Producer p1 = new Producer(resource);
        //消费线程
        Consumer c1 = new Consumer(resource);
 
        new Thread(p1).start();
        
        new Thread(c1).start();
    }
}
  1. 输出结果
生产者线程为:Thread-0--资源数量:10
生产者进入等待
消费者线程为:Thread-1--资源数量:9
消费者线程为:Thread-1--资源数量:8
消费者线程为:Thread-1--资源数量:7
消费者线程为:Thread-1--资源数量:6
消费者线程为:Thread-1--资源数量:5
消费者线程为:Thread-1--资源数量:4
消费者线程为:Thread-1--资源数量:3
消费者线程为:Thread-1--资源数量:2
消费者线程为:Thread-1--资源数量:1
消费者线程为:Thread-1--资源数量:0
消费者进入等待
生产者线程为:Thread-0--资源数量:1
生产者线程为:Thread-0--资源数量:2
生产者线程为:Thread-0--资源数量:3
生产者线程为:Thread-0--资源数量:4
生产者线程为:Thread-0--资源数量:5
生产者线程为:Thread-0--资源数量:6
生产者线程为:Thread-0--资源数量:7
生产者线程为:Thread-0--资源数量:8
生产者线程为:Thread-0--资源数量:9
生产者线程为:Thread-0--资源数量:10
生产者进入等待
消费者线程为:Thread-1--资源数量:9
消费者线程为:Thread-1--资源数量:8
消费者线程为:Thread-1--资源数量:7
消费者线程为:Thread-1--资源数量:6
消费者线程为:Thread-1--资源数量:5
消费者线程为:Thread-1--资源数量:4
消费者线程为:Thread-1--资源数量:3
消费者线程为:Thread-1--资源数量:2
消费者线程为:Thread-1--资源数量:1
消费者线程为:Thread-1--资源数量:0
消费者进入等待
生产者线程为:Thread-0--资源数量:1
生产者线程为:Thread-0--资源数量:2
...

另一种通过lock, condition, await, signal实现
参考:https://blog.csdn.net/u010452388/article/details/82624599?utm_source=blogxgwz8

有些时候他们甚至会问怎么实现哲学家进餐问题。其实就是如何解决防死锁?

参考:https://www.cnblogs.com/biglucky/p/4633706.html

6)用 Java 编写一个会导致死锁的程序,你将怎么解决?

这是我最喜欢的 Java 线程面试问题,因为即使死锁问题在写多线程并发程序时非常普遍,但是很多侯选者并不能写 deadlock free code(无死锁代码?),他们很挣扎。只要告诉他们,你有 N 个资源和 N 个线程,并且你需要所有的资源来完成一个操作。为了简单这里的n 可以替换为 2,越大的数据会使问题看起来更复杂。

程序在main()方法中启动2个线程,“线程-A”和“线程-B”。 线程-A 先拿到 lockA,再寻求拿到 lockB;线程-B 先拿到locB,再需求拿到lockA,如下图,于是变成循环等待,造成死锁。

package test1;

public class DeadLockDemo {

	public static void main(String[] args) {
		Object lockA = new Object();
		Object lockB = new Object();
		new Thread(new Runnable() {
			@Override
			public void run() {
				String name = Thread.currentThread().getName();
				synchronized (lockA) {
					System.out.println(name + " got lockA,  want LockB");
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {

						e.printStackTrace();
					}
					synchronized (lockB) {
						System.out.println(name + " got lockB");
						System.out.println(name + ": say Hello!");
					}
				}
			}
		}, "线程-A").start();

		new Thread(new Runnable() {
			@Override
			public void run() {

				String name = Thread.currentThread().getName();
				synchronized (lockB) {
					System.out.println(name + " got lockB, want LockA");
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {

						e.printStackTrace();
					}
					synchronized (lockA) {
						System.out.println(name + " got lockA");
						System.out.println(name + ": say Hello!");
					}
				}

			}
		}, "线程-B").start();
	}
}

死锁

7) 什么是原子操作,Java 中的原子操作是什么?

非常简单的 java 线程面试问题,接下来的问题是你需要同步一个原子操作。
原子(atom)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为"不可被中断的一个或一系列操作" 。
Java中的原子操作(atomic operation)是不需要synchronized,是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch。
java中一般事务管理里面用到原子操作。
原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分,将整个操作视作一个整体是原子性的核心特征;
使用原子操作的好处:
⑴. 性能角度:它执行多次的所消耗的时间远远小于由于线程所挂起到恢复所消耗的时间,因此无锁的CAS操作在性能上要比同步锁高很多;
⑵. 业务需求:业务本身的需求上,无锁机制本身就可以满足我们绝不多数的需求,并且在性能上也可以大大的进行提升。
例子:我们使用的版本控制工具与之其实非常的相似,如果使用锁来同步,其实就意味着只能同时一个人对该文件进行修改,此时其他人就无法操作文件,如果生活中真正遇到这样的情况我们一定会觉得非常不方便,而现实中我们其实并不是这样,我们大家都可以修改这个文件,只是谁提交的早,那么他就把他的代码成功提交的版本控制服务器上,其实这一步就对应着一个原子操作,而后操作的人往往却因为冲突而导致提交失败,此时他必须重新更新代码进行再次修改,重新提交。

8) Java 中的 volatile 关键是什么作用?怎样使用它?在 Java 中它跟 synchronized 方法有什么不同?

自从 Java 5 和 Java 内存模型改变以后,基于 volatile 关键字的线程问题越来越流行。应该准备好回答关于volatile变量怎样在并发环境中确保可见性。

参考:《并发volatile关键字如何保证可见性和有序性及底层实现原理

9) 什么是竞争条件?你怎样发现和解决竞争?

当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则我们认为这发生了竞争条件(racecondition)。
这是一道出现在多线程面试的高级阶段的问题。大多数的面试官会问最近你遇到的竞争条件,以及你是怎么解决的。有些时间他们会写简单的代码,然后让你检测出代码的竞争条件。
推荐阅读关于这方面最好的书《Concurrency practices in Java》 电子书下载

10) 你将如何使用 threaddump?你将如何分析 Thread dump?

在 UNIX 中你可以使用 kill -3,然后 thread dump 将会打印日志;
在 windows 中你可以使用”CTRL+Break”。
非常简单和专业的线程面试问题,但是如果他问你怎样分析它,就会很棘手。

11) 为什么我们调用 start()方法时会执行run()方法,为什么我们不能直接调用run()方法?

当你调用 start()方法时你将创建新的线程,并且执行在 run()方法里的代码。但是如果你直接调用 run()方法,它不会创建新的线程也不会执行调用线程的代码。
阅读《认识多线程中start和run方法的区别?》这篇文章来获得更多信息。

12) Java 中你怎样唤醒一个阻塞的线程?

这是个关于线程和阻塞的棘手的问题,它有很多解决方法。如果线程遇到了 IO 阻塞,我并不认为有一种方法可以中止线程。如果线程因为调用 wait()、sleep()、或者 join()方法而导致的阻塞,你可以中断线程,并且通过抛出 InterruptedException 来唤醒它。

13)在 Java 中 CycliBarriar 和 CountdownLatch 有什么区别?

这个线程问题主要用来检测你是否熟悉 JDK5 中的并发包。这两个的区别是CyclicBarrier 可以重复使用已经通过的障碍,而 CountdownLatch 不能重复使用。

14) 什么是不可变对象,它对写并发应用有什么帮助?

另一个多线程经典面试问题,并不直接跟线程有关,但间接帮助很多。这个 java 面试问题可以变的非常棘手,如果他要求你写一个不可变对象,或者问你为什么 String 是不可变的。

15) 你在多线程环境中遇到的常见的问题是什么?你是怎么解决它的?

多线程和并发程序中常遇到的有 Memory-interface、竞争条件、死锁、活锁和饥饿。问题是没有止境的,如果你弄错了,将很难发现和调试。这是大多数基于面试的,而不是基于实际应用的 Java 线程问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

月球程序猿

你的鼓励将是我创作的最大动

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值