在讨论什么是线程前有必要先说下什么是进程,因为线程是进程中的一个实体,线程本身是不会独立存在的。进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。
o线程操作系统在分配资源时是把资源分配给进程的,但是CPU资源比较特殊,它是被分配到线程的,因为真正要占用CPU运行的是线程,所以也说线程是CPU分配的基本单位。在Java中,当我们启动main函数时其实就启动了一个JVM的进程,而main函数所在的线程就是这个进程中的一个线程,也称主线程。一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。
- 程序计数器是一块内存区域,用来记录线程当前要执行的指令地址
- 每个线程都有自己的栈资源,用于存储该线程的局部变量,这些局部变量是该线程私有的,其他线程是访问不了的,除此之外栈还用来存放线程的调用栈帧
- 堆是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建时分配的,堆里面主要存放使用new操作创建的对象实例
- 方法区则用来存放JVM加载的类、常量及静态变量等信息,也是线程共享的
1. 继承 Thread 类并重写 run()方法 extends 任务没有返回值 过程 1. 创建一个类继承Thread类2. 在main函数内创建一个该类的实例3. 然后调用该实例的 start()方法启动线程
2. 实现 Runnable 接口的 run()方法 implement 任务没有返回值 过程 1. 创建一个类实现 Runnable 接口2. 在main函数内创建一个该类的实例3. 然后使用该实例作为任务创建多个线程并启动这些线程
3. 使用 FutureTask 的方式 过程 1. 创建一个类实现 Callable 接口的 call()方法。2. 在main函数内首先创建了一个 FutrueTask 对象 (构造函数为该类的实例),3. 然后使用创建的 FutrueTask 对象作为任务创建了一个线程并且启动它,4. 最后通过 futureTask.get 等待任务执行完毕并返回结果
三种方式对比
- 使用继承方式的好处是方便传参,你可以在子类里面添加成员变量,通过set方法设置参数或者通过构造函数进行传递不好的地方是Java不支持多继承,如果继承了Thread类,那么就不能再继承其他类另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码,而Runable则没有这个限制
- 使用Runnable的好处是任务与代码分离,当多个线程执行一样的任务时只需要一份任务代码不好的地方是只能使用主线程里面被声明为fìnal的变量
- 前两种方式都没办法拿到任务的返回结果,但是Futuretask方式可以
没有锁,醒后抢锁我们使用wait() 的目的是控制生产线程生产的速度 wait() 当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回其他线程调用了该共享对象的notify()或者notifyAll()方法其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回
- o注意:调用 wait() 的线程需要事先获取该对象的监视器锁,否则调用wait()时调用线程会抛出 IllegalMonitorStateException异常那么一个线程如何才能获取一个共享变量的临视器句呢?1. 执行synchronized同步代码块时,使用该共享变量作为参数 synchronized(共享变量)2. 调用该共享变量的方法,并且该方法使用了synchronized修饰
- o另外需要注意:虚假唤醒 :一个线程可以从挂起状态变为可以运行状态 (也就是被唤醒),即使该线程没有被其他线程调用 notify()、notifyAll() 方法进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒虽然虚假唤醒在应用实践中很少发生,但要防患于未然,做法就是:不停地去测试该线程被唤醒的条件是否满足,不满足则继续等待,也就是说在一个循环中调用 wait() 方法进行防范。退出循环的条件是满足了唤醒该线程的条件经典常用实例:在 while 循环内调用 wait()
wait (long timeout) 该方法相比wait方法多了一个超时参数(wait(0)和wait方法效果一样),如果一个线程调用共享对象的该方法挂起后,没有在指定的 timeout ms 时间内被其他线程调用该共享变量的notify()或者notifyAll()方法唤醒,那么该函数还是会因为超时而返回
notify() 一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用 wait 系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。此外,被唤醒的线程不能马上从 wait 方法返回并继续执行,也就是唤醒它的线程释放了共享变量上的监视器锁后,它必须和其他线程竞争获取了共享对象的监视器锁后才可以继续执行
notifyAll() 一个线程调用共享对象的notifyAll()方法后,会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程
等待线程执行中止 加入 join()join 用于多个线程加载资源,需要等待多个线程全部加载完毕后再汇总处理的情况。也可以说是 join把指定的线程加入当前线程,可以将两个交替执行的线程合并为顺序执行的线程。 比如在线程 B 中调用了线程 A 的 join 方法:那么直到 线程 A 执行完毕后才会继续执行线程 B
线程睡眠 sleep()让CPU 不让锁 不让资源 (处于阻塞挂起状态,不参与线程调度)醒后抢 CPU使用sleep()目的是:可以使低优先级的线程得到执行的机会,当然也可以让同优先级、高优先级的线程有执行的机会 当一个执行中的线程调用了 Thread 的静态 sleep 方法后,调用线程会暂时让出指定时间的 CPU 使用权,但是该线程所拥有的监视器资源,比如锁还是持有不让出的指定的睡眠时间到了后该函数会正常返回,然后处于就绪状态来参与CPU的调度,获取到CPU资源后就可以继续运行了如果在睡眠期间其他线程调用了该线程的interrupt()方法中断了该线程,则线程会在调用sleep方法的地方抛出InterruptedException异常而返回
线程礼让 yield()让CPU 让资源 (不阻塞,参与线程调度) 醒后抢 CPU使用yield()的目的是让相同优先级的线程之间能适当的轮转执行 当一个线程调用了 Thread 的静态 yield 方法后,调用线程会让出剩余的 CPU 使用权,然后处于就绪状态来参与CPU的调度
线程中断是一种线程间的协作模式。 通过设置线程的中断标志并不难呢过直接终止该线程的执行,而是被中断的线程根据中断状态自行处理 (如果线程因为调用了wait系列函数、join方法或者seep方法而被阻塞挂起,这时候若调用该线程的interrupt()方法,线程A会在调用这些方法的地方抛出InterruptedException异常而返回)
- interrupt() 中断线程
- isInterrupted() 判断当前线程是否被中断,是返回true,否返回false
- interrupted() 判断当前线程是否被中断,是返回true并清除中断标志,否返回false
从当前线程的上下文切换到了其他线程 在多线程编程中,线程个数一般都大于CPU个数,而每个CPU同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的,CPU资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用CPU执行任务。当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用,这就是上下文切换
那么让出 CPU 后下次轮到自己占有CPU时如何知道自己之前运行到哪里了?所以在切换线程上下文时需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场 线程上下文切换时机有:当前线程的CPU时间片使用完处于就绪状态时当前线程被其他线程中断时
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象
死锁产生条件 1. 互斥条件:指线程对已经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源 2. 请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源 3. 不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源 4. 环路等待条件:指在发生死锁时,必然存在一个线程一资源的环形链,即线程T0正在等待T1占用的资源,T1正在等待T2占用的资源…… 如何避免线程死锁 想要避免死锁,只需要破坏掉至少一个构成死锁的必要条件即可。目前只有两个死锁条件可以被破坏:1. 请求并持有2. 环路等待 造成死锁的原因和申请资源的顺序有很大关系,所以进行资源的 有序分配 破坏了请求并持有条件和环路等待条件,因此避免了死锁
Java线程分为两大类:守护线程与用户线程 守护线程(daemmon) 在JVM 启动时会调用 main 函数,main 函数所在的线程就是一个用户线程 守护线程是否结束不影响JVM 的退出 用户线程(user) 在JVM 内部同时还启动了好多守护线程,比如垃圾回收线程 只要有一个用户线程还没结束,正常情况下JVM 就不会退出 注意:设置守护线程时,需要在线程启动start前将线程设置为守护线程 总结:如果你希望在主线程结束后 JVM 进程马上结束,那么在创建线程时可以将其设置为守护线程 如果你希望在主线程结束后子线程继续工作,等子线程结束后再让 JVM 进程结束,那么就将子线程设置为用户线程