(整理)java面试题(六)线程相关

1、如何正确的使用wait()?使用if还是while?
wait() 方法应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。下面是一段标准的使用 wait 和 notify 方法的代码:

synchronized (obj){
while(condition does not hold)
obj.wait();
...
}

2、什么是线程局部变量ThreadLocal
线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。

3、ThreadLoal的作用是什么?
简单说ThreadLocal就是一种以空间换时间的做法在每个Thread里面维护了一个ThreadLocal.ThreadLocalMap把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。

4、生产者消费者模型的作用是什么?
(1)通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用。
(2)解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约。

5、写一个生产者-消费者队列
可以通过阻塞队列实现,也可以通过wait-notify来实现。

使用阻塞队列来实现

/**
* @ClassName: Producer
* @Description: 生产者
* @author: niran
* @date: 2019/05/04
*/
//生产者
class Producer implements Runnable{
private final BlockingQueue<Integer> 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);
System.out.println("Thread:" + Thread.currentThread().getId() + " produce:" + n);
return n;
}
}
//消费者
class Consumer implements Runnable {
private final BlockingQueue<Integer> 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<Integer> queue=new ArrayBlockingQueue<Integer>(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来实现
该种方式应该最经典,这里就不做说明了

/**
* @ClassName: Producer
* @Description: 生产者
* @author: niran
* @date: 2019/05/04
*/
class Producer implements Runnable {

private final LinkedList<Integer> list;

/**
* 缓冲区大小
*/
private final int maxSize;

public Producer(LinkedList list, int size) {
this.list = list;
maxSize = size;
}

@Override
public void run() {
try {
while (true) {
//模拟耗时1s
Thread.sleep(1000);
synchronized (list) {
if (list.size() == maxSize) {
System.out.println("缓冲区已满,正在等待消费者消费..." + System.currentTimeMillis());
list.wait();
} else {
list.add(produce());
list.notifyAll();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

private int produce() {
int n = new Random().nextInt(10000);
System.out.println("Thread: " + Thread.currentThread().getName() + "生产 produce: " + n);
return n;
}
}


class Consumer implements Runnable {
private final LinkedList<Integer> list;

public Consumer(LinkedList list) {
this.list = list;
}

@Override
public void run() {
while (true) {
try {
synchronized (list) {
//模拟耗时
Thread.sleep(1000);
if (list.isEmpty()) {
System.out.println("缓冲区已空,正在等待生产者生产..." + System.currentTimeMillis() + Thread.currentThread().getName());
list.wait();
} else {
consume(list.poll());
list.notifyAll();
}
}

} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void consume(Integer n) {
System.out.println("Thread:" + Thread.currentThread().getName() + " 消费consume: " + n);
}
}

public class Main {
public static void main(String[] args) {
LinkedList<Integer> list = new LinkedList<>();
Producer p = new Producer(list, 10);
Consumer c1 = new Consumer(list);
Consumer c2 = new Consumer(list);

Thread producer = new Thread(p);
producer.setName("生产者线程");
Thread consumer1 = new Thread(c1);
consumer1.setName("消费者1");
Thread consumer2 = new Thread(c2);
consumer2.setName("消费者2");

producer.start();
consumer1.start();
consumer2.start();
}
}

6、如果你提交任务时,线程池队列已满,这时会发生什么
如果你使用的LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务;如果你使用的是有界队列比方说ArrayBlockingQueue的话,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy。

7、为什么要使用线程池
避免频繁地创建和销毁线程,达到线程对象的重用。另外,使用线程池还可以根据项目灵活地控制并发的数目。

8、java中用到的线程调度算法是什么
抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

9、Thread.sleep(0)的作用是什么
由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。

10、什么是CAS
CAS,全称为Compare and Swap,即比较-替换。假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。

11、什么是乐观锁和悲观锁
乐观锁:乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。

悲观锁:悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。

12、ConcurrentHashMap的并发度是什么?
ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势,任何情况下,Hashtable能同时有两条线程获取Hashtable中的数据吗?

13、ConcurrentHashMap的工作原理
ConcurrentHashMap在jdk 1.6和jdk 1.8实现原理是不同的。

jdk 1.6:
ConcurrentHashMap是线程安全的,但是与Hashtablea相比,实现线程安全的方式不同。Hashtable是通过对hash表结构进行锁定,是阻塞式的,当一个线程占有这个锁时,其他线程必须阻塞等待其释放锁。ConcurrentHashMap是采用分离锁的方式,它并没有对整个hash表进行锁定,而是局部锁定,也就是说当一个线程占有这个局部锁时,不影响其他线程对hash表其他地方的访问。
具体实现:ConcurrentHashMap内部有一个Segment.

jdk 1.8
在jdk 8中,ConcurrentHashMap不再使用Segment分离锁,而是采用一种乐观锁CAS算法来实现同步问题,但其底层还是“数组+链表->红黑树”的实现。

14、CyclicBarrier和CountDownLatch区别
这两个类非常类似,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:

CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行。
CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务
CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了。

15、java中的++操作符线程安全么?
不是线程安全的操作。它涉及到多个指令,如读取变量值,增加,然后存储回内存,这个过程可能会出现多个线程交差。

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

  • 给线程命名
  • 最小化同步范围
  • 优先使用volatile
  • 尽可能使用更高层次的并发工具而非wait和notify()来实现线程通信,如BlockingQueue,Semeaphore
  • 优先使用并发容器而非同步容器.
  • 考虑使用线程池
-------------本文结束感谢您的阅读-------------