Java多线程知识点
- 2020 年 3 月 27 日
- 筆記
基础概念
进程和线程的区别?多线程有什么好处? 进程:正在进行中的程序(直译)。 线程:就是进程中一个负责程序执行的控制单元(执行路径)
- 一个进程中可以多执行路径,称之为多线程,一个进程中至少要有一个线程。
- 开启多个线程是为了同时运行多部分代码。 每一个线程都有自己运行的内容。这个内容可以称为线程要执行的任务。
- 多线程好处:解决了多部分同时运行的问题。
- 什么时候使用多线程?当需要多部分代码同时执行的时候,可以使用。
编写多线程程序有几种实现方式? 一种是继承Thread类;另一种是实现Runnable接口。两种方式都要通过重写run()方法来定义线程的行为,推荐使用后者,因为Java中的继承是单继承,一个类有一个父类,如果继承了Thread类就无法再继承其他类了,显然使用Runnable接口更为灵活。Runnable不是线程,是线程里运行的代码
class Demo implements Runnable// extends Fu //准备扩展Demo类的功能,让其中的内容可以作为线程的任务执行 { public void run() { show(); } public void show() { for (int x = 0; x < 20; x++) { System.out.println(Thread.currentThread().getName() + "....." + x); } } } class ThreadDemo { public static void main(String[] args) { Demo d = new Demo(); Thread t1 = new Thread(d); Thread t2 = new Thread(d); t1.start(); t2.start(); } }
Java并发Concurrent包——Callable/Future/FutureTask解析
Callable 接口类似于 Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常。 省了自己写回调了
public interface Callable<V> { // 计算结果,如果无法计算结果,则抛出一个异常 V call() throws Exception; }
FutureTask、RunnableFuture相比runnable有结果的返回
public static class CountTask implements Callable<Integer>{ @Override public Integer call() throws Exception { System.out.println("子线程开始计算"); Thread.sleep(5000); System.out.println("子线程结束计算,共用时5秒"); return 100; } } ExecutorService executor = Executors.newCachedThreadPool(); // Future的使用 //Future<Integer> result = executor.submit(new CountTask()); // FutureTask的使用 FutureTask<Integer> futureTask = new FutureTask<>(new CountTask()); executor.submit(futureTask); executor.shutdown(); // Future获取结果 //Integer i = result.get(); // futureTask获取结果 Integer i = futureTask.get();
ThreadLocal 原理分析
- 多个线程都有一个资源,这个资源可以放在ThreadLocal里,在一个线程中修改不影响其他线程的使用。每个线程都有这个资源。
- 简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个ThreadLocalMap,而这个map的key就是当前线程,值就是我们set的那个值,每次线程在get的时候,都从自己的变量中取值,既然从自己的变量中取值,所以不会影响其他线程。在这个线程是独享的,也没有线程安全方面的问题。
线程的基本状态以及状态之间的关系? 创建并运行线程:
- 新建状态(New Thread):在Java语言中使用new 操作符创建一个线程后,该线程仅仅是一个空对象,它具备类线程的一些特征,但此时系统没有为其分配资源,这时的线程处于创建状态。线程处于创建状态时,可通过Thread类的方法来设置各种属性,如线程的优先级(setPriority)、线程名(setName)和线程的类型(setDaemon)等。
- 就绪状态(Runnable):使用start()方法启动一个线程后,系统为该线程分配了除CPU外的所需资源,使该线程处于就绪状态。此外,如果某个线程执行了yield()方法,那么该线程会被暂时剥夺CPU资源,重新进入就绪状态。
- 运行状态(Running):Java运行系统通过调度选中一个处于就绪状态的线程,使其占有CPU并转为运行状态。此时,系统真正执行线程的run()方法。
可以通过Thread类的isAlive方法来判断线程是否处于就绪/运行状态:当线程处于就绪/运行状态时,isAlive返回true,当isAlive返回false时,可能线程处于阻塞状态,也可能处于停止状态。
- 阻塞和唤醒线程阻塞状态(Blocked):一个正在运行的线程因某些原因不能继续运行时,就进入阻塞 状态。这些原因包括: a) 当执行了某个线程对象的sleep()等阻塞类型的方法时,该线程对象会被置入一个阻塞集内,等待超时而自动苏醒。 b) 当多个线程试图进入某个同步区域时,没能进入该同步区域的线程会被置入锁定集,直到获得该同步区域的锁,进入就绪状态。 c) 当线程执行了某个对象的wait()方法时,线程会被置入该对象的等待集中,知道执行了该对象的notify()方法wait()/notify()方法的执行要求线程首先获得该对象的锁。
- 死亡状态(Dead):线程在run()方法执行结束后进入死亡状态。此外,如果线程执行了interrupt()或stop()方法,那么它也会以异常退出的方式进入死亡状态。
join join(插队):一种特殊的wait,当前运行线程调用另一个线程的join方法,当前线程进入阻塞状态直到另一个线程运行结束等待该线程终止。 注意该方法也需要捕捉异常。
线程的sleep()方法和yield()方法有什么区别?
- sleep()方法给其他线程运行机会时不考虑线程的优先级,因此可能会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会; Thread.setPriority(Thread.MAX_PRIORITY);
- 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(Runnable)状态。
- yield() 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。
- sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常
wait 和 sleep 区别? sleep来自Thread类,和wait来自Object类 调用sleep()方法的过程中,线程不会释放对象锁。而 调用 wait 方法线程会释放对象锁 sleep(milliseconds)需要指定一个睡眠时间,时间一到会自动唤醒
wait和notify wait:使一个线程处于等待(阻塞/冻结)状态,并且释放所持有的对象的锁,让其他线程可以进入Synchronized数据块,当前线程被放入对象等待池中; notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关; notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态; 用法 在未达到目标时 wait() 用 while 循环检查 设置完成后 notifyAll() wait() 和 notify() / notifyAll() 都需要放在同步代码块里
为什么stop()方法被废弃而不被使用呢? 原因是stop()方法太过于暴力,会强行把执行一半的线程终止。这样会就不会保证线程的资源正确释放,通常是没有给与线程完成资源释放工作的机会,因此会导致程序工作在不确定的状态下。 使用boolean类型的变量,来终止线程 或者使用interrupt
启动一个线程是调用run()还是start()方法? 启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM 调度并执行,这并不意味着线程就会立即运行。run()方法是线程启动后要进行回调(callback)的方法。
一个线程如果出现了运行时异常会怎么样 如果这个异常没有被捕获的话,这个线程就停止执行了。另外重要的一点是:如果这个线程持有某个某个对象的监视器(锁),那么这个对象监视器会被立即释放
守护进程 t1.start(); t2.setDaemon(true); // setDameon是守护线程,可以理解为后台线程,你停我也停。前台必须手动结束 t2.start();
多次start一个线程会怎么样 会抛出java.lang.IllegalThreadStateException 线程状态非法异常
线程安全问题
线程安全问题的本质 在多个线程访问共同的资源时,在某⼀个线程对资源进行写操作的中途(写入已经开始,但还没结束),其他线程对这个写了一半的资源进行行了读操作,或者基于这个写了一半的资源进行了写操作,导致出现数据错误。 锁机制的本质 通过对共享资源进行访问限制,让同一时间只有一个线程可以访问资源,保证了数据的准确性。
不论是线程安全问题,还是针对线程安全问题所衍生出的锁机制,它们的核心都在于共享的资源,而不是某个方法或者某几行代码。
解决思路 就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程时不可以参与运算。必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。 使用锁机制:synchronized 或 lock 对象 同步的好处:解决了线程的安全问题。 同步的弊端:相对降低了效率,因为同步外的线程的都会判断同步锁。 同步的前提:同步中必须有多个线程并使用同一个锁。
当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法? 不能,一个对象的一个synchronized方法只能由一个线程访问。
同步函数和同步代码块的区别 同步函数的锁是固定的this。同步代码块的锁是任意的对象。建议使用同步代码块。 静态方法的同步函数的锁是class类,不是this对象,静态方法不属于某个对象,多个类是共享的 private final Object monitor1 = new Object(); 一般用object当锁就行了
简述synchronized 和Lock的异同?
- jdk1.5以后将同步和锁封装成了对象。并将操作锁的隐式方式定义到了该对象中,将隐式动作变成了显示动作。
- synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且最好在finally 块中释放(这是释放外部资源的最好的地方)。
- Lock接口: 出现替代了同步代码块或者同步函数。将同步的隐式锁操作变成现实锁操作。同时更为灵活。可以一个锁上加上多组监视器。
- lock():获取锁。unlock():释放锁,通常需要定义finally代码块中。
读写锁
finally 的作用:保证在方法提前结束或出现 Exception 的时候,依然能正常释放锁。
一般并不会只是使用 Lock ,而是会使用更复杂的锁,例如ReadWriteLock
public class ReadWriteLockDemo implements TestDemo { private int x = 0; ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); Lock readLock = lock.readLock(); Lock writeLock = lock.writeLock(); private void count() { writeLock.lock(); try { x++; } finally { writeLock.unlock(); } } private void print(int time) { readLock.lock(); try { System.out.print(x + " "); } finally { readLock.unlock(); } } @Override public void runTest() { } }
悲观锁
- 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。
- 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
- 乐观锁,总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁。更多的时候用在数据库里(经常改变,先判断,如果需要的话再锁,悲观锁是直接就锁))。
Java内存模型 Java内存模型定义了一种多线程访问Java内存的规范。 Java内存模型将内存分为了主内存和工作内存。类的状态,也就是类之间共享的变量,是存储在主内存中的,每次Java线程用到这些主内存中的变量的时候,会读一次主内存中的变量,并让这些内存在自己的工作内存中有一份拷贝,运行自己线程代码的时候,用到这些变量,操作的都是自己工作内存中的那一份。在线程代码执行完毕之后,会将最新的值更新到主内存中去
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。 有序性:即程序执行的顺序按照代码的先后顺序执行。 可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
volatile关键字的作用 volatile关键字的作用主要有两个:
- 使用volatile关键字修饰的变量,保证了其在多线程之间的可见性(有序性,看2)
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。保证了每次读取到volatile变量,一定是最新的数据。
- 代码底层执行不像我们看到的高级语言,Java程序这么简单,它的执行是Java代码–>字节码–>根据字节码执行对应的C/C++代码–>C/C++代码被编译成汇编语言–>和硬件电路交互,现实中,为了获取更好的性能JVM可能会对指令进行重排序,多线程下可能会出现一些安全的问题。使用volatile则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率 从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见java.util.concurrent.atomic包下的类,比如AtomicInteger( /əˈtɑːmɪk/ )、 AtomicBoolean 等类,作用和 volatile 基本一致,可以看做是通用版的 volatile
AtomicInteger atomicInteger = new AtomicInteger(0); ... atomicInteger.getAndIncrement();//++count
volatile为什么不能保证原子性 一个变量i被volatile修饰,两个线程想对这个变量修改,都对其进行自增操作也就是i++,i++的过程可以分为三步,首先获取i的值,其次对i的值进行加1,最后将得到的新值写会到缓存中。 线程A首先得到了i的初始值100,但是还没来得及修改,就阻塞了,这时线程B开始了,它也得到了i的值,由于i的值未被修改,即使是被volatile修饰,主存的变量还没变化,那么线程B得到的值也是100,之后对其进行加1操作,得到101后,将新值写入到缓存中,再刷入主存中。根据可见性的原则,这个主存的值可以被其他线程可见。 问题来了,线程A已经读取到了i的值为100,也就是说读取的这个原子操作已经结束了,所以这个可见性来的有点晚,线程A阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷入主存,所以即便是volatile具有可见性,也不能保证对它修饰的变量具有原子性。
Synchronized和Volatile的比较
- Synchronized保证内存可见性和操作的原子性 ,保证不了有序性
- Volatile只能保证内存可见性,有序性,保证不了原子性
- Volatile不需要加锁,比Synchronized更轻量级,并不会阻塞线程(volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。)
- volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化(如编译器重排序的优化).
- volatile是变量修饰符,仅能用于变量,而synchronized是一个方法或块的修饰符。
- volatile本质是在告诉JVM当前变量在寄存器中的值是不确定的,使用前,需要先从主存中读取,因此可以实现可见性。而对n=n+1,n++等操作时,volatile关键字将失效,不能起到像synchronized一样的线程同步(原子性)的效果。
线程池
线程池的好处
- 因为不需要每次处理复杂逻辑耗时操作都创建一个线程,比如加载网络,避免了线程的创建和销毁所带来的性能开销和消耗的时间,能有效控制线程池的最大并发数,避免了大量线程间抢占资源而导致的阻塞现象
- 能够对线程进行简单的管理,并提供定时执行以及指定间隔循环执行等功能(ExecutorService 是安全的)
- 避免频繁地创建和销毁线程,达到线程对象的重用。另外,使用线程池还可以根据项目灵活地控制并发的数目。
线程池相关方法
- Java为我们提供了 ExecutorService 线程池来优化和管理线程的使用。
- Executors类是官方提供的一个工厂类,它里面封装好了众多功能不一样的线程池,他们的内部其实是通过:ThreadPoolExecutor
- 既然线程池就是ThreadPoolExecutor,所以我们要创建一个线程池只需要new ThreadPoolExecutor(…);就可以创建一个线程池
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {...}
线程池的参数
- 核心线程数(最多同时运行几个),当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;默认是0,需要自己去实现。
- 表示在线程池中最多能创建多少个线程;
- 如果线程池没有要执行的任务 存活多久
- 存活时间的单位
- 如果 线程池里管理的线程都已经用了,剩下的任务 临时存到LinkedBlockingQueue对象中排队,先进先出
- threadFactory
线程工厂
,如何去创建线程的,可以自定义线程创建的执行者,他们有适当的线程名称、优先级,甚至他们还可以守护进程。
LinkedBlockingQueue 在Java多线程应用中,队列的使用率很高,多数生产消费模型的首选数据结构就是队列(先进先出)。Java提供的线程安全的Queue可以分为阻塞队列(安全)和非阻塞队列,其中阻塞队列的典型例子是BlockingQueue,非阻塞队列的典型例子ConcurrentLinkedQueue(也是安全的,cas)。
LinkedBlockingQueue 是线程安全的队列,通过ReentrantLock保证的。(链表实现的队列) 由于LinkedBlockingQueue实现是线程安全的,实现了先进先出等特性,是作为生产者消费者的首选,LinkedBlockingQueue 可以指定容量,也可以不指定,不指定的话,默认最大是Integer.MAX_VALUE,其中主要用到put和take方法,put方法在队列满的时候会阻塞直到有队列成员被消费,take方法在队列空的时候会阻塞,直到有队列成员被放进来。 2 的 31 次方 – 1
Java自己的线程池
- newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
- newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
- newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
- newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。 。
- 如果希望在服务器上使用线程池,强烈建议使用newFixedThreadPool方法来创建线程池,这样能获得更好的性能。
自定义线程池ThreadPoolExecutor 自定义ThreadManager类管理多线程,例如
- 请求网络数据线程交由长时间任务线程池执行
- 访问数据库交由短时间任务线程池执行
- 图片下载任务将由单任务线程池执行
- 20个图片都做一些操作,创建固定线程池
开启线程数一般是cpu的核数* 2+1
Executors弊端 Executors的4个功能线程池虽然方便,但现在已经不建议使用了,而是建议直接通过使用ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 Executors的4个功能线程有如下弊端: FixedThreadPool和SingleThreadExecutor:主要问题是堆积的请求处理队列均采用LinkedBlockingQueue,可能会耗费非常大的内存,甚至OOM。 CachedThreadPool和ScheduledThreadPool:主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
newFixedThreadPool 创建一个固定线程数量的线程池
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。 应用场景:控制线程最大并发数。
newSingleThreadExecutor 创建一个只有一个线程的线程池,每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待线程处理完再依次处理任务队列中的任务
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory)); }
特点:只有1个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。 应用场景:不适合并发但可能引起IO阻塞性及影响UI线程响应的操作,如数据库操作、文件操作等。
newCachedThreadPool 创建一个可以根据实际情况调整线程池中线程的数量的线程池
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), threadFactory); }
特点:无核心线程,非核心线程数量无限,执行完闲置60s后回收,任务队列为不存储元素的阻塞队列。 应用场景:执行大量、耗时少的任务。 newScheduledThreadPool 创建一个可以定时或者周期性执行任务的线程池
public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue()); }
特点:核心线程数量固定,非核心线程数量无限,执行完闲置10ms后回收,任务队列为延时阻塞队列。 应用场景:执行定时或周期性的任务。
生产者消费者
多个线程在处理同一资源,但是任务却不同。 生产者和消费者在同一时间段内共用同一个存储空间,生产者向空间里存放数据,而消费者取用数据,如果不加以协调可能会出现以下情况: 存储空间已满,而生产者占用着它,消费者等着生产者让出空间从而去除产品,生产者等着消费者消费产品,从而向空间中添加产品。互相等待,从而发生死锁。
wait()和notify()方法的实现 缓冲区满和为空时都调用wait()方法等待,当生产者生产了一个产品或者消费者消费了一个产品之后会唤醒所有线程。
public class Test1 { private static Integer count = 0; private static final Integer FULL = 10; private static String LOCK = "lock"; public static void main(String[] args) { Test1 test1 = new Test1(); new Thread(test1.new Producer()).start(); new Thread(test1.new Consumer()).start(); new Thread(test1.new Producer()).start(); new Thread(test1.new Consumer()).start(); new Thread(test1.new Producer()).start(); new Thread(test1.new Consumer()).start(); } class Producer implements Runnable { @Override public void run() { for (int i = 0; i < 10; i++) { try { Thread.sleep(3000); } catch (Exception e) { e.printStackTrace(); } synchronized (LOCK) { while (count == FULL) { try { LOCK.wait(); } catch (Exception e) { e.printStackTrace(); } } count++; System.out.println(Thread.currentThread().getName() + "生产者生产,目前总共有" + count); LOCK.notifyAll(); } } } } class Consumer implements Runnable { @Override public void run() { for (int i = 0; i < 10; i++) { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (LOCK) { while (count == 0) { try { LOCK.wait(); } catch (Exception e) { } } count--; System.out.println(Thread.currentThread().getName() + "消费者消费,目前总共有" + count); LOCK.notifyAll(); } } } } }
死锁
指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象。就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。
public class DeadlockTest { public static void main(String[] args) { String str1 = new String("资源1"); String str2 = new String("资源2"); new Thread(new Lock(str1, str2), "线程1").start(); new Thread(new Lock(str2, str1), "线程2").start(); } } class Lock implements Runnable { private String str1; private String str2; public Lock(String str1, String str2) { super(); this.str1 = str1; this.str2 = str2; } @Override public void run() { try { System.out.println(Thread.currentThread().getName() + "运行"); synchronized (str1) { System.out.println(Thread.currentThread().getName() + "锁住"+ str1); Thread.sleep(1000); synchronized (str2) { // 执行不到这里 System.out.println(Thread.currentThread().getName() + "锁住" + str2); } } } catch (Exception e) { e.printStackTrace(); } } } 线程1运行 线程1锁住资源1 线程2运行 线程2锁住资源2
线程1运行线程1锁住资源1,线程2运行线程2锁住资源2,两个线程是同时执行的,线程1锁住了资源1,线程2锁住了资源2,线程1企图锁住资源2,但是资源2已经被线程2锁住了,线程2企图锁住资源1,但是资源1已经被线程1锁住了,然后就死锁了。 你的同步(锁)有我的同步,我的同步有你同步
要出现死锁问题需要满足以下条件
- 互斥条件:一个资源每次只能被一个线程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
解决方法
- 如果想要打破互斥条件,我们需要允许进程同时访问某些资源,这种方法受制于实际场景,不太容易实现条件;
- 打破不可抢占条件,这样需要允许进程强行从占有者那里夺取某些资源,或者简单一点理解,占有资源的进程不能再申请占有其他资源,必须释放手上的资源之后才能发起申请,这个其实也很难找到适用场景;
- 进程在运行前申请得到所有的资源,否则该进程不能进入准备执行状态。这个方法看似有点用处,但是它的缺点是可能导致资源利用率和进程并发性降低;
- 避免出现资源申请环路,即对资源事先分类编号,按号分配。这种方式可以有效提高资源的利用率和系统吞吐量,但是增加了系统开销,增大了进程对资源的占用时间。