总体目录
一.进程和线程基本概念
二.线程的生命周期
三.线程的调度
四.线程的创建方法
五.线程的同步
六.线程的并发
七.线程死锁
八.线程安全
九.线程池
一、进程和线程基本概念
概述
几乎任何操作系统都支持运行多个任务,通常一个任务就是一个程序,而一个程序就是一个进程。当一个进程运行时,内部可能包括多个顺序执行流,每个顺序执行流就是一个线程。
概念
进程是指处于运行过程中的程序,并且具有一定的独立功能。进程是系统进行资源分配和调度的一个单位。确切地说,当一个程序进入内存运行,即变成一个进程。通常一个应用程序运行时对应一个进程。
进程的特点
- 独立性:进程是系统中独立存在的实体,它可以独立拥有资源,每一个进程都有自己独立的地址空间。
- 动态性:进程和程序的区别在于进程是动态的,进程中有时间的概念,进程具有自己的声明周期和各种不同的状态。
- 并发性:多个进程可以在单个处理器上并发执行,互不影响。
对于一个CPU而言,只能在某一时间点执行一个程序。并发性和并行性是不同的概念,并行是指同一时刻,多个命令在多个处理器上同时执行;并发是指同一时刻,只有一条命令是在处理器上执行的,但多个进程命令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。
线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。
线程的特点
- 线程可以完成一定任务,可以拥有自己的堆栈,自己的程序计数器和局部变量,但不能拥有系统资源。可以和其他线程共享父进程的共享变量和部分环境等资源,相互协作来完成任务。
- 线程是独立运行的,其不知道进程中是否还有其他线程存在。
- 线程的执行是抢占式的,即当前执行的线程随时可能被挂起,以便运行另一个线程。
- 一个线程可以创建或销毁另一个线程,一个进程中的多个线程可以并发执行。
多线程即一个程序中有多个线程在同时执行。
单线程程序即若有多个任务只能依次执行。当上一个任务执行结束后,下一个任务开始执行。例子:去网吧上网,网吧只能让一个人上网,当这个人下机后,下一个人才能上网。
多线程程序即若有多个任务可以同时执行。当一个进程中有多个线程时,这个应用程序称为多线程程序。例子:去网吧上网,网吧能够让多个人同时上网。
简而言之,一个程序运行后至少有一个进程,一个进程中可以包含多个线程,而一个线程必须拥有一个父进程。
多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。
多线程在内存中的运行方法多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间来进行方法的压栈和出栈。
当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那进程就结束了。
主线程:JVM启动后,必然有一个执行路径(线程)从main()方法开始,一直执行到main()方法结束,这个线程在Java中称之为主线程。
当程序的主线程执行时,如果遇到了循环导致程序在指定位置停留时间过长,则无法马上执行下面的程序,需要等待循环结束后才能够执行。如何实现同时执行,可通过Java中的多线程技术来解决该问题。进行多线程编程时,Java程序运行是从主线程开始的,main()方法就是主线程的线程执行内容。
二、线程的生命周期
Java中线程的状态分为6种:
- 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
- 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。 - 阻塞(BLOCKED):表示线程阻塞于锁。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
- 终止(TERMINATED):表示该线程已经执行完毕。
下面对这6种状态进行详细解释下:
1. 初始状态
实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。
2.1. 就绪状态
就绪状态只是说有资格运行,但是若没有被调度程序挑选到,就永远是就绪状态。
- 调用线程的start()方法,此线程进入就绪状态。
- 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
- 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
- 锁池里的线程拿到对象锁后,进入就绪状态。
2.2. 运行中状态
线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一的一种方式。
3. 阻塞状态
阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。
4. 等待
处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。
5. 超时等待
处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
6. 终止状态
当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
示例
- 1.线程1获取对象A的锁,正在使用对象A。
- 2.线程1调用对象A的wait()方法。
- 3.线程1释放对象A的锁,并马上进入等待队列。
- 4.锁池里面的对象争抢对象A的锁。
- 5.线程2获得对象A的锁,进入synchronized块,使用对象A。
- 6.线程2调用对象A的notifyAll()方法,唤醒所有线程,所有线程进入同步队列。若线程2调用对象A的notify()方法,则唤醒一个线程,不知道会唤醒谁,被唤醒的那个线程进入同步队列。
- 7.notifyAll()方法所在synchronized结束,线程2释放对象A的锁。
- 8.同步队列的线程争抢对象锁,但线程1什么时候能抢到就不知道了。
同步队列状态
当前线程想调用对象A的同步方法时,发现对象A的锁被别的线程占有,此时当前线程进入同步队列。简言之,同步队列里面放的都是想争夺对象锁的线程。
当一个线程1被另外一个线程2唤醒时,1线程进入同步队列,去争夺对象锁。
同步队列是在同步的环境下才有的概念,一个对象对应一个同步队列。
线程等待时间到了或被notify/notifyAll唤醒后,会进入同步队列竞争锁,如果获得锁,进入RUNNABLE状态,否则进入BLOCKED状态等待获取锁。
三、线程的调度
分时调度
所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。
抢占式调度
优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
四、线程的创建
创建目的
是为了建立程序单独的执行路径,让多部分代码实现同时执行。即线程创建并执行需要给定线程要执行的任务。
对于上文提到的主线程,它的任务定义在main()函数中。自定义线程需要执行的任务都定义在run()方法中。
创建方法
- 第一种方法是将类声明为Thread的子类。该子类要重写Thread类的run()方法。创建对象,开启线程。run方法相当于其他线程的main()方法。
- 第二种方法是声明一个实现Runnable接口的类。该类要实现run方法,然后创建Runnable的子类对象,传入到某个线程的构造方法中,开启线程。
- 第三种方法是声明一个实现Callable接口的类。该类要实现call()方法,使用Future接口来代表Callable接口的call()的返回值,还要用Future接口的实现类FutureTask,FutureTask实现类既实现了Future接口,也实现了Runnable接口,所以可以作为Thread的target。
前两种方法的区别
- 实现Runnable接口避免了继承Thread类的单继承局限性。实现Runnable接口的方法较为常用,更加的符合面向对象。线程分为两部分,一部分线程对象,一部分线程任务。 继承Thread类,线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,即是线程对象,又有线程任务。实现runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦。
- 使用继承Thread类的子类对象来创建线程类时,多个线程无法共享线程类的实例变量。采用Runnable接口的方式创建多个线程可以共享线程类的实例变量,因为程序创建的Runnable对象只是线程的target,而多个线程可以共享一个target,所以多个线程可以共享一个实例变量。
采取Runnable、Callable的优势
线程类只是实现了Runnable或Callable接口,还可以继承其它类;在这种方法下,多个线程可以共享一个target对象,因此非常适合多个相同线程处理同一份资源的情况,从而将CPU、代码和数据分开,形参清晰的模型,体现了面对对象的编程思想。劣势在于编程复杂度略高。
法一、继承Thread类
Thread类是程序中的执行线程。所有的线程对象都必须是Thread或其子类的实例。
每个线程的作用是完成一定任务,实际上就是执行一段程序流(一段顺序执行的代码)。
Java虚拟机允许应用程序并发地运行多个执行线程。
构造方法
- Thread(): 分配新的Thread对象。
- Thread(String name): 分配新的Thread对象。将指定的name作为其线程名称。
常用方法
- start(): 使该线程开始执行。JVM调用该线程的run()方法。
- run(): 该线程要执行的操作。如循环打印100次变量的值。
- sleep(long millis): 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。
- currentThread(): 返回对当前正在执行的线程对象的引用。
- getName(): 返回该线程的名称。使用:Thread.currentThread().getName()
创建线程的步骤
- 1.定义一个类继承Thread(定义Thread的子类)。
- 2.重写该类的run()方法(该run()方法体就代表了线程需要完成的任务)。
- 3.创建子类对象,即创建线程对象。
- 4.调用start()方法,开启线程并让线程执行(同时会告诉JVM去调用run()方法)。
示例
public class Demo1 {
public static void main(String[] args) {
//创建自定义线程对象
MyThread mt = new MyThread("新的线程!");
//开启新线程
mt.start();
//在主方法中执行for循环
for (int i = 0; i < 10; i++) {
System.out.println("main线程!"+i);
}
}
}
// 自定义线程类
class MyThread extends Thread {
//定义指定线程名称的构造方法
public MyThread(String name) {
//调用父类的String参数的构造方法,指定线程的名称
super(name);
}
/**
* 重写run方法,完成该线程执行的逻辑
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在执行!"+i);
}
}
}
线程对象调用run()方法和调用start()方法的区别
线程对象调用run()方法不开启线程,仅是对象调用方法。线程对象调用start()方法开启线程,并让JVM调用run()方法在开启的线程中执行。
法二、实现Runnable接口
Runnable接口用来指定每个线程要执行的任务。包含了一个run()的无参数抽象方法,需要由接口实现类重写该方法。覆盖Runnable接口中的run()方法,将线程任务代码定义到run()方法中。
构造方法(Thread类)
- Thread(Runnable target): 分配新的Thread对象,以便将target作为其运行对象。
- Thread(Runnable target, String name): 分配新的Thread对象,以便将target作为其运行对象,将指定的name作为其名称。
只有创建Thread类的对象才可以创建线程。线程任务已被封装到Runnable接口的run()方法中,而这个run()方法属于Runnable接口的子类对象,所以需要将这个子类对象作为参数传递给Thread的构造函数,这样线程对象创建时就可以明确要运行的线程的任务。
常用方法
- run(): 使用实现接口Runnable的对象创建一个线程时,启动该线程将导致在独立执行的线程中调用对象的run()方法。
创建线程的步骤
- 1.定义类实现Runnable接口(定义Runnable接口的实现类)。
- 2.重写接口中的run()方法(run()方法是该线程的执行体)。
- 3.创建Thread类的对象(创建Runnable实现类的实例,并将此实例作为Thread的target创建一个Thread对象,该Thread对象才是真正的线程对象)。
- 4.将Runnable接口的子类对象作为参数传递给Thread类的构造函数。
- 5.调用Thread类的start()方法开启线程。
示例
public class Demo2 {
public static void main(String[] args) {
//创建线程执行目标类对象
Runnable runn = new MyRunnable();
//将Runnable接口的子类对象作为参数传递给Thread类的构造函数
Thread thread = new Thread(runn);
Thread thread2 = new Thread(runn);
//开启线程
thread.start();
thread2.start();
for (int i = 0; i < 10; i++) {
System.out.println("main线程:正在执行!"+i);
}
}
}
// 自定义线程执行任务类
class MyRunnable implements Runnable{
//定义线程要执行的run方法逻辑
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("我的线程:正在执行!"+i);
}
}
}
线程的匿名内部类使用
使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。
- 方式1:创建线程对象时,直接重写Thread类中的run方法
new Thread() {
public void run() {
for (int x = 0; x < 40; x++) {
System.out.println(Thread.currentThread().getName()+ "...X...." + x);
}
}
}.start();
- 方式2:使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法
Runnable r = new Runnable() {
public void run() {
for (int x = 0; x < 40; x++) {
System.out.println(Thread.currentThread().getName()+ "...Y...." + x);
}
}
};
new Thread(r).start();
法三、实现Callable接口
实现步骤
- 1.创建Callable接口的实现类,并实现call()方法。该call()方法会成为线程执行体,且具有返回值。
- 2.使用FutureTask类来包装Callable对象,该FutureTask封装类Callable的call()方法的返回值。
- 3.使用FutureTask对象作为Thread的target创建并启动新线程。
- 4.使用FutureTask的get()方法获取执行结束后的返回值。
示例
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class target implements Callable<Integer> {
int i=0;
@Override
public Integer call() throws Exception {
for (; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+""+i);
}
return i;
}
}
class ThridThread {
public static void main(String[] args) {
target t1=new target();
FutureTask<Integer> ft=new FutureTask<Integer>(t1);
Thread t2=new Thread(ft,"新线程");
t2.start();
try {
System.out.println(ft.get());
} catch (Exception e) {
// TODO: handle exception
}
}
}