慕课网java并发课程体系面试题总结

面试高频考点
1.有多少种实现线程的方法?典型错误答案和正确答案
答:实现线程方式有两种,一种继承Thread类,一种实现Runnable接口。
典型错误观点
(1).线程池创建线程也算是一种新建线程的方式
(2).通过Callable和FutureTask创建线程,也算是一种新建线程的方式
(3).无返回值是实现runnable接口,有返回值是实现callable接口,所以callable是新的实现线程的方法
(4).定时器
(5).匿名内部类
(6)Lambda表达式
以上错误观点本质上还是通过实现Runnable接口和继承Thread类。

2.实现Runnable接口和继承Thread类哪种方式更好?
答:实现Runnable接口更好。
1.从代码架构角度:具体的任务(run方法)应该和“创建和运行线程的机制(Thread类)”解耦,用runnable对象可以实现解耦。
2.由于Java语言不支持多继承,这样就无法再继承其他的类,限制了可扩展性。

3.一个线程两次调用start()方法会出现什么情况?为什么?
答:会抛出一个异常IllegalThreadStateException异常,因为在启动线程时候,源码中会对线程状态进行判断,必须是new状态的线程才能正常启动。

4.既然start()方法会调用run()方法,为什么我们选择调用start()方法,而不是直接调用run()方法呢?
答:当你调用 start() 方法时,它会新建一个线程然后执行 run() 方法中的代码。如果直接调用 run() 方法,并不会创建新线程,和调用的普通方法没有区别。

5.如何正确停止一个线程?
答:应该用interrupt方法来标志中断的线程。
在这里插入图片描述
具体停止还需要被停止线程来配合。

6.如何处理不可中断的阻塞(例如抢锁时ReentrantLock.lock()或者Socket I/O时无法响应中断,那应该怎么让该线程停止呢? )
答:通过ReentrantLock的lock()方法或者Synchronized持有锁的线程是不会响应其他线程的interrupt()方法的,直到该方法主动释放锁之后才会响应interrupt()方法。并且对阻塞如FILE,SOCKET的BIO模式也是不可被中断的。如果要想阻塞中响应中断可以用java.util.concurrent.locks包下的ReentrantLock中的lockInterruptibly()方法使得线程可以在被阻塞时响应中断。
7.线程有哪几种状态?生命周期是什么?
new,runnable,blocked,waiting,timed_waiting,terminated(结束)
在这里插入图片描述
8.如何用wait()实现两个线程交替打印0~ 100的奇偶数?

public class Test5 {

	static int count = 0;
	public static void main(String[] args) throws InterruptedException {
		//第一种实现 方法:效率高
		Number number = new Number();
		Thread thread1 = new Thread(number);
		thread1.setName("偶数");
		Thread thread2 = new Thread(number);
		thread2.setName("奇数");
		thread1.start();
	    Thread.sleep(1);
        thread2.start();
		//第二种实现 方法:效率低
		
//		new Thread(new Runnable() {
//			@Override
//			public void run() {
//				// TODO Auto-generated method stub
//				while(count<=100) {
//					synchronized (object) {
//						if((count & 1) == 0) {
//							System.out.println(Thread.currentThread().getName()+":"+count++);
//						}
//					}
//				}
//			}
//		},"偶数").start();
//		
//		new Thread(new Runnable() {
//			@Override
//			public void run() {
//				// TODO Auto-generated method stub
//				while(count< 100) {
//					synchronized (object) {
//						if((count & 1) == 1) {
//							System.out.println(Thread.currentThread().getName()+":"+count++);
//						}
//					}
//				}
//			}
//		},"奇数").start();
		
		
	}
}

class Number implements Runnable{

	private int number;
	private static Object object = new Object();
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(number<=100) {
			
			synchronized (object) {
				System.out.println(Thread.currentThread().getName()+":"+number++);
				object.notify();
				try {
					if(number<=100) {
						object.wait();
					}
					
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			
		}
		
	}
	
}

为什么说第二种效率低呢,因为它会来回竞争锁,虽然你竞争到了,但是如果不是顺着偶数或者奇数线程,也是不会打印的,意思是会产生很多无效的竞争。

9.为什么要使用生产者和消费者模式?
答:主要是为了解耦:假设生产者和消费者分别是两个类。如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合)。将来如果消费者的代码发生变化,可能会影响到生产者。而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了。.
10.什么是生产者消费者模式。
答:生产者消费者模式:通过一个容器来解决生产者和消费者的强耦合关系,这个容器就是仓库,生产者生成数据无需等待消费者索取,消费者无需直接索要数据。两者并不进行任何通讯,而是通过容器来进行操作
作用:解耦、支持并发、支持忙闲不均。
在这里插入图片描述
11.如何用wait实现生产者模式?

public class ProducerConsumerModel {
    public static void main(String[] args) {
        EventStorage eventStorage = new EventStorage();
        Producer producer = new Producer(eventStorage);
        Consumer consumer = new Consumer(eventStorage);
        new Thread(producer).start();
        new Thread(consumer).start();
    }
}

class Producer implements Runnable {

    private EventStorage storage;

    public Producer(
            EventStorage storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
    	while(true) {
    		 storage.put();
    	}
    }
}

class Consumer implements Runnable {

    private EventStorage storage;

    public Consumer(
            EventStorage storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        while(true) {
   		 storage.take();
   	}
    }
}

class EventStorage {

    private int maxSize;
    private LinkedList<Date> storage;

    public EventStorage() {
        maxSize = 10;
        storage = new LinkedList<>();
    }

    public synchronized void put() {
        while (storage.size() == maxSize) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        storage.add(new Date());
        System.out.println("仓库里有了" + storage.size() + "个产品。");
        notify();
    }

    public synchronized void take() {
        while (storage.size() == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("拿到了" + storage.poll() + ",现在仓库还剩下" + storage.size());
        notify();
    }
}

12.为什么wait必须在同步代码块中使用?
答:Java设计者为了避免使用者出现lost wake up问题而搞出来的,什么是lost wake up呢,看如下图:
在这里插入图片描述
现在有两个线程生产者消费者,此时消费者首先抢到cpu资源,发现count = 0 ,他准备去休息了,等待生产者生产出来通知他,而在多线程环境下,发生了上下文切换,此时生产者进来了,将count+1 ,然后唤醒消费者线程,而此时notify是无效的,因为wait()方法在notify()后面执行,消费者又继续等待去了。所以我们必须写在同步代码块中,防止多线程环境下的,上下文切换带来的线程安全。

13.为什么线程通信的方法wait(), notify()和notifyAll()被定义在Object类里?而sleep定义在Thread类里?
答: 因为在java中,wait(),notify()和notifyAll()属于锁级别的操作,而锁是属于某个对象的,所以决定当前对象的锁的方法就应该在对象中。而sleep针对于某个线程而不是对象。

14.wait方法是属于Object对象的,那调用Thread.wait会怎么样?
答:Thread也是个对象,这样调用也没有问题,但是Thread是个特殊的对象,线程退出的时候会自动执行notify,这样会是我们设计的流程受到干扰,所以我们一般不这么用。

15.如何选择用notify还是nofityAll ?
答:唤醒多个线程和一个线程的区别。notify的api文档说是随机唤醒一个线程,但是并不是这样,它是由hostspot虚拟机实现,也就是说不同虚拟机会有不同的结果。
16.notifyAll之后所有的线程都会再次抢夺锁,如果某线程抢夺失败怎么办?
答:没抢到锁的线程继续处于等待状态,等待锁的释放。

17.用suspend()和resume()来阻塞线程可以吗?为什么?
答:不可以,这两个方法被弃用了,推荐使用wait、notify。

18.wait、sleep的异同 (方法属于哪个对象?线程状态怎么切换? )
不同点: 1.wait释放锁 sleep不释放锁 2.wait必须写在同步代码块中,sleep不需要 3.wait()是Object类的方法, sleep()是Thread类的方法。 4.sleep方法短暂休眠之后会主动退出阻塞,而没有指定时间的wait方法则需要被其他线程中断后才能退出阻塞
相同点:1.wait和sleep方法都可以使线程阻塞,对应线程状态是Waiting或Time_Waiting。
2.wait和sleep方法都可以响应中断Thread.interrupt()。

19.在join期间,线程处于哪种线程状态?
答:阻塞状态,因为join源码中也是调用wait方法。
20.yield和sleep区别?
答:yield暂停当前正在执行的线程对象。yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。 而sleep是阻塞当前线程。

21.守护线程和普通线程的区别?
答:主要区别在于是否会让jvm退出,如果JVM中所有的线程都是守护线程,那么JVM就会退出,进而守护线程也会退出。如果JVM中还存在用户线程,那么JVM就会一直存活,不会退出。
22.我们是否需要给线程设置为守护线程?
答:不需要,没必要。
23.为什么程序设计不应依赖于线程优先级?
答:优先级是不稳定的,所以不应该依赖优先级。
24.讲讲Java异常体系?
在这里插入图片描述
25.run方法是否可以抛出异常?如果抛出异常,线程的状态会怎么样?
答:run方法不能抛出异常,他已经属于最顶层,必须对异常做出处理。
26.共有哪几类线程安全问题 ?
1.当多个线程共享一个全局变量,对其做写操作时,可能会受到其他线程的干扰,从而引发线程安全问题
2.死锁等活跃性问题(死锁,活锁,饥饿)也是属于线程安全问题
3.对象发布和初始化会存在线程安全问题

27.Java代码如何一 步步转化,最终被CPU执行?
答:它首先被编译器编译成class文件,然后由jvm解读成机器码指令,最终被cpu执行。
28.单例模式的作用和适用场景
答:作用是使得该类的一个对象成为系统中的唯一实例。
适用场景:1.有频繁实例化然后销毁的情况,也就是频繁的 new 对象,可以考虑单例模式;
2、创建对象时耗时过多或者耗资源过多,但又经常用到的对象;
3、频繁访问 IO 资源的对象,例如数据库连接池或访问本地文件;

29.单例模式多种写法、单例和高并发的关系?‘
答:有饿汉式,懒汉式,枚举,双重检查。单例模式注意需要私有化构造方法。在高并发下,使用单例模式,因为syn关键字会引起类资源的争用,实例化速度急剧降低,所以可用双重检查提升速度。

public class Singleton6 {

    private volatile static Singleton6 instance;

    private Singleton6() {

    }

    public static Singleton6 getInstance() {
        if (instance == null) {
            synchronized (Singleton6.class) {
                if (instance == null) {
                    instance = new Singleton6();
                }
            }
        }
        return instance;
    }
}

30.单例各种写法的适用场合?
答:推荐使用枚举和双重检查即可。

31.饿汉式的缺点和懒汉式各自缺点?
答:
1、时间和空间
懒汉式是典型的时间换空间,也就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间。
饿汉式是典型的空间换时间,当类装载的时候就会创建类实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断了,节省了运行时间。
2、线程安全
不加syn关键字的懒汉式存在线程安全问题。加了又有性能问题,所以可以使用双重检查。饿汉式不存在线程安全。

33.为什么要用double-check ?不用就不安全吗?
答:double-check即保证了线程安全,又提升了性能。
34.为什么双重检查模式要用volatile ?
答:因为new 对象不是一个原子操作,在new期间可能会发生重排序这个问题。而volatile作用不仅保证了可见性也禁止了指令重排序优化。由于 volatile禁止对象创建时指令之间重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性。

35.讲一讲什么是Java内存模型?
答:java内存模型(Java Memory Model,JMM)是java虚拟机规范定义的,用来屏蔽掉java程序在各种不同的硬件和操作系统对内存的访问的差异。 Java内存模型的主要目标是定义程序中变量的访问规则,即在虚拟机中将变量存储到主内存或者将变量从主内存取出这样的底层细节。 Java内存模型将内存抽象为工作内存和主内存,工作内存是每个线程私有的。此处的变量与Java编程时所说的变量不一样,指包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,不会被共享。

36.什么是happens-before ?
先行发生原则是Java内存模型中定义的两个操作之间的偏序关系。比如说操作A先行发生于操作B,那么在B操作发生之前,A操作产生的“影响”都会被操作B感知到。这里的影响是指修改了内存中的共享变量、发送了消息、调用了方法等。

37…为什么会有内存可见性问题?
答:因为cpu是多层缓存结构,多个线程间读写内存就会发生可见性问题,因为线程都是拷贝一份副本到自己工作内存(本地内存)中,然后从其中读取值,多线程下就可能因为刷新不及时导致出现可见性问题。

38.你知道主内存和本地内存吗?什么是主内存和本地内存?
答:java 的内存模型分为主内存和工作内存,所有线程共享主内存,每个线程都有自己的工作内存,是私有的。一个线程不能访问另一个线程的工作内存。线程之间通过主内存来实现线程兼间的通信。
线程的工作内存是所需变量的主内存的一份拷贝副本,一个线程对主内存的操作包括(读取、载入、使用,赋值、存储、写入)。

39.什么是原子操作?
答:如果一个操作全部执行,或者全部不执行就是原子操作。如 a = 5 是原子操作 ,a++不是原子操作。

40.Java中的原子操作有哪些?
答:1.除了long和double之外的基本类型的赋值操作。
2.所有引用的赋值操作
3.java.concurrent.Atomic *包中所有类的原子操作

41.long和double的原子性你了解吗?
答:因为long和double类型是64位的,所以它们的操作在32位机器上不算原子操作,而在64位的机器上是原子操作。

42.生成对象的过程是不是原子操作?
答:生成对象不是原子操作。一个new操作包括这几个指令:分配空间、初始化对象值、将对象指向空间,最关键的是这几个步骤间还有可能发生重排序,导致部分初始化情况出现,所以为了保证正确初始化,通常对象上要加volatile禁止重排。
43。写一个必然死锁的例子?

public class MustDeadLock implements Runnable {

    int flag = 1;

    static Object o1 = new Object();
    static Object o2 = new Object();

    public static void main(String[] args) throws InterruptedException {
        MustDeadLock r1 = new MustDeadLock();
        MustDeadLock r2 = new MustDeadLock();
        r1.flag = 1;
        r2.flag = 0;
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
       
    }

    @Override
    public void run() {
        System.out.println("flag = " + flag);
        for (int i = 0; i < 2; i++) {
        if (flag == 1) {
            synchronized (o1) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                	if(!Thread.currentThread().isInterrupted()) {
                		break;
                	}
                    System.out.println("线程1成功拿到两把锁");
                }
            }
        }
        if (flag == 0) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    System.out.println("线程2成功拿到两把锁");
                }
            }
        }
        }
    }
}

44.生产中什么场景下会发生死锁?
答:一个方法获取多个锁,很容易导致死锁。
45.发生死锁必须满足哪些条件?
答:1. 互斥条件:一个资源同时只能被一个进程或者线程享用,如果这个资源可以无限共享,那么这个资源就不是互斥的,就不会发生死锁。 2. 请求与保持条件:一个线程去请求第二把锁,但是同时又保持第一把锁不释放。
3. 不剥夺条件:通过外界某种干扰,强制释放掉某个锁,这叫剥夺,它就想一个裁判,而数据库就像一个裁判一样,当两个事务同时要锁,又发生死锁了,数据库就会剥夺其中一个,这样就不会发生死锁了。而如果满足不剥夺条件,就有可能发生死锁。
4. 循环等待条件:线程1等待的资源为 P1 他被线程2占有,而线程2等待资源 P2,被线程3占有,而线程3等待的资源p3又被线程1占有。构成一个环形。循环等待条件意味着请求与保持条件。

46.如何用工具定位死锁?
1.用jdk自带的jstack命令定位。
2.用java.lang.management.ThreadMXBean;的ThreadMXBean来定位死锁

47.有哪些解决死锁问题的策略?
答:打破发生死锁4个必要条件其中一个即可。如可以改变锁的顺序。让他们上锁的顺序必须一致 。

48.讲一讲经典的哲学家就餐问题?怎么解决?
答:哲学家就餐问题:五名哲学家围坐在一张圆桌旁,桌子中央有一盘通心面,每个人面前有一个空盘子,每两个人之间放一只筷子(每位哲学家进餐是一个进程,筷子是竞争的临界资源)。在进餐时,每个人只能从自己的左边和右边取筷子,要同时拿到两根筷子哲学家方能就餐;就餐完毕后,哲学家将手中的筷子放回原位,并进入思考状态;如果不能同时拿到两根筷子,哲学家进入饥饿状态;如果每位哲学家仅拿到一只筷子,都等待相邻哲学家放下筷子,每位哲学家将会都将会无休止的等待下去。
如何解决呢:换手策略,比如说大家筷子都是从左往右拿筷子,那么最后一位哲学家可以先拿右边的筷子再拿左边的。
代码演示:

public class ZheXueJiaEating {
	
	private static class ZheXueJia implements Runnable{

		private Object leftChopsticks = null;
		private Object rightChopsticks = null;
		
		
		public ZheXueJia(Object leftChopsticks, Object rightChopsticks) {
			this.leftChopsticks = leftChopsticks;
			this.rightChopsticks = rightChopsticks;
		}

		@Override
		public void run() {
			// TODO Auto-generated method stub
			while(true) {
				doAction("思考中");
				synchronized (leftChopsticks) {
					doAction("拿到左边的筷子");
					try {
						Thread.sleep(2000);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					synchronized (rightChopsticks) {
						doAction("拿到右边的筷子");
						
						doAction("开始进食");
					}
				}
			}
		}
		
		private void doAction(String str) {
			System.out.println(Thread.currentThread().getName()+":"+str);
			try {
				Thread.sleep((long) (Math.random()*10));
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
		}
		
	}
	public static void main(String[] args) {
		ZheXueJia[] zheXueJias = new ZheXueJia[5];
		Object[] chopsticks = new Object[zheXueJias.length];
		
		for (int i = 0; i < chopsticks.length; i++) {
			chopsticks[i] = new Object();
		}
		
		for (int i = 0; i < zheXueJias.length; i++) {
			Object leftChopsticks = chopsticks[i];
			Object rightChopsticks = chopsticks[(i+1)%chopsticks.length];
			ZheXueJia zheXueJias1 = null;
			//最后一位哲学家先拿左边的筷子,如果没有拿到就等待,这样不会发生死锁了
	        if(i == zheXueJias.length - 1) {
			 zheXueJias1 = new ZheXueJia(leftChopsticks, rightChopsticks);
			}else {
			 zheXueJias1 = new ZheXueJia(rightChopsticks, leftChopsticks);
			}
			new Thread(zheXueJias1,"哲学家"+(i+1)+"号").start();
		}
	}

}

49.实际开发中如何避免死锁?
1.设置超时时间
2.尽量使用并发包下的类
3.用不同的锁,而不是同一个锁
4.如果能使用同步代码块,就不使用同步方法,尽量自己指定锁对象
5.给线程取个有意义的名字,排查就很好定位
6.避免锁的嵌套
7.专锁专用,不要好几个功能引入同一把锁

50.活锁、饥饿和死锁有什么区别?
答:1,死锁 多个线程,各自占对方的资源,都不愿意释放,从而造成死锁
2.答:饥饿 多个线程访问同一个同步资源,有些线程总是没有机会得到此资源,这种就叫做饥饿。
出现饥饿的三种情况
a,高优先级的线程吞噬了低优先级的线程的CPU时间片
b,线程被永久阻塞在等待进入同步代码块的状态
c,等待的线程永远不被唤醒
3.活锁 该问题不会阻塞线程,但是也不能继续执行,因为线程将不断重复执行相同的操作,而且总是失败。
要解决这种活锁问题,需要在失败重试机制中引入随机性。

总结到此结束部分题目感觉不太会问到,就没有写上去。咳,搞了一天~~

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值