一、线程的概念
线程(thread):比进程更小的运行单位,是程序中单个顺序的流控制。一个进程中可以包含多个线程。
简单来讲,线程是一个独立的执行流,是进程内部的一个独立执行单元,相当于一个子程序。
一个进程中的所有线程都在该进程的虚拟地址空间中,使用该进程的全局变量和系统资源。
操作系统给每个线程分配不同的CPU时间片,在某一时刻,CPU只执行一个时间片内的线程,多个时间片中的相应线程在CPU内轮流执行。
二、线程的创建与启动
创建多线程
每个Java程序启动后,虚拟机将自动创建一个主线程。
可以通过以下两种方式自定义线程类:
- 创建 java.lang.Thread 类的子类,重写该类的 run方 法
- 创建 java.lang.Runnable接 口的实现类,实现接口中的 run 方法
继承 Thread 类
Thread类中的重要方法:
- run方法:包括线程运行时执行的代码,通常在子类中重写它。
- start方法:启动一个新的线程,然后虚拟机调用新线程的run方法
/**
* ThreadTest类是一个线程类
* Created by WangYiYu.
*/
public class ThreadTest extends Thread{
//重写父类的run方法
public void run(){
for(int i = 1;i <= 100; ++i){
System.out.println("线程" + getName() + "输出" + i);
}
}
public static void main(String [] args){
//获取当前线程的名字
String name = Thread.currentThread().getName();
System.out.println(name + "开始执行");
//创建线程对象
ThreadTest thread = new ThreadTest();
//启动线程
thread.start();
for(int i = 1; i <= 100; ++i) {
System.out.println("线程" + name + "输出" + i);
}
}
}
Runnable 接口
Runnable 接口中只有一个未实现的 run 方法,实现该接口的类必须重写该方法。
Runnable 接口与 Thread 类之间的区别:
- Runnable 接口必须实现 run 方法,而 Thread 类中的run 方法是一个空方法,可以不重写
- Runnable 接口的实现类并不是真正的线程类,只是线程运行的目标类。要想以线程的方式执行 run 方法,必须依靠 Thread 类
- Runnable 接口适合于资源的共享
/**
* Created by WangYiYu.
*/
public class MyRunnable implements Runnable {
//实现Runnable接口的run方法
public void run(){
//获取当前线程的名字
String name = Thread.currentThread().getName();
for(int i = 1; i <= 100; ++i){
System.out.println("线程" + name + "正在输出" + i);
}
}
public static void main(String [] args){
//获取当前线程的名字
String name = Thread.currentThread().getName();
System.out.println(name + "开始执行。。");
//创建线程运行的目标对象
MyRunnable myRun1 = new MyRunnable();
//创建线程对象1
Thread thread1 = new Thread(myRun1);
//启动线程
thread1.start();
for(int i = 1; i <= 100; ++i){
System.out.println("线程" + name + "输出" + i);
}
System.out.println(name + "执行完毕!");
}
}
三、线程的生命周期
指线程从创建到启动,直至运行结束
可以通过调用 Thread 类的相关方法影响线程的运行状态
线程的运行状态:
- 新建(New)
- 可执行(Runnable)
- 运行(Running)
- 阻塞(Blocking)
- 死亡(Dead)
1.新建状态(New)
当创建了一个Thread对象时,该对象就处于“新建状态”
没有启动,因此无法运行
2.可执行状态(Runnable)
其他线程调用了处于新建状态线程的start方法,该线程对象将转换到“可执行状态”
线程拥有获得CPU控制权的机会,处在等待调度阶段。
3.运行状态(Running)
- 处在“可执行状态”的线程对象一旦获得了 CPU 控制权,就会转换到“执行状态”
- 在“执行状态”下,线程状态占用 CPU 时间片段,执行run 方法中的代码
- 处在“执行状态”下的线程可以调用 yield 方法,该方法用于主动出让 CPU 控制权。线程对象出让控制权后回到“可执行状态”,重新等待调度。
4.阻塞状态(Blocking)
线程在“执行状态”下由于受某种条件的影响会被迫出让CPU控制权,进入“阻塞状态”。
进入阻塞状态的三种情况:
- 调用sleep方法
- 调用join方法
- 执行I/O操作
sleep() 方法
调用sleep方法
public void sleep(long millis)
–Thread类的sleep方法用于让当前线程暂时休眠一段时间
–参数 millis 的单位是毫秒
调用 join 方法(合并某个线程)
处在“执行状态”的线程如果调用了其他线程的 join 方法,将被挂起进入“阻塞状态” 。
目标线程执行完毕后才会解除阻塞,回到 “可执行状态”
执行I/O操作
线程在执行过程中如果因为访问外部资源(等待用户键盘输入、访问网络)时发生了阻塞,也会导致当前线程进入“阻塞状态”。
/**
* Created by WangYiYu.
*/
public class JoinThreadTest extends Thread{
private int no; //线程编号
public JoinThreadTest(int no) {
this.no = no;
}
//重写父类的run方法
public void run(){
//打印50遍线程编号
for(int i = 1; i <= 50; ++i) {
System.out.println(no);
}
}
public static void main(String [] args){
//创建线程对象
JoinThreadTest thread = new JoinThreadTest(1);
//创建线程
thread.start();
try{
//调用thread对象的join方法,挂起主线程
thread.join();
}catch (Exception e){
e.printStackTrace();
}
for(int i = 1; i <= 50; ++i) {
System.out.println("我是主线程。。");
}
System.out.println("线程执行结束。。");
}
}
解除阻塞:
- 睡眠状态超时
- 调用 join 后等待其他线程执行完毕
- I/O 操作执行完毕
- 调用阻塞线程的 interrupt 方法(线程睡眠时,调用该线程的interrupt方法会抛出InterruptedException)
5.死亡状态(Dead)
处于“执行状态”的线程一旦从run方法返回(无论是正常退出还是抛出异常),就会进入“死亡状态”。
已经“死亡”的线程不能重新运行,否则会抛出IllegalThreadStateException。
可以使用 Thread 类的 isAlive 方法判断线程是否活着。
/**
* Created by WangYiYu.
*/
public class DeadThreadTest extends Thread{
//重写父类的run方法
public void run(){
for(int i = 1; i <=3; ++i) {
System.out.println("正在执行线程。。");
}
try{
sleep(10000);
}catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String [] args) {
DeadThreadTest thread = new DeadThreadTest();
System.out.println("未启动的线程" + (thread.isAlive()?"正在活动":"没有活动"));
thread.start();
System.out.println("已启动的线程" + (thread.isAlive()?"正在活动":"没有活动"));
try{
//挂起当前的主线程,直到thread执行完毕
thread.join();
}catch(Exception e) {
e.printStackTrace();
}
System.out.println("已终止的线程" + (thread.isAlive()?"正在活动":"没有活动"));
}
}
/**
* 输出:
*
* 未启动的线程没有活动
* 已启动的线程正在活动
* 正在执行线程。。
* 正在执行线程。。
* 正在执行线程。。
* 已终止的线程没有活动
*/
四、线程调度
按照特定机制为线程分配 CPU 时间片段的行为
Java程序运行时,由 Java 虚拟机负责线程的调度
线程调度的实现方式:
- 分时调度模型:让所有线程轮流获得CPU的控制权,并且为每个线程平均分配CPU时间片段
- 抢占式调度模型:选择优先级相对较高的线程执行,如果所有线程的优先级相同,则随机选择一个线程执行 。Java虚拟机采用此种调度模型。
五、线程的优先级
1.Thread类提供了获取和设置线程优先级的方法
- getPriority:获取当前线程的优先级
- setPriority:设置当前线程的优先级
2.Java语言为线程类设置了10个优先级,分别使用1~10内的整数表示 ,整数值越大代表优先级越高。每个线程都有一个默认的优先级,主线程的默认优先级是5。
3.Thread类定义的三个常量分别代表了几个常用的优先级:
- MAX_PRIORITY::代表了最高优先级10
- MIN_PRIORITY::代表了最低优先级1
- NORM_PRIORITY::代表了正常优先级5
4.setPriority 不一定起作用,在不同的操作系统、不同的 JVM 上,效果也可能不同。操作系统也不能保证设置了优先级的线程就一定会先运行或得到更多的CPU时间。在实际使用中,不建议使用该方法。
六、线程同步
/**
* 问题:通过多线程解决小朋友分苹果的问题:一共有5个苹果,2个小朋友同时拿苹果,
* 每次拿一个,拿完为止
*
* 演示线程访问共享对象时出现的问题
* Created by WangYiYu.
*/
public class ShareApple implements Runnable{
//苹果的个数
private int appleCount = 5;
//控制线程运行的标识
private boolean isRun = true;
//getapple代表苹果被取走了
public boolean getApple() {
if(appleCount == 0) {
return false;
}
appleCount--;
if(appleCount >=0) {
System.out.println(Thread.currentThread().getName()
+ "小朋友拿走了一个苹果,还剩" + appleCount + "个苹果");
}else {
System.out.println(Thread.currentThread().getName()
+"发现苹果没了");
}
return true;
}
public void run() {
while(isRun){
isRun = getApple();
}
if(appleCount <= 0) {
System.out.println(Thread.currentThread().getName() +
"的线程进入死亡状态");
return;
}
}
public static void main(String [] args){
//创建了ShareApple对象
ShareApple shareApple = new ShareApple();
//创建线程对象
Thread child1 = new Thread(shareApple);
child1.setName("小明");
Thread child2 = new Thread(shareApple);
child2.setName("小强");
//启动线程对象
child1.start();
child2.start();
}
}
/**
* 输出:
* 小明小朋友拿走了一个苹果,还剩4个苹果
* 小明小朋友拿走了一个苹果,还剩3个苹果
* 小明小朋友拿走了一个苹果,还剩2个苹果
* 小明小朋友拿走了一个苹果,还剩1个苹果
* 小明小朋友拿走了一个苹果,还剩0个苹果
* 小明的线程进入死亡状态
* 小强的线程进入死亡状态
*
*/
1.线程安全
多线程应用程序同时访问共享对象时,由于线程间相互抢占CPU的控制权,造成一个线程夹在另一个线程的执行过程中运行,所以可能导致错误的执行结果。
2.Synchronized 关键字
为了防止共享对象在并发访问时出现错误,Java中提供了“synchronized”关键字。
synchronized关键字----确保共享对象在同一时刻只能被一个线程访问,这种处理机制称为“线程同步”或“线程互斥”。Java中的“线程同步”基于“对象锁”的概念。
使用 synchronized 关键字
//定义同步方法
public synchronized void methd(){
//方法实现
}
- 修饰方法:被“synchronized”关键字修饰的方法称为”同步方法”
- 当一个线程访问对象的同步方法时,被访问对象就处于“锁定”状态,访问该方法的其他线程只能等待,对象中的其他同步方法也不能访问,但非同步方法则可以访问
/**
* 问题:通过多线程解决小朋友分苹果的问题:一共有5个苹果,2个小朋友同时拿苹果,
* 每次拿一个,拿完为止
*
* 演示线程访问共享对象时使用同步方法
* Created by WangYiYu.
*/
public class ShareApple implements Runnable{
//苹果的个数
private int appleCount = 5;
//控制线程运行的标识
private boolean isRun = true;
//getapple代表苹果被取走了
public boolean getApple() {
synchronized (this) {
if (appleCount == 0) {
return false;
}
appleCount--;
try{
Thread.sleep(1000);
}catch(Exception e) {
e.printStackTrace();
}
if (appleCount >= 0) {
System.out.println(Thread.currentThread().getName()
+ "小朋友拿走了一个苹果,还剩" + appleCount + "个苹果");
} else {
System.out.println(Thread.currentThread().getName()
+ "发现苹果没了");
}
return true;
}
}
public void run() {
while(isRun){
isRun = getApple();
}
if(appleCount <= 0) {
System.out.println(Thread.currentThread().getName() +
"的线程进入死亡状态");
return;
}
}
public static void main(String [] args){
//创建了ShareApple对象
ShareApple shareApple = new ShareApple();
//创建线程对象
Thread child1 = new Thread(shareApple);
child1.setName("小明");
Thread child2 = new Thread(shareApple);
child2.setName("小强");
//启动线程对象
child2.start();
child1.start();
}
}
/**
* 输出:
* 小强小朋友拿走了一个苹果,还剩4个苹果
* 小强小朋友拿走了一个苹果,还剩3个苹果
* 小强小朋友拿走了一个苹果,还剩2个苹果
* 小明小朋友拿走了一个苹果,还剩1个苹果
* 小明小朋友拿走了一个苹果,还剩0个苹果
* 小明的线程进入死亡状态
* 小强的线程进入死亡状态
*
*/
七、线程通信
当一个线程正在使用同步方法时,其他线程就不能使用这个同步方法,而有时涉及一些特殊情况:
当一个线程使用的同步方法中用到某个变量,而此变量又需要其他线程修改后才能符合本线程的需要,那么可以在同步方法中使用 wait() 方法。
wait()方法:
中断方法的执行,使本线程等待,暂时让出 cpu 的使用权,并允许其他线程使用这个同步方法。
notify()方法:
唤醒由于使用这个同步方法而处于等待线程的某一个线程,结束等待
notifyall()方法:
唤醒所有由于使用这个同步方法而处于等待的线程,结束等待