线程
谈及线程就不得不提到进程,进程是指一个内存中运行的应用程序,每个进程都会有自己的一个独立的内存空间。
线程的基本概念
一个线程是一个程序内部的顺序控制流。也可以说,一个线程是进程中的一个执行路径,多个线程会共享一个内存空间,线程之间可以自由切换,并发执行,一个进程中最少会存在一个线程。
线程与进程的区别
- 每个进程都有独立的代码和数据空间(进程上下文),进程切换的开销大。
- 线程是轻量的进程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小。
- 多进程:在操作系统中,能同时运行多个任务(程序)。
- 多线程:在同一个应用程序中,有多个顺序流同时执行。
同步与异步
同步:排队执行,效率低但安全。
异步:同时执行,效率高但是数据不安全。
并发与并行
并发:指两个或多个事件在同一个时间段发生。
并行:指两个或多个事件在同一个时刻发生(同时发生)。
线程调度
在程序中,线程的使用有两种调度方式,一种是分时调度,一种是抢占式调度。
分时调度是所有线程会轮流拥有CPU的使用权,平均分配每个线程占用CPU的时间。
抢占式调度会给优先级高的线程使用CPU,若出现多个线程优先级相同,那么会随机选择一个线程(线程随机性)。Java使用的便是抢占式调度的方法。
线程体
Java的线程是通过java.lang.Thread类来实现的。每个线程都是通过某个特定Thread对象的run()方法来完成其操作的,方法run()称为线程体。
构造线程的两种方法
方法一:
定义一个线程类,它继承类Thread并重写其中的run()方法。
public
调用start()方法来启动该线程,自动进入run()方法。
public
- 方法二:
提供一个实现接口Runnable的类作为线程的目标对象,在初始化一个Thread类或者Thread子类的线程对象时,把目标对象传递给这个线程实例,由该目标对象提供线程体run()方法。
/**
创建线程的任务对象,创建一个线程,并为其分配任务对象然后调用start()方法来启动线程。
public
两种线程构造方法的比较
- 使用Runnable接口:
- 可以将CPU,代码和数据分开,形成清晰的模型。
- 可以避免单继承所带来的局限性。
- 可以使用线程池技术,接收Runnable类型的任务对象,不接受Thread类型的线程。
- 可以创建任务,并分配给多个线程实现同时执行相同的任务的情况。
2. 直接继承Thread类:
- 编写简单,直接继承,重写run()方法。
- 不能再从其他类继承。
多线程的同步控制
有的时候,线程之间彼此不独立,同时运行的几个线程需要共享一个或一些数据,但共享的数据在某一时刻只运行一个线程对其进行操作。这时候就需要对线程进行同步操作。
线程同步(Synchronization)
- 互斥:许多线程在同一个共享数据上操作而互不干扰,同一时刻只能有一个线程访问该共享数据。因此有些方法或程序段在同一时刻只能被一个线程执行,这些方法或程序段被称之为监视区。
- 协作:多个线程可以有条件地同时操作共享数据。执行监视区代码的线程在满足条件的情况下可以允许其他线程进入监视区。
Synchronized ——线程同步关键字,实现Java多线程中的互斥。
该关键字用于指定需要同步的代码段或方法,也就是监视区。可以实现与一个锁的交互,使用格式为:
synchronized
synchronized的功能是:首先判断对象的锁是否存在,如果在就获得锁,然后就可以执行紧随其后的代码段;如果对象的锁不在(已被其他线程拿走),就进入等待状态,知道获得锁。当被synchronized限定的代码段执行完毕后,就会释放锁。
用一个售票模型举例,三个线程同时售出10张票,在票数为0后不再继续售出。
public
Ticket类,实现Runnable接口,提供线程共享的售票方法。
public
synchronized除了可以对指定的代码段进行同步控制之外,还可以定义整个方法在同步控制下执行,只要在方法定义前加上synchronized关键字即可。
同步与锁的要点
- 只能同步方法,不能同步变量。
- 每个对象只有一个锁,当提到同步时,应该清除在哪个对象上同步。
- 类可以同时拥有同步和非同步方法,非同步方法可以被多个线程自由访问而不受锁的限制。
- 如果两个线程使用相同的实例来调用synchronized方法,那么一次只能有一个线程执行方法,另一个需要等待锁。
- 线程睡眠时,它持有的任何锁都不会被释放。
- 线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。
- 同步损害并发性,应该尽可能缩小同步范围。
- 在使用同步代码块的时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁。
线程的等待与唤醒
为了更有效地协调不同线程地工作,需要在线程间建立沟通渠道,通过线程间地交流来解决线程间的同步问题。
线程的等待——wait()方法
java.lang.Object类的一些方法为线程间的通讯提供了有效手段——wait()方法。如果当前状态不适合本线程执行,正在执行同步代码(synchronized)的某个线程A调用wait()方法(在对象x上),该线程展厅执行而进入对象x的等待池,并释放已获得的对象x的锁。线程A要一直等到其他线程在对象x上调用notify()或notifyAll()方法,才能够重新获得对象x的锁后继续执行(从wait语句后继续执行)。
线程的唤醒——notify()和notifyAll()方法
notify()随机唤醒一个等待的线程,本线程继续执行。线程被唤醒以后,还要等发出唤醒消息者释放监视器,这期间关键数据仍可能被改变。被唤醒的线程开始执行时,一定要判断当前状态是否适合自己运行。
notifyAll()唤醒所有等待的线程,本线程继续执行。
后台线程
后台线程也叫守护线程,通常是为了辅助其他线程而运行的线程。它不妨碍程序终止。
一个进程中只要还有一个前台线程在运行,这个进程就不会结束;如果一个进程中的所有前台线程都已经结束,那么无论是否还有未结束的后台线程,这个进程都会结束。
在Java虚拟机中,垃圾回收便是一个后台线程。
设置后台线程
在对某个线程对象启动(调用start()方法)之前调用setDaemon(true)方法,便将该线程设置为后台线程。
// 创建一个循环10次的后台线程,每次循环停顿一秒,
创建一个继承Thread类的线程类。
public
输出结果为:
Thread-0:1
main:1
main:2
Thread-0:2
main:3
Thread-0:3
main:4
Thread-0:4
main:5
Thread-0:5
Thread-0:6
当最后一个前台线程结束时,所有的后台线程都会自动死亡。
线程的生命周期
线程的六种状态:
- NEW:尚未启动的线程处于此状态。
- RUNNABLE:Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
- BLOCKED:被阻塞等待监视器锁定的线程处于此状态。
- WAITING:无限期等待另一个线程执行待定操作的线程处于此状态。
- TIMED_WAITING:正在等待另一个线程执行最多指定等待时间的操作的线程处于此状态。
- TERMINATED:已退出的线程处于此状态。
![dbb605ae50abd691be78bf6f1678a25b.png](https://img-blog.csdnimg.cn/img_convert/dbb605ae50abd691be78bf6f1678a25b.png)
线程的优先级
线程调度
在单CPU的系统上,多线程需要共享CPU,在任何时间点上实际只能由一个线程在运行。控制多个线程在同一个CPU上以某种顺序运行称为线程调度。
Java虚拟机支持以一种非常简单、确定的调度算法,叫做固定优先级算法。这个算法基于线程的优先级对其进行调度。
Java线程的优先级
每个Java线程都有一个优先级,其范围都在1和10之间。默认情况下,每个线程的优先级都为5。
在线程A运行过程中创建的新线程对象B,初始状态会和线程A的优先级相同。如果A是一个后台线程,则B也会是后台线程。
Java中的线程可在创建后的任何时候,通过setPriority(int priority)方法来改变其优先级。
基于线程优先级的线程调度
Java中,具有较高优先级的线程会比优先级低的线程先执行,对于同优先级的线程,Java的处理是随机的。
底层操作系统支持的优先级可能要少于10个,这样会造成一些混乱。因此,只能将优先级作为以中很粗略的工具使用。最后的控制可以通过明智地使用yield()函数来完成。
我们只能基于效率的考虑来使用线程优先级,而不能依靠线程优先级来保证算法的正确性。