一 概述
所谓的多线程就是一个程序中存在多个顺序执行流,每个执行流便是一个线程。Java提供了优秀的多线程支持,下面将会详细介绍Java多线程编程的相关知识。
二 线程与进程的比较
现在的操作系统基本都支持多进程,即可以同时运行多个任务,每个任务通常是一个程序,每个运行的程序便是一个进程。而一个程序运行时往往有多个顺序执行流,这每个顺序执行流便是一个线程。
线程与进程的区别:
- 线程比进程更轻,并发性更高
- 线程可以共享进程的资源,便于实现相互之间的通信
- 创建线程的代价比创建进程的代价小得多
- java内置了多线程编程支持,可以非常方便的操作线程
并发与并行的区别:
并行是某一时刻同时运行,并发是某一时刻只有一个指令运行,但是多个指令之间切换过快,所以看起来像是同时运行。
三 创建线程的三种方式
1.继承Thread类创建线程
使用Thread类创建线程的步骤如下:
- 定义Thread的子类并重写它的run方法,run方法的方法体里面就是线程需要完成的任务
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动线程
代码示例:
抽空写
2.实现Runnable接口创建线程
步骤如下:
- 实现接口Runnable接口,并实现它的run方法。
- 创建Runnable接口实现类的实例,并把它作为参数传给Thread的构造函数,创建Thread对象(即将该实例作为Thread类的Target),并调用它的start方法启动线程。(Runnable接口时函数式接口,可使用Lambda表达式)
代码示例:
- 使用Runnable创建线程的一个优势是多个线程之间可以共享线程类中的实例变量。这也是最常用的一种创建线程的方式,但一般都采用匿名内部类的方式简写。
3. 使用Callable和Future创建线程
从java5开始,java提供了一个Callable接口,接口中提供了一个call()方法来替代run()方法,它比run方法的功能更强大。主要有以下两点:
- call方法可以有返回值
- call方法可以声明抛出异常
由于Callable接口没有继承Runnable接口,且call方法有返回值,因此实现Callable接口的类无法作为Target传入Thread类中。为此java5提供了Future接口来包装Callable接口创建线程,该接口有一个FutureTask实现类,该类同时也实现了Runnable接口,因此该类的对象可以作为Target传入Thread类中来创建线程。
创建并启动有返回值的线程的步骤如下:
- 创建Callable接口的实现类,并实现它的call方法。、
- 创建FutureTask类来包装Callable接口的实现类的对象(Callable是函数式接口,这一步可以是Lambda表达式)
- 将FutureTask对象作为Target传入Thread类中,创建线程。
- 使用FutureTask对象的get方法得到call的返回值
Callab接口实现线程与Runnable接口类似,因此它也有Runnable接口的优势。
4. 创建线程的三种方式对比
使用Runnable和Callable接口创建线程的优势:
- 因为是实现接口,还可以继承其他类,避免由于Java的单继承特性而带来的局限。
- 这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而将CPU,代码和数据分开,更清晰。
劣势:
- 编程稍稍复杂,且要访问当前线程,需要使用Thread.currentThread()方法获取当前线程的实例
使用继承Thread类的方式优势:
- 编程简单
劣势:
因为已经继承了Thread类,无法再继承其他的类。
三 线程的生命周期
Java中的线程总结起来可以划分为5个生命周期:
- 新建:创建Thread类或其子类对象
- 就绪(可运行):调用Thread类的start方法启动线程,此时线程处于可运行状态,但何时运行取决于JVM线程调度器的调度。一个就绪的线程只有获得CPU时间片才可以进入运行状态。
- 运行:获得CPU时间片开始运行。现在的操作系统基本上都是抢占式调度策略(当然也有些小型设备是协作式调度策略),即当前时间片用完,即会被其他线程抢占(取决于优先级)。
- 阻塞:有sleep、join或者IO造成的线程阻塞(调用suspend()也可以使线程挂起,但是该方法容易导致死锁,已不推荐使用)
- 死亡:run结束或者异常。(调用stop方法也可以结束线程,但该方法容易造成死锁,已不推荐使用)
下面是线程状态转换图:
四 Java中内置的用于线程控制的方法
常见的有以下几个方法:(详细请查看Thread源码)
- join:加入一个线程,是当前线程(调用join的线程所处的线程)阻塞,直到调用join的线程执行完。
- setDaemon:将一个线程设置为后台线程(守护线程),该方法必须在start方法前调用;isDaemon判断一个线程是否是后台线程。
- sleep:是当前正在执行的线程阻塞
- yield:使当前正在执行的线程进入就绪状态。将机会让给与它优先级相同或者比它高的线程,如其优先级很高,完全有可能被JVM的线程调度器再次调用
- setPriority:设置当前线程的优先级
五 线程同步
1. 使用synchronized关键实现同步
- 同步代码块
使用synchronized对对象进行加锁,如:
1 | synchronized(obj){ |
- 同步方法
使用synchronized对方法进行加锁
1 | public synchronized void lock(Object obj){ |
synchronized关键字可以对代码块,方法加锁,但不能修饰构造器,成员变量等。
使用synchronized加锁后,线程什么情况下会释放锁:
- 同步方法,代码块执行完毕
- 在同步方法,代码块执行中遇到了break,return
- 出现了未处理的异常和Error
- 调用wait方法
下列情况下不会释放锁:
- 调用sleep,yield方法
- 调用suspend方法
2. 同步锁
从java5开始java提供了接口Lock和ReadWriteLock来实现锁同步。Lock接口有个实现类ReentrantLock,ReadWriteLock有个实现类ReentrantReadWriteLock。使用这两个类可以实现锁同步。如:
1 | public void test(){ |
Java8又新增了一个StampedLock类,在大多数情况下它可以替代ReentrantReadWriteLock。
六 线程通信
1.传统的线程通信
使用wait、notify、notifyAll实现线程通信,适用于使用synchronized关键字来保证线程同步的情况下。代码示例:
2.使用Contition实现线程通信
若是使用Lock对象实现线程同步,则无法使用上述的线程通信的方法,java5提供了一个Condition类来实现线程通信。可通过Lock对象的newCondition()方法获得Condition对象,Condition对象中有await,signal,signalAll方法分别对应传统的wait,notify,notifyAll方法,用于实现线程通信。
3.使用阻塞队列实现线程通信
java5提供了一个BlockingQueue接口,它有一个特征:当生产者线程试图向BlockingQueue里放入元素时,若队列已满,则该线程被阻塞;当消费者试图从BlockingQueue里取出元素时,若队列为空,这该线程被阻塞。利用此原理可以实现线程通信。
该接口有几个实现类,可用于构造线程池。
七 线程池
线程池即是一个对象池,传入的都是Runnable对象,底层使用数组或者集合来实现。java中内置了一个Executeors工厂类来实现线程池(它还有一个子类ForkjoinPool),此外还可以使用其他的第三方框架,如ThreadPoolExecutor。
八 ThreadLocal类
在处理多线程并发问题上还有一个解决方案,是使用ThreadLocal类将需要并发的资源封装起来,这样当多个线程并发访问这些资源时,ThreadLocal类会为没一个线程均复制一份该资源的副本,从根本上解决了资源的共享冲突问题。但是此方式虽解决了冲突,但多个线程之间也无法共享资源。