Java 多线程基础
文章目录
线程
程序、进程和线程
程序:Program,是一个指令的集合
进程:Process,(正在执行中的程序)是一个静态的概念
进程是程序的一次静态态执行过程, 占用特定的地址空间.
每个进程都是独立的,由3部分组成cpu,data,code
缺点:内存的浪费, cpu的负担
线程:是进程中一个“单一的连续控制流程” (a singles Thread,equential flow of control)/执行路径
Threads run at the same time, independently of one another
一个进程可拥有多个并行的(concurrent)线程
一个进程中的线程共享相同的内存单元/内存地址空间 -> 可以访问相同的变量和对象,而且它们从同一堆中分配对象 -> 通信、 数据交换、同步操作
由于线程间的通信是在同一地址空间上进行的,所以不需要额外的通信机制,这就使得通信更简便而且信息传递的速度也更快。
Java虚拟机启动的时候会有一个进程java.exe,该进程中至少有一个线程,在负责java程序的执行。而且这个线程运行的代码存在于main方法中,该线程称之为主线程。
一个进程中的线程共享代码和数据空间
线程结束,进程未必结束,但进程结束,线程一定结束
进程中包含线程,线程是进程的一部分
区别 | 进程 | 线程 |
---|---|---|
根本区别 | 作为资源分配的单位 | 调度和执行的单位 |
开销 | 每个进程都有独立的代码和数据空间( 进程上下文),进程间的切换会有较大 的开销。 | 线程可以看成时轻量级的进程,同一 类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC), 线程切换的开销小 |
所处环境 | 在操作系统中能同时运行多个任务(程序) | 在同一应用程序中有多个顺序流同时执行 |
分配内存 | 系统在运行的时候会为每个进程分配不同的内存区域 | 除了CPU之外,不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程组只能共享资源 |
包含关系 | 没有线程的进程是可以被看作单线程 的,如果一个进程内拥有多个线程, 则执行过程不是一条线的,而是多条 线(线程)共同完成的。 | 线程是进程的一部分,所以线程有的 时候被称为是轻权进程或者轻量级进程。 |
线程的创建和启动
在Java中负责线程的这个功能的是Java.lang.Thread 这个类
可以通过创建 Thread 的实例来创建新的线程。
每个线程都是通过某个特定Thread对象所对应的方法run( )来完成其操作的,方法run( )称为线程体。
通过调用Thead类的start()方法来启动一个线程。
1、继承Thread类
继承Thread类
重写run方法
创建对象,调用start()方法启动线程
MyThread mt = new MyThread();
mt.start();
2、实现Runnable接口 (Callable接口及Future)
实现Runnable接口
重写run方法
创建对象,调用start()方法启动线程
Thread t = new Thread(new MyRunnable());
t.start();
小结
继承Thread类方式的缺点:那就是如果我们的类已经从一个类继承(如小程序必须继承自 Applet 类),则无法再继承 Thread类
通过Runnable接口实现多线程
优点:可以同时实现继承。实现Runnable接口方式要通用一些。
1)避免单继承
2)方便共享资源 同一份资源 多个代理访问
线程的生命周期
- 新生状态
- 用new关键字建立一个线程后,该线程对象就处于新生状态。
- 处于新生状态的线程有自己的内存空间,通过调用start()方法进入就绪状态。
- 就绪状态
- 处于就绪状态线程具备了运行条件,但还没分配到CPU,处于线程就绪队列,等待系统为其分配CPU。
- 当系统选定一个等待执行的线程后,它就会从就绪状态进入执行状态,该动作称为“CPU调度”。
- 运行状态
- 在运行状态的线程执行自己的run方法中代码,直到等待某资源而阻塞或完成任何而死亡。
- 如果在给定的时间片内没有执行结束,就会被系统给换下来回到等待执行状态。
- 阻塞状态
- 处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己运行,进入阻塞状态。
- 在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续执行。
- 死亡状态
- 死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有三个,一个是正常运行的线程完成了它的全部工作;另一个是线程被强制性地终止,如通过stop方法来终止一个线程【不推荐使用】 ;三是线程抛出未捕获的异常。
线程操作的相关方法
方法名称 | 描述 |
---|---|
public static Thread currentThread() | 返回目前正在执行的线程 |
public final String getName() | 返回线程的名称 |
public final int getPriority() | 返回线程的优先级 |
public final void setPriority(String name) | 设定线程优先级 |
public final boolean isAlive() | 判断线程是否在活动,如果是,返回true,否则返 回false |
public final void join() | 调用该方法的线程强制执行,其它线程处于阻塞状态,该线程执行完毕后,其它线程再执行 |
public static void sleep(long millis) | 使用当前正在执行的线程休眠millis秒,线程处于阻塞状态 |
public static void yield() | 当前正在执行的线程暂停一次,允许其他线程执行,不阻塞,线程进入就绪状态,如果没有其他等待执行的线程,这个时候当前线程就会马上恢复执行 |
public final void stop() | 强迫线程停止执行。已过时。不推荐使用。 |
阻塞状态
有三种方法可以暂停Thread执行:
- sleep:
不会释放锁, Sleep时别的线程也不可以访问锁定对象。- yield:
让出CPU的使用权,从运行态直接进入就绪态。让CPU重新挑选哪一个线程进入运行状态。- join:
当某个线程等待另一个线程执行结束后,才继续执行时,使调用该方法的线程在此之前执行完毕,也就是等待调用该方法的线程执行完毕后再往下继续执行
线程同步
安全性问题
同步
同步的前提:
必须有两个或两个以上的线程
必须是多个线程使用同一资源
必须保证同步中只能有一个线程在运行
同步代码块
public void run() {
while(true){
synchronized (this) {//通常将当前对象作为同步对象
//代码块
}
}
}
同步方法
public void run() {
while(true){
foo();
}
}
public synchronized void foo(){
//通常将当前对象作为同步对象
if ( ... ) {
Thread.sleep(10);
...
}
}
小结:
同步监视器
– synchronized(obj){}中的obj称为同步监视器
– 同步代码块中同步监视器可以是任何对象,但是推荐使用共享资源作为同步监视器
– 同步方法中无需指定同步监视器,因为同步方法的监视器是this,也就是该对象本身
同步监视器的执行过程
– 第一个线程访问,锁定同步监视器,执行其中代码
– 第二个线程访问,发现同步监视器被锁定,无法访问
– 第一个线程访问完毕,解锁同步监视器
– 第二个线程访问,发现同步监视器未锁,锁定并访问
死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
出现死锁条件:
互斥条件
- 即某个资源在一段时间内只能由一个进程占有,不能同时被两个或两个以上的进程占有。
不剥夺条件
- 进程所获得的资源在未使用完毕之前,资源申请者不能强行地从资源占有者手中夺取资源,而只能由该资源的占有者进程自行释放。
请求和保持条件
- 进程至少已经占有一个资源,但又申请新的资源;由于该资源已被另外进程占有,此时该进程阻塞;但是,它在等待新资源之时,仍继续占用已占有的资源。
环路条件
线程间通信
Java提供了3个方法解决线程之间的通信问题
方法名 | 作用 |
---|---|
final void wait() | 表示线程一直等待,直到其它线程通知 |
final void wait(long timeout) | 线程等待指定毫秒参数的时间 |
final void wait(long timeout,int nanos) | 线程等待指定毫秒、微妙的时间 |
final void notify() | 唤醒一个处于等待状态的线程 |
final void notifyAll() | 唤醒同一个对象上所有调用wait()方法的线程, 优先级别高的线程优先运行 |
注意事项: 以上方法都只能在同步方法或者同步代码块中使用,否则会抛出异常
生产者与消费者问题
未完待续
线程池
优点:
1、使用线程池可以重复利用已有的线程继续执行任务,避免线程在创建和销毁时造成的消耗
2、由于没有线程创建和销毁时的消耗,可以提高系统响应速度
3、通过线程可以对线程进行合理的管理,根据系统的承受能力调整可运行线程数量的大小等
线程池执行所提交的任务过程:
1、先判断线程池中核心线程池所有的线程是否都在执行任务。如果不是,则新创建一个线程执行刚提交的任务,否则,核心线程池中所有的线程都在执行任务,则进入第2步;
2、判断当前阻塞队列是否已满,如果未满,则将提交的任务放置在阻塞队列中;否则,则进入第3步;
3、判断线程池中所有的线程是否都在执行任务,如果没有,则创建一个新的线程来执行任务,否则,则交给饱和策略进行处理
线程池分类
生命周期
RUNNING : 能接受新提交的任务,并且也能处理阻塞队列中的任务;
SHUTDOWN: 关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。
STOP: 不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。
TIDYING: 如果所有的任务都已终止了, workerCount (有效线程数) 为0,线程池进入该状态后会调用terminated() 方法进入TERMINATED 状态。
TERMINATED: 在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做
创建及参数意义
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExcutionHandler handler){
}
corePoolSize:核心线程池的大小
maximumPoolSize:线程池能创建线程的最大个数
keepAliveTime:空闲线程存活时间
unit:时间单位,为keepAliveTime指定时间单位
workQueue:阻塞队列,用于保存任务的阻塞队列
threadFactory:创建线程的工程类
handler:饱和策略(拒绝策略)
阻塞队列:
▪ ArrayBlockingQueue
▪ LinkedBlockingQueue
▪ DelayQueue
▪ PriorityBlockingQueue
▪ SynchronousQueue
拒绝策略
▪ ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
▪ ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
▪ ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
▪ ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
execute方法执行逻辑
如果当前运行的线程少于corePoolSize,则会创建新的线程来执行新的任务;
如果运行的线程个数等于或者大于corePoolSize,则会将提交的任务存放到阻塞队列workQueue中;
如果当前workQueue队列已满的话,则会创建新的线程来执行任务;
如果线程个数已经超过了maximumPoolSize,则会使用饱和策略RejectedExecutionHandler来进行处理
线程池关闭
关闭线程池,可以通过shutdown和shutdownNow两个方法
原理:遍历线程池中的所有线程,然后依次中断
1、 shutdownNow首先将线程池的状态设置为STOP,然后尝试停止所有的正在执行和未执行任务的线程,并返回等待执行任务的列表;
2、 shutdown只是将线程池的状态设置为SHUTDOWN状态,然后中断所有没有正在执行任务的线程