1. 基本概念
1.1 程序/进程/线程
1.1.1 程序(program):一段静态的代码,静态对象。
1.1.2 进程(process):是程序的一次执行过程,或是正在运行的一个程序。
- 进程是一个动态的过程,有它自身的产生、存在、消亡的过程。
- 程序是静态的,进程是动态的。
- 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
1.1.3 线程(thread):进程可进一步细化为线程,一个程序内部的一条执行路径。
- 若一个进程同一时间并行执行多个线程,就是支持多线程的。
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。
- 一个进程中的多个线程共享相同的内存单元。它们从同一堆中分配对象,可以访问相同的变量和对象,使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能会带来安全隐患。
- 一个java应用程序至少有3个线程:main主线程、垃圾回收线程、异常处理线程。
1.2 线程相关特性
1.2.1 单核CPU和多核CPU
- 单核CPU是一种假的多线程,但是因为CPU很快,感觉不出来。
- 单核CPU只用单个线程先后完成多个任务,比用多个线程来完成用的时间更短,因为切换线程需要花费时间。
- 多核CPU才是真正意义上的多线程
1.2.2 并行与并发
- 并行:多个CPU同时执行多个任务。
- 并发:一个CPU采用时间片策略“同时”执行多个任务。
1.2.3 使用多线程的优点
- 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
- 提高计算机系统的CPU利用率。
- 改善程序结构。将长而复杂的进程分为多个线程,独立运行,利于理解和修改。
1.2.4 何时需要多线程
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时。如:用户输入、文件读写、网络操作、搜索等。
- 需要一些后台运行的程序时。
1.2.5 线程的分类
Java中线程分为两类:守护线程&用户线程。两者唯一的区别是判断JVM何时离开,若JVM中都是守护线程,当前JVM退出。守护线程是用来服务用户线程的,Java垃圾回收就是一个守护线程。
2. 线程的创建和使用
2.1 继承Thread类的方式创建多线程
2.1.1 创建步骤
Java可通过继承java.lang.Thread类来实现多线程,具体步骤如下:
- 创建一个继承与Thread的子类
- 重写Thread类中的run()方法,方法内实现此子线程要完成的功能
- 创建该子类的对象
- 调用该对象的start()方法:启用线程,且会调用当前线程的run()方法
public class ThreadTest {
public static void main(String[] args) {
MyThread myThread = new MyThread(); //对象由主线程创建
myThread.start(); //两个循环在不同线程中运行
for (int i = 0; i < 100; i++) {
if (i % 2 != 0) {
System.out.println(i);
}
}
}
}
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
}
}
2.1.2 问题说明
- 不能直接调用run()方法来启动线程,此时只是单纯地调用了一个方法。
- 不能让已经start()的线程再次执行,需要重新创建一个线程的对象执行start()。
2.1.3 Thread类的常用方法
- start():启动当前线程,并调用当前线程的run()方法
- run():存放子线程要执行的代码
- currentThread():静态方法,返回执行当前代码的线程
- getName()/setName():获取/设置当前线程的名字
- yield():当前线程释放CPU的执行权(释放后CPU可能又分配给了该线程)
- join():在A线程中调用B线程的join方法,则A线程进入阻塞状态,直至B线程执行完毕,A线程才结束阻塞状态
- sleep(long millitime):显示地让当前线程睡眠millitime毫秒,睡眠过程中当前线程阻塞
- isAlive():判断当前线程是否还存活
2.1.4 线程的优先级
Java的线程调度方法的特点:
- 时间片策略:同优先级线程组成先进先出队列
- 抢占式策略:对高优先级线程优先调度
Java的线程优先级:
- Java中线程的优先级分为10档,最大(MAX_PRIORITY)为10,最小(MIN_PRIORITY)为1,默认(NORM_PRIORITY)为5
- 线程创建时继承父线程的优先级
- 高优先级仅表示抢到CPU的可能性大,并非一定是高优先级线程在低优先级线程前调用
- getPriority()方法返回线程优先级,setPriority(int newPriority)设置线程的优先级
2.2 实现Runnable接口的方式创建多线程
2.2.1 创建步骤
- 创建一个实现了Runnable接口的类
- 实现类取实现Runnable中的抽象方法run()
- 创建实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 通过Thread类的对象调用start()方法
public class RunnableTest {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1();
Thread thread1 = new Thread(myThread1);
Thread thread2 = new Thread(myThread1);
thread1.start();
thread2.start();
}
}
class MyThread1 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
}
}
2.2.2 两种创建多线程方式的对比
- 开发中优先选择实现Runnable接口的方式,该方式避免了类的单继承的局限性
- 实现Runnable的方式创建的线程共用实现类中的属性,而用继承Thread的方式创建的线程想要共用类中的属性需要static
- Thread类本身也实现了Runnable接口
2.3 实现Callable接口的方式创建多线程
2.3.1 与实现Runnable接口的比较
为JDK5.0新增的多线程创建方式,与使用Runnable相比功能更强大:
- 相比run()方法,call()方法可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask类,比如获取返回结果
2.3.2 Future接口
- 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
- FutureTask是Future接口的唯一实现类。
- FutureTask同时实现了Runnable、Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
2.3.3 创建步骤
- 创建一个实现Callable接口的实现类
- 实现call()方法,将此线程需要执行的操作声明在call方法中
- 创建实现类的对象
- 将实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
- 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread类的对象,并调用start()方法
- 调用FutureTask对象的get()方法获取call()方法的返回值
public class CallableTest {
public static void main(String[] args) {
MyThread2 myThread2 = new MyThread2();
FutureTask futureTask = new FutureTask<>(myThread2);
new Thread(futureTask).start();
try {
Object sum = futureTask.get();
System.out.println(sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class MyThread2 implements Callable {
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
sum += i;
}
}
return sum;
}
}
2.4 使用线程池的方式创建多线程
2.4.1 线程池介绍
- 背景:线程池为JDK5.0新增的线程创建方式。若经常创建和销毁、使用量很大的资源,对性能影响很大。
- 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁,实现重复利用。
- 好处:提高响应速度、降低资源消耗、便于线程管理。
2.4.2 线程池相关API
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor。
- void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
- < T >Future< T >submit(Callable< T >task):执行任务,有返回值,一般用来执行Callable
- void shutdown():关闭连接池
Ececutors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
- Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
- Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
- Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或定期执行
2.4.3 创建步骤
- 创建线程池
- 通过线程池执行线程操作
- 关闭连接池
public class ThreadPoolTest {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1= (ThreadPoolExecutor) service;
service1.setCorePoolSize(15);
service1.execute(new MyThread3());
service1.shutdown();
}
}
class MyThread3 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
}
}
可用于管理的线程池属性有:
- corePoolSize:核心池的大小
- maxmiumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后终止
3. 线程的生命周期
JDK中用Thread.State类定义了线程的几种状态,Java中线程的一个完整的声明周期通常要经历如下5种状态:
- 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
- 就绪:处于新建状态的线程调用start()后,将进入线程队列等待CPU时间片,此时它以具备了运行的条件,只是没分配到CPU资源。
- 运行:当就绪的线程被调度并获得CPU资源时,并进入运行状态,开始调用run()方法。
失去CPU执行权、或调用yield()方法到就绪状态。执行完run()方法、调用stop()方法、出现异常未处理到死亡状态。调用sleep()/join()/wait()/suspend()方法、等待同步锁时进入阻塞状态。 - 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态。
sleep()时间到、join()结束、获取了同步锁、调用notify/notifyAll()/resume()方法进入就绪状态。 - 死亡:线程完成了它的全部工作,或被提前强制性中止,或出现异常结束。
4. 线程的同步机制
出现线程安全问题的原因:由于一个线程在操作共享数据的过程中,未执行完成时另外的线程参与进来。在Java中通过同步机制解决线程安全问题,有多种方式实现同步机制。
4.1 方式一:同步代码块
synchronized(同步监视器){
需要同步的代码
}
- 操作共享数据的代码,即为需要同步的代码。共享数据指多个线程共同操作的变量。
- 同步监视器:俗称锁。任何一个类的对象都可以充当锁,要求多个线程必须共用同一把锁。
- 通过实现Runnable接口创建多线程时,通常用this充当同步监视器。通过继承Thread类创建多线程时,常用“类名.class”充当同步监视器(类也是对象)。
- 被同步的那部分代码相当于单线程运行,效率变低。
4.2 方式二:同步方法
如果操作共享数据的代码完整地声明在一个方法中,不妨将此方法声明为同步方法。
public synchronized void show(){
}
- 同步方法也有锁,不需要显式地声明。非静态的同步方法以this为锁,静态的同步方法以当前类为锁(继承方式实现多线程要用静态的同步方法)。
4.3 方式三:使用Lock锁
- 从JDK5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁用Lock对象充当。
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
- ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,可以显式加锁、释放锁。
- synchronized在执行完同步代码后,自动释放同步锁。Lock需要手动启动和结束同步。
ReentrantLock lock = new ReentrantLock();//继承的方式时要申明为static
try{
lock.lock();
同步代码块
}finally{
lock.unlock();
}
4.4 懒汉式的线程安全
class Bank{
private Bank(){}
private static Bank instance = null;
//方式一:效率低
public static Bank getInstance(){
synchronized(Bank.class){
if(instance == null){
instance = new Bank();
}
return instance;
}
}
//方式二:效率高
public static Bank getInstance(){
if(instance == null){
synchronized(Bank.class){
if(instance == null){
instance = new Bank();
}
}
}
return instance;
}
}
4.5 线程的死锁
- 死锁的产生:不同的线程分别占用对方需要的同步资源不放弃。(当两个线程同时需要两个锁,且各占用一个锁时发生死锁)
- 死锁的解决:使用专门的算法、尽量减少同步资源的定义、尽量避免嵌套同步。
5. 线程的通信
5.1 线程通信的例题
使用两个线程打印1-100,交替打印。
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
class Number implements Runnable {
private int number = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
notify();
if (number <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
5.2 线程通信的方法
线程通信的以下3个方法必须使用在同步代码快或同步方法中(Lock中使用其他方法),这三个方法的调用者为同步监视器。
- wait():当前线程进入阻塞状态,并释放同步监视器。
- notify():唤醒被wait的一个线程。若有多个线程被wait,则唤醒优先级高的那个。
- notifyAll():唤醒所有被wait的线程。
sleep()和wait()的异同:
- 相同点:都能使当前线程进入阻塞状态。
- 不同点:① 声明位置不同:Thread类中声明sleep(),Object类中声明wait();
② 调用的要求不同:sleep()可以在任何需要的场景下调用,wait()必须使用在同步代码块或同步方法中;
③ sleep()不会释放锁,wait()会释放锁。