多线程生产者消费者问题详解-附面试题大全【精】
多线程
生产者与消费者(线程通信)
实现生产者和消费者案例(一)
public class Resource {
//定义布尔类型的成员,标志位,指示线程该做什么
//false没有,需要生产, true需要消费
boolean flag = false;
int count ;//包子的计数器
}
public class Product implements Runnable {
//创建资源对象
Resource r = new Resource();
public void run() {
while(true) {
//对象资源进行操作,判断变量
if(r.flag == true) {
//没有被消费,不能生产,等待
try{wait();}catch(Exception ex) {ex.printStackTrace();}
}
//可以生产
r.count++;
System.out.println("生产了第"+r.count+"个");
//修改标志位
r.flag = true;//可以消费
//唤醒消费的线程
notify();
}
}
}
public class Customer implements Runnable{
//创建资源对象
Resource r = new Resource();
public void run() {
while(true) {
//判断标志位
if(r.flag == false) {
//需要生产,不能消费,等待
try{wait();}catch(Exception ex) {ex.printStackTrace();}
}
//可以消费
System.out.println("消费了第"+r.count+"个");
//修改标志位,已经消费,可以生产
r.flag = false;
//唤醒生产线程
notify();
}
}
}
以上程序启动线程后,抛出异常 java.lang.IllegalMonitorStateException(运行时期的异常) 异常称为无效的监视器状态异常. 使用了线程的方法,来自于Object类: wait() notify() , 方法必须出现在同步中
解决异常问题,使用同步代码块
实现生产者和消费者案例(二)
添加了同步代码块后,程序居然停止不动了
分析原因 : notify()方法的时候,没有唤醒任何一个线程,导致的生产和消费的两个线程都处于wait()状态
两个方向的线程,使用的锁不是一个
解决办法 : 生产者线程和消费者线程,使用同一个对象锁, wait() notify()方法调用者,必须是锁对象
public class Product implements Runnable {
//创建资源对象
Resource r ;
public Product(Resource r) {
this.r = r;
}
public void run() {
while(true) {
synchronized(r) {
//对象资源进行操作,判断变量
if(r.flag == true) {
//没有被消费,不能生产,等待
try{r.wait();}catch(Exception ex) {ex.printStackTrace();}
}
//可以生产
r.count++;
System.out.println("生产了第"+r.count+"个");
//修改标志位
r.flag = true;//可以消费
//唤醒消费的线程
r.notify();
}
}
}
}
public class Customer implements Runnable{
//创建资源对象
Resource r ;
public Customer(Resource r) {
this.r = r;
}
public void run() {
while(true) {
synchronized(r) {
//判断标志位
if(r.flag == false) {
//需要生产,不能消费,等待
try{r.wait();}catch(Exception ex) {ex.printStackTrace();}
}
//可以消费
System.out.println("消费了第"+r.count+"....个");
//修改标志位,已经消费,可以生产
r.flag = false;
//唤醒生产线程
r.notify();
}
}
}
}
线程的方法问题:
Object类的方法wait(), Thread类的方法sleep()
- 问题 : 为什么等待唤醒的方法,定义在了Object类中,而不是Thread
- 同步锁导致,任何对象都能做为锁,保证任何一个锁对象都能调用等待与唤醒
- wait()和sleep()方法区别
- wait()只能出现同步中,必须是锁对象调用
- sleep()方法可以随时使用,比依赖同步
- wait()方法释放同步锁,被唤醒后,重写获取锁
- sleep()方法不释放同步锁
实现生产者和消费者案例(三)
私有修饰成员变量,提供方法访问
public class Resource {
//定义布尔类型的成员,标志位,指示线程该做什么
//false没有,需要生产, true需要消费
private boolean flag = false;
private int count ;//包子的计数器
//提供方法,生产
public synchronized void proruct() {
//判断变量=true,不能生产,等待
if(flag == true) {
try{this.wait();}catch(Exception ex) {ex.printStackTrace();}
}
count++;
System.out.println("...生产了第"+count+"个");
//修改标志位
flag = true;
//唤醒消费线程
this.notify();
}
//提供方法,消费
public synchronized void customer() {
//判断变量=false,不能消费,等待
if(flag == false) {
try{this.wait();}catch(Exception ex) {ex.printStackTrace();}
}
System.out.println("消费了第"+count+"个");
//修改标志位
flag = false;
//唤醒生产线程
this.notify();
}
}
public class Product implements Runnable {
//创建资源对象
private Resource r ;
public Product(Resource r) {
this.r = r;
}
public void run() {
while(true)
r.proruct();
}
}
public class Customer implements Runnable{
//创建资源对象
private Resource r ;
public Customer(Resource r) {
this.r = r;
}
public void run() {
while(true)
r.customer();
}
}
多生产者与多消费者(线程通信)
notify() 方法唤醒的时候,往往是最先等待的
我们需要什么效果 : 生产线程唤醒的是消费线程,消费线程唤醒生产线程. 现在唤醒的有可能是本方线程(NO).
但是,线程是一个独立的方法栈,CPU去里面数据运行,线程之间不认识谁是本方,谁是对方
使用Object类的方法 notifyAll() 唤醒所有的线程
线程全部唤醒后,依然没有达到需要的结果,因为线程唤醒后会直接就运行了,不会在判断flag标志是什么,线程已经判断过了
线程全部唤醒后也不能立刻就执行,再次判断flag标志,允许生产再生产,如果不允许生产,继续等
public class Resource {
//定义布尔类型的成员,标志位,指示线程该做什么
//false没有,需要生产, true需要消费
private boolean flag = false;
private int count ;//包子的计数器
//提供方法,生产
public synchronized void proruct() {
//判断变量=true,不能生产,等待
while(flag == true) {
try{this.wait();}catch(Exception ex) {ex.printStackTrace();}
}
count++;
System.out.println("...生产了第"+count+"个");
//修改标志位
flag = true;
//唤醒全部的线程
this.notifyAll();
}
//提供方法,消费
public synchronized void customer() {
//判断变量=false,不能消费,等待
while(flag == false) {
try{this.wait();}catch(Exception ex) {ex.printStackTrace();}
}
System.out.println("消费了第"+count+"个");
//修改标志位
flag = false;
//唤醒全部的线程
this.notifyAll();
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vjUnTUF5-1596026465193)(images/多生产和多消费_2.JPG)]
多生产者与多消费者问题分析
- notifyAll()唤醒的全部的线程,资源的浪费
- 能不能只唤醒对方的一个
- wait(),notify(),notifyAll() 都是本地方法,C++编写方法,和操作系统交互
- 通知操作系统,将线程挂起不要允许, 通知操作系统,让线程继续允许
- 操作系统找CPU来实现线程的等待和唤醒
- 频繁的和操作系统交互,降低程序的效率
多生产者与多消费者(线程通信)改进
JDK1.5出现接口Lock, 方法lock(),unlock()
接口出现目的取代synchronized
Lock接口
接口中定义方法 :
-
Condition newCondition()
返回一个Condition的接口类型- Condition接口取代Object类的监视器方法
Condition接口
作用 : 线程的阻塞队列,本身是一个容器,存储的是线程.
进入到队列的线程,释放同步锁
一个锁Lock接口对象,可以产生出多个线程的阻塞队列.让线程释放同步锁后,进入到队列
需要唤醒线程的时候,指定唤醒哪一个队列中的线程
好处 : 不会全部唤醒, 不会和操作系统的进行交互,提高的效率
Condition接口方法 :
-
void await()
线程释放锁,并进去到线程的阻塞队列 , 取代了Object类的方法wait() -
void singal()
线程从阻塞队列出来,获取锁在运行 , 取代了Object类的方法notify()
public class Resource {
private boolean flag = false;
private int count ;
//创建Lock接口的实现类对象,做为锁,去掉synchronized
private Lock lock = new ReentrantLock();
//通过这个锁对象,产生出2个存储线程的容器 (阻塞队列)
//Lock接口方法 newCondition()
private Condition proCondition = lock.newCondition();//线程的阻塞队列,生产队列
private Condition customerCondition = lock.newCondition(); //线程的阻塞队列,消费队列
//提供方法,生产
public void proruct() {
//获取锁
lock.lock();
//判断变量=true,不能生产,等待
while(flag == true) {
//线程释放锁,到阻塞队列中呆着
try{proCondition.await();}catch(Exception ex) {ex.printStackTrace();}
}
count++;
System.out.println("...生产了第"+count+"个");
//修改标志位
flag = true;
//唤醒线程,必须要唤醒消费队列中的线程
customerCondition.signal();
//释放锁
lock.unlock();
}
//提供方法,消费
public void customer() {
//获取锁
lock.lock();
//判断变量=false,不能消费,等待
while(flag == false) {
//线程等待,释放锁,进入队列
try{customerCondition.await();}catch (Exception ex) {ex.printStackTrace();}
}
System.out.println("消费了第"+count+"个");
//修改标志位
flag = false;
//唤醒线程,生产队列中的线程
proCondition.signal();
lock.unlock();
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ziRL6DPr-1596026465195)(images/线程的阻塞队列.JPG)]
线程池
概念 : 缓冲池是为了提高效率使用的. 线程池也是缓冲池的一种.(连接池,对象池)
为什么要出现线程池技术 : 创建线程是要和操作系统进行交互的,线程允许完毕( run()方法结束 ).频繁的创建线程,大量的浪费操作系统的资源. 为了解决资源消耗和提高效率问题,人们设计出了线程池.
线程池的思想 : 创建一些线程,存储在一个容器中,不要让线程销毁,需要的时候拿一个线程出来,线程执行完任务的时候,放回到容器中. 存储线程的容器,线程池
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UAu96ayB-1596026465198)(images/线程池的实现原理.jpg)]
JDK内置线程池
java.util.concurrent.Executors
工厂类,创建对象, 创建线程池对象
创建线程池对象的方法 :
static ExecutorService newFixedThreadPool(int n)
创建具有固定个数的线程池,返回的是接口类型,实现类就是创建的线程池的对象
java.util.ExecutorService
接口中的方法submit( Runnable r ) 提交线程执行的任务
java.util.concurrent.Callable
接口,视为Runnable接口的扩展
接口的抽象方法: V call() 具有返回值的方法,而且还能抛出异常. 此方法做为线程的任务使用,可以获取到方法的返回值
线程池提交线程任务方法 submit(Callable c)传递接口实现类, 提交任务的方法submit有返回值
返回的是Futrue的接口类型,接口的实现类,就是线程允许的结果返回值
public class MyCallable implements Callable<String>{
public String call() throws Exception{
return "线程的执行结果";
}
}
public static void main(String[] args) throws Exception {
ExecutorService es = Executors.newFixedThreadPool(2);
//提交线程任务 submit, 接收submit方法返回值,接口类型
Future<String> f = es.submit(new MyCallable());
//Futrue接口的方法 get()拿到线程的计算结果
String string = f.get();
System.out.println(string);
}
线程池的异步计算
public class MyCallable implements Callable<Integer>{
private int a;
public MyCallable(int a) {
this.a = a;
}
public Integer call() {
int sum = 0;
for(int i = 1 ; i <= a; i++) {
sum = sum + i;
}
return sum;
}
}
public static void main(String[] args) throws Exception{
//创建2个固定个数的线程池
ExecutorService es = Executors.newFixedThreadPool(2);
//提交任务
Future<Integer> f = es.submit(new MyCallable(100));
System.out.println(f.get());
//es.submit(task);
f = es.submit(new MyCallable(200));
System.out.println(f.get());
}
ConcurrentHashMap
java.util.concurrent.ConcurrentHashMap
实现Map接口,是键值对的集合
集合特点 :
- 底层哈希表结构
- 保证键的唯一性,键对象重写hashCode和equals
- 是线程安全的集合,半安全
- 不更改集合中的元素,不会锁定 (synchronized)
- 改变的元素,使用同步锁
- 操作的是哪个数组的索引,锁定当前的数组索引
- 不会出现并发修改异常
- 不能存储null值,null键
public static void main(String[] args) {
// HashMap<String, String> map = new HashMap<String, String>();
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<String, String>();
map.put("a", "1");
map.put("b", "2");
map.put("c", "3");
Set<String> set = map.keySet();
Iterator<String> it = set.iterator();
while(it.hasNext()) {
String key = it.next(); map.put("d", "4");
System.out.println(key + map.get(key));
}
}
public static void main(String[] args)throws Exception {
// HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<Integer, Integer>();
//集合存储2000个元素
for(int i = 1; i <= 2000; i++) {
map.put(i, 0);
}
//线程,删除Map集合中的前500
new Thread( new Runnable() {
public void run() {
for(int i = 1 ; i <= 500 ;i++) {
map.remove(i);
}
}
} ) .start();
//线程,删除Map集合,501-1000
new Thread( new Runnable() {
public void run() {
for(int i = 501 ; i <= 1000 ;i++) {
map.remove(i);
}
}
} ) .start();
Thread.sleep(2000);
System.out.println(map.size());
}
{
map.remove(i);
}
}
} ) .start();
Thread.sleep(2000);
System.out.println(map.size());
}
## 原子操作
原子操作,就是不可分割的操作
i++ 非原子操作,出现线程的安全问题
### AtomicInteger
整数的原子类,实现对整数的操作保证线程的安全
```java
public static void main(String[] args) throws InterruptedException {
//创建原子类对象, 内部变量默认是0
AtomicInteger atomicInteger = new AtomicInteger(100);
//变量自增
while(true) {
int i = atomicInteger.getAndIncrement();
System.out.println(i);
Thread.sleep(100);
}
}
面试题
1. 什么是线程?
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对 运算密集型任务提速。比如,如果一个线程完成一个任务要100毫秒,那么用十个线程完成改任务只需10毫秒。Java在语言层面对多线程提供了卓越的支 持,它也是一个很好的卖点。
2. 线程和进程有什么区别?
线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。
3. 如何在Java中实现线程?
在语言层面有三种方式。java.lang.Thread 类的实例就是一个线程但是它需要调用java.lang.Runnable接口来执行,由于线程类本身就是调用的Runnable接口所以你可以继承 java.lang.Thread 类或者直接调用Runnable接口来重写run()方法实现线程。第三种 实现Callable<>接口并重写call方法。
4. 用Runnable还是Thread?
这个问题是上题的后续,大家都知道我们可以通过继承Thread类或者调用Runnable接口来实现线程,问题是,那个方法更好呢?什么情况下使 用它?这个问题很容易回答,如果你知道Java不支持类的多重继承,但允许你调用多个接口。所以如果你要继承其他类,当然是调用Runnable接口好 了。
6. Thread 类中的start() 和 run() 方法有什么区别?
start()方法被用来启动新创建的线程,而且start()内部 调用了run()方法,这和直接调用run()方法的效果不一样。当你单独调用run()方法的时候,它只是一个普通的函数,只会是在原来的线程中调用,没有新的线程启动,也就无法形成多线程,只有start()方法才会启动新线程。
7. Java中Runnable和Callable有什么不同?
Runnable和Callable都代表那些要在不同的线程中执行的任务。Runnable从JDK1.0开始就有了,Callable是在 JDK1.5增加的。它们的主要区别是Callable的 call() 方法可以返回值和抛出异常,而Runnable的run()方法没有这些功能。Callable可以返回装载有计算结果的Future对象。
8. Java中的volatile 变量是什么?
volatile是一个特殊的修饰符,只有成员变量才能使用它。保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
11. 什么是线程安全?Vector是一个线程安全类吗?
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量 的值也和预期的是一样的,就是线程安全的。一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现计算失误。很显然你可以将集合类分 成两组,线程安全和非线程安全的。Vector 是用同步方法来实现线程安全的, 而和它相似的ArrayList不是线程安全的。且通过查看源码得知Vector内部的有些方法比ArrayList多一个synchronized的关键字,保证了线程的同步,也就保证了线程的安全。
12. Java中notify 和 notifyAll有什么区别?
这又是一个刁钻的问题,因为多线程可以等待单监控锁,Java API 的设计人员提供了一些方法当等待条件改变的时候通知它们,但是这些方法没有完全实现。notify()方法不能唤醒某个具体的线程,所以只有一个线程在等 待的时候它才有用武之地。而notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。但是由于每次调用notifyAll()方法唤醒的全部的线程,频繁的和操作系统交互,资源很浪费,大大降低了程序的效率,所以一般也不建议使用
17. 为什么wait, notify 和 notifyAll这些方法不在thread类里面?
这是个设计相关的问题,它考察的是面试者对现有系统和一些普遍存在但看起来不合理的事物的看法。回答这些问题的时候,你要说明为什么把这些方法放在 Object类里是有意义的,还有不把它放在Thread类里的原因。一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通 过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁 就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。
18. 线程池中 submit()和 execute()方法有什么区别?
- 接收的参数不一样
- submit有返回值,而execute没有
- submit方便Exception处理
19. 什么是FutureTask?
在Java并发程序中FutureTask表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完 成的时候结果才能取回,如果运算尚未完成get方法将会阻塞。一个FutureTask对象可以对调用了Callable和Runnable的对象进行包 装,由于FutureTask也是调用了Runnable接口所以它可以提交给Executor来执行。
20. 为什么wait和notify方法要在同步块中调用?
主要是因为Java API强制要求这样做,如果你不这么做,你的代码会抛出IllegalMonitorStateException(无效的监视器异常)。还有一个原因是为了避免wait和notify之间产生竞态条件。
21. 为什么你应该在循环中检查等待条件?
处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。因此,当一个等待线程醒来 时,不能认为它原来的等待状态仍然是有效的,在notify()方法调用之后和等待线程醒来之前这段时间它可能会改变。这就是在循环中使用wait()方 法效果更好的原因,你可以在Eclipse中创建模板调用wait和notify试一试。如果你想了解更多关于这个问题的内容,我推荐你阅读《Effective Java》这本书中的线程和同步章节。
22. Java中堆和栈有什么不同?
为什么把这个问题归类在多线程和并发面试题里?因为栈是一块和线程紧密相关的内存区域。每个线程都有自己的栈内存,用于存储本地变量,方法参数和栈 调用,一个线程中存储的变量对其它线程是不可见的。而堆是所有线程共享的一片公用内存区域。对象都在堆里创建,为了提升效率线程会从堆中弄一个缓存到自己 的栈,如果多个线程使用该变量就可能引发问题,这时volatile 变量就可以发挥作用了,它要求线程从主存中读取变量的值。
23. 什么是线程池? 为什么要使用它?
创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。为了避免这些问题,在程序启动的时 候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。从JDK1.5开始,Java API提供了Executor框架让你可以创建不同的线程池。比如单线程池,每次处理一个任务;数目固定的线程池或者是缓存线程池(一个适合很多生存期短 的任务的程序的可扩展线程池)。
24. Java中synchronized 和 ReentrantLock 有什么不同?
Java在过去很长一段时间只能通过synchronized关键字来实现互斥,它有一些缺点。比如你不能扩展锁之外的方法或者块边界,尝试获取锁 时不能中途取消等。Java 5 通过Lock接口提供了更复杂的控制来解决这些问题。 ReentrantLock 类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义且它还具有可扩展性。
25. 如何强制启动一个线程?
这个问题就像是如何强制进行Java垃圾回收,目前还没有觉得方法,虽然你可以使用System.gc()来进行垃圾回收,但是不保证能成功。在Java里面没有办法强制启动一个线程,它是被线程调度器控制着且Java没有公布相关的API。
26. 在 java 程序中怎么保证多线程的运行安全?
- 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
- 可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
- 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序。
27. ThreadLocal 是什么?有哪些使用场景?
线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。
28. 说一下 synchronized 底层实现原理?
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
- 普通同步方法,锁是当前实例对象
- 静态同步方法,锁是当前类的class对象
- 同步方法块,锁是括号里面的对象
29. synchronized 和 volatile 的区别是什么?
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
- synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
30. synchronized 和 Lock 有什么区别?
- 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
- synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
- synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
- 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
- synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可);
- Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
31. 说一下 runnable 和 callable 有什么区别?
有点深的问题了,也看出一个Java程序员学习知识的广度。
Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
本文地址:https://blog.csdn.net/debugEDM/article/details/107674768
上一篇: java基础(集合简单了解)
下一篇: 生活与工作之中的幽默总结