Java 多线程
Java 多线程
sinarcsinx线程的概念
对于一般程序而言,其结构大都可以分为一个入口、一个出口、一个顺次执行的语句序列。这样的语句结构称为进程,它是程序的一次动态执行,对应了代码加载、执行至完毕的全过程。
进程即是程序在处理机中的一次运行。在这样一个结构中不仅包含程序代码,也包括了系统资源的概念。
在单 CPU 计算机内部,微观上讲,同一时间只能有一个线程运行。实现多线程即从宏观上使多个作业同时执行。
程序:为完成特定任务,用某种语言编写的一组指令的集合。
进程:运行中的程序。当你运行一个程序,系统就会为该进程分配空间。进程是程序的一次执行过程。是一个动态过程:有其自身产生、存在、消亡的过程。
线程:由进程创建的,进程的一个实体。一个进程可以有多个线程。
单线程:同一时刻,只允许执行一个线程。
多线程:同一时刻,可以执行多个线程。
并发:同一时刻,多个任务交替执行,造成一种貌似并行的状态。单核 CPU 实现的多任务就是并发。
并行:同一时刻,多个任务同时进行。多核 CPU 可以实现并行。
线程的结构
在 Java 中,线程由以下 3 部分组成:
- 虚拟 CPU:封装在 java.lang.Thread 类中,控制着整个线程的运行
- 执行的代码:传递给 Thread 类,由其控制按序执行
- 处理的数据:传递给 Thread 类,是在代码执行过程中需要处理的数据
线程的状态
Java 的线程是通过包 java.lang 中定义的类 Thread 来实现的。当生成了一个 Thread 类后就产生了一个线程。通过该对象实例,可以启动线程、终止线程,或暂时挂起线程
线程共有 4 种状态:新建(New)、可运行(Runnable)、死亡(Dead)、阻塞(Blocked)
新建(New):
线程对象刚刚创建,还未启动(New)。此时还处于不可运行状态,但已有了相应内存空间及其他资源
可运行(Runnable):
此时线程已经启动,处于线程的
run()
方法中。这种情况下线程可能正在运行;也可能没有运行,但只要 CPU 空闲就会立刻运行。可以运行但没在运行的线程都排在一个队列中,这个队列称为就绪队列。
可运行状态下,运行中的线程处于运行状态(Running),未运行线程处于就绪状态(Ready)。
调用
start()
方法可以让线程进入可运行状态。死亡(Dead):
线程死亡(Terminated)的原因有两个:一是
run()
方法最后一个语句执行完毕,二是线程遇到异常退出阻塞(Blocked):
一个正常运行的线程因为特殊原因被暂停执行,就进入阻塞状态(Blocked)。
阻塞时线程不能进入就绪对流排队,必须等到引起阻塞的原因消除,才能重新进入队列排队。
引起阻塞的方法很多,
sleep()
和wait()
是两个常用的阻塞方法中断线程:
void interrupt()
:向一个线程发送一个中断请求,并把该线程的 interruptd 状态变为 true。中断阻塞线程的场合,会抛出 InterruptException 异常
static boolean interrupted()
:检测当前线程是否被中断,并重置状态 interrupted 的值。连续调用该方法的场合,第二次调用会返回 false
boolean isInterrupted()
:检测当前线程是否中断。不改变 interrupted 的值
线程的使用
在 Java 中线程使用有两种方法:
继承
Thread
类,重写run
方法1
public class Thread implements Runnable //可见 Thread 也是实现了 Runable 接口
实现
Runable
接口,重写run
方法
继承 Thread 类
Thread 类是 Java 用于表示线程的类。那么,一个类被定义为其子类,则该类也能用来表示线程
1 | public static void main(String[] args) { |
1 | class Type extends Thread { //先继承 Thread 类 |
关于 start()
方法
1 | public synchronized void start() { |
start()
方法调用了一个start0()
底层方法start0()
是本地方法,由 JVM 调用,底层是 c/c++ 实现- 真正的多线程效果,是
start0()
,而不是run()
start()
方法调用start0()
方法后,该线程不一定会立刻执行,只是将线程变成了可运行状态。具体何时运行,由 CPU 统一调度
实现 Runable 接口
Runnable 是 Java 用以实现线程的接口。从根本上将,任何实现线程的类都必须实现该接口。
1 | public static void main(String[] args) { |
1 | class Type implements Runnable { //这部分和 Thread 相似 |
关于 静态代理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 class Thread implements Runable {}
...
private Runnable target;
...
public Thread(Runnable target) { //构造器
init(null, target, "Thread-" + nextThreadNum(), 0);
//这句话可以先理解为 this.target = target;
}
...
public void run() {
if (target != null) {
target.run();
}
}
...
}相当于,先创建了一个新线程,然后在新线程中调用 run 方法
继承 Thread 和 实现 Runable 的区别
- 从 Java 设计来看,两者本质上没有区别。
Thread
类本身就实现了Runable
接口 - 实现
Runable
接口的方式更加适合多个线程共享一个资源的情况,且避免了单继承的限制。建议使用。
线程中止
当线程结束后,会自动退出
还可以通过使用变量来控制
run
方法退出的方式来停止线程,即 通知方式。1
2
3
4
5
6
7
8
9
10public void run() {
while (active) { //这个场合,只要外部控制 active 即可
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
move();
}
}
线程常用方法
setName(name)
:设置线程名称,使之与参数 name 相同getName()
:返回线程名称start()
:线程开始执行。JVM 调用start0
方法该方法会创建新的线程,新线程调用
run
。run()
:到下面玩跑步就是简单的方法调用,不会产生新线程。
setPriority(int priority)
:更改线程优先级getPriority()
:获取线程优先级priority 范围:
- MAX_PRIORITY:最高优先级(10)
- MIN_PRIORITY:最低优先级(1)
- NORM_PRIORITY:不高不低,真是好极了的优先级(5)
每个线程都有一个优先级。Java 线程调度采用如下优先级策略:
- 优先级高的先执行,优先级低的后执行
- 每个线程创建时会被自动分配一个优先级。默认的场合,继承父类优先级
- 任务紧急的线程,优先级较高
- 同优先级线程按 “先进先出” 原则调度
sleep(int millsecond)
:让线程休眠指定的时间该方法是 Thread 类的静态方法,可以直接调用
interrupt()
:中断线程(不是 中止)yield()
:线程的礼让。让出 CPU 让其他线程执行。因为礼让的时间不确定,所以不一定礼让成功。本质是 RUNNING 切换为 READY,即让当前线程放弃执行权
wait()
:导致当前线程等待直到其他线程调用此对象的
notify()
方法或notifyAll()
方法才能唤醒此线程notify()
、notifyAll()
:唤醒因wait()
阻塞的线程。这些方法(
wait()
、notify()
、notifyAll()
)只能在 synchrnized 方法或代码块中调用join()
:线程的插队。插队的线程一旦插入成功,则必定先执行完插队线程的所有任务将导致其他线程的等待,直到
join()
方法的线程结束join(long timeout)
:join,但是时间到后也能结束其他线程的等待isAlive()
:测试当前线程是否在活动Thread.currentThread()
:引用当前运行中的线程
用户线程和守护线程
用户线程:也叫工作线程。当线程任务执行完毕或通知方式结束
守护线程:一般是为工作线程服务的。当所有线程结束,守护线程自动结束
常见的守护线程:垃圾回收机制
1
2
3Thread thraed = new Thread(bullet);
thread.setDeamon(true); //这样,子线程被设置为主线程的守护线程
thread.start();
线程的生命周期
线程的状态有
NEW:尚未启动
RUNNABLE:在 JVM 中执行的线程
可细分为 READY 和 RUNNING
BLOCKED:被阻塞等待监视器锁定的线程
WAITING:正等待另一个线程执行特定动作的线程
TIMED_WAITING:正等待另一个线程执行特定动作达到等待时间的线程
TERMINATED:已退出的线程
线程的互斥
在多线程编程,一些敏感数据不允许被多个线程同时访问。此时就用同步访问技术,保证数据在任意时刻,最多有一个线程同时访问,以保证数据的完整性。
也可以这样理解:线程同步,即当有一个线程对内存进行操作时,其他线程都不能对这个内存地址进行操作(被阻塞),直到该线程完成操作,再让下一线程进行操作。
互斥锁
在 Java 语言中,引入了 “对象互斥锁” 的概念,也称为监视器,来保证共享数据操作的完整性
每个对象都对应一个可称为 “互斥锁” 的标记,这个标记用来保证在任一时刻都只能有一个线程访问对象。
Java 语言中,有 2 种方式实现互斥锁:
- 用关键字 volatile 声明一个共享数据(变量)。一般很少使用该关键字
- 用关键字 synchronized 声明共享数据的一个方法或一个代码
同步的局限性:导致程序的执行效率要降低。
非静态的对象,同步方法的锁可以是 this,也可以是其他对象(要求是同一对象)
静态对象,同步方法的锁为当前类本身
同步代码块
1
2
3synchronized (对象) { //得到对象的锁,才能操作同步代码
需要被同步代码;
}在第一个线程持有锁定标记时,如果另一个线程企图执行该代码块语句,将从对象中索取锁定标记。
因为此时该标记不可得,古该线程不能继续执行,而是加入等待队列。
程序运行完 synchronized 代码块后,锁定标记会被自动返还。即使该同步代码块执行过程中抛出异常也是如此。一个线程多次调用该同步代码块的场合,也会在最外层执行完毕后正确返还。
放在方法声明中,表示整个方法为同步方法
因为 synchronized 语句的参数必须是 this,因此允许下面这种简洁的写法:
1
2
3public synchronized void method(){
代码;
}
线程死锁
多个线程都占用了对方的资源,不肯相让,就导致了死锁。编程时要避免死锁的产生。
以下操作会释放锁
- 当前线程的同步方法、同步代码块执行结束。
- 当前线程在同步方法、同步代码块中遇到
break
、return
- 当前线程在同步方法、同步代码块中出现了未处理的
Error
- 当前线程在同步方法、同步代码块中执行了
wait()
方法,当前线程暂停,并释放锁
以下操作不会释放锁
执行同步方法、同步代码块时,程序调用
Thread.sleep()
或Thread.yield()
方法暂停当前线程的执行,不会释放锁线程执行同步代码块时,其他线程调用了该线程的
suspend()
方法将该线程挂起,该线程不会释放锁所以,应尽量避免使用
suspend()
和resume()
来控制线程
线程的同步
Java 中,可以使用
wait()
、notify()
、notifyAll()
来协调线程间的运行速度关系。这些方法都被定义在 java.lang.Object 中Java 中的每个对象实例都有两个线程队列和它相连。一个用以实现等待锁定标志的线程,另一个用来实现
wait()
和notify()
的交互机制
wait()
:让当前线程释放所有其持有的 “对象互斥锁”,进入等待队列notify()
、notifyAll()
:唤醒一个或所有在等待队列中等待的线程,并将他们移入同一个等待 “对象互斥锁” 的队列。执行这些方法时如果没有等待中的线程,则其不会生效,也不会被保留到以后再生效
1 | synchronized (key) { |
1 | synchronized (key) { |
因为调用这些方法时必须持有对象的 “对象互斥锁”,所以上述方法只能在 synhronized 方法或代码块中执行。