多线程专题复习整理
1. 线程与进程
①进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间。
②线程:是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行一个进程最少有一个线程 。线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分 成若干个线程。
③线程调度分为两种:首先是分时调度,所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。第二种是抢占式调度,优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性), Java使用的为抢占式调度。CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核新而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的 使 用率更高。
2. 同步与异步&并发与并行
①同步:排队执行 , 效率低但是安全
②异步:同时执行 , 效率高但是数据不安全
③并发:指两个或多个事件在同一个时间段内发生。
④并行:指两个或多个事件在同一时刻发生(同时发生)。
3. 继承Thread
4. 实现Runnable
与继承Thread相比的好处:
①通过创建任务,然后给线程分配的方式来实现的多线程,更适合多个线程同时执行相同任务的情况.。
②可以避免单继承所带来的局限性(即在上图所示的MyRunnable类后,与Thread相比依旧可以继续继承其他类)
③任务与线程本身是分离的,提高了程序的健壮性。
④线程池技术接受Runnable类型的任务,不接收Thread类型的线程
补充:通过匿名内部类实现Runnable,如下图所示
5. Thread类
常用方法:
设置和获取线程名称:
6 . 线程阻塞
阻塞状态指的是,代码不行继续执行,而在等待,阻塞解除后,重新进入就绪状态。
阻塞的方法有四种:一、sleep()方法,是占用资源在睡觉的,可以限制等待多久;二、wait() 方法,和 sleep() 的不同之处在于,是不占用资源的,限制等待多久;三、join() 方法,加入、合并或者是插队,这个方法阻塞线程到另一个线程完成以后再继续执行;四、有些 IO 阻塞,比如 write() 或者 read() ,因为IO方法是通过操作系统调用的。上面这些方法和start() 一样,不是说调用了就立即阻塞了,而是看CPU。
7. 线程的中断
调用interrupt()方法后,如下图所示:
给线程添加中断标记
在进行如下操作时会去检查标记,如若发现则会抛出异常进入catch块。
如若想让线程因此“死亡”,则可在catch块中添加return;代码。
8. 守护线程
线程默认是用户线程,如若想创建守护线程,则应进行如下操作:
这样会造成当最后一个用户线程结束时,不论守护线程是否把任务执行结束,都会自我结束。
9. 线程安全问题
解决方案一:同步代码块
解决方案二:同步方法
用synchronized,如下图所示
如果该方法被static修饰,则锁对象为 类名.class
如果没有被static修饰,则为this。
解决方案三:显示锁Lock(子类:ReentrantLock)
首先创建对象
调用lock()方法则可进行“加锁”
调用unlock()方法则可进行解锁
10. 公平锁与非公平锁
java中默认都为非公平锁,如若想要创建得到公平锁则才创建ReentrantLock对象时传入参数true即可。
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
11. 线程死锁
死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
死锁产生的原因:
①系统资源的竞争通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在 运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可剥夺资源的竞争 才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的。
②进程推进顺序非法进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都 会因为所需资源被占用而阻塞。
③信号量使用不当也会造成死锁。进程间彼此相互等待对方发来的消息,结果也会使得这 些进程间无法继续向前推进。例如,进程A等待进程B发的消息,进程B又在等待进程A 发的消息,可以看出进程A和B不是因为竞争同一资源,而是在等待对方的资源导致死锁。
防止死锁的常用方法:在任何有可能产生锁的方法里不要再去调用其他也可能产生锁的方法。
12. 多线程通信问题,生产者与消费者问题
public class Demo { public static void main(String[] args){ Food f=new Food(); new Cook(f).start(); new Waiter(f).start(); } static class Cook extends Thread{ private Food f; public Cook(Food f){ this.f=f; } @Override public void run() { for(int i=0;i<100;i++){ if(i%2==0){ f.setNameAndTaste("南瓜粥", "微甜"); }else{ f.setNameAndTaste("口水鸡","麻辣"); } } } } static class Waiter extends Thread{ private Food f; public Waiter(Food f) { this.f = f; } @Override public void run() { for(int i=0;i<100;i++){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } f.get(); } } } static class Food{ private String name; private String taste; public void setNameAndTaste(String name,String taste){ this.name=name; try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } this.taste=taste; } public void get(){ System.out.println("服务员端走的菜名是"+name+",味道:"+taste); } } }
一、不做任何处理时
运行可能产生的结果如下:
即setNameAndTaste()方法中刚对name赋值完成时,服务员线程就开始调用get()方法。
二、即使对setNameAndTaste()方法和get()方法添加synchronized修饰也无法解决问题,只会导致出现当锁解除后,可能继续调用setNameAndTaste()方法,而不是get()方法。
三、
public class Demo { public static void main(String[] args){ Food f=new Food(); new Cook(f).start(); new Waiter(f).start(); } static class Cook extends Thread{ private Food f; public Cook(Food f){ this.f=f; } @Override public void run() { for(int i=0;i<100;i++){ if(i%2==0){ f.setNameAndTaste("南瓜粥", "微甜"); }else{ f.setNameAndTaste("口水鸡","麻辣"); } } } } static class Waiter extends Thread{ private Food f; public Waiter(Food f) { this.f = f; } @Override public void run() { for(int i=0;i<100;i++){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } f.get(); } } } static class Food{ private String name; private String taste; //true代表厨师可以生产做菜 //private boolean flag=true; public synchronized void setNameAndTaste(String name,String taste){ //if(flag){ this.name=name; try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } this.taste=taste; //flag=false; this.notifyAll(); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } //} } public synchronized void get(){ //if (!flag) { System.out.println("服务员端走的菜名是"+name+",味道:"+taste); //flag=true; this.notifyAll(); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } //} } } }
将代码进行如上的修改即可解决问题。
13. 线程的六种转态
① 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
②运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
③阻塞(BLOCKED):表示线程阻塞于锁。
④等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
⑤超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
⑥终止(TERMINATED):表示该线程已经执行完毕。
14. 带返回值的线程Callable
使用步骤:
Runnable 与 Callable的相同点:
都是接口,都可以编写多线程程序,都采用Thread.start()启动线程 。
Runnable 与 Callable的不同点:
Runnable没有返回值,Callable可以返回执行结果,Callable接口的call()允许抛出异常,Runnable的run()不能抛出。
补充:Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执 行,如果不调用不会阻塞。
15. 线程池
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程 就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间. 线程池就是一个容纳多个线程的容 器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
线程池分为四类:
一、. 缓存线程池(长度无限制) *
执行流程: ①判断线程池是否存在空闲线程
②存在则使用
③不存在,则创建线程 并放入线程池, 然后使用
使用示例如下:
二、定长线程池(长度是指定的数值)
执行流程: ① 判断线程池是否存在空闲线程
②存在则使用
③不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
④不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
使用示例如下:
三、单线程线程池(效果与定长线程池 创建时传入数值1 效果一致.)
执行流程: ①判断线程池 的那个线程 是否空闲
② 空闲则使用
③不空闲,则等待 池中的单个线程空闲后使用
使用示例如下:
四、周期性任务定长线程池
执行流程: ①判断线程池是否存在空闲线程
②存在则使用
③不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
④不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
使用示例可分为以下两种:
15. Lambda表达式
上面两种代码编程方式所得到的结果是一样的,而第二种方法就是Lambda表达式。
下面再举一个例子:
首先是没有运用Lambda表达式的代码书写情况:
如若将main方法中对print方法的调用改成
就实现了Lambda表达式。