一、主要内容
1. 线程的概念模型
2. 线程的创建和启动
3. 临界资源、对象锁
4. 线程的互斥和同步
二、程序、进程与多任务
程序(program)是对数据描述与操作的代码的集合,是应用程序执行的脚本。
进程(process)是程序的一次执行过程,是系统运行程序的基本单位。程序是静态的,进程是动态的。系统运行一个程序即是一个进程从创建、运行到消亡的过程。
多任务(multi task)在一个系统中可以同时运行多个程序,即有多个独立运行的任务,每个任务对应一个进程。
线程(thread):比进程更小的运行单元,是程序中单个程序的流控制。一个进程中可以包含多个线程。
简单来讲,线程是一个独立的执行流,是进程内部的一个独立执行单元,相当于一个子程序。
一个进程中的所有线程都在该进程的虚拟地址空间中,使用该进程的全局变量和系统资源。
操作系统给每个线程分配不同的CPU时间片,在某一时刻,CPU只执行一个时间片内的线程,多个时间片中的相应线程在CPU内轮流执行。
三、创建多线程
1. 每个java程序启动后,虚拟机将自动创建一个主线程。
2. 可以通过以下两种方式自定义线程类:
1)创建java.lang.Thread类的子类,重写该类的run方法
2)创建java.lang.Runnable接口的实现类,实现接口中的run方法
四、继承Thread类
Thread:代表一个线程类
构造方法 | 含义 |
---|---|
Thread() | 创建一个新的线程对象 |
Thread(Runnable target) | 基于Runnable接口实现类的实例创建一个线程对象 |
Thread(Runnable t, String name) | 基于给定的Runnable接口实现类的实例和指定名字创建一个线程对象 |
Thread(String name) | 基于给定的名称创建一个线程对象 |
Thread类中的重要方法:
- run方法:包括线程运行时执行的代码,通常在子类中重写它。
- start方法:启动一个新的线程,然后虚拟机调用新线程的run方法。
package com.sgg.thread;
public class FirstThread extends Thread {
public static void main(String[] args) {
SecondThread t1 = new SecondThread();
t1.start();
for (int num = 0; num < 100; num++) {
System.out.println(Thread.currentThread().getName() + ":" + num);
}
}
}
class SecondThread extends Thread {
public void run() {
for (int num = 0; num < 100; num++) {
System.out.println(Thread.currentThread().getName() + ":" + num);
}
}
}
执行结果:两个线程交替执行,分别打印各自线程名,并各自把num从0加到100。
package com.sgg.thread;
public class FirstThread extends Thread {
public void run() {
for (int num = 0; num < 100; num++) {
System.out.println(Thread.currentThread().getName() + ":" + num);
}
}
public static void main(String[] args) {
FirstThread t1 = new FirstThread();
t1.start();
for (int num = 0; num < 100; num++) {
System.out.println(Thread.currentThread().getName() + ":" + num);
}
}
}
执行结果:两个线程交替执行,分别打印各自线程名,并各自把num从0加到100。
package com.sgg.thread;
public class FirstThread extends Thread {
public FirstThread(String name) {
super(name);
}
public void run() {
for (int num = 0; num < 100; num++) {
System.out.println(Thread.currentThread().getName() + ":" + num);
}
}
public static void main(String[] args) {
FirstThread t1 = new FirstThread("t1");
t1.start();
for (int num = 0; num < 100; num++) {
System.out.println(Thread.currentThread().getName() + ":" + num);
}
}
}
创建两个线程,共同打印0-99。两个线程共享一个变量。
方法一:共享一个变量,static修饰变量
package com.sgg.thread;
public class PrintNumStatic {
public static void main(String[] args) {
PrintN t1 = new PrintN("线程1");
PrintN t2 = new PrintN("线程2");
t1.start();
t2.start();
}
}
class PrintN extends Thread{
private static int i = 0;
public PrintN(String name) {
super(name);
}
@Override
public void run() {
for(;i<100;i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
方法二:两个线程共享一个对象
package com.sgg.thread;
public class PrintNum extends Thread {
private int num = 0;
@Override
public void run() {
for(num = 0; num < 100; num++) {
System.out.println(Thread.currentThread().getName() + ":" + num);
}
}
public static void main(String[] args) {
PrintNum pn = new PrintNum();
Thread t1 = new Thread(pn, "t1");
Thread t2 = new Thread(pn, "t2");
t1.start();
t2.start();
}
}
五、执行流程图
package com.sgg.thread;
public class MyThreadDemo extends Thread{
public MyThreadDemo(String name) {
super(name);
}
//重写父类的run方法
@Override
public void run() {
for(int i = 1; i<=10; i++) {
System.out.println("当前线程"+getName()+"正在打印"+i);
}
}
public static void main(String[] args) {
//获取当前线程的名字
String name = Thread.currentThread().getName();
System.out.println(name + "开始执行!");
//创建线程对象
MyThreadDemo thread0 = new MyThreadDemo("thread0");
MyThreadDemo thread1 = new MyThreadDemo("thread1");
//启动线程
thread0.start();
thread1.start();
for(int i = 1; i<=10; i++) {
System.out.println("当前线程" + name + "正在打印" + i);
}
System.out.println(name + "执行完毕!");
}
}
六、Runnable接口
1. Runnable接口中只有一个未实现的run方法,实现该接口的类必须重写该方法。
2. Runnable接口与Thread类之间的区别
1)Runnable接口必须实现run方法,而Thread类中的run方法是一个空方法,可以不重写
2)Runnable接口的实现类并不是真的线程类,只是线程运行的目标类。要想以线程的方式执行run方法,必须依靠Thread类
3)Runnable接口适合于资源的共享
package com.sgg.thread;
public class NewMyRunnableDouble implements Runnable{
@Override
public void run() {
for(int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
public static void main(String[] args) {
NewMyRunnableDouble mrd = new NewMyRunnableDouble();
Thread thread1 = new Thread(mrd, "thread1");
Thread thread2 = new Thread(mrd, "thread2");
thread1.start();
thread2.start();
}
}
执行结果:两个线程无规律交替执行,分别各自打印1-10
package com.sgg.thread;
public class MyRunnableSingle implements Runnable{
private int i = 1;
@Override
public void run() {
for(; i<= 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
public static void main(String[] args) {
MyRunnableSingle mrs = new MyRunnableSingle();
Thread thread1 = new Thread(mrs);
Thread thread2 = new Thread(mrs);
thread1.start();
thread2.start();
}
}
执行结果:两个线程交替执行,共同打印1-10。
package com.sgg.thread;
public class MyRunnable {
private static int num;
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (; num < 10; num++) {
System.out.println(Thread.currentThread().getName()+":"+num);
}
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (; num < 10; num++) {
System.out.println(Thread.currentThread().getName()+":"+num);
}
}
},"t2");
t1.start();
t2.start();
}
}
执行结果:两个线程交替执行,共同打印1-10。
七、线程的生命周期
线程的生命周期:线程从创建到启动,直到运行结束。
可以通过调用Thread类的相关方法影响线程的运行状态。
线程的运行状态
- 新建(New)
- 可执行(Runnable)
- 运行(Running)
- 阻塞(Blocking)
- 死亡(Dead)
八、线程的生命周期状态图
九、线程的生命周期状态
1. 新建状态(New)
- 当创建了一个Thread对象时,该对象就处于“新建状态”
- 没有启动,因此无法运行
2. 可执行状态(Runnable)
- 其他线程调用了处于新建状线程的start方法,该线程对象将转换到“可执行状态”
- 线程拥有获得CPU控制权的机会,处在等待调度阶段。
3. 运行状态(Runnable)
- 处在“可执行状态”的线程对象一旦获得了CPU控制权,就会转换到“执行状态”。
- 在“执行状态”下,线程状态占用CPU时间片段,执行run方法中的代码。
- 处在“执行状态”下的线程可以调用yield方法,该方法用于主动出让CPU控制权。线程对象出让控制权后回到“可执行状态”,重新等待调度。
4. 阻塞状态(Blocking)
- 线程在“执行状态”下由于受某种条件的影响会被迫出让CPU控制权,进入“阻塞状态”。
- 进入阻塞状态的三种情况:1、调用sleep方法 2、调用join方法 3、执行I/O操作
1)public void sleep(long millis)
Thread类的sleep方法用于让当前线程暂时休眠一段时间
参数millis的单位是毫秒
sleep举例:每隔1秒钟打印一次
package com.sgg.thread;
public class SleepThreadTest extends Thread{
@Override
public void run() {
for(int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
public static void main(String[] args) {
SleepThreadTest stt = new SleepThreadTest();
Thread t1 = new Thread(stt,"t1");
t1.start();
}
}
2)调用join方法(合并某个线程)
处在“执行状态”的线程如果调用了其他线程的join方法,将被挂起进入“阻塞状态”
目标线程执行完毕后才会解除阻塞,回到“可执行状态”
package com.sgg.thread;
public class JoinThreadTest extends Thread{
@Override
public void run() {
for(int i = 0; i<10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
public static void main(String[] args) {
JoinThreadTest thread = new JoinThreadTest();
thread.start();
for(int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
if(i == 5) {
try {
thread.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
执行结果:main线程的i=6,7,8,9一定是最后执行。
3)执行I/O操作
线程在执行过程中如果因为访问外部资源(等待用户键盘输入、访问网络)时发生了阻塞,也会导致当前线程进入“阻塞状态”。
5. 解除阻塞
1)睡眠状态超时
2)调用join后等待其他线程执行完毕
3)I/O操作执行完毕
4)调用阻塞线程的interrupt方法(线程睡眠时,调用该线程的interrupt方法会抛出InterruptedException)打断阻塞
package com.sgg.thread;
public class InterruptThreadTest extends Thread {
@Override
public void run() {
for(int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
if(i == 10) {
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
InterruptThreadTest thread = new InterruptThreadTest();
thread.start();
thread.interrupt();
}
}
运行结果:程序不会阻塞,但是会报一个运行时异常,java.lang.InterruptedException
6. 死亡状态(Dead):处于“执行状态”的线程一旦从run方法返回(无论是正常退出还是抛出异常),就会进入“死亡状态”。
已经“死亡”的线程不能重新运行,否则会抛出IllegalThreadStateException
可以使用Thread类的isAlive方法判断线程是否活着
package com.sgg.thread;
public class IsAliveThreadTest extends Thread{
@Override
public void run() {
for(int i = 0; i<100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
public static void main(String[] args) {
IsAliveThreadTest thread = new IsAliveThreadTest();
System.out.println(thread.isAlive());
thread.start();
System.out.println(thread.isAlive());
try {
thread.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(thread.isAlive());
}
}
执行结果:
false
true
for循环打印输出
false
十、线程调度
1. 线程调度
- 按照特定机制为线程分配CPU时间片段的行为
- java程序运行时,由java虚拟机负责线程的调度
2. 线程调度的实现方式
- 分时调度模型:让所有线程轮流获得CPU的控制权,并且为每个线程平均分配CPU时间片段
- 抢占式调度模型:选择优先级相对较高的线程执行,如果所有线程的优先级相同,则随机选择一个线程执行。Java虚拟机采用此种调度模型。
十一、线程的优先级
1. Thread类提供了获取和设置线程优先级的方法
- getPriority:获取当前线程的优先级
- setPriority:设置当前线程的优先级
2. java语言为线程类设置了10个优先级,分别使用1-10内的整数表示,整数值越大代表优先级越高。每个线程都有一个默认的优先级,主线程的默认优先级是5。
- MAX_PRIORITY:代表了最高优先级10
- MIN_PRIORITY:代表了最低优先级1
- NORM_PRIORITY:代表了正常优先级5
3. setPriority不一定起作用,在不同的操作系统,不同的JVM上,效果也可能不同。操作系统也不能保证设置了优先级的线程就一定会先运行或得到更多的CPU事件。
4. 在实际使用中,不建议使用该方法。