第九节 Java 多线程
1. 线程的概述
在我们的操作系统中,每个独立执行的程序都可称为一个进程,我们之所以可以在一台电脑上同时做很多事情就是因为,多进程的存在,CPU在一个时间点只能执行一段代码,但是由于CPU执行的非常快速,因此我们感觉不到,好像多个应用在CPU同时执行一样。
线程定义:
在多任务操作系统中,每个运行的程序都是一个进程,用来执行不同的任务,而在一个进程中还可以有多个执行单元同时运行,来完成一个或多个程序任务,这些执行单元可以看做程序执行的一条线索,被称为线程。每个进程都会有一个主线程。这样的好处就是可以充分的利用CPU的资源。
2. 线程的创建
线程的创建有三种方式:
(1)继承Thread类重写run()方法
(2)实现Runnable接口重写run()方法
(3)实现Callable接口,重写call()方法,并使用Future来获取call()方法的返回结果。
2.1 Thread类实现多线程
步骤:(1)创建一个Thread的子类,重写run()方法
(2)创建该类子类实例对象,调用start()方法启动线程
示例如下:
public class ThreadDemo extends Thread{
public ThreadDemo(String name){
super(name);
}
@Override
public void run(){
int i=0;
while(i++<10){
System.out.println(Thread.currentThread().getName()+"的run方法在执行");
}
}
}
public class ThreadExample01 {
public static void main(String[] args) {
ThreadDemo thread1=new ThreadDemo("thread1");
ThreadDemo thread2=new ThreadDemo("thread2");
thread1.start();
thread2.start();
}
}
2.2 Runnable 接口实现多线程
Thread的类的局限就是Java只支持单继承,继承了Thread类就不能再继承其他类了。Runnable是一个函数式接口。
步骤:
(1)创建一个Runnable接口的实现类,重写run()方法
(2)创建Runnable接口的实现类
(3)使用Thread有参构造方法创建线程实例,并将Runnable接口的实现类对象作为参数传进去
(4)调用线程的start()方法
示例如下:
public class RunnableDemo implements Runnable{
@Override
public void run() {
int i=0;
while(i++<10){
System.out.println(Thread.currentThread().getName()+"的run方法在运行");
}
}
}
public class RunnableExample01 {
public static void main(String[] args) {
//创建实现类
RunnableDemo mythread1=new RunnableDemo();
RunnableDemo mythread2=new RunnableDemo();
//创建线程对象
Thread thread1=new Thread(mythread1,"thread1");
Thread thread2=new Thread(mythread2,"thread2");
thread1.start();
thread2.start();
}
}
2.3 Callable 接口实现多线程
以上两种方法没有返回值,无法从多个线程中获取返回结果。
通过Callable接口实现多线程的方式与Runnable接口实现多线程的方式一样,都是通过Thread类的有参构造方法传入Runnable接口类型的参数来实现多线程,不同的就是这个传入的是Runable接口的子类FutureTask对象作为参数,而FutureTask对象中则封装带有返回值的Callable接口实现类。Callable也是函数式接口。
步骤:
(1)创建一个Callable接口的实现类,同时重写Callable接口的call()方法
(2)创建Callable接口的实现类对象
(3)通过FutureTask线程结果处理类的有参构造方法来封装Callable接口实现类对象
(4)使用参数为FutureTask类对象的Thread有参构造方法创建Thread线程
(5)调用线程实例的start()方法开启线程
示例如下:
public class CallableDemo implements Callable {
@Override
public Object call() throws Exception {
int i=0;
while (i++<10){
System.out.println(Thread.currentThread().getName()+"的call方法在执行");
}
return i;
}
}
public class CallableExample01 {
public static void main(String[] args) {
CallableDemo mythread1=new CallableDemo();
CallableDemo mythread2=new CallableDemo();
FutureTask ft1=new FutureTask(mythread1);
FutureTask ft2=new FutureTask(mythread2);
Thread thread1=new Thread(ft1,"thread1");
Thread thread2=new Thread(ft2,"thread2");
thread1.start();
thread2.start();
//通过FutureTask对象的方法管理返回值
System.out.println("thread1返回结果:"+ft1.get());
System.out.println("thread2返回结果:"+ft2.get());
}
}
FutureTask类的继承关系:FutureTask实现了RunableFuture接口,RunableFuture继承了Runable接口和Future接口,其中Future接口是JDK5提供用来管理线程执行返回结果的。
Future接口的方法:
方法声明 | 功能描述 |
---|---|
boolean cancel(boolean MayInterruptIfRunning) | 用于取消任务,参数MayInterruptIfRunning表示是否允许取消正在执行却没有执行执行完毕的任务,如果设置为true则表示可以取消正在执行的任务 |
boolean isCancelled() | 判断任务是否被取消成功,如果在任务正常完成前被取消成功,则返回true |
boolean isDone() | 判断任务是否已经完成,若任务完成,则返回true |
V get() | 用于获取执行结果,这个方法会发生阻塞,一直等到任务执行完毕才返回执行结果 |
V get(long timeout,TimeUnit unit) | 用于在指定时间内获取执行结果,如果在指定时间内还没获取到结果,就直接返回null |
2.4 三种实现多线程方式比较
(1)我们用继承Thread的方式来创建多线程不会共享一个 资源。而利用Runnable和Callable方式创建的可以
(2)利用Thread和Runnable创建的线程没有返回值,而Callable可以返回一个值。
(3)利用Thread去实现由于Java的单根性,导致实现类不能再去继承其他的类。而实现Runnable和Callable的方式可以避免这个问题。
2.5 后台线程
后台线程也称守护线程,就是当前台线程全部执行完毕之后,整个进程就结束了,此时JVM就会通知后台线程结束,但是由于后台线程从接收到指令到做出响应需要时间,因此也会执行。(注意:后台线程和普通线程一样,并不是只有前台线程执行完毕它才执行。
新创建的线程默认都是前台线程,如果某个线程对象再启动之前调用了setDaemon(true)语句,这个线程就变成了后台线程
示例:
public class BgThreadDemo implements Runnable{
@Override
public void run() {
while (true){
System.out.println(Thread.currentThread().getName()+"---在运行");
}
}
}
public class BgThreadExample {
public static void main(String[] args) {
System.out.println("main是后台线程吗?"+Thread.currentThread().isDaemon());
BgThreadDemo bt=new BgThreadDemo();
Thread thread=new Thread(bt,"thread");
System.out.println("thread是后台线程吗?"+thread.isDaemon());
//设置线程thread为后台线程
thread.setDaemon(true);
thread.start();
//模拟主线程
for(int i=0;i<1000;i++){
System.out.println(i);
}
}
}
3. 线程的生命周期及状态转换
(1)NEW(新建状态)
仅仅是JVM为其分配了内存
(2)RUNNABLE(可运行状态)
调用了start()方法,内部细分为:READY(就绪状态)、RUNNING(运行状态)
(3)BLOCKED(阻塞状态)
由于某些原因失去CPU(比如IO等待、锁等等),进入阻塞的要想运行需要重新先进入就绪状态,再进入运行状态不能直接进入运行状态。一般是由于以下两种情况进入阻塞状态
1.线程A运行过程中,试图获取同步锁时,却被B获取,JVM就会把当前线程丢到对象的锁池,A就进入了阻塞状态
2.当线程运行过程中发出了I/O请求时
(4)WAITING(等待状态)
处于运行状态的使用了无时间参数限制的方法之后进入等待状态,例如wait()、join()等方法
处于等待状态的线程不能立即争夺CPU使用权,必须等待其他线程执行特定操作后才有机会再此争夺,例如其他线程调用notify()或notifyAll(),调用join()方法而处于等待状态的线程,必须等待其他加入线程终止。
(5)TIMED_WAITING(定时等待状态)
与等待状态类似,不同的是只是运行线程调用了有时间参数限制的方法
(6)TERMINATED(终止状态)
正常执行完毕或发生错误。
只有处于就绪状态下的线程才有资格来争夺CPU。
4. 线程的调度
由于我们的计算机CPU个数有限,因此不能真正意义上的去实现并行执行,只能是并发执行,而按照特定的机制为程序中的线程分配CPU的使用权就称之为调度。
我们单处理机上通常采用的有分时调度和抢占式,我们java采用的是抢占式,我们为运行池中所有就绪状态的线程有优先级,优先级越高的获得CPU的概率越大。这样我们可以通过程序来控制CPU的调度。
4.1 线程的优先级
优先级用1~10之间的整数表示,数字越大优先级越高
Thread类的优先级常量:
Thread类的静态常量 | 功能描述 |
---|---|
static int MAX_PRIORITY | 表示线程的最高优先级,相当于值10 |
static int MIN_PRIORITY | 表示线程的最低优先级,相当于1 |
static int NORM_PRIORITY | 表示线程的普通优先级,相当于值5 |
主线程默认是普通优先级,通过setPriority(int newPriority)方法进行设置。
4.2 线程休眠
通过Tread的静态方法sleep(long millis)方法进入休眠等待状态,当休眠时间到了,线程就会进入就绪状态,来重新的进行CPU的争夺。
4.3 线程让步
通过yield()来实现线程让步,他是让正在运行的线程转换成就绪状态,让系统的调度器重新调度一次。
4.4 线程插队
在A个线程中,B线程调用了join()方法,那么A线程将进入阻塞状态直到B线程执行完毕,A才又进入到就绪状态。还有join(long millis)这个方法是用来指定一定时间后A进入就绪状态。
5. 多线程同步
例如我们在售票的时候,如果开启多个线程来共享同一个资源(比如说票数),可能几个线程同时执行到票数等于1,然后线程A再执行减一操作,线程A执行后,线程B再执行减1,这样就出现0票的数据,以此类推就可能出现负数票,显然这样是不正确的。
因此线程同步就是来解决这一问题。
5.1 同步代码块
保证共享资源的代码在任意时刻只能有一个线程访问。我们把共享资源放在一个使用synchronized关键字来修饰的代码块中
语法格式如下:
synchronized(lock){
//操作共享资源代码块
……}
示例:
public class LockDemo implements Runnable{
private int tickets=10;
Object lock=new Object();//用作同步代码块的锁
@Override
public void run() {
while (true){
synchronized (lock){
if(tickets>0){
try {
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在发售第"+tickets-- +"张票");
}
}
}
}
}
public class LockExample {
public static void main(String[] args) {
LockDemo ld=new LockDemo();
new Thread(ld,"窗口1").start();
new Thread(ld,"窗口1").start();
new Thread(ld,"窗口1").start();
new Thread(ld,"窗口1").start();
}
}
结果如下:
注意:这里的锁对象要放到run()方法外,不然每次运行的线程对象都会创建锁对象,这样每个锁都有自己的标志位,线程之间就不能达到同步的效果。
5.2 同步方法
语法格式:
[修饰符] synchronized 返回值类型 方法名([参数1,…]){ }
被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会被阻塞,直到当前线程访问完毕后,其他线程才有机会执行。
例如上面的代码块可以如下写法:
public class MethodLockDemo implements Runnable{
private int tickets=10;
@Override
public void run() {
while (true){
saleTicket();
}
}
private synchronized void saleTicket(){
if (tickets>0){
try{
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在发售第"+tickets-- +"张票");
}
}
}
public class MethodLockExample {
public static void main(String[] args) {
MethodLockDemo mld=new MethodLockDemo();
new Thread(mld,"窗口1").start();
new Thread(mld,"窗口2").start();
new Thread(mld,"窗口3").start();
new Thread(mld,"窗口4").start();
}
}
在同步方法中也有锁,它的锁就是当前调用该方法的对象,也就是this对象,这样同步方法被所有线程共享,方法所在对象也就唯一,也就是说锁唯一了。但是有时候我们同步代码块是静态的,这样我们没有创建对象,那么静态代码块就不会是this,它的锁是该方法所在类的class对象,该对象可以直接用 “类名. class" 的方式获取.
同步方法和同步代码块带来的弊端:线程多次去询问锁的状态非常的消耗资源,效率比较低
5.3 同步锁
一种封闭式的锁机制,使用也很简单,但是它无法中断一个正在等候获得锁的线程,也无法通过轮询得到锁,如果不想等下去,也就没法得到锁。
JDK 5开始Java增加了一个功能强大的Lock锁。与上面同步synchronized功能基本相同,但是Lock锁可以让某个线程在持续获取同步锁失败后返回,不再继续等待,而且Lock锁在使用更加的灵活。
示例如下:
public class SynchronizedDemo implements Runnable{
private int tickets=10;
//定义一个Lock对象
private final Lock lock=new ReentrantLock();
@Override
public void run() {
while (true){
lock.lock();
if(tickets>0){
try{
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"正在发售第"+tickets-- +"张票");
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}else{
System.exit(0);
}
}
}
}
public class SynchronizedExample {
public static void main(String[] args) {
SynchronizedDemo sd=new SynchronizedDemo();
new Thread(sd,"窗口1").start();
new Thread(sd,"窗口2").start();
new Thread(sd,"窗口3").start();
new Thread(sd,"窗口4").start();
}
}
这里是通过Lock接口的实现类ReentrantLock来创建的锁对象。
5.4 死锁问题
A拥有B资源,但是它需要C才能将程序执行完毕,D拥有C资源,但是 它需要B资源才能执行完毕,这样就会发生死锁。
示例如下:
public class DeadLockDemo implements Runnable{
static Object chopsticks=new Object();
static Object knifeAndFork=new Object();
private boolean flag;
public DeadLockDemo(boolean flag){
this.flag=flag;
}
@Override
public void run() {
if(flag){
while (true){
//chopsticks锁对象上的同步代码块
synchronized (knifeAndFork){
System.out.println(Thread.currentThread().getName()+"---if---knifeAndFork");
//knifeAndFork锁对象上的同步代码块
synchronized (chopsticks){
System.out.println(Thread.currentThread().getName()+"---if---chopsticks");
}
}
}
}else{
while (true){
//chopsticks锁对象上的同步代码块
synchronized (chopsticks){
System.out.println(Thread.currentThread().getName()+"---if---chopsticks");
//knifeAndFork锁对象上的同步代码块
synchronized (knifeAndFork){
System.out.println(Thread.currentThread().getName()+"---if---knifeAndFork");
}
}
}
}
}
}
public class DeadLockExample {
public static void main(String[] args) {
DeadLockDemo dld=new DeadLockDemo(true);
DeadLockDemo dld2=new DeadLockDemo(false);
new Thread(dld,"Chinese").start();
new Thread(dld2,"American").start();
}
}
6. 多线程通信
目的是:完成多线程之间的更好的协同工作。
6.1 生产者与消费者的问题
在我们的生活中,一件产品总要经历从生产到销售这样一个步骤,那么我们生产可以看作是一条线程,而销售也可以看作是一个线程,这两个线程之间很明显要进行协同,我们必须要保证有产品生产出来再去销售,同时也要尽可能的让生产的速度跟上销售的速度。
但是我们如果直接的去用两条多线程而不加控制的话就可能出现问题,比如我们还没有生产消费者线程先去跑,然后就去销售了等。
在Java中,我们提供了线程同步,和wait()、notify()、notifyAll()等方法来解决线程之间的通信问题。由于Java中任何的类都是Object的子类,因此任何类的实例对象都可以直接的使用这些方法。
线程通信的常用方法:
方法声明 | 功能描述 |
---|---|
void wait() | 使当前线程放弃同步锁并进入等待,直到其他线程进入此同步锁,并调用notify()或notifyAll()方法唤醒该线程为止 |
void notify() | 唤醒此同步锁上等待的第一个调用wait()方法的线程 |
void notifyAll() | 唤醒此同步锁上调用wait()方法的所有线程 |
注意:这三个方法的调用者必须是同步锁对象。
示例如下:
public class ThreadTalkDemo {
public static void main(String[] args) {
//定义一个集合用来存放商品
List<Object> goods=new ArrayList<>();
//记录线程开始执行时间
long start=System.currentTimeMillis();
//创建生产线程
Thread thread1=new Thread(()->{
int num=0;
while (System.currentTimeMillis()-start<=100){
//使用同步锁
synchronized (goods){
if(goods.size()>0){
try {
goods.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}else {
//生产者生产商品
goods.add("商品"+ ++num);
System.out.println("生产商品"+num);
}
}
}
},"生产者");
//创建消费者
Thread thread2=new Thread(()->{
int num=0;
while (System.currentTimeMillis()-start<=100){
synchronized (goods){
if(goods.size()<=0){
goods.notify();
}else {
//消费商品
goods.remove("商品"+ ++num);
System.out.println("消费商品"+num);
}
}
}
},"消费者");
//开启线程
thread1.start();
thread2.start();
}
}
7. 线程池
目的:进行线程的管理
我们手动式的去创建、管理线程是不可取的在复杂任务中,因为线程对象使用了大量内存,在大规模应用程序中,创建、分配和释放多线程对象会产生大量内存管理开销。这时候我们就需要借助线程池来帮助我们进行管理优化。
7.1 Executor 接口实现线程池管理
从JDK 5开始,在java.util.concurrent包下增加了Executor接口及其子类,允许使用线程池技术来管理线程并发问题。Executor接口提供了一个常用的ExecutorService子接口,通过该接口可以很方便的进行线程池管理。
通过Executor接口实现线程池管理的主要步骤:
(1)创建实现Runable接口或者Callable接口的实现类,同时重写run()或者call()方法;
(2)创建Runnable接口或者Callable接口的实现类对象
(3)使用Executor线程执行器类创建线程池
(4)使用ExecutorService执行器服务类的submit()方法将Runable接口或者Callable接口的实现类对象提交到线程池进行管理
(5)线程任务执行完成后,可以使用shutdown()方法关闭线程池
示例如下:
public class ThreadExecutorDemo implements Callable<Object> {
@Override
public Object call() throws Exception {
int i=0;
while (i++<5){
System.out.println(Thread.currentThread().getName()+"的call()方法在运行");
}
return i;
}
}
public class ThreadExecutorExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建线程的 实现类对象
ThreadExecutorDemo ted=new ThreadExecutorDemo();
//使用Executor线程执行器类创建可扩展的线程池
ExecutorService executor= Executors.newCachedThreadPool();
//将线程对象提交到线程池进行管理
Future<Object> result1=executor.submit(ted);
Future<Object> result2=executor.submit(ted);
//关闭线程池
executor.shutdown();
//对于有返回值的线程任务,获取执行结果
System.out.println("thread-1返回的结果:"+result1.get());
System.out.println("thread-2返回的结果:"+result2.get());
}
}
效果如下:
其中的Executors是JDK 5中增加的线程执行器类工具,提供了4种方法来创建用于不同需求的线程池
Executors 创建线程池的方法:
方法声明 | 功能描述 |
---|---|
ExecutorService newCachedThreadPool() | 创建一个可扩展线程池的执行器,这个线程池执行器适用于启动许多短期任务的应用程序 |
ExecutorService newFixedThreadPool(int nThreads) | 创建一个固定线程数量线程池的执行器。这种线程池执行器可以很好的控制多线程任务,也不会导致由于响应过多导致的程序崩溃 |
ExecutorService newSingleThreadExecutor() | 在特殊需求下创建一个只执行一个任务的单个线程 |
ScheduledExecutorSerivice newScheduledThreadPool(int corePoolSize) | 创建一个定长线程池,支持定时及周期性任务执行 |
7.2 CompletableFuture 类实现线程池管理
在使用Callable接口实现多线程时,会用到FutureTask类对线程执行结果进行管理和获取,由于该类在获取结果时是通过阻塞或者轮询的方式,违背多线程编程的初衷且耗费过多资源。为此,JDK 8中对FutureTask存在的不足进行了改进,增加了一个强大的函数式异步编程辅助类,该类同时实现了Future接口和CompletionStage接口(Java 8中增加的一个线程任务完成结果接口),并对Future进行了强大的扩展,简化异步编程的复杂性。
CompletableFuture类在进行线程管理时,通常使用以下四个静态方法来为一段异步执行的代码创建CompletableFuture对象:
方法声明 | 功能描述 |
---|---|
static CompletableFuture<Void> runAsync(Runnable runnable) | 以Runnable 函数式接口类型为参数,并使用ForkJoinPool.commonPool()作为它的线程池执行异步代码获取CompletableFuture计算结果为空的对象 |
static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor) | 以Runnable函数式接口类型为参数,并传入指定的线程执行器executor来获取CompletableFuture计算结果为空的对象 |
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) | 以Supplier函数式接口类型为参数,并使用ForkJoinPool.commonPool()作为它的线程池执行异步代码获取CompletableFuture计算结果非空的对象 |
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,Executor executor | 以Supplier函数式接口类型为参数,并传入指定的线程池执行器executor来获取CompletableFuture计算结果非空的对象 |
示例如下:
public class CompletableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// //创建第一个线程,执行1到5相加
CompletableFuture<Integer> completableFuture1=CompletableFuture.supplyAsync(()->{
int sum=0,i=0;
while (i++<5){
sum+=i;
//显示线程任务执行过程
System.out.println(Thread.currentThread().getName()+"线程任务正在执行....i"+i);
}
return sum;
});
//创建第二个线程,执行6到10相加运算
CompletableFuture<Integer> completableFuture2=CompletableFuture.supplyAsync(()->{
int sum=0,j=5;
while (j++<10){
sum+=j;
//显示线程任务执行过程
System.out.println(Thread.currentThread().getName()+"线程任务正在执行....j"+j);
}
return sum;
});
// //将两个线程执行结果获取整合
CompletableFuture<Integer> completableFuture3=completableFuture1.thenCombine(completableFuture2,(result1,result2)->result1+result2);
System.out.println("1相加到10的结果为:"+completableFuture3.get());
}
}