本篇讲解java多线程
基本概念: 程序、进程、线程
**程序(program)**是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
**进程(process)**是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。 ——生命周期
- 如:运行中的QQ,运行中的MP3播放器
- 程序是静态的,进程是动态的
- 进程作为资源分配的单位, 系统在运行时会为每个进程分配不同的内存区域
线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。
若一个进程同一时间并行执行多个线程,就是支持多线程的
线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
一个进程中的多个线程共享相同的内存单元/内存地址空间 --> 它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
单核CPU和多核CPU的理解
- 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就好比收费人员。如果有某个人不想交钱, 那么收费人员可以把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费) 。 但是因为CPU时间单元特别短,因此感觉不出来。
- 如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
- 一个Java应用程序java.exe,其实至少有三个线程:
main()
主线程,gc()
垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
并行与并发
- 并行: 多个CPU同时执行多个任务。比如:多个人同时做不同的事。
- 并发: 一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。
使用多线程的优点
-
提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
-
提高计算机系统CPU的利用率
-
改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
线程的创建和使用
Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread
类来体现。
-
Thread类的特性
每个线程都是通过某个特定Thread对象的
run()
方法来完成操作的,经常把run()
方法的主体称为线程体
通过该Thread对象的start()
方法来启动这个线程,而非直接调用run()
Thread类
Thread类的使用
- 构造器
Thread()
: 创建新的Thread对象
Thread(String threadname)
: 创建线程并指定线程实例名
Thread(Runnable target)
: 指定创建线程的目标对象,它实现了Runnable接口中的run方法
Thread(Runnable target, String name)
: 创建新的Thread对象
-
创建线程的两种方式
JDK1.5之前创建新执行线程有两种方法:
-
继承Thread类的方式
-
定义子类继承Thread类。
-
子类中重写Thread类中的run方法。
-
创建Thread子类对象,即创建了线程对象。
-
调用线程对象
start()
方法:启动线程,调用run方法//1. 创建一个继承于Thread类的子类 class MyThread extends Thread { //2. 重写Thread类的run() @Override public void run() { for (int i = 0; i < 100; i++) { if(i % 2 == 0){ System.out.println(Thread.currentThread().getName() + ":" + i); } } } } //另一个类中 public static void main(String[] args) { //3. 创建Thread类的子类的对象 MyThread t1 = new MyThread(); //4.通过此对象调用start():①启动当前线程 ② 调用当前线程的run() t1.start(); }
注意:
- 调用
start()
方法,如果调用的是run()
,并不会开启新的线程,而是当前线程直接执行内部的代码,和之前定义方法然后让对象调用是一样的。 - 如果想再生成一个线程,那就再new一个线程对象。不可以还让已经
start()
的线程去执行。会报IllegalThreadStateException
使用匿名子类简化创建方式:
new Thread(){ public void run(){ for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } } }.start();
- 调用
-
-
实现Runnable接口的方式
- 创建一个实现了Runnable接口的类
- 实现类去实现Runnable中的抽象方法:
run()
- 创建实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 通过Thread类的对象调用
start()
class MyThreadObj implements Runnable{//1. 创建一个实现了Runnable接口的类 @Override public void run() {//2.实现类去实现Runnable中的抽象方法:run() for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } } } public class ThreadTest { public static void main(String[] args) { //3.创建实现类的对象 MyThreadObj myThreadObj = new MyThreadObj(); //4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象 Thread thread = new Thread(myThreadObj); thread.setName("hengxing"); thread.start();//5.通过Thread类的对象调用`start()` } }
-
-
比较创建线程的两种方式
开发中:优先选择–>实现Runnable接口的方式
原因:
- 实现的方式没有类的单继承性的局限性
- 实现的方式更适合来处理多个线程有共享数据的情况。
联系:
public class Thread implements Runnable
thread其实也是实现了Runnable接口,实际上和第二种方式没区别相同点:两种方式都需要重写
run()
,将线程要执行的逻辑声明在run()
中。
Thread类的常用方法
方法 | 作用 |
---|---|
void start() | 启动线程,并执行对象的run() 方法 |
run() | 线程在被调度时执行的操作 |
String getName() | 返回线程的名称 |
void setName(String name) | 设置该线程名称 |
static Thread currentThread() | 返回当前线程。 在Thread子类中就是this,通常用于主线程和Runnable实现类 |
static void yield() | 线程让步 - 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程 - 若队列中没有同优先级的线程,忽略此方法 |
join() | 当某个程序执行流中调用其他线程的join() 方法时, 调用线程将被阻塞,直到join() 方法加入的 join 线程执行完为止低优先级的线程也可以获得执行 但是要注意:执行前确保线程已被启动。这个方法是等待join的线程完成,但是你如果连线程都没有开始执行,那不就直接结束了吗? |
static void sleep(long millis) | 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。 抛出 InterruptedException 异常 |
boolean isAlive() | 返回boolean ,判断线程是否还活着 |
线程的调度
调度策略
- 时间片
- 抢占式: 高优先级的线程抢占CPU
Java的调度方法
- 同优先级线程组成先进先出队列(先到先服务),使用时间片策略
- 对高优先级,使用优先调度的抢占式策略
线程的优先级
- MAX_PRIORITY: 10
- MIN _PRIORITY: 1
- NORM_PRIORITY: 5 --> 默认优先级
如何获取和设置当前线程的优先级:
getPriority()
: 获取线程的优先级
setPriority(int p)
: 设置线程的优先级
说明:
高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。
线程创建时继承父线程的优先级
理解线程
线程的分类
Java中的线程分为两类:一种是守护线程,一种是用户线程
- 它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开
- 守护线程是用来服务用户线程的,通过在
start()
方法前调用thread.setDaemon(true)
可以把一个用户线程变成一个守护线程 - Java垃圾回收就是一个典型的守护线程
- 若JVM中都是守护线程,当前JVM将退出
线程的生命周期
线程有五种状态:
- 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪: 处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
- 运行: 当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
- 阻塞: 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
- 死亡: 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
-
关于声明周期我们需要关注两个概念:
状态、相应方法
状态a --> 状态b:哪些方法执行了(回调方法)
某个方法主动调用:状态a --> 状态b (例如:
wait()
,sleep()
) -
阻塞只是临时状态,死亡才是最终状态。程序如果一直卡在阻塞状态,就是一种异常的状态。例如:死锁。
线程的同步
同步是为了解决线程安全问题。先来看一个例子会更好理解:
创建三个窗口卖票,总票数为100张。使用实现Runnable
接口的方式
public class WindowTest1 {
public static void main(String[] args) {
window w = new window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1:");
t2.setName("窗口2:");
t3.setName("窗口3:");
t1.start();
t2.start();
t3.start();
}
}
class window implements Runnable{
private int ticket = 100;
@Override
public void run() {
if (ticket <= 0) {
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":买票票号为;" + ticket--);
}
}
我们会发现,有重票的情况出现,这是因为在某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。这就是线程安全问题。
理想状态下,三个线程会同时进入判断语句,均判断票号为0,跳出循环
极端状态下,三个线程均进入阻塞状态,结束阻塞后,都执行后面的买票代码。后两个线程便会输出错票。
所以我们使用同步代码块的方式解决这个问题。
同步代码块
先来介绍同步代码块:
synchronized (同步监视器){
//需要被同步的代码
}
-
操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。
-
共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
-
同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
⭐要求:多个线程必须要共用同一把锁。
在上面的例子中,我们需要在操作共享数据时使用同步锁:
@Override
public void run() {
while (true) {
synchronized (this) {
if (ticket <= 0) {
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":买票票号为;" + ticket--);
}
}
}
但如果时使用继承方式实现的线程,由于其生成了多个对象,所以不能使用this作为当前同步的锁,考虑使用window.class
当前类名来作为锁(类在程序中只会加载一次,这个知识会在讲“反射”时提到。)
@Override
public void run() {
while (true) {
synchronized (window.class) {
if (ticket <= 0) {
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":买票票号为;" + ticket--);
}
}
}
同步方法
如果一整个方法都需要同步,那不妨将方法声明为同步方法。
private synchronized void ticket(){
if (ticket <= 0) return;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":买票票号为;" + ticket--);
}
但如果是在使用继承Thread类方式实现的线程中,你会发现同步锁失效了。这就是我们的另一个知识点:
细心的你一定发现了,同步方法没有要求我们写同步监视器,那,他就不存在了吗?
不是的。它默认使用this
代替。恰巧我们这种方式实现的线程又会生成多个对象,用当前对象肯定不行。
解决方式就是将此同步方法声明为静态的,这时他会使用当前类来代替this
–>window.class
class window extends Thread{
private static int ticket = 100;
@Override
public void run() {
while (true) {
ticket();
}
}
private static synchronized void ticket(){
if (ticket <= 0) {
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":买票票号为;" + ticket--);
}
}
同步的利弊
同步的方式,解决了线程的安全问题。—好处
操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。 —局限性
懒汉式单例改进
之前我们写懒汉式单例提到,它是线程不安全的。现在进行改进
public static Bank getInstance(){
//方式一:只解决线程安全,效率低,所有
synchronized (Bank.class) {
if (bank == null) {
bank = new Bank();
}
return bank;
}
//方式二:效率更高,之后的线程不必在同步锁外等待
if (bank == null) {
synchronized (Bank.class) {
if (bank == null) {
bank = new Bank();
}
}
}
return bank;
}
死锁问题
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
如何解决?
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
一个死锁的实例:
class A {
public synchronized void foo(B b) { //同步监视器:A类的对象:a
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了A实例的foo方法"); // ①
try {
Thread.sleep(200);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用B实例的last方法"); // ③
b.last();
}
public synchronized void last() {//同步监视器:A类的对象:a
System.out.println("进入了A类的last方法内部");
}
}
class B {
public synchronized void bar(A a) {//同步监视器:b
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了B实例的bar方法"); // ②
try {
Thread.sleep(200);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用A实例的last方法"); // ④
a.last();
}
public synchronized void last() {//同步监视器:b
System.out.println("进入了B类的last方法内部");
}
}
public class DeadLock implements Runnable {
A a = new A();
B b = new B();
public void init() {
Thread.currentThread().setName("主线程");
// 调用a对象的foo方法
a.foo(b);
System.out.println("进入了主线程之后");
}
public void run() {
Thread.currentThread().setName("副线程");
// 调用b对象的bar方法
b.bar(a);
System.out.println("进入了副线程之后");
}
public static void main(String[] args) {
DeadLock dl = new DeadLock();
new Thread(dl).start();//副线程启动
dl.init();//主线程启动
}
}
Lock(锁)
从JDK 5.0开始, Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的工具。 锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock
类实现了 Lock ,它拥有与synchronized
相同的并发性和内存语义, 在实现线程安全的控制中,比较常用的是ReentrantLock
, 可以显式加锁、释放锁。
注意:Lock方式中并没有同步监视器这个概念,但是我们可以把private ReentrantLock lock = new ReentrantLock();
中的lock视为同步监视器,如果线程间没有使用同一个lock对象,就相当于没有使用同一把锁。lock不可调用wait()
、notify()
、notifyAll()
方法,但是可以通过相关的Condition
对象来实现更多操作。
使用方式
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try{
//2.调用锁定方法lock()
lock.lock();
//需要同步的代码
}finally {
//3.调用解锁方法:unlock()
lock.unlock();
}
}
}
因为最后一步一定要解锁,所以使用try--finally
的方式
synchronized 与 Lock 的对比
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁), synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁, synchronized有代码块锁和方法锁
- 使用Lock锁, JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
推荐使用顺序
优先使用顺序:
Lock --> 同步代码块(已经进入了方法体,分配了相应资源)–> 同步方法(在方法体之外)
练习
银行有一个账户。
有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
- 使用继承Thread方式,synchronized同步方法
public class DepositTest {
public static void main(String[] args) {
Account account = new Account();
Customer c1 = new Customer(account);
Customer c2 = new Customer(account);
c1.setName("Tom");
c2.setName("Jerry");
c1.start();
c2.start();
}
}
class Account{
double balance;
public synchronized void deposit(double awt){
if (awt <= 0) {
return;
}
balance += awt;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":sucessful deposit.The balance is " + balance);
}
}
class Customer extends Thread{
Account acct;
public Customer(Account acct) {
this.acct = acct;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
acct.deposit(1000);
}
}
}
- 使用继承Thread方式,lock同步
public void deposit(double awt){
if (awt <= 0) {
return;
}
lock.lock();
try {
balance += awt;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":sucessful deposit.The balance is " + balance);
} finally {
lock.unlock();
}
}
- 使用继承Runnable方式,synchronized同步方法
import java.util.concurrent.locks.ReentrantLock;
public class DepositTest {
public static void main(String[] args) {
Account account = new Account();
Customer c1 = new Customer(account);
Customer c2 = new Customer(account);
Thread t1 = new Thread(c1);
Thread t2 = new Thread(c2);
t1.setName("Tom");
t2.setName("Jerry");
t1.start();
t2.start();
}
}
class Account{
double balance;
ReentrantLock lock = new ReentrantLock();
public synchronized void deposit(double awt){
if (awt <= 0) {
return;
}
balance += awt;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":sucessful deposit.The balance is " + balance);
}
class Customer implements Runnable{
Account acct;
public Customer(Account acct) {
this.acct = acct;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
acct.deposit(1000);
}
}
}
- 使用继承Runnable方式,lock同步
import java.util.concurrent.locks.ReentrantLock;
public class DepositTest {
public static void main(String[] args) {
Account account = new Account();
Customer c1 = new Customer(account);
Customer c2 = new Customer(account);
Thread t1 = new Thread(c1);
Thread t2 = new Thread(c2);
t1.setName("Tom");
t2.setName("Jerry");
t1.start();
t2.start();
}
}
class Account{
double balance;
ReentrantLock lock = new ReentrantLock();
public void deposit(double awt){
if (awt <= 0) {
return;
}
lock.lock();
try {
balance += awt;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":sucessful deposit.The balance is " + balance);
} finally {
lock.unlock();
}
}
}
class Customer implements Runnable{
Account acct;
public Customer(Account acct) {
this.acct = acct;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
acct.deposit(1000);
}
}
}
线程通信
使用两个线程打印 1-100。线程1, 线程2 交替打印
/**
* 使用两个线程打印 1-100。线程1, 线程2 交替打印
*/
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("线程一");
t2.setName("线程二");
t1.start();
t2.start();
}
}
class Number implements Runnable{
private int number = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
notify();//将阻塞的进程唤醒
if (number > 100) {
return;
}
System.out.println(Thread.currentThread().getName() + ":" + number++);
try {
wait();//令当前进程阻塞,等待唤醒。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
涉及到的三个方法:
wait()
:一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
notify()
:一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
notifyAll()
:一旦执行此方法,就会唤醒所有被wait的线程。
说明:
-
wait()
,notify()
,notifyAll()
三个方法必须使用在同步代码块或同步方法中,lock锁的方式都不可以。意味着这三个方法是依赖于同步监视器的。 -
wait()
,notify()
,notifyAll()
三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现
IllegalMonitorStateException
异常。 -
wait()
,notify()
,notifyAll()
三个方法三个方法是定义在java.lang.Object
类中。
面试题:sleep() 和 wait()的异同?
相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
不同点:
-
两个方法声明的位置不同:
Thread类中声明
sleep()
Object类中声明
wait()
-
调用的要求不同:
sleep()
可以在任何需要的场景下调用。wait()
必须使用在同步代码块或同步方法中 -
关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,
sleep()
不会释放锁,wait()
会释放锁。
线程通信的应用
经典例题:生产者/消费者问题
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
分析:
- 是否是多线程问题?是,生产者线程,消费者线程
- 是否有共享数据?是,店员(或产品)
- 如何解决线程的安全问题?同步机制,有三种方法
- 是否涉及线程的通信?是,生产者通知消费者进行消费,消费者通知生产者进行生产
public class CommunicationTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Customer customer = new Customer(clerk);
Productor productor = new Productor(clerk);
customer.setName("customer");
productor.setName("productor");
productor.start();
customer.start();
}
}
class Clerk{
private int number = 0;
public synchronized void produce(){
if (number >= 20) {
//等待消费
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
return;
}
System.out.println(Thread.currentThread().getName() + "开始生产第" + ++number + "个产品");
notify();//生产后,唤醒消费者
}
public synchronized void custom(){
if (number <= 0) {
//等待生产
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
return;
}
System.out.println(Thread.currentThread().getName() + "开始消费第" + number-- + "个产品");
notify();//消费后,唤醒生产者
}
}
class Productor extends Thread{
Clerk clerk;
public Productor(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(250);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produce();
}
}
}
class Customer extends Thread{
Clerk clerk;
public Customer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.custom();
}
}
}
新增线程创建方式
JDK5.0 新增
新增方式一:实现Callable接口
与使用Runnable相比, Callable功能更强大些
- 相比
run()
方法,可以有返回值 - 方法可以抛出异常
- 支持泛型的返回值
- 需要借助
FutureTask
类,比如获取返回结果
实现步骤为:
-
创建一个实现
Callable
的实现类 -
实现call方法,将此线程需要执行的操作声明在
call()
中 -
创建
Callable
接口实现类的对象 -
将此
Callable
接口实现类的对象作为传递到FutureTask
构造器中,创建FutureTask
的对象 -
将
FutureTask
的对象作为参数传递到Thread
类的构造器中,创建Thread
对象,并调用start()
-
获取
Callable
中call方法的返回值(可选)get()
返回值即为FutureTask
构造器参数Callable
实现类重写的call()
的返回值。
//1.创建一个实现`Callable`的实现类
class Number implements Callable{
private int count = 0;
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0){
System.out.println(i);
}
count += i;
}
return count;
}
}
public class NewThread {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
Number number = new Number();
//4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(number);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
Thread thread = new Thread(futureTask);
thread.start();
//6.获取Callable中call方法的返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
try {
Object o = futureTask.get();
System.out.println("总和为:" + o);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
call()
可以有返回值的。call()
可以抛出异常,被外面的操作捕获,获取异常的信息Callable
是支持泛型的
新增方式二:使用线程池
背景: 经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
思路: 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具
好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
corePoolSize
:核心池的大小maximumPoolSize
:最大线程数keepAliveTime
:线程没有任务时最多保持多长时间后会终止- …
线程池相关API
JDK 5.0起提供了线程池相关API: ExecutorService
和Executors
ExecutorService
:真正的线程池接口。常见子类ThreadPoolExecutor
void execute(Runnable command)
:执行任务/命令,没有返回值,一般用来执行
Runnable
<T> Future<T> submit(Callable<T> task)
:执行任务,有返回值,一般又来执行
Callable
- ``void shutdown()`:关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool()
:创建一个可根据需要创建新线程的线程池Executors.newFixedThreadPool(n)
; 创建一个可重用固定线程数的线程池Executors.newSingleThreadExecutor()
:创建一个只有一个线程的线程池Executors.newScheduledThreadPool(n)
:创建一个线程池,它可安排在给定延迟后运
行命令或者定期地执行
使用实例:
public static void main(String[] args) {
//1. 创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//2. 放入线程并启动
service.execute(new OddNum());//Runnable线程启动
FutureTask futureTask = new FutureTask(new EvenNum());
service.submit(futureTask);//Callable线程启动
//3. 关闭线程池
service.shutdown();
}
线程管理
由于我们接收线程池对象时是使用多态方式接收的,我们可以查看newFixedThreadPool
源码,看到它返回的是ThreadPoolExecutor
,若想使用线程管理,就必须先进行强转。
/**
* Creates a thread pool that reuses a fixed number of threads
* operating off a shared unbounded queue. At any point, at most
* {@code nThreads} threads will be active processing tasks.
* If additional tasks are submitted when all threads are active,
* they will wait in the queue until a thread is available.
* If any thread terminates due to a failure during execution
* prior to shutdown, a new one will take its place if needed to
* execute subsequent tasks. The threads in the pool will exist
* until it is explicitly {@link ExecutorService#shutdown shutdown}.
*
* @param nThreads the number of threads in the pool
* @return the newly created thread pool
* @throws IllegalArgumentException if {@code nThreads <= 0}
*/
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
转换为ThreadPoolExecutor
再进行管理
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
service1.setCorePoolSize(3);//核心池的大小