目录
什么是线程
- 进程是指可执行程序并存放在计算机存储器的一个指令序列,它是一个动态执行的过程。
- 线程是比进程还要小的运行单位,一个进程包含多个线程,它被包含在进程之中,是进程中的实际运作单位。
- 线程可以看作一个子程序
- CPU使用时间片轮转的工作方式,可以让多个程序轮流占用CPU,达到同时运行的效果
创建线程
在 Java 中,创建线程有以下 3 种方式:
- 继承
Thread
类,重写run()
方法,该方法代表线程要执行的任务; - 实现
Runnable
接口,实现run()
方法,该方法代表线程要执行的任务; - 实现
Callable
接口,实现call()
方法,call()
方法作为线程的执行体,具有返回值,并且可以对异常进行声明和抛出。
Thread类和Runnable接口介绍
Thread
- 定义
Thread
类的子类,重写run()
方法,run()
方法的方法体就代表了线程要完成的任务; - Thread类实现了Runnable接口
- 创建
Thread
子类的实例,即创建线程对象; - 调用线程对象的
start
方法来启动该线程,一个线程只能启动一次,执行线程时会调用run方法 - 线程获得CPU的使用权是随机的,不同线程,执行顺序是随机的。
// Thread类实现了Runnable接口
class MyThread extends Thread{
public MyThread(String name){
super(name);
}
@Override
public void run() {
for(int i = 1; i <= 5; i++){
System.out.println(getName() + "该线程正在执行第" + i + "次");
}
}
}
/**
* 线程获得CPU的使用权是随机的,所以下面代码的运行会有很多种情况
*/
public class ThreadTest {
public static void main(String[] args) {
MyThread mt1 = new MyThread("线程1");
MyThread mt2 = new MyThread("线程2");
mt1.start();
mt2.start();
}
}
输出:线程获得CPU的使用权是随机的,所以运行会有很多种情况
线程2该线程正在执行第1次
线程1该线程正在执行第1次
线程2该线程正在执行第2次
线程1该线程正在执行第2次
线程1该线程正在执行第3次
线程1该线程正在执行第4次
线程2该线程正在执行第3次
线程1该线程正在执行第5次
线程2该线程正在执行第4次
线程2该线程正在执行第5次
Runnable
接口
通过实现 Runnable
接口的方案来创建线程,要优于继承 Thread
类的方案,主要有以下原因:
- Java 不支持多继承,所有的类都只允许继承一个父类,但可以实现多个接口。如果继承了
Thread
类就无法继承其它类,这不利于扩展; - 继承
Thread
类通常只重写run()
方法,其他方法一般不会重写。继承整个Thread
类成本过高,开销过大。
用Runnable接口创建线程的主要工作如下
- 声明实现Runnable接口的类
- 在实现类内实现run()方法
- 创建实现类的对象
- 通过实现类的对象创建线程类的对象
- 调用start()方法启动线程
class PrintRunnable implements Runnable{
@Override
public void run() {
// 获取当前运行的线程的线程名,可以通过Thread的静态方法currentThread
int i = 1;
while(i <= 5){
System.out.println(Thread.currentThread().getName() + "正在运行第" + (i++) + "次");
}
}
}
public class Test {
public static void main(String[] args){
// 创建 Runnable 实现类的实例,
PrintRunnable pr1 = new PrintRunnable();
// 用该实例并以此实例作为 Thread 的 target 来创建 Thread 对象
Thread t1 = new Thread(pr1); // 启动线程只能通过Thread及其子类去启动
PrintRunnable pr2 = new PrintRunnable();
Thread t2 = new Thread(pr1);
// 通过Thread的对象调用start方法启动线程
t1.start();
t2.start(); //当多线程访问同一资源,会发生争抢,造成线程安全问题。
}
}
输出:与下例不同,这边的i是方法的局部变量,所以每个线程会精确的运行5次,只不过打印顺序会是随机的。
Thread-1正在运行第1次
Thread-0正在运行第1次
Thread-1正在运行第2次
Thread-0正在运行第2次
Thread-1正在运行第3次
Thread-1正在运行第4次
Thread-1正在运行第5次
Thread-0正在运行第3次
Thread-0正在运行第4次
Thread-0正在运行第5次
class PrintRunnable implements Runnable{
int i = 1;
@Override
public void run() {
// 获取当前运行的线程的线程名,可以通过Thread的静态方法currentThread
while(i <= 5){
System.out.println(Thread.currentThread().getName() + "正在运行第" + (i++) + "次");
}
}
}
public class Test {
public static void main(String[] args){
// 创建 Runnable 实现类的实例,
PrintRunnable pr1 = new PrintRunnable();
// 用该实例并以此实例作为 Thread 的 target 来创建 Thread 对象
Thread t1 = new Thread(pr1); // 启动线程只能通过Thread及其子类去启动
Thread t2 = new Thread(pr1);
// 通过Thread的对象调用start方法启动线程
t1.start();
t2.start();
}
}
输出结果:貌似有点线程不安全,
因为一个线程只能启动一次,通过Thread实现线程时,线程和线程所要执行的任务是捆绑在一起的。也就使得一个任务只能启动一个线程,不同的线程执行的任务是不相同的,所以没有必要,也不能让两个线程共享彼此任务中的资源。
一个任务可以启动多个线程,通过Runnable方式实现的线程,实际是开辟一个线程,将任务传递进去,由此线程执行。可以实例化多个 Thread对象,将同一任务传递进去,也就是一个任务可以启动多个线程来执行它。这些线程执行的是同一个任务,所以他们的资源是共享,在本例中他们共享数据i,因为i是pr1的成员属性,t1和t2的运行均会改变pr1的成员属性i。
可以理解为,Thread实现线程是不同的对象来运行run()方法,不同对象,自然无法共享变量。而Runnable方式实现的线程是同一个对象运行run()方法,所以可以实现共享变量。
Thread-0正在运行第1次
Thread-1正在运行第1次
Thread-0正在运行第2次
Thread-0正在运行第3次
Thread-1正在运行第5次
Thread-0正在运行第4次
Callable接口
使用Callable接口创建多线程的步骤:
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并具有返回值。
- 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FUtureTask对象封装了该Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread对象的构造参数创建并启动新进程。
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
线程的状态
- 新建(New)
- 可运行(Runnable)
- 正在运行状态(Running)
- 阻塞(Blocked)
- 终止(Dead)
生命周期
线程同步
1、多线程运行问题
- 各个线程是通过竞争CPU时间而获得运行机会的
- 各线程什么时候得到CPU时间,占用多久,是不可预测的
- 一个正在运行着的线程在什么地方被暂停是不确定的
2、线程同步关键字synchronized,使用方式:
- 修饰一个代码块,被修饰的代码块称为同步代码块,作用范围是大括号{}括起来的代码;
- 修饰一个方法,被修饰的方法称为同步方法,其作用范围是整个方法;
- 修饰一个静态方法,作用范围是整个静态方法;
- 修饰一个类,作用范围是synchronized后面括号括起来的部分。
3、synchronized关键字作用
- synchronized关键字保证共享对象在同一时刻只能被一个线程访问
- 当两个并发线程(thread1和thread2)访问同一个对象(syncThread)中的synchronized代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后,其他线程才能执行该代码块。Thread1和thread2是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,其他线程才能执行并锁定该对象。
- 但是由于线程的执行顺序是随机的,所以两个线程的执行顺序也是随机的
4、线程间通信
- wait()方法:中断方法的执行,使线程等待
- notify()方法:唤醒处于等待的某一个线程,使其结束等待
- notifyAll()方法:唤醒所有处于等待的线程,使它们结束等待
- notifyAll方法是唤醒所有线程,本线程不可能把自己唤醒,如果能执行到唤醒这一步就证明自己是在运行的状态。
- notify()方法是随机唤醒一个线程,notifyAll()方法是唤醒所有线程,一般用notifyAll()
- wait通常和notifyAll配合使用,只使用wait不唤醒,有可能造成死锁
参考:慕课网-Java工程师