JAVA学习笔记(9)——线程

时间:2023-02-26 21:20:39
一、多线程编程基础
1.进程和线程的区别
(1)进程
– 一个独立程序的每一次运行称为一个进程,例如
用字处理软件编辑文稿时,同时打开mp3播放程序听音乐,这两个独立的程序在同时运行,称为两个进程
– 设置一个进程要占用相当一部分处理器时间和内存资源
– 大多数操作系统不允许进程访问其他进程的内存空间,进程间的通信很不方便,编程模型比较复杂
(2)线程
– 一个程序中多段代码同时并发执行,称为多线程
– 通过多线程,一个进程表面上看同时可以执行一个以上的任务——并发
– 创建线程比创建进程开销要小得多,线程之间的协作和数据交换也比较容易
– Java是第一个支持内置线程操作的主流编程语言
– 多数程序设计语言支持多线程要借助于操作系统“原语(primitives)”
2.Thread类
– 在Java程序中创建多线程的方法之一是继承Thread类
– 封装了Java程序中一个线程对象需要拥有的属性和方法
– 从Thread类派生一个子类,并创建这个子类的对象,就可以产生一个新的线程。这个子类应该重写Thread类的run方法,在run方法中写入需要在新线程中执行的语句段。这个子类的对象需要调用start方法来启动,新线程将自动进入run方法。原线程将同时继续往下执行
– Thread类直接继承了Object类,并实现了Runnable接口。它位于java.lang包中,因而程序开头不用import任何包就可直接使用
(1)常用API函数
a.public Thread() 构造一个新的线程对象,默认名为Thread-n,n是从0开始递增的整数
b.public Thread(Runnable target) 构造一个新的线程对象,以一个实现Runnable接口的类的对象为参数。默认名为Thread-n,n是从0开始递增的整数
c.public Thread(String name) 构造一个新的线程对象,并同时指定线程名
d.public static Thread currentThread() 返回当前正在运行的线程对象
e.public static void yield() 使当前线程对象暂停,允许别的线程开始运行
f.public static void sleep(long millis) 使当前线程暂停运行指定毫秒数,但此线程并不失去已获得的锁旗标。
g.public void start() 启动线程,JVM将调用此线程的run方法,结果是将同时运行两个线程,当前线程和执行run方法的线程
h.public void run() Thread的子类应该重写此方法,内容应为该线程应执行的任务。
i.public final void stop() 停止线程运行,释放该线程占用的对象锁旗标。
j.public void interrupt() 打断此线程
k.public final void join() 在当前线程中加入调用join方法的线程A,直到线程A死亡才能继续执行当前线程
l.public final void join(long millis) 在当前线程中加入调用join方法的线程A,直到到达参数指定毫秒数或线程A死亡才能继续执行当前线程
m.public final void setPriority(int newPriority) 设置线程优先级
n.public final void setDaemon(Boolean on) 设置是否为后台线程,如果当前运行线程均为后台线程则JVM停止运行。这个方法必须在start()方法前使用
o.public final void checkAccess() 判断当前线程是否有权力修改调用此方法的线程
p.public void setName(String name) 更该本线程的名称为指定参数
q.public final boolean isAlive() 测试线程是否处于活动状态,如果线程被启动并且没有死亡则返回true
3.Runnable接口
– Java多线程机制的一个重要部分,实际上它只有一个run()方法
– Thread类实现了Runnable接口,相对于Thread类,它更适合于多个线程处理同一资源
– 实现Runnable接口的类的对象可以用来创建线程,这时start方法启动此线程就会在此线程上运行run()方法
– 在编写复杂程序时相关的类可能已经继承了某个基类,而Java不支持多继承,在这种情况下,便需要通过实现Runnable接口来生成多线程
4.线程间的共享
(1)代码共享
–多个线程的执行代码来自同一个类的run方法时,即称它们共享相同的代码
(2)数据共享
–当多个线程访问相同的对象时,即它们共享相同的数据
–使用Runnable接口可以轻松实现多个线程共享相同数据,只要用同一个实现了Runnable接口的实例作为参数创建多个线程就可以了
(3)资源共享
独立的同时运行的线程有时需要共享一些数据并且考虑到彼此的状态和动作
– 例如生产/消费问题:生产线程产生数据流,然后这些数据流再被消费线程消费
– 假设一个Java应用程序,其中有一个线程负责往文件写数据,另一个线程从同一个文件中往出读数据,因为涉及到同一个资源,这里是同一个文件,这两个线程必须保证某种方式的同步
5.多线程的同步控制
(1)有时线程之间彼此不独立、需要同步
–线程间的互斥
同时运行的几个线程需要共享一个(些)数据
一个线程对共享的数据进行操作时,不允许其他线程打断它,否则会破坏数据的完整性。即被多个线程共享的数据,在某一时刻只允许一个线程对其进行操作
–“生产者/消费者”问题
生产者产生数据,消费者消费数据,具体来说,假设有一个Java应用程序,其中有一个线程负责往数据区写数据,另一个线程从同一数据区中读数据,两个线程可以并行执行(类似于流水线上的两道工序)
如果数据区已满,,生产者要等消费者取走一些数据后才能再放;而当数据区没有数据时,消费者要等生产者放入一些数据后再取
(2)Java 使用的同步机制是监视器
– 每个对象都只有一个“锁旗标”与之相连,利用多线程对其的争夺可实现线程间的互斥操作
– 当线程A获得了一个对象的锁旗标后,线程B必须等待线程A完成规定的操作、并释放出锁旗标后,才能获得该对象的锁旗标,并执行线程B中的操作
(3)线程同步的概念,包括互斥和协作
– 互斥:许多线程在同一个共享数据上操作而互不干扰,同一时刻只能有一个线程访问该共享数据。因此有些方法或程序段在同一时刻只能被一个线程执行,称之为监视区
– 协作:多个线程可以有条件地同时操作共享数据。执行监视区代码的线程在条件满足的情况下可以允许其它线程进入监视区
(4)synchronized关键字
synchronized ——线程同步关键字
– 用于指定需要同步的代码段或方法,也就是监视区
– 可实现与一个锁旗标的交互。例如:
synchronized(对象){ 代码段} 
– synchronized的功能是:首先判断对象的锁旗标是否在,如果在就获得锁旗标,然后就可以执行紧随其后的代码段;如果对象的锁旗标不在(已被其他线程拿走),就进入等待状态,直到获得锁旗标
– 当被synchronized限定的代码段执行完,就释放锁旗标
6.线程之间的通信
为了更有效地协调不同线程的工作,需要在线程间建立沟通渠道,通过线程间的“对话”来解决线程间的同步问题
java.lang.Object 类的一些方法为线程间的通讯提供了有效手段
– wait()如果当前状态不适合本线程执行,正在执行同步代码(synchronized)的某个线程A调用该方法(在对象x上),该线程暂停执行而进入对象x的等待池,并释放已获得的对象x的锁旗标。线程A要一直等到其他线程在对象x上调用notify或notifyAll方法,才能够在重新获得对象x的锁旗标后继续执行(从wait语句后继续执行)
–notify() 随机唤醒一个等待的线程,本线程继续执行
线程被唤醒以后,还要等发出唤醒消息者释放监视器,这期间关键数据仍可能被改变
被唤醒的线程开始执行时,一定要判断当前状态是否适合自己运行
–notifyAll() 唤醒所有等待的线程,本线程继续执行
7.后台线程
– 也叫守护线程,通常是为了辅助其它线程而运行的线程
– 它不妨碍程序终止
– 一个进程中只要还有一个前台线程在运行,这个进程就不会结束;如果一个进程中的所有前台线程都已经结束,那么无论是否还有未结束的后台线程,这个进程都会结束
– “垃圾回收”便是一个后台线程

– 如果对某个线程对象在启动(调用start方法)之前调用了setDaemon(true)方法,这个线程就变成了后台线程

二、线程的生命周期
1.线程的几种基本状态
诞生状态
– 线程刚刚被创建
就绪状态
– 线程的start 方法已被执行
– 线程已准备好运行
运行状态
– 处理机分配给了线程,线程正在运行
阻塞状态(Blocked)
– 在线程发出输入/输出请求且必须等待其返回
– 遇到用synchronized标记的方法而未获得其监视器暂时不能进入执行时
休眠状态(Sleeping)
– 执行sleep方法而进入休眠
死亡状态
– 线程已完成或退出
2.死锁问题
死锁
–线程在运行过程中,其中某个步骤往往需要满足一些条件才能继续进行下去,如果这个条件不能满足,线程将在这个步骤上出现阻塞
–线程A可能会陷于对线程B的等待,而线程B同样陷于对线程C的等待,依次类推,整个等待链最后又可能回到线程A。如此一来便陷入一个彼此等待的轮回中,任何线程都动弹不得,此即所谓死锁(deadlock)
–对于死锁问题,关键不在于出现问题后调试,而是在于预防
3.控制线程的生命
结束线程的生命
–用stop方法可以结束线程的生命
但如果一个线程正在操作共享数据段,操作过程没有完成就用stop结束的话,将会导致数据的不完整,因此并不提倡使用此方法
–通常,可通过控制run方法中循环条件的方式来结束一个线程
4.线程的优先级
线程调度
–在单CPU的系统中,多个线程需要共享CPU,在任何时间点上实际只能有一个线程在运行
–控制多个线程在同一个CPU上以某种顺序运行称为线程调度
–Java虚拟机支持一种非常简单的、确定的调度算法,叫做固定优先级算法。这个算法基于线程的优先级对其进行调度
线程的优先级
–每个Java线程都有一个优先级,其范围都在1和10之间。默认情况下,每个线程的优先级都设置为5
–在线程A运行过程中创建的新的线程对象B,初始状态具有和线程A相同的优先级
–如果A是个后台线程,则B也是个后台线程
–可在线程创建之后的任何时候,通过setPriority(int priority)方法改变其原来的优先级
基于线程优先级的线程调度
–具有较高优先级的线程比优先级较低的线程优先执行
–对具有相同优先级的线程,Java的处理是随机的
–底层操作系统支持的优先级可能要少于10个,这样会造成一些混乱。因此,只能将优先级作为一种很粗略的工具使用。最后的控制可以通过明智地使用yield()函数来完成
–我们只能基于效率的考虑来使用线程优先级,而不能依靠线程优先级来保证算法的正确性
假设某线程正在运行,则只有出现以下情况之一,才会使其暂停运行
– 一个具有更高优先级的线程变为就绪状态(Ready);
– 由于输入/输出(或其他一些原因)、调用sleep、wait、yield方法使其发生阻塞;
– 对于支持时间分片的系统,时间片的时间期满