多线程

1.相关概念

  • 程序(program):为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象

  • 进程(process):程序的一次执行过程,或是正在内存中运行的应用程序。如:运行中的QQ,运行中的网易音乐播放器。

  • 线程(thread):进程可进一步细化为线程,是程序内部的一条执行路径。一个进程中至少有一个线程。

    • 进程同一时间若并行执行多个线程,就是支持多线程的。

2.创建和启动线程

2.1 方式1:继承Thread类

Java通过继承Thread类来创建并启动多线程的步骤如下:

  1. 定义Thread 类的子类,并重写该类的 run() 方法,该 run() 方法的方法体就代表了线程需要完成的任务

  2. 创建 Thread 子类的实例,即创建了线程对象

  3. 调用线程对象的 start() 方法来启动该线程
代码如下:
package Thread;

public class ThreadByThread extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

测试类:
package Thread;

public class ThreadTestByThread {
    public static void main(String[] args) {
        ThreadByThread t1 = new ThreadByThread();
        ThreadByThread t2 = new ThreadByThread();
        t1.start();
        t2.start();
    }
}

注意:

  1. 如果自己手动调用 run() 方法,那么就只是普通方法,没有启动多线程模式。

  2. run() 方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。

  3. 想要启动多线程,必须调用 start 方法。

  4. 一个线程对象只能调用一次 start() 方法启动,如果重复调用了,则将抛出以上的异常 “ IllegalThreadStateException ” 。

2.2 方式2:实现Runnable接口

步骤如下:

  1. 定义 Runnable 接口的实现类,并重写该接口的 run() 方法,该 run() 方法的方法体同样是该线程的线程执行体。

  2. 创建 Runnable 实现类的实例,并以此实例作为 Thread 的 target 参数来创建 Thread 对象,该 Thread 对象才是真正的线程对象。

  3. 调用线程对象的 start() 方法,启动线程。调用 Runnable 接口实现类的 run 方法。
代码如下:
package Thread;

public class ThreadByRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}


测试类:
package Thread;

public class ThreadTestByRunnable {
    public static void main(String[] args) {

        ThreadByRunnable r = new ThreadByRunnable();

        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);

        t1.start();
        t2.start();
    }

}

说明:

 Runnable 对象仅仅作为 Thread 对象的 target, Runnable 实现类里包含的

run() 方法仅作为线程执行体。 而实际的线程对象依然是 Thread 实例,只是该

Thread 线程负责执行其 target 的 run() 方法。

2.3 使用匿名内部类对象来实现线程的创建和启动

new Thread("新的线程!"){
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(getName()+":正在执行!"+i);
		}
	}
}.start();

new Thread(new Runnable(){
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName()+":" + i);
		}
	}
}).start();

2.4 对比继承 Thread 类和实现 Runnable 接口两种方式

联系

 Thread类实际上也是实现了Runnable接口的类。即:public class Thread extends Object implements Runnable

区别

  • 继承Thread:线程代码存放Thread子类run方法中。

  • 实现Runnable:线程代码存在接口的子类的run方法。

实现Runnable接口比继承Thread类所具有的优势

  • 避免了单继承的局限性。

  • 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。

  • 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。

3. Thread类的常用结构

3.1 构造器

  • public Thread() :分配一个新的线程对象。

  • public Thread(String name) :分配一个指定名字的新的线程对象。

  • public Thread(Runnable target) :指定创建线程的目标对象,它实现了Runnable接口中的run方法。

  • public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。

3.2 常用方法

  • public void run() :此线程要执行的任务在此处定义代码。

  • public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。

  • public String getName() :获取当前线程名称。

  • public void setName(String name):设置该线程名称。

  • public static Thread currentThread() :返回对当前正在执行的线程对象的引用。在Thread子类中就是this,通常用于主线程和Runnable实现类

  • public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。

  • public static void yield():yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这个不能保证,完全有可能的情况是,当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。

  • public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。

  • void join() :等待该线程终止。

    • void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果millis时间到,将不再等待。
    • void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。

 每个线程都有一定的优先级,同优先级线程组成先进先出队列(先到先服

务),使用分时调度策略。优先级高的线程采用抢占式策略,获得较多的执

行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。

Thread类的三个优先级常量:

MAX_PRIORITY(10):最高优先级

MIN _PRIORITY (1):最低优先级

NORM_PRIORITY (5):普通优先级,默认情况下main线程具有普通优先级。

  • public final int getPriority() :返回线程优先级

  • public final void setPriority(int newPriority) :改变线程的优先级,范围在[1,10]之间。

4. 多线程的生命周期

4.1 jdk 1.5之前

 线程的生命周期有五种状态:新建(New)、就绪(Runnable)、运行

(Running)、阻塞(Blocked)、死亡(Dead)。CPU需要在多条线程之间切

换,于是线程状态会多次在运行、阻塞、就绪之间切换。



4.2 jdk 1.5之后

在java.lang.Thread.State的枚举类中这样定义:

 public enum State {

  NEW,
  RUNNABLE,
  BLOCKED,
  WAITING,
  TIMED_WAITING,
  TERMINATED;

}

  • NEW(新建):线程刚被创建,但是并未启动。还没调用start方法。

  • RUNNABLE(可运行):这里没有区分就绪和运行状态。

  • Teminated(被终止):表明此线程已经结束生命周期,终止运行。

  • 重点说明,根据Thread.State的定义,阻塞状态分为三种:BLOCKED、WAITING、TIMED_WAITING。

    • BLOCKED(锁阻塞):一个正在阻塞、等待一个监视器锁(锁对象)的线程处于这一状态。只有获得锁对象的线程才能有执行机会。

    • TIMED_WAITING(计时等待):一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。

    • WAITING(无限等待):一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。

      • 通过Object类的wait进入WAITING状态的要有Object的notify/notifyAll唤醒;

      • 通过Condition的await进入WAITING状态的要有Condition的signal方法唤醒;

      • 通过LockSupport类的park方法进入WAITING状态的要有LockSupport类的unpark方法唤醒

      • 通过Thread类的join进入WAITING状态,只有调用join方法的线程对象结束才能让当前线程恢复;

说明:当从 WAITING 或 TIMED_WAITING 恢复到 Runnable 状态时,如果

发现当前线程没有得到监视器锁,那么会立刻转入 BLOCKED 状态。




5. 线程安全问题以及解决

当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一

条记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题。

但是如果多个线程中对资源有读和写的操作,就容易出现线程安全问题。


案例

 火车站要卖票,我们模拟火车站的卖票过程。因为疫情期间,本次列车的座位

共100个(即,只能出售100张火车票)。我们来模拟车站的售票窗口,实现多

个窗口同时售票的过程。注意:不能出现错票、重票

1. 继承 Thread 方式 ( 局部变量以及不同对象的实例变量不共享,使用静态变量共享)

package unsafe;

class TicketSaleThread extends Thread {
    private static int ticket = 100;

    public void run() {
        while (ticket > 0) {
            try {
                Thread.sleep(10);//加入这个,使得问题暴露的更明显
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getName() + "卖出一张票,票号:" + ticket);
            ticket--;
        }
    }
}

public class SaleTicket {
    public static void main(String[] args) {
        TicketSaleThread t1 = new TicketSaleThread();
        TicketSaleThread t2 = new TicketSaleThread();
        TicketSaleThread t3 = new TicketSaleThread();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}


问题1:但是有重复票或负数票问题。

 原因:线程安全问题

 问题2:如果要考虑有两场电影,各卖100张票等

 原因:TicketThread类的静态变量,是所有TicketThread类的对象共享


2. 使用实现 Runnable 接口的方式

package safe;

class TicketSaleRunnable implements Runnable {
    private int ticket = 100;

    public void run() {
        while (ticket > 0) {
            try {
                Thread.sleep(10);//加入这个,使得问题暴露的更明显
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket);
            ticket--;
        }
    }
}

public class SaleTicket {
    public static void main(String[] args) {
        TicketSaleRunnable tr = new TicketSaleRunnable();
        Thread t1 = new Thread(tr, "窗口一");
        Thread t2 = new Thread(tr, "窗口二");
        Thread t3 = new Thread(tr, "窗口三");

        t1.start();
        t2.start();
        t3.start();
    }
}


结果:发现卖出近100张票。

 问题:但是有重复票或负数票问题。

 原因:线程安全问题


解决方法:同步机制


 要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在

票问题,Java中提供了同步机制 (synchronized)来解决。



根据案例简述:

 窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结

束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改

共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢

夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现

象。

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。注意:在任

何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能

在外等着 (BLOCKED) 。

同步机制的原理,其实就相当于给某段代码加“锁”,任何线程想要执行这

段代码,都要先获得“锁”,我们称它为同步锁。因为Java对象在堆中的数

据分为分为对象头、实例变量、空白的填充。而对象头中包含:

  • Mark Word:记录了和当前对象有关的GC、锁标记等信息。

  • 指向类的指针:每一个对象需要记录它是由哪个类创建出来的。

  • 数组长度(只有数组对象才有)

哪个线程获得了“同步锁”对象之后,”同步锁“对象就会记录这个线程的

ID,这样其他线程就只能等待了,除非这个线程”释放“了锁对象,其他

线程才能重新获得/占"同步锁"对象。



同步代码块:synchronized 关键字可以用于某个区块前面,表示只对这个区块

的资源实行互斥访问。 格式:

synchronized(同步锁){

     需要同步操作的代码

}



同步方法:synchronized 关键字直接修饰方法,表示同一时刻只有一个线程能

进入这个方法,其他线程在外面等着。

public synchronized void method(){
    
    可能会产生线程安全问题的代码

}



静态方法加锁代码:

package safe;

class TicketSaleThread extends Thread{
    private static int ticket = 100;
    public void run(){//直接锁这里,肯定不行,会导致,只有一个窗口卖票
        while (ticket > 0) {
            saleOneTicket();
        }
    }

    public synchronized static void saleOneTicket(){//锁对象是TicketSaleThread类的Class对象,而一个类的Class对象在内存中肯定只有一个
        if(ticket > 0) {//不加条件,相当于条件判断没有进入锁管控,线程安全问题就没有解决
            System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket);
            ticket--;
        }
    }
}
public class SaleTicket {
    public static void main(String[] args) {
        TicketSaleThread t1 = new TicketSaleThread();
        TicketSaleThread t2 = new TicketSaleThread();
        TicketSaleThread t3 = new TicketSaleThread();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}



非静态方法加锁:

package safe;


public class SaleTicket {
    public static void main(String[] args) {
        TicketSaleRunnable tr = new TicketSaleRunnable();
        Thread t1 = new Thread(tr, "窗口一");
        Thread t2 = new Thread(tr, "窗口二");
        Thread t3 = new Thread(tr, "窗口三");

        t1.start();
        t2.start();
        t3.start();
    }
}

class TicketSaleRunnable implements Runnable {
    private int ticket = 100;

    public void run() {//直接锁这里,肯定不行,会导致,只有一个窗口卖票
        while (ticket > 0) {
            saleOneTicket();
        }
    }

    public synchronized void saleOneTicket() {//锁对象是this,这里就是TicketSaleRunnable对象,因为上面3个线程使用同一个TicketSaleRunnable对象,所以可以
        if (ticket > 0) {//不加条件,相当于条件判断没有进入锁管控,线程安全问题就没有解决
            System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket);
            ticket--;
        }
    }
}



6. 再谈同步


6.1 死锁

 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要

的同步资源,就形成了线程的死锁。



诱发死锁的原因:

  • 互斥条件
  • 占用且等待
  • 不可抢夺(或不可抢占)
  • 循环等待

以上4个条件,同时出现就会触发死锁。

解决死锁:

死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。

针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。

针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。

针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。

针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。

6.2 JDK5.0 新特性:LOCK(锁)

  • JDK5.0的新增功能,保证线程的安全。与采用synchronized相比,Lock可

    提供多种锁方案,更灵活、更强大。Lock通过显式定义同步锁对象来实现同

    步。同步锁使用Lock对象充当。

  • java. util. concurrent. locks. Lock 接口是控制多个线程对共享资源进行访

    问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对

    象加锁,线程开始访问共享资源之前应先获得Lock对象。

  • 实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

    • ReentrantLock类实现了 Lock 接口,它拥有与 synchronized 相同的

      并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等

      候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。
  • Lock锁也称同步锁,加锁与释放锁方法,如下:

    • public void lock() :加同步锁。

    • public void unlock() :释放同步锁。



代码如下:

import java.util.concurrent.locks.ReentrantLock;

class Window implements Runnable{
	int ticket = 100;
    //1. 创建Lock的实例,必须确保多个线程共享同一个Lock实例
	private final ReentrantLock lock = new ReentrantLock();
	public void run(){
		
		while(true){
			try{
                //2. 调动lock(),实现需共享的代码的锁定
				lock.lock();
				if(ticket > 0){
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(ticket--);
				}else{
					break;
				}
			}finally{
                //3. 调用unlock(),释放共享代码的锁定
				lock.unlock();
			}
		}
	}
}

public class ThreadLock {
	public static void main(String[] args) {
		Window t = new Window();
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		
		t1.start();
		t2.start();
	}
}



synchronized 与 Lock 的对比

  1. Lock 是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域、遇到异常等自动解锁

  2. Lock 只有代码块锁,synchronized 有代码块锁和方法锁

  3. 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类),更体现面向对象。

  4. (了解)Lock 锁可以对读不加锁,对写加锁,synchronized 不可以

  5. (了解)Lock 锁可以有多种获取锁的方式,可以从 sleep 的线程中抢到锁,synchronized 不可以

    说明:开发建议中处理线程安全问题优先使用顺序为:

    Lock ----> 同步代码块 ----> 同步方法

7. 线程的通信

生产者与消费者问题

 等待唤醒机制可以解决经典的“生产者与消费者”的问题。生产者与消费者问

题,也称有限缓冲问题,是一个多线程同步问题的经典案例。该问题描述了两个

(多个)共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——

在实际运行时会发生的问题。

 生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此

同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在

缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

举例:

 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员

处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试

图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通

知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果

店中有产品了再通知消费者来取走产品。


类似的场景,比如厨师和服务员等。

生产者与消费者问题中其实隐含了两个问题:

  • 线程安全问题:

    因为生产者与消费者共享数据缓冲区,产生安全问题。不过这个问题可以使

    用同步解决。

  • 线程的协调工作问题:

    要解决该问题,就必须让生产者线程在缓冲区满时等待(wait),暂停进入阻

    塞状态,等到下次消费者消耗了缓冲区中的数据的时候,通知(notify)正在

    等待的线程恢复到就绪状态,重新开始往缓冲区添加数据。同样,也可以让

    消费者线程在缓冲区空时进入等待(wait),暂停进入阻塞状态,等到生产者

    往缓冲区添加数据之后,再通知(notify)正在等待的线程恢复到就绪状态。

    通过这样的通信机制来解决此类问题。

    代码实现:

public class ConsumerProducerTest {
	public static void main(String[] args) {
		Clerk clerk = new Clerk();
		Producer p1 = new Producer(clerk);
		
		Consumer c1 = new Consumer(clerk);
		Consumer c2 = new Consumer(clerk);
		
		p1.setName("生产者1");
		c1.setName("消费者1");
		c2.setName("消费者2");
		
		p1.start();
		c1.start();
		c2.start();
	}
}

//生产者
class Producer extends Thread{
	private Clerk clerk;
	
	public Producer(Clerk clerk){
		this.clerk = clerk;
	}
	
	@Override
	public void run() {
		
		System.out.println("=========生产者开始生产产品========");
		while(true){
			
			try {
				Thread.sleep(40);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			//要求clerk去增加产品
			clerk.addProduct();
		}
	}
}

//消费者
class Consumer extends Thread{
	private Clerk clerk;
	
	public Consumer(Clerk clerk){
		this.clerk = clerk;
	}
	@Override
	public void run() {
		System.out.println("=========消费者开始消费产品========");
		while(true){
			
			try {
				Thread.sleep(90);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			//要求clerk去减少产品
			clerk.minusProduct();
		}
	}
}

//资源类
class Clerk {
	private int productNum = 0;//产品数量
	private static final int MAX_PRODUCT = 20;
	private static final int MIN_PRODUCT = 1;
	
	//增加产品
	public synchronized void addProduct() {
		if(productNum < MAX_PRODUCT){
			productNum++;
			System.out.println(Thread.currentThread().getName() + 
					"生产了第" + productNum + "个产品");
			//唤醒消费者
			this.notifyAll();
		}else{
			
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	//减少产品
	public synchronized void minusProduct() {
		if(productNum >= MIN_PRODUCT){
			System.out.println(Thread.currentThread().getName() + 
					"消费了第" + productNum + "个产品");
			productNum--;
			
			//唤醒生产者
			this.notifyAll();
		}else{
			
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	
}



8. JDK5.0 新增线程创建方式

8.1 新增方式一:实现 Callable接口

  • 与使用Runnable相比, Callable功能更强大些

– 相比run()方法,可以有返回值

– 方法可以抛出异常

– 支持泛型的返回值(需要借助FutureTask类,获取返回结果)

  • Future接口(了解)

– 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。

– FutureTask是Futrue接口的唯一的实现类

– FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值

  • 缺点:在获取分线程执行结果的时候,当前线程(或是主线程)受阻塞,效率较低。

代码举例:

/*
 * 创建多线程的方式三:实现Callable (jdk5.0新增的)
 */
//1.创建一个实现Callable的实现类
class NumThread implements Callable {
    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}

public class CallableTest {
    public static void main(String[] args) {
        //3.创建Callable接口实现类的对象
        NumThread numThread = new NumThread();

        //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(numThread);
        //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        new Thread(futureTask).start();
//      接收返回值
        try {
            //6.获取Callable中call方法的返回值
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
            Object sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

}



8.2 新增方式二:使用线程池

现有问题:

 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束

了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程

需要时间。

 那么有没有一种办法使得线程可以复用,即执行完一个任务,并不被销毁,而是

可以继续执行其他的任务?

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

好处:

  • 提高响应速度(减少了创建新线程的时间)

  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)

  • 便于线程管理

– corePoolSize:核心池的大小

– maximumPoolSize:最大线程数

– keepAliveTime:线程没有任务时最多保持多长时间后会终止

– …


线程池相关API

  • JDK5.0之前,我们必须手动自定义线程池。从JDK5.0开始,Java内置线程池相关的API。在java.util.concurrent包下提供了线程池相关API:ExecutorService 和 Executors。

  • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor

– void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable

Future submit(Callable task):执行任务,有返回值,一般又来执行Callable

– void shutdown() :关闭连接池

  • Executors:一个线程池的工厂类,通过此类的静态工厂方法可以创建多种类型的线程池对象。

– Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池

– Executors.newFixedThreadPool(int nThreads); 创建一个可重用固定线程数的线程池

– Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池

– Executors.newScheduledThreadPool(int corePoolSize):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。



代码举例:

class NumberThread implements Runnable{
    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

class NumberThread1 implements Runnable{
    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}
class NumberThread2 implements Callable {
    @Override
    public Object call() throws Exception {
        int evenSum = 0;//记录偶数的和
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                evenSum += i;
            }
        }
        return evenSum;
    }
}
public class ThreadPoolTest {
    public static void main(String[] args) {
        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//        //设置线程池的属性
//        System.out.println(service.getClass());//ThreadPoolExecutor
        service1.setMaximumPoolSize(50); //设置线程池中线程数的上限
        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合适用于Runnable
        service.execute(new NumberThread1());//适合适用于Runnable
        try {
            Future future = service.submit(new NumberThread2());//适合使用于Callable
            System.out.println("总和为:" + future.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
        //3.关闭连接池
        service.shutdown();
    }
}
posted @ 2024-04-30 16:47  pine1203  阅读(58)  评论(0编辑  收藏  举报