目录儿
- 一、创建线程的四种方式
- 1. 继承Thread类
- 2. 实现Runnable接口
- 3. 实现Callable接口
- 4. 线程池
- 禁止使用 Executors 构建线程池
- 构建线程池的正确方式
一、创建线程的四种方式
1. 继承Thread类
① 创建一个类继承Thread
类,重写run()
方法
② 调用start()
方法启动线程
例:
/* 创建三个窗口卖票,总票数为100 */
public class TicketWindow extends Thread {
//线程共享资源(100张票)
private static int ticket = 100;
//同步锁必须是唯一的
private static final Object obj = new Object();
@Override
public void run() {
while(true){
synchronized(obj){
if(ticket > 0){
//暂停当前线程,允许其它具有相同优先级的线程获得运行机会
Thread.yield();
System.out.println(getName() + "卖票: 票号:" + ticket);
ticket--;
}else {
break;
}
}
}
}
public static void main(String[] args) {
TicketWindow w1 = new TicketWindow();
w1.setName("一号窗口");
w1.start();
TicketWindow w2 = new TicketWindow();
w2.setName("二号窗口");
w2.start();
Thread w3 = new TicketWindow(); //TicketWindow继承了Thread,可以用Thread接收TicketWindow对象
w3.setName("三号窗口");
w3.start();
}
}
2. 实现Runnable接口
① 创建类实现Runnable
接口,重写run()
方法
② 以实现类作为构造器参数,创建一个线程(Thread
)对象
③ 调用start()
方法启动线程
例
/* 创建三个窗口卖票,总票数为100 */
public class TicketWindow implements Runnable {
//线程共享资源(100张票)
private static int ticket = 100;
//同步锁必须是唯一的
private static final Object obj = new Object();
@Override
public void run() {
while(true){
synchronized(obj){
if(ticket > 0){
//暂停当前线程,允许其它具有相同优先级的线程执行(不能保证运行顺序)
Thread.yield();
System.out.println(Thread.currentThread().getName() + "卖票: 票号:" + ticket);
ticket--;
}else {
break;
}
}
}
}
public static void main(String[] args) {
TicketWindow ticketWindow = new TicketWindow();
new Thread(ticketWindow, "一号窗口").start();
new Thread(ticketWindow, "二号窗口").start();
new Thread(ticketWindow, "三号窗口").start();
}
}
注意:实现Runnable接口方式中,调用的不是Thread类的run()方法,而是在线程启动后,去调用Runnable类型的run()方法,也就是我们传入的实现类中的run()方法
3. 实现Callable接口
① 创建类实现Callable
接口,重写call()
方法
② 创建实现类对象
③ 将实现类对象作为构造器参数,创建FutureTask
对象
④ FutureTask
对象作为构造器参数,创建Thread
对象
⑤ 调用Thread
对象的start()
方法启动线程
⑥ 调用FutureTask
对象的get()
方法获取返回值
例:
// 1. 创建一个类来实现Callable接口
public class TicketWindow implements Callable<Object> {
//线程共享资源(100张票)
private static int ticket = 100;
//同步锁必须是唯一的
private static final Object obj = new Object();
// 2. 重写call()方法,返回值类型可以根据需求指定,但必须与创建FutureTask对象时里面的泛型一致
@Override
public Object call() {
while(true){
synchronized(obj){
if(ticket > 0){
//暂停当前线程,允许其它具有相同优先级的线程获得运行机会
Thread.yield();
System.out.println(Thread.currentThread().getName() + "卖票: 票号:" + ticket);
ticket--;
}else {
break;
}
}
}
return Thread.currentThread().getName() + "执行完毕 ~ ~";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 3. 创建Callable接口实现类的对象
TicketWindow ticketWindow = new TicketWindow();
// 4. 将实现类对象作为参数传到FutureTask类的构造器中,创建FutureTask类的对象
FutureTask<Object> futureTask1 = new FutureTask<Object>(ticketWindow);
// 5. 将FutureTask类对象传到Thread类的构造器中,创建线程对象(因为FutureTask实现了Runnable接口,所以可以这样传)
Thread t1 = new Thread(futureTask1, "1号窗口");
// 6. 通过线程对象调用start()方法开启线程
t1.start();
FutureTask<Object> futureTask2 = new FutureTask<Object>(ticketWindow);
Thread t2 = new Thread(futureTask2, "2号窗口");
t2.start();
FutureTask<Object> futureTask3 = new FutureTask<Object>(ticketWindow);
Thread t3 = new Thread(futureTask3, "3号窗口");
t3.start();
// 8. 调用FutureTask类的get()方法获取call()方法的返回值,如果不需要返回值可以省略这一步
Object result1= futureTask1.get();
Object result2= futureTask2.get();
Object result3= futureTask3.get();
System.out.println(result);
}
}
4. 线程池
使用线程池创建线程,是实际项目开发中最常用的方式,它拥有许多好处:
- 提高响应速度 (因为减少了创建新线程的时间)
- 降低资源损耗 (重复利用线程池中的线程,不需要每次都创建和销毁)
- 便于线程的管理
import java.util.concurrent.*;
/* 水果售卖窗口 */
class FruitWindow implements Runnable {
private int fruitNumber = 100;
@Override
public void run() {
while (true) {
/* 1.任何一个类的对象,都可以充当锁,一般使用Object类对象 */
/* 2.多个线程必须共用一把锁,多个线程抢一把锁,谁抢到谁执行 */
synchronized (this) {
if (fruitNumber > 0) {
try {
//休眠30毫秒
TimeUnit.MILLISECONDS.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖第:" + fruitNumber + "份水果");
fruitNumber--;
} else {
break;
}
}
}
}
}
/* 蔬菜售卖窗口 */
class VegetableWindow implements Callable<Object> {
private int vegetableNumber = 100;
@Override
public Object call() {
while (true) {
/* 1.任何一个类的对象,都可以充当锁,一般使用Object类对象 */
/* 2.多个线程必须共用一把锁,多个线程抢一把锁,谁抢到谁执行 */
synchronized (this) {
if (vegetableNumber > 0) {
try {
//休眠20毫秒
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖第:" + vegetableNumber + "份蔬菜");
vegetableNumber--;
} else {
break;
}
}
}
return null;
}
}
public class ThreadPool {
public static void main(String[] args) {
// 1.提供指定线程数量的线程池
ExecutorService pool= Executors.newFixedThreadPool(10);
// 设置线程池的属性 (线程管理)
ThreadPoolExecutor poolconfig = (ThreadPoolExecutor) pool; //设置属性前,需要将接口类型 强转为 实现类类型
poolconfig .setCorePoolSize(10); //核心池的大小(最小线程数)
poolconfig .setMaximumPoolSize(15); //最大线程数
//参数1:时间值。时间值为零将导致多余的线程在执行任务后立即终止
//参数2:时间单位,使用TimeUnit类指定,TimeUnit.DAYS为天、TimeUnit.HOURS为小时、TimeUnit.MINUTES为分钟、TimeUnit.SECONDS为秒,TimeUnit.MICROSECONDS为微秒
poolconfig .setKeepAliveTime(5, TimeUnit.MINUTES); //空闲线程存活时间
// . . .
// 2.执行指定的线程操作 (需要提供实现了Runnable接口 或 Callable接口实现类的对象 )
FruitWindow fruitWindow = new FruitWindow();
/* 注意:操作共享数据的线程必须共用同一把锁 */
pool.execute(fruitWindow); //适用于Runnable
pool.execute(fruitWindow); //适用于Runnable
pool.execute(fruitWindow); //适用于Runnable
VegetableWindow vegetableWindow = new VegetableWindow();
/* 注意:操作共享数据的线程必须共用同一把锁 */
pool.submit(vegetableWindow); //适用于Callable
pool.submit(vegetableWindow); //适用于Callable
Future<Object> future = pool.submit(vegetableWindow); //适用于Callable
// 获取call()方法的返回值
Object result = future.get();
System.out.println(result);
pool.shutdown(); //关闭连接池
}
}
禁止使用 Executors 构建线程池
上面的例子用到了Executors
静态工厂构建线程池,但一般不建议这样使用
Executors
是一个Java
中的工具类。提供工厂方法来创建不同类型的线程池。
虽然它很大程度的简化的创建线程池的过程,但是它有一个致命缺陷:在Java
开发手册中提到,使用Executors
创建线程池可能会导致OOM
(Out-OfMemory , 内存溢出 )
原因:
Executors
的静态方法创建线程池时,用的是 LinkedBlockingQueue
阻塞队列,如创建固定线程池的方法newFixedThreadPool()
:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}
Java
中的 BlockingQueue
主要有两种实现,分别是 :
ArrayBlockingQueue
LinkedBlockingQueue
。
ArrayBlockingQueue
是一个用数组实现的有界阻塞队列,必须设置容量。
LinkedBlockingQueue
是一个用链表实现的有界阻塞队列,在不设置的情况下,将是一个无边界的阻塞队列,最大长度为 Integer.MAX_VALUE
。
这里的问题就出在这里,newFixedThreadPool
中创建 LinkedBlockingQueue
时,并未指定容量。此时,LinkedBlockingQueue
就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。
上面提到的问题主要体现在 newFixedThreadPool
和 newSingleThreadExecutor
两个工厂方法上
而 newCachedThreadPool
和newScheduledThreadPool
这两个方法虽然没有用LinkedBlockingQueue
阻塞队列,但是它默认的最大线程数是Integer.MAX_VALUE
,也会有导致OOM
的风险。
// 缓存线程池
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
}
// 周期线程池
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue());
}
构建线程池的正确方式
避免使用 Executors
创建线程池,主要是避免使用其中的默认实现,那么我们可以自己直接调用 ThreadPoolExecutor
的构造函数来自己创建线程池。在创建的同时,给 BlockQueue
指定容量就可以了:
private static ExecutorService executor = new ThreadPoolExecutor(
10,
10,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue(10)
);
还要指定拒绝策略,处理好任务队列溢出时的异常问题。
参考资料:
- CSDN CD4356 Java 多线程详解(二):创建线程的4种方式
https://blog.csdn.net/weixin_42950079/article/details/124862582
- Java开发手册
http://static.kancloud.cn/mtdev/java-manual/content/%E4%B8%BA%E4%BB%80%E4%B9%88%E7%A6%81%E6%AD%A2%E4%BD%BF%E7%94%A8Executors%E5%88%9B%E5%BB%BA%E7%BA%BF%E7%A8%8B%E6%B1%A0%EF%BC%9F.md