多线程概述
一个软件可以同时干多件事情,就是多线程,目前我们编写的程序都是单线程的,在main方法中从上到下的进行,执行完上面的程序才能执行后面的程序。下面就将讲解如何写多线程程序。
在Java中创建多线程有两种放法,一种是继承Thread类,然后重写它的run()方法。第二种是实现Runnable接口,并且实现run()方法。
继承Thread类
继承Thread类实现多线程的步骤为
- 继承Thread类,重写run()方法
- 创建类对象,调用继承的start()方法
我们写一个MyThread类继承自Thread类,并重写run方法如下
public class MyThread extends Thread { |
新建一个测试类,在其main方法中创建MyThread对象,并且调用对象继承自Thread的start()方法
MyThread thread = new MyThread(); |
程序输出为
观察到main方法中的程序和MyThread的run()方法中的程序在交替的进行。而不是在等待我执行完了,另一个在执行,而是两个在同时的执行,这就是多线程。
实现Runnable接口
实现Runnable接口实现多线程的步骤为
- 实现Runnable接口并实现run()方法
- 在main中创建实现类对象
- 将实现类对象多为参数传入Thread()的构造方法,得到一个Thread对象
- 该Thread对象调用start()方法
下面进行演示,首先创建一个类实现Runnable接口
public class MyRun implements Runnable{ |
下面在实现类中实现多线程,如下
Runnable run = new MyRun(); //创建实现类对象 |
输出为
可以观察到main中的程序和run()方法中的程序是在同时进行的。
Thread中的常见方法
- getName()
- 获得线程的名称
- currentThread()
- 获得正在执行的线程
- setName(String str)
- 设置线程的名字
- 也可以通过new Thread(String str)设置线程的名字
- sleep(long l)
- 线程休眠l毫秒
我们修改MyRun中的run()方法为
for (int i = 0; i < 20; i++) { |
修改main方法为
Runnable run = new MyRun(); |
输出结果为
线程安全
安全问题
现在考虑这么一个问题,有三个售票机在售票,那么它们不能发生售出同样的票,也不能售出不存在的票。现在我要用一个类模拟售票机售票,并使用多线程模拟同时售票,新建一个SellTicket类
public class SellTicket implements Runnable{ |
在测试类的main方法中创建三个线程同时售票
Runnable sellTicket = new SellTicket(); |
输出为
我们发现出现了不同的售票线程售出了相同的票,并且有的售票线程售出了不存在的票-1。
线程同步
之所以会出现上面的问题,就是因为在有的售票机卖出了票,即进入了if语句后,但是还未进行ticket–操作,但是这个时候这个线程失去了CPU的执行权,并且别的线程拿到了CPU的执行权,由于未对ticket–,所以它们拿到的是同一张票,所以这就是为什么它们能卖出同一张票的原因。同理卖出不存在的票也是同种原因。
那么如何解决这个问题,我们必须要求在售票机在完成售票并且对ticket–之前,别的售票机不能对ticket进行操作,这样就不会出现票重复和卖出不存在的票的情况了,而实现这个的技术就叫做同步。有三种实现方式,分别是
- synchronized代码块
- 同步方法
- Lock锁
下面具体介绍用法。
sychronized代码块
sychronized代码块的格式为
sychronized(锁对象) { |
其中锁对象可以是任意的对象,当一个线程执行到同步代码块时,会将该锁对象交给这个线程,当这个线程执行完同步代码块时,会释放锁对象,所以如果这个线程在同步代码块内失去了CPU的执行权,因为别的线程没有锁对象,就不能进入同步代码块执行,就会进入堵塞状态,等待锁对象被释放。所以锁对象就相当于是钥匙了,要保证多个线程的锁对象要相同,这样就只有一把钥匙了。
我们重新修改SellTicket的类如下
public class SellTicket implements Runnable{ |
输出结果为
这次我们发现没有卖出重复的票,也没有卖出不可能的票。
同步方法
同步方法其实就是使用synchronized修饰的方法,这个方法每次也只能有一个线程执行,它的锁对象是this,我们把上面买票的程序抽取出为一个方法sellTicket()
public synchronized void sellTicket() { |
这时run()可简化为
|
输出为
也达到了同样的效果。
注意:
- 同步方法也可以为静态方法,不过这时的锁对象不在是this了,而是本类的class属性,也是一个对象。
Lock锁
Lock是一个接口,它比较灵活。之前我们讲到,在线程执行到synchronized代码块时,会获得锁对象,在执行完代码块时,会释放锁对象,但是这些对我们都是不可见的,而Lock灵活在我们自己觉得在哪里加锁,哪里释放锁。它有两个方法
- lock()
- 加锁
- unlock()
- 释放锁
lock()一般写在同步代码前,unlock()写在同步代码后。ReentrantLock是Lock的实现类,下面我们将演示如何使用Lock锁同步,修改run方法为
Lock lock = new ReentrantLock(); //创建Lock锁实现类对象 |
输出为
可见达到了同步的效果。
等待唤醒
当我们排队买奶茶时,我们对老板说我们要一杯奶茶,然后我们就等着,老板去制作奶茶,等老板制作好奶茶后去喊我们。这其实就是等待唤醒,当多个线程去操作同一个资源时,比如奶茶,就需要一方(顾客)等着,等待另一方(老板)唤醒,总不能奶茶没有好我去抢吧。
线程状态
在讲解等待唤醒之前,我们先对线程的状态有一个大致的了解,看图
当我们创建一个线程对象还没有start()时,这时它处于New状态;当我们执行start()方法后,这时的线程状态由New转向Runnable运行状态;如果执行完run()方法,或者调用了stop()方法或者抛出了异常那么该线程进入死亡状态。如果在Runnable状态失去了CPU的执行权,那么就会进入Blocked阻塞状态;线程在这里等待CPU的执行权,拿到了CPU的执行权就会从该状态来到Runnable状态;如果线程在运行时执行了sleep(l)或wait(l)(l为等待的时间)方法,那么就会由Runnable状态进入Timed waiting状态,在这个状态中,线程放弃争夺CPU的执行权,当等待的时间到了之后,如果CPU空闲,那么就进入Runnable状态,如果忙碌,那么就进入Blocked状态,与其他线程一起争夺CPU的执行权。如果在Runnable执行了wait()(不带参数的)方法,就会进入Waiting永久等待状态,直到锁对象执行notify()方法唤醒,如果CPU空闲,就进入Runnable状态,否则进入Blocked状态争夺CPU执行权。
等待唤醒
这里的等待唤醒指的就是上面提及的Runnable状态执行wait()方法到Waiting永久等待状态,以及执行notiify()方法有永久等待状态到Runnable状态。前者为等待,后者为唤醒。
注意:
- 只有锁对象才能调用wait()和notify()方法
- wait()和notify()的调用者应该是同一锁对象,并且必须写在同步代码块中
- 执行wait()被唤醒后,会继续执行wait()后面的代码
- notify()一次只能唤醒一个线程,唤醒的是睡眠最久的线程,notifyAll()能够唤醒所有的线程
下面以最先提及的买奶茶为例演示这一过程。首先创建Runnable顾客类和老板类和奶茶类
public class MilkTea { |
public class CustomerThread implements Runnable { |
public class Shopper implements Runnable { |
下面在测试类中创建两个线程
public class Test { |
输出为
这就是等待唤醒的过程。
线程池
当我们需要一个新的线程执行任务,我们就会创建一个新的线程,但是如果这个线程执行的任务很少,并且我们需要频繁的创建线程,这个创建线程的过程会很耗费时间,所以我们就想有没有一个机制,我们不用创建线程,当我们需要线程时我们去取,当我们用完时,我们还给它。这样就不需要频繁创建线程,省去时间,提高效率。线程池可以帮我们实现这一个想法。
那接下来的问题我们怎么使用Java为我们准备的线程池,Executors提供了一个静态方法newFixedThreadPool(int nThreads),这个方法接收的参数是线程池中线程的个数,返回一个ExecutorService对象,然后我们就可以使用该对象的submit(Runnable task)方法,传入一个Runnable实现类对象就可以了。下面我们来示例一番
import java.util.concurrent.ExecutorService; |
输出为
pool-1-thread-1 0 |
Lambda表达式
我们在创建一个线程时,我们一般需要做一下的步骤
- 创建一实现类实现Runnable接口
- 重写run方法
- 创建实现类对象
- 将该对象传入Thread的构造方法中
上面的写法可以简化,省去创建一个实现类,直接创建一个匿名内部类
- 创建一个Runnable匿名内部类
- 重写run方法
- 将该对象传入Thread的构造方法中
其实上面有很多的代码是多余,真正有用的代码就是run()方法里面的代码,但是为了创建一个线程我们不得不要创建一个对象,然后巴拉巴拉。其实有时候我们不关心谁来做,只需要告诉我怎么做,比如一个线程你只需要告诉我run()方法就可以了,告诉我怎么做就可以了,但是我们却要创建一个对象等等一系列的操作才能达到这个目的。
Lambda的使用
Java在JDK 1.8中引入了Lambda表达式,可以极大简化我们的编程,可以做到我上面所说的只关心怎么做的问题,不需要创建对象。我们来看看下面这段代码用Lambda怎么写
new Thread(new Runnable() { |
Lambda的写法为
new Thread(() -> { |
现在你可能没有看懂这个写法,下面让我为你解释一番。首先我们注意到
new Runnable() { |
被简单的替换为了
() -> { |
两段代码很像,但是Lambda省略了很多的东西。首先我们知道Thread()里面传的是一个Runnable实现类的对象,该类重写类run方法,真正有用的就是run方法,所以我们把这些全部省略了,直接传入一个run()就可以了,并且由于run()方法的方法名是确定的,我们连run方法名都可以省去,返回值类型也是确定,所以我们也可以省去,最后只剩一个参数列表,在参数列表与方法体之间加入->就是Lambda表达式。使用Lambda不用创建对象,我们只需要传入一个方法,告诉它怎么做就可以了。这个也叫做函数式编程。
Lambda表达式的格式为
(参数列表) -> { |
为了熟悉Lambda表达式的使用,我们来看一个例子,定义一个Calculator的接口,里面有一个方法叫calculate(int a, int b);
,如下
public interface Calculator { |
在测试类中定义一个方法,该方法需要Calculator
接口作为参数
public static int cal(int a, int b, Calculator calculator) { |
这个方法表示的是,a,b经过Calculator计算之后得到一个数,而计算方法,根据我们传入的calculator而定,这明显是我们只需要告诉计算器怎么做就行,我们把做的方法告诉它,使用Lambda表达式
//加法计算器 |
输出为
5 |
根据我们传入的方法不同,这个计算器就不同,计算器关心的就是怎么做,你告诉怎么做就可以。
Lambda的省略格式
其实上面的Lambda还可以进行化简,因为还有很多是可以推断出来的,比如参数列表里面的参数类型可以省略,因为这个参数类型时确定的,不可能会变的。如
int result1 = cal(2,3, (a, b) -> { |
如果方法体里面只有一条语句时,那么花括号也可以省略,这时分号也可以省略,如果这条语句是return语句,那么return也可以省略,因为必须是要返回一个值的,这个可以推断出来,所以可以省略,所以上面又可以简写为
int result1 = cal(2,3, (a, b) -> a + b); |
如果参数列表里面只有一个参数的话,那么小括号也可以省略
param -> { |
Lambda表达式的使用前提
虽然Lambda表达式这么好用,但是是有使用前提的
- 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。
- 比如Runnable接口,里面只有一个run()方法是抽象方法
- 比如上面定义的Calculator接口,里面也只有一个抽象方法calculate()
- 使用Lambda必须具有上下文推断。
- 也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
- 不能是我要一个Calculator接口的calculate()方法,你给我传一个Runnable的run()方法,兄弟,暗号对不上啊。
备注:有且仅有一个抽象方法的接口,称为”函数式接口“。