Java进阶-----多线程
基础概念
并发与并行
进程与线程
概念
一个线程就是一个程序内部的顺序控制流
线程是轻量的进程同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器,线程切换的开销小
进程图示:
线程图示:
进程的生命周期
运行态:该进程正在执行。
就绪态:进程已经做好了准备,只要有机会就开始执行。
阻塞态(等待态):进程在某些事情发生前不能执行,等待阻塞进程的事件完成。
新建态:刚刚创建的进程,操作系统还没有把它加入到可执行进程组中,通常是进程控制块已经创建但是还没有加载到内存中的进程。
退出态:操作系统从可执行进程组中释放出的进程,或由于自身或某种原因停止运行。
导致转换的事件
空->新建:创建执行一个程序的新进程,可能的事件有:新的批处理作业、交互登录(终端用户登录到系统)、操作系统因为提供一项服务而创建、由现有的进程派生等。
新建->就绪:操作系统准备好再接纳一个进程时,把一个进程从新建态转换为就绪态。
就绪->运行:需要选择一个新进程运行时,操作系统的调度器或分配器根据某种调度算法选择一个处于就绪态的进程。
运行->退出:导致进程终止的原因有:正常完成、超过时限、系统无法满足进程需要的内存空间、进程试图访问不允许访问的内存单元(越界)、算术错误(如除以0或存储大于硬件可以接纳的数字)、父进程终止(操作系统可能会自动终止该进程所有的后代进程)、父进程请求终止后代进程等。
运行->就绪:最常见原因是,正在运行的进程到达了“允许不中断执行”的最大时间段,该把处理器的资源释放给其他在就绪态的进程使用了;还有一中原因可能是由于具有更改优先级的就绪态进程抢占了该进程的资源,使其被中断转换到就绪态。
运行->阻塞:如果进程请求它必须等待的某些事件,例如一个无法立即得到的资源(如I/O操作),只有在获得等待的资源后才能继续进程的执行,则进入等待态(阻塞态)。
阻塞->就绪:当等待的事件发生时,处于阻塞态的进程转换到就绪态。
就绪->退出:在上图中没有标出这种转换,在某些进程中,父进程可以在任何时刻终止一个子进程,如果一个父进程终止,所有相关的子进程都被终止。
阻塞->退出:跟上一项原因类似。
多进程与多线程
多进程:在操作系统中,能同时运行多个任务(程序)
多线程:在同一应用程序中,有多个顺序流同时执行
一个线程的生命周期
线程是一个动态执行的过程,它也有一个从产生到死亡的过程
新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
阻塞状态:
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
• 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
• 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
• 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
死亡状态:
• 一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
线程调度
Java多线程
Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。
这里定义和线程相关的另一个术语 - 进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。
Java实现多线程
1) 继承Thread类,重写 run()方法
2) 实现Runnable接口,实现接口的run()方法,用接口的对象作为参数实例化Thread对象
3) 实现 Callable接口,重写call方法
守护线程与用户线程
Java提供了两种线程,守护线程和用户线程,守护线程又称“服务进程”、“精灵线程”、“后台线程”,程序运行时在后台提供一种通用服务的线程。如果用户线程已经全部退出运行,只剩下守护线程存在,JVM也就退出了。守护线程一个经典例子----垃圾回收器
线程创建与实现
主线程(执行main方法的线程)
创建多线程
方法一(继承Thread类)
创建Thread类的子类并重写run方法
创建对象并 start()
原理
详解Thread类
方法二(实现Runnable接口,实现接口的run()方法,用接口的对象作为参数实例化Thread对象)
创建一个Runnable接口的实现类
实现run方法
创建一个Runnable接口的实现类对象
创建Thread类对象,构造方法中传递Runnable接口的实现类对象
调用Thread类中的start方法,开启新的线程执行ran方法
方法二(实现Runnable接口)的好处
线程内部的数据共享
多线程中的重要方法
wait()方法-----线程的等待
notify() 和 notifyAll()方法-----线程的唤醒
notify():随机唤醒一个等待的线程,本线程继续执行
1、线程被唤醒后,还要等待发出唤醒消息者释放监视器,这期间关键数据仍可能被改变
2、被唤醒的线程开始执行时,一定要判断当前状态是否合适自己运行
notifyAll():唤醒所有等待的线程,本线程继续执行
start() 方法与run() 方法
系统通过调用线程类的start()方法来启动一个线程,此时线程处于就绪状态而非运行状态;
JVM通过调用线程类的run()方法完成实际的操作;
Start()方法能异步的调用run()方法,但是直接调用run()方法却是同步的,因此无法达到多线程的目的。
结束线程的方法
1) Thread.stop()。会释放已经锁定的所有监视资源,如果当前任何一个受这些监视资源保护的对象处于一个不一致状态,会导致程序执行的不确定性(不推荐)。
2) Suspend()方法挂起。不会释放锁,如果挂起一个有锁的线程,可能发生死锁(不推荐)。
3) 建议让线程自行结束进入Dead状态。
join()方法
让调用该方法的线程在执行完run()方法之后,再执行join()之后的代码。
join某个线程A,会使当前线程B阻塞。直到线程A结束生命周期。
t.join()方法只会使主线程进入等待池并等待t线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程。
join()方法能够使得t.join()中的t优先执行,当t执行完后才会执行其他线程。能够使得线程之间的并行执行变成串行执行。
线程的安全问题
线程安全
线程安全的相关概念
通俗理解线程安全
举例
不可变、绝对线程安全、相对线程安全
不可变
绝对线程安全
相对线程安全
线程兼容和线程对立
实现卖票案例(存在线程安全问题)
实现Runnable接口
3个线程同时开始卖票
出现重复卖某张票的安全问题
线程同步
线程同步的一些概念
互斥:
许多线程在同一个共享数据上的操作互不干扰,同一时刻只能有一个线程访问该共享数据。因此有些方法或者程序段在同一时刻只能被一个线程执行,称之为监视区。
协作:
多个线程可以有条件地同时操作共享数据。执行监视区代码的线程在条件满足的情况下可以允许其他线程进入监视区
Java使用监视器机制:
每个对象只有一个“锁”,利用多线程对“锁”的争夺实现线程间的互斥
当线程A获得一个对象的锁后,线程B必须等待线程A完成规定的操作、并释放出锁后,才能获得该对象的锁,并执行B中的操作
同步与锁的要点:
只能同步方法,而不能同步变量
每个对象只有一个锁;当提到同步时,应该清楚在什么上同步?也就是说在哪个对象上同步?
类可以同时拥有同步和非同步方法,非同步方法可以被多个线程自由访问而不受锁的限制
如果两个线程使用相同的实例来调用的synchronize方法,那么一次只能有一个线程执行方法,另一个需要等待锁
线程睡眠时,它持有的任何锁都不会释放
线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁
同步锁损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块
同步方法一:synchronize关键字
用于指定需要同步的代码段或方法,也就是监视区
可实现一个锁的交互
synchronize的功能:
首先判断对象的锁是否在,如果在就获得锁,然后就可以执行紧随其后的代码段;
如果对象的锁不在(已被其他线程拿走),就进入等待状态,直到获得锁
同步处理卖票案例(synchronize同步代码块)
同步处理卖票案例(synchronize同步方法)
同步方法二:Wait()方法notify()方法 (在生产者消费者模式中常使用)
在synchronized代码执行期间,线程可以调用对象的wait()方法,使其释放对象锁,进入等待状态;并可以调用 notify()或者 notifyAll()方法通知唤醒一个或全部等待的线程。
同步方法三:Lock锁
锁优化
自旋锁
自适应自旋
锁消除
锁粗化
偏向锁
高级锁
Synchronized 与 Lock (ReentrantLock)有什么异同
1) Synchronized 使用Object本身的notify, wait, notifyAll调度机制;Lock使用Condition进行线程间调度。
2) 用法不同。在需要同步的地方(方法、代码块)加入synchronized控制,托管给JVM执行;Lock需要显示置顶 起始位置和终止位置,Lock的锁定由代码实现。
3) 性能不一样。 Lock接口中的实现类ReentrantLock除了拥有synchronized相同的并发性和内存语义,还有……功能,在资源竞争不激烈时,synchronized性能优于Reentratock,竞争激烈时不如。
4) 锁机制不同。Synchronized获得锁和释放的方式都是在块结构中,当获得多个锁时候,必须以相反的顺序释放,不会因为出了异常而导致锁没有被释放从而引发死锁。
Lock需要开发人员手动去释放,而且必须在finally块内释放,否则会引发死锁发生
最好不要同时使用这两种同步机制。
5) 等待是否可中断:
synchronized不可中断,除非抛出异常或者正常运行完成
ReentrantLock可中断
6) 加锁是否公平:
synchronized非公平锁
ReentrantLock两者都可以,默认是非公平锁。
线程的状态
线程状态的概述
线程间的通信
线程间通信的概述
等待唤醒机制
概念
生产者消费者问题
概念
实现
线程池
线程池的思想概述
线程池的概念
线程池的使用