【java】java多线程及线程池面试题
目录
- 前言
- 线程是什么?多线程是什么?
- 多线程的作用和好处以及缺点
- 守护线程和用户线程
- 并发和并行的区别
- 一.线程的状态和常用方法
- 1.线程各种状态转化图
- 2.线程相关常用方法有
- ① wait()
- ② sleep(long timeout)
- ③ join()
- ④ yield()
- ⑤ notify()和notifyAll()
- 3.wait()和sleep()的区别?
- 4.为什么 wait()、notify()、notifyAll()方法定义在 Object 类里面,而不是 Thread 类?
- 三.实现多线程的方式
- 1.继承Thread类(只能继承一个类)
- 2.实现Runnable接口(可以实现多个接口)
- 3.使用Callable、FutureTask实现有返回结果的多线程
- 4.使用线程池--下面会详细讲解
- 5.附加问题:runnable和callable的区别
- 四.线程池(4大方法、7大参数、4种拒绝策略)
- 1.线程池的好处
- 2.七大参数(用银行柜台举例)
- 3.四大方法
- ① ExecutorService executor = Executors.newCachedThreadPool()
- ② ExecutorService executor = Executors.newFixedThreadPool()
- ③ ExecutorService executor = Executors.newSingleThreadPool()
- ④ ExecutorService executor = Executors.newScheduleThreadPool()
- 4.四大拒绝策略
- 五.线程死锁
- 1.什么是死锁?
- 2.进程死锁、饥饿、死循环的区别
- 3.造成死锁的四个必要条件
- 3.什么时候会发生死锁?
- 4.怎么解决死锁?
- ① 预防死锁
- ② 避免死锁
- ③ 死锁的检测和解决
- 死锁的检测
- 死锁的解决
- 如何决定"对谁动手(选择进程解决死锁)"
- 五.线程安全
- 1.线程安全主要是三方面
- 2.保证原子性
- 3.保证可见性
- 4.保证有序性
- 5.volatile和synchronized的区别
- 6.synchronized和lock的区别
前言
线程是什么?多线程是什么?
线程: 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流(比如一段程序、一个函数等)。
多线程: 多个线程并发执行的技术。
多线程的作用和好处以及缺点
作用: 充分利用CPU的资源,采用多线程的方法去同时完成几件事情而互不干扰。
好处:
① 使用线程可以把程序中占据时间长的任务放到后台去处理,如图片、视频的下载。
② 发挥多核处理器的优势,并发执行让系统运行的更快、更流畅,用户体验更好。
坏处
① 大量的线程降低代码的可读性。
② 更多的线程需要更多的内存空间。
③ 存在线程不安全问题。
守护线程和用户线程
守护线程: jvm给的线程。比如:GC线程。
用户线程: 用户自己定义的线程。比如:main线程。
扩展:
Thread.setDaemon(false)设置为用户线程
Thread.setDaemon(true)设置为守护线程
并发和并行的区别
**并发:**一个处理器同时处理多个任务。(一个人同时吃两个苹果)
**并行:**多个处理器同时处理多个任务。(两个人同时吃两个苹果)
一.线程的状态和常用方法
1.线程各种状态转化图
2.线程相关常用方法有
① wait()
- wait(): 线程等待,会释放锁 (用于同步代码块或同步方法中,不然会报错),然后进入等待状态和notify()和notifyAll()一起使用。
- wait(long timeout): 等待规定的时间,如果在规定时间内被唤醒就继续执行,如果超过规定时间也会继续向下执行。
public class TestWait implements Runnable {
private Object lock;
public TestWait(Object lock) {
this.lock = lock;
}
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
// 创建两个线程
Thread t1 = new Thread(new TestWait(lock),"t1");
Thread t2 = new Thread(new TestWait(lock),"t2");
t1.start();
t2.start();
}
@Override
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "准备进入等待状态");
try {
//唤醒等待中的线程,进入就绪状态
lock.notify();
//线程等待,释放锁
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "等待结束,继续执行");
}
}
}
运行结果:
t2准备进入等待状态
t1准备进入等待状态
t2等待结束,本线程继续执行
运行结果不唯一,但是总会有一个线程执行不完。
以上运行步骤如下:
1.t2线程运行,调用notify()方法唤醒等待中的线程(现在等待队列中没有线程),然后调用wait()方法进入等待队列。
2.t1线程运行,调用notify()方法唤醒等待中的线程(这时唤醒了t2线程,因为只有等待队列中只有t2,所以一定是t2获取cpu使用权),然后wait()方法进入等待队列。
3.t2线程继续向下运行,然后结束,t1还在等待队列,没有被唤醒,所以t1执行不完。
注意:被唤醒的线程,会从等待队列,进入就绪状态,然后去竞争cpu的使用权。
② sleep(long timeout)
- sleep(): 线程睡眠,不会释放锁(规定时间到了之后继续执行线程) 。一搬用于模拟网络延时。
public class TestSleep implements Runnable {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
// 创建两个线程
Thread t1 = new Thread(new TestSleep(),"t1");
Thread t2 = new Thread(new TestSleep(),"t2");
t1.start();
t2.start();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "准备进入等待状态");
try {
//线程睡眠两秒之后,继续执行,不会释放锁。
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "等待结束,继续执行");
}
}
运行结果:
t1准备进入睡眠状态
t2准备进入睡眠状态
t1睡眠结束,继续执行
t2睡眠结束,继续执行
以上结果不唯一。
③ join()
- join(): 指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。底层是利用wait()方法实现。可以应用于必须多个线程完成才能执行主线程,比如:三个人去酒店吃饭,三个都到酒店才能上菜。
正常执行
public class TestJoin {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() { //线程t1
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println("执行t1");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(new Runnable() { //线程t2
@Override
public void run() {
try {
Thread.sleep(3000);
System.out.println("执行t2");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
System.out.println("执行main");
}
}
运行结果:
执行main
执行t1
执行t2
让主线程(main)最后执行,让 t1 和 t2 先执行完成。
public class TestJoin {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() { //线程t1
@Override
public void run() {
try {
System.out.println("执行t1");
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(new Runnable() { //线程t2
@Override
public void run() {
try {
System.out.println("执行t2");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
t1.join(); //t1参与到当前线程(main)的执行中,所以主线程需要等待t1执行完成才会继续执行,但是t2不受影响、。
System.out.println("执行main");
}
}
运行结果:
执行t2
执行t1
执行main
或者
执行t1
执行t2
执行main
让 t1 和 t2 按顺序执行
public class TestJoin {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() { //线程t1
@Override
public void run() {
try {
System.out.println("执行t1");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(new Runnable() { //线程t2
@Override
public void run() {
try {
System.out.println("执行t2");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t1.join(); //t1参与到当前线程的执行中,所以主线程需要等待t1执行完成才会继续执行,这是t2需要等待t1执行完毕之后才能执行。
t2.start();
}
}
运行结果:
执行t1
执行t2
④ yield()
- yield(): 线程让步(正在执行的线程),会使线程让出cpu使用权,进入就绪状态。
public class TestYield {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() { //线程t1
@Override
public void run() {
System.out.println("执行t1");
Thread.yield();//t1让出cpu使用权限,回到就绪状态
System.out.println("执行结束t1");
}
});
Thread t2 = new Thread(new Runnable() { //线程t2
@Override
public void run() {
System.out.println("执行t2");
System.out.println("执行结束t2");
}
});
t1.start();
t2.start();
}
}
如果先执行t1,然后t1会让出线程,进入就绪状态,然后t2执行完成,t1再去竞争cpu的使用权,在继续执行。
⑤ notify()和notifyAll()
- notify(): 随机唤醒一个在等待状态的线程,进入就绪状态。
public class TestNotify implements Runnable{
static Object lock = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(new TestNotify(), "t1");
Thread thread2 = new Thread(new TestNotify(), "t2");
thread1.start();
thread2.start();
try {
Thread.sleep(1000); //主线程睡眠,让线程t1或t2执行
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " 获得了锁");
System.out.println("唤醒前" + thread1.getName() + "状态是" + thread1.getState());
System.out.println("唤醒前" + thread2.getName() + "状态是 " + thread2.getState());
lock.notify(); // 随机唤醒一个在等待状中的线程
}
try {
Thread.sleep(1000);//主线程睡眠,让线程t1或t2执行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("唤醒后" + thread1.getName() + "状态是" + thread1.getState());
System.out.println("唤醒后" + thread2.getName() + "状态是" + thread2.getState());
}
@Override
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " 开始执行");
try {
lock.wait(); // 进入等待状态
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 线程结束");
}
}
}
运行结果:
t1 开始执行
t2 开始执行
main 获得了锁
唤醒前t1状态是WAITING
唤醒前t2状态是 WAITING
t1 线程结束
唤醒后t1状态是TERMINATED
唤醒后t2状态是WAITING
运行结果不唯一,由上面可以看出,notify()随机唤醒了一个线程,唤醒的是t1,也有可能唤醒的是t2。然后没被唤醒的线程一直处于等待状态,这样就会导致程序结束不了。
- notifyAll(): 唤醒在等待队列的所有线程,进入就绪状态。
package com.navi.vpx;
public class TestNotify implements Runnable{
static Object lock = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(new TestNotify(), "t1");
Thread thread2 = new Thread(new TestNotify(), "t2");
thread1.start();
thread2.start();
try {
Thread.sleep(1000); //主线程睡眠,让线程t1或t2执行
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " 获得了锁");
System.out.println("唤醒前" + thread1.getName() + "状态是" + thread1.getState());
System.out.println("唤醒前" + thread2.getName() + "状态是 " + thread2.getState());
lock.notifyAll(); // 唤醒全部在等待状态的线程
}
try {
Thread.sleep(1000);//主线程睡眠,让线程t1或t2执行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("唤醒后" + thread1.getName() + "状态是" + thread1.getState());
System.out.println("唤醒后" + thread2.getName() + "状态是" + thread2.getState());
}
@Override
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " 开始执行");
try {
lock.wait(); // 进入等待状态
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 线程结束");
}
}
}
运行结果:
t1 开始执行
t2 开始执行
main 获得了锁
唤醒前t1状态是WAITING
唤醒前t2状态是 WAITING
t2 线程结束
t1 线程结束
唤醒后t1状态是TERMINATED
唤醒后t2状态是TERMINATED
运行结果不唯一,有上面可得notifyAll()唤醒的是所有在等待状态的线程,使线程都进入就绪状态,然后竞争锁,最后全部执行完成,程序结束。
3.wait()和sleep()的区别?
① wait() 来自Object,sleep()来自Thread。
② wait()会释放锁,sleep()不会释放锁。
③ wait()只能用在同步方法或代码块中,sleep()可以用在任何地方。
④ wait()不需要捕获异常,sleep()需要捕获异常。
4.为什么 wait()、notify()、notifyAll()方法定义在 Object 类里面,而不是 Thread 类?
① 锁可以是任何对象,如果在Thread类中,那只能是Thread类的对象才能调用上面的方法了。
② java中进入临界区(同步代码块或同步方法),线程只需要拿到锁就行,而并不关心锁被那个线程持有。
③ 上面方法是java两个线程之间的通信机制,如果不能通过类似synchronized这样的Java关键字来实现这种机制,那么Object类中就是定义它们最好的地方,以此来使任何Java对象都可以拥有实现线程通信机制的能力。
三.实现多线程的方式
1.继承Thread类(只能继承一个类)
public class MyThread extends Thread{
public void run(){
System.out.println("执行run()方法");
}
public static void main(String[] args) {
//创建对象
MyThread myThread = new MyThread();
//启动线程,执行run()方法
myThread.start();
}
}
2.实现Runnable接口(可以实现多个接口)
public class MyRunable implements Runnable{
//重写的run()方法
@Override
public void run() {
System.out.println("执行run()方法");
}
public static void main(String[] args) {
//创建对象
MyRunable myRunable = new MyRunable();
//创建线程对象
Thread thread = new Thread(myRunable);
//启动线程,执行run()方法
thread.start();
}
}
3.使用Callable、FutureTask实现有返回结果的多线程
public class MyCallable implements Callable<String> {
//重写的run()方法
@Override
public String call() {
System.out.println("执行run()方法");
return "call返回值";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建对象
MyCallable myCallable = new MyCallable();
//创建futureTask
FutureTask<String> futureTask = new FutureTask(myCallable);
//创建线程对象
Thread thread = new Thread(futureTask);
//开启线程,执行run()方法
thread.start();
//接收call()的返回值
String result = futureTask.get();
System.out.println(result);
}
}
4.使用线程池–下面会详细讲解
5.附加问题:runnable和callable的区别
① runnable没有返回值,callable有返回值。
②runnable 只能抛出异常,不能捕获,callable 能抛出异常,也能捕获。
四.线程池(4大方法、7大参数、4种拒绝策略)
1.线程池的好处
① 线程是稀缺资源,使用线程池可以减少线程的创建和销毁,每个线程都可重复使用。
② 可以根据系统的需求,调整线程池里面线程的个数,防止了因为消耗内存过多导致服务器崩溃。
2.七大参数(用银行柜台举例)
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize:核心线程数(最少开放的柜台数)
线程池维护的最小线程数,创建后不会被回收(除非:设置allowCoreThreadTimeout=true后,空闲的核心线程超过存活时间(keepAliveTime)也会被回收)。当线程池使用 executor() 方法执行一个新任务时,会先判断运行的线程是否超过核心线程数,如果不超过,就会创建一个新线程并执行任务。
/** ThreadPoolExecutor 源码
* If false (default), core threads stay alive even when idle.
* If true, core threads use keepAliveTime to time out waiting
* for work.翻译如下:
* 如果为false(默认值),则核心线程即使在空闲时也保持活动状态。
* 如果为true,则核心线程使用keepAliveTime超时等待工作。
*/
private volatile boolean allowCoreThreadTimeOut; //核心线程能否被回收
- maximumPoolSize:最大线程数(最大开方法的柜台数)
线程池允许创建的最大线程数量。
当有有新的任务时,当核心线程满了,还没到达最大线程数,并且没有空闲线程,工作队列已满,那就创建一个新线程去执行任务。
- keepAliveTime:空闲线程存活时间(当柜台没有人时就关闭的时间)
当一个可被回收的线程,空闲时间超过keepAliveTime就会被回收。
可被回收的线程:
① 设置allowCoreThreadTimeout=true的核心线程。
② 大于核心线程数的线程(非核心线程)。
- unit:空闲线程存活时间
keepAliveTime的时间单位
可选项 | 含义 |
---|---|
NANOSECONDS | 纳秒(1000纳秒 = 1微秒) |
MICROSECONDS | 微秒(1000微妙 = 1毫秒) |
MILLISECONDS | 毫秒(1000毫秒 = 1秒) |
SECONDS | 秒 |
MINUTES | 分钟 |
HOURS | 小时 |
DAYS | 天 |
- workQueue:工作队列(等候区,等候区满了的话,就会开放最大柜台数)
用于存放任务的队列
- handler:拒绝策略(柜台满了,等候区也满了,不接待的策略)
超出线程范围和队列容量的任务的处理程序
3.四大方法
注意:为什么不推荐使用jdk自带的executors的方式来创建线程池?
图片内容来自阿里开发手册
① ExecutorService executor = Executors.newCachedThreadPool()
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
优点:可以灵活的回收线程。
缺点:如果创建任务过多,会导致OOM(内存溢出)。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
② ExecutorService executor = Executors.newFixedThreadPool()
创建一个指定工作线程数量的线程池。
优点:提高线程池的效率和创建线程时的开销。
缺点:因为可能堆积大量请求(队列中),导致OOM。
//如果核心线程满了,那就存入队列中,提高了线程池效率和创建的线程时的开销,即使空闲也不会被回收,
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
③ ExecutorService executor = Executors.newSingleThreadPool()
创建一个单线程
优点:可以保证线程的有序执行,如果出现异常,会在创建一个新的线程。
缺点:因为只能创建一个,可能速度没有那么快。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
④ ExecutorService executor = Executors.newScheduleThreadPool()
创建一个定长的线程池,支持定时及周期性任务执行。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
因为这个线程池比较特殊,所以咱们写一下示例增强理解
第一种:
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
log.info("延长5秒后执行");
}
}, 5, TimeUnit.SECONDS);
第二种:
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
log.info("延迟一秒,每5秒执行一次");
}
}, 1,5, TimeUnit.SECONDS);
4.四大拒绝策略
- new ThreadPoolExecutor.AbortPolicy()
添加线程池被拒绝,会抛出异常(默认策略) - new ThreadPoolExecutor.CallerRunsPolicy()
添加线程池被拒绝,不会放弃任务,也不会抛出异常,会让调用者线程去执行这个任务(就是不会使用线程池里的线程去执行任务,会让调用线程池的线程去执行) - new ThreadPoolExecutor.DiscardPolicy()
添加线程池被拒绝,丢掉任务,不抛异常。 - new ThreadPoolExecutor.DiscardOldestPolicy()
添加线程池被拒绝,会把线程池队列中等待最久的任务放弃,把拒绝任务放进去。
五.线程死锁
1.什么是死锁?
各进程互相等待对方手里的资源,导致各进程都阻塞,无法向前推进的现象。死锁会浪费大量系统资源,导致系统崩溃,所以我们一定要避免死锁。
2.进程死锁、饥饿、死循环的区别
死锁: 各进程互相等待对方手里的资源,导致各进程都阻塞,无法向前推进的现象。死锁会浪费大量系统资源,导致系统崩溃,所以我们一定要避免死锁。
饥饿: 线程一直得不到想要的资源(比如:在短进程优先(SPF)算法中,若有源源不断的短进程到来,则长进程将一直得不到处理机,从而发生长进程“饥饿”。)
死循环: 某进程执行过程中一直跳不出某个循环的现象。
3.造成死锁的四个必要条件
- 互斥:当资源被一个线程占用时,别的线程不能使用。
- 不可抢占:进程阻塞时,对占用的资源不释放。
- 不剥夺:进程获得资源未使用完,不能被强行剥夺。
- 循环等待:若干进程之间形成头尾相连的循环等待资源关系。
3.什么时候会发生死锁?
① 对系统资源的竞争
对不可剥夺资源(磁带机、打印机)的竞争可能导致死锁。像CPU这种可剥夺的资源就不会形成死锁。
② 进程推进顺序非法
请求和释放资源不当,也会导致死锁。比如:线程A和线程B并行占用了资源1和资源2,然后线程A需要资源2,线程B需要资源1,因为资源被对方所占用而阻塞导致死锁。
③ 信号量不当
如在生产者-消费者问题中,若实现互斥的P操作,在实现同步的P操作之前,就会导致死锁。
信号量: 其实就是一个变量(可以是一个整数,也可以是更复杂的记录型变量),可以用一个信号量来表示系统中某种资源的数量。
一对原语: wait(S) 原语和 signal(S) 原语,简称为P(S)和V(S),可以把原语理解为我们自己写的函数,函数名分别为 wait 和 signal,括号里的信号量 S 其实就是函数调用时传入的一个参数。
生产者-消费者设置互斥和同步
生产者和消费者共享一个初始为空、大小为n的缓冲区:
二者必须互斥的访问缓冲区(互斥)
只有缓冲区没满的时候,生产者才能把产品放入缓冲区,否则必须等待(同步)
只有缓冲区不空时,消费这才能从中取走产品,否则等待(同步
4.怎么解决死锁?
① 预防死锁
- 破坏互斥
只允许互斥使用的资源改造成共享使用; 比如:SPOOLing技术;但是很多时候有些资源改不了互斥,有的还要保护互斥。 - 破坏不剥夺
Ⅰ 当某个进程请求新的资源被拒绝,那它就释放自己占有的资源,等以后重新申请。就是资源没用完也得释放,破了不剥夺条件。
Ⅱ 当某个进程需要的资源被其他进程占用时,可以由操作系统协调,进行资源强行剥夺。这种方式一般要考虑每个进程的优先级。
缺点:
(1).实现比较复杂。
(2).释放已获得的资源可能造成前一阶段工作的失效。因此这种方法一般只适用于易保存和恢复状态的资源。
(3).反复地申请和释放资源会增加系统开销,降低系统吞吐量(吞吐量是指对网络、设备、端口、虚电路或其他设施,单位时间内成功地传送数据的数量(以比特、字节、分组等测量))。
(4).如采用 Ⅰ 有可能一直会得不到自己想要的资源,可能会导致饥饿。 - 破坏请求和保持
采用静态分配方法:即进程在运行前一次申请完它所需要的全部资源,在它的资源未满足前,不让它投入运行。一旦投入运行后,这些资源就一直归它所有,该进程就不会再请求别的任何资源了。
缺点:
因为会一直占有资源,所以资源利用率极低,严重浪费资源。还可能会导致饥饿。 - 破坏循环等待
采用顺序资源分配法:给系统资源编号,然后申请时,只能按编号从小到大申请,同类资源必须一次性申请完。
缺点
因为资源编号相对固定,所以添加新资源不方便,因为需要重新分配编号。
② 避免死锁
银行家算法: 每一个新进程进入系统时,必须声明需要每种资源的最大数目,其数目不能超过系统所拥有的的资源总量。当进程请求一组资源时,系统必须首先确定是否有足够的资源分配给该进程,若有,再进一步计算在将这些资源分配给进程后,是否会使系统处于不安全状态如果不会才将资源分配给它,否则让进程等待。
安全状态: 指系统按照这种序列分配资源,则每个进程都能顺利完成。只要能找出一个安全序列系统就是安全状态。当然,安全序列可能有多个。
③ 死锁的检测和解决
死锁的检测
Ⅰ.用某种数据结构(图结构)来保存资源的请求和分配信息;
Ⅱ.提供一种算法,利用上述信息来检测系统是否已进入死锁状态。
(1)在上图中,找出既不阻塞又不是孤点的进程Pi(即找出一条有向边与它相连,且该有向边对应资源的申请数量小于等于系统中已有空闲资源数量。如下图中,R1没有空闲资源,R2有空闲资源。若所有连接该进程的边均满足上述条件,则这个进程能继续运行直至完成,然后释放它所占有的所有资源)。消去它所有的请求边和分配边,使之成为孤立的结点。在下图中,P1是满足这一条件的进程结点,于是将P1的所有边消去。
(2)进程Pi所释放的资源,可用唤醒某些某些因等待这些资源而阻塞的进程,原来的阻塞进程可能变为非阻塞进程。在下图,P2就满足这样的条件。根据1中的方法进行一系列简化后,若能消区图中所有的边,则称该图是完全简化的。
有向边:若顶点Vi到Vj之间的边有方向,则称这条边为有向边。是图结构的名词。
死锁的解决
Ⅰ资源剥夺法: 挂起(暂时放到外存上)某些死锁进程,并抢占它的资源,将这样资源分配给其他的死锁进程。但是应该防止被挂起的进程长时间得不到资源而饥饿。
Ⅱ 撤销进程法(或终止进程法):强制撤销部分、甚至全部死锁进程,并剥夺这些进程的资源。这种方式的优点是实现简单,但所付出的代价可能会很大。如果有的进程已经运行了很长时间了,被撤销,还得从新执行。
Ⅲ 进程回退法: 让一个或多个死锁进程回退到足以避免死锁的地步。这就要求系统要记录进程的历史信息,设置还原点。
如何决定"对谁动手(选择进程解决死锁)"
- 进程优先级
- 已执行多长时间
- 还要多久能完成
- 进程已经使用了多少资源
- 进程是交互式的还是批处理式的
五.线程安全
1.线程安全主要是三方面
- 原子性: 操作不可分割,要么全部执行,并且不能被打断,否则全部不执行。
- 可见性: 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- 有序性: 程序执行的顺序按照代码的先后顺序执行。
2.保证原子性
- 使用锁 synchronized和 lock。
- 使用CAS (compareAndSet:比较并交换),CAS是cpu的并发原语)。
3.保证可见性
- 使用锁 synchronized和 lock。
- 使用volatile关键字 。
4.保证有序性
- 使用 volatile 关键字
- 使用 synchronized 关键字。
5.volatile和synchronized的区别
① volatile仅能使用在变量级别的,synchronized可以使用在变量、方法、类级别的
② volatile不具备原子性,具备可见性,synchronized有原子性和可见性。
③ volatile不会造成线程阻塞,synchronized会造成线程阻塞。
④ volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好。
6.synchronized和lock的区别
① synchronized是关键字,lock是java类,默认是不公平锁(源码)。
② synchronized适合少量同步代码,lock适合大量同步代码。
③ synchronized会自动释放锁,lock必须放在finally中手工unlock释放锁,不然容易死锁。
线程安全,后面我会具体写一下原因和讲解。