【Java】6、Java 多线程

多线程

进程和线程概念

进程概念

进程是一个很抽象的概念,指在系统中能独立运行并作为资源分配的基本单位,它是由一组机器指令、数据和堆栈等组成的,是一个能独立运行的活动实体。具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。进程简单来理解就是每个应用都是一个进程。

线程概念

线程:是用来执行具体功能和任务的,需要进程为载体,是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竟争计算机系统资源的基本单位。

注:一个程序至少有一个进程,一个进程至少有一个线程(主线程),进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

进程和线程的区别

  1. 操作系统资源管理方式是不一样的,进程有独立的地址空间,进程崩溃后会有保护模式让其不会对其他的进程产生影响。而线程则不然,线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,所以一个线程挂掉可能影响整个进程挂掉。

  2. 进程的并发性没有线程高。

  3. 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中。由应用程序提供多个线程执行控制。

  4. 对于应用程序来说,多线程是可以同时有多个执行部分同时执行。但对于操作系统来说是没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配

注:多线程容易调度,有效地实现并发性。对内存的开销比较小。创建线程比创建进程要快。

线程的状态转换

新建状态(New)

创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,和其他 Java 对象一样,仅仅由 Java 虚拟机为其分配了内存,没有表现出任何线程的动态特征。

就绪状态(Runnable)

当线程对象调用了 start()方法后,该线程就进入就绪状态(也称可运行状态)。处于就绪状态的线程位于可运行池中,此时它只是具备了运行的条件,能否获得 CPU 的使用权开始运行,还需要等待系统的调度。

运行状态(Running)

如果处于就绪状态的线程获得了 CPU 的使用权,开始执行 run()方法中的线程执行体,则该线程处于运行状态。当一个线程启动后,它不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就结束了),当使用完系统分配的时间后,系统就会剥夺该线程占用的 CPU 资源,让其他线程获得执行的机会。需要注意的是,只有处于就绪状态的线程才可能转换到运行状态。

阻塞状态(Blocked)

一个正在执行的线程在某些特殊情况下,如执行耗时的输入/输出操作时,会放弃 CPU 的使用权,进入阻塞状态。线程进入阻塞状态后,就不能进入排队队列。只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。下面就列举一下线程由运行状态转换成阻塞状态的原因,以及如何从阻塞状态转换成就绪状态。

  • 当线程试图获取某个对象的同步锁时,如果该锁被其他线程所持有,则当前线程会进入阻塞状态,如果想从阻塞状态进入就绪状态必须得获取到其他线程所持有的锁。

  • 当线程调用了一个阻塞式的 IO 方法时,该线程就会进入阻塞状态,如果想进入就绪状态就必须要等到这个阻塞的 IO 方法返回。

  • 当线程调用了某个对象的 wait()方法时,也会使线程进入阻塞状态,如果想进入就绪状态就需要使用 notify()方法唤醒该线程。

  • 当线程调用了 Thread 的 sleep(longmillis)方法时,也会使线程进入阻塞状态,在这种情况下,只需等到线程睡眠的时间到了以后,线程就会自动进入就绪状态。

  • 当在一个线程中调用了另一个线程的 join()方法时,会使当前线程进入阻塞状态,在这种情况下,需要等到新加入的线程运行结束后才会结束阻塞状态,进入就绪状态。

需要注意的是,线程从阻塞状态只能进入就绪状态,而不能直接进入运行状态,也就是说结束阻塞的线程需要重新进入可运行池中,等待系统的调度。

死亡状态(Terminated)

线程的 run()方法正常执行完毕或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其他状态。

线程状态转换图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-197tA1qV-1642058668240)(http://47.107.171.232/easily-j/images/20190309/e1ba3031-659f-4707-9cc1-7b219640f4c2.jpg)]

线程的调度

线程的优先级

线程休眠

线程让步

线程插队

多线程同步

线程安全

同步代码块

同步方法

死锁问题

多线程的 6 种实现方式

多线程的形式上实现方式主要有两种,一种是继承 Thread 类,一种是实现 Runnable 接口。本质上实现方式都是来实现线程任务,然后启动线程执行线程任务(这里的线程任务实际上就是 run 方法)。这里所说的 6 种,实际上都是在以上两种的基础上的一些变形。

一、继承 Thread 类

万物皆对象,那么线程也是对象,对象就应该能够抽取其公共特性封装成为类,使用类可以实例化多个对象,那么实现线程的第一种方式就是继承 Thread 类的方式。继承 Thread 类是最简单的一种实现线程的方式,通过 jdk 给我们提供的 Thread 类,重写 Thread 类的 run 方法即可,那么当线程启动的时候,就会执行 run 方法体的内容。代码如下:

package com.hy.thread.t1;

/**
 * 继承Thread类的方式实现多线程演示
 */
public class ThreadDemo extends Thread {

	@Override
	public void run() {
		while (true) {
			System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
			try {
				Thread.sleep(1000); // 休息1000ms
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	public static void main(String[] args) {
		ThreadDemo td = new ThreadDemo();
		td.start(); // 启动线程

		while (true) {
			System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
			try {
				Thread.sleep(1000); // 休息1000ms
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

运行结果如下

main is running ...
Thread-0 is running ...
main is running ...
Thread-0 is running ...
Thread-0 is running ...
main is running ...
Thread-0 is running ...
main is running ...

这里要注意,在启动线程的时候,我们并不是调用线程类的 run 方法,而是调用了线程类的 start 方法。那么我们能不能调用 run 方法呢?答案是肯定的,因为 run 方法是一个 public 声明的方法,因此我们是可以调用的,但是如果我们调用了 run 方法,那么这个方法将会作为一个普通的方法被调用,并不会开启线程。这里实际上是采用了设计模式中的模板方法模式,Thread 类作为模板,而 run 方法是在变化的因此放到子类。

创建多个线程

上面的例子中除了我们创建的一个线程以外其实还有一个主线程也在执行。除了这两个线程以外还有没有其他的线程在执行了呢,其实是有的,比如我们看不到的垃圾回收线程,也在默默的执行。这里我们并不去考虑有多少个线程在执行,上面我们自己创建了一个线程,那么能不能多创建几个一起执行呢,答案是肯定的。一个 Thread 类就是一个线程对象,那么多创建几个 Thread 类,并调用其 start 方法就可以启动多个线程了。代码如下

package com.hy.thread.t1;

public class MultiThreadDemo extends Thread {

	@Override
	public void run() {
		while (true) {
			System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
			try {
				Thread.sleep(1000); // 休息1000ms
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	public static void main(String[] args) {

		// 创建四个线程对象,代表四个线程
		MultiThreadDemo td1 = new MultiThreadDemo();
		MultiThreadDemo td2 = new MultiThreadDemo();
		MultiThreadDemo td3 = new MultiThreadDemo();
		MultiThreadDemo td4 = new MultiThreadDemo();

		td1.start(); // 启动线程
		td2.start(); // 启动线程
		td3.start(); // 启动线程
		td4.start(); // 启动线程

		while (true) {
			System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
			try {
				Thread.sleep(1000); // 休息1000ms
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

运行结果如下

main is running ...
Thread-2 is running ...
Thread-1 is running ...
Thread-3 is running ...
Thread-0 is running ...
Thread-3 is running ...
Thread-2 is running ...
main is running ...
Thread-1 is running ...
Thread-0 is running ...
Thread-1 is running ...
main is running ...
Thread-2 is running ...
Thread-0 is running ...
Thread-3 is running ...

我们发现这里有个问题,多个线程的名字都是系统定义好的,就是 Thread-开头,后面跟数字,如果我们每个线程处理不同的任务,那么我们能不能给线程起上不同的名字,方便我们排查问题呢?答案是可以的。只要在创建线程实例的时候,在构造方法中传入指定的线程名称即可。如下

package com.hy.thread.t1;

public class MultiThreadDemo extends Thread {

	@Override
	public void run() {
		while (true) {
			System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
			try {
				Thread.sleep(1000); // 休息1000ms
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * 指定线程名称的构造方法
	 *
	 * @param name
	 */
	public MultiThreadDemo(String name) {
		super(name);
	}

	public static void main(String[] args) {

		// 创建四个线程对象,代表四个线程
		MultiThreadDemo td1 = new MultiThreadDemo("t1"); // 指定线程的名字
		MultiThreadDemo td2 = new MultiThreadDemo("t2");
		MultiThreadDemo td3 = new MultiThreadDemo("t3");
		MultiThreadDemo td4 = new MultiThreadDemo("t4");

		td1.start(); // 启动线程
		td2.start(); // 启动线程
		td3.start(); // 启动线程
		td4.start(); // 启动线程

		while (true) {
			System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
			try {
				Thread.sleep(1000); // 休息1000ms
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

运行的结果如下:

main is running ...
t1 is running ...
t2 is running ...
t3 is running ...
t4 is running ...
main is running ...
t1 is running ...
t2 is running ...
t4 is running ...
t3 is running ...

二、实现 Runnable 接口

实现 Runnable 接口也是一种常见的创建线程的方式。使用接口的方式可以让我们的程序降低耦合度。Runnable 接口中仅仅定义了一个方法,就是 run。我们来看一下 Runnable 接口的代码。

package java.lang;

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

其实 Runnable 就是一个线程任务,线程任务和线程的控制分离,这也就是上面所说的解耦。我们要实现一个线程,可以借助 Thread 类,Thread 类要执行的任务就可以由实现了 Runnable 接口的类来处理。 这就是 Runnable 的精髓之所在!

使用 Runnable 实现上面的例子步骤如下:

  1. 定义一个类实现 Runnable 接口,作为线程任务类
  2. 重写 run 方法,并实现方法体,方法体的代码就是线程所执行的代码
  3. 定义一个可以运行的类,并在 main 方法中创建线程任务类
  4. 创建 Thread 类,并将线程任务类做为 Thread 类的构造方法传入
  5. 启动线程

线程任务类代码如下

package com.hy.thread.t2;

public class ThreadTarget implements Runnable {

	@Override
	public void run() {
		while(true) {
			System.out.println(Thread.currentThread().getName() + " is running .. ");
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

可运行类代码如下

package com.hy.thread.t2;

public class Main {

	public static void main(String[] args) {

		ThreadTarget tt = new ThreadTarget(); // 实例化线程任务类
		Thread t = new Thread(tt); // 创建线程对象,并将线程任务类作为构造方法参数传入
		t.start(); // 启动线程

		// 主线程的任务,为了演示多个线程一起执行
		while(true) {
			System.out.println(Thread.currentThread().getName() + " is running .. ");
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

线程任务和线程的控制分离,那么一个线程任务可以提交给多个线程来执行。这是很有用的,比如车站的售票窗口,每个窗口可以看做是一个线程,他们每个窗口做的事情都是一样的,也就是售票。这样我们程序在模拟现实的时候就可以定义一个售票任务,让多个窗口同时执行这一个任务。那么如果要改动任务执行计划,只要修改线程任务类,所有的线程就都会按照修改后的来执行。相比较继承 Thread 类的方式来创建线程的方式,实现 Runnable 接口是更为常用的。

三、使用内部类的方式

这并不是一种新的实现线程的方式,只是另外的一种写法。比如有些情况我们的线程就想执行一次,以后就用不到了。那么像上面两种方式都还要再定义一个类,显得比较麻烦,我们就可以通过匿名内部类的方式来实现。使用内部类实现依然有两种,分别是继承 Thread 类和实现 Runnable 接口。代码如下:

package com.hy.thread.t3;

public class DemoThread {

	public static void main(String[] args) {

		// 基于子类的实现
		new Thread() {
			public void run() {
				while (true) {
					System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
					try {
						Thread.sleep(1000); // 休息1000ms
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			};
		}.start();

		// 基于接口的实现
		new Thread(new Runnable() {
			@Override
			public void run() {
				while (true) {
					System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
					try {
						Thread.sleep(1000); // 休息1000ms
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}).start();

		// 主线程的方法
		while (true) {
			System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
			try {
				Thread.sleep(1000); // 休息1000ms
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}

	}
}

可以想象一下,我能不能既基于接口,又基于子类呢?像下面的代码会执行出什么样子呢?

package com.hy.thread.t3;

public class DemoThred2 {

	public static void main(String[] args) {


		new Thread(new Runnable() {

			@Override
			public void run() {
				while (true) {
					System.out.println("runnable is running ... "); // 打印当前线程的名字
					try {
						Thread.sleep(1000); // 休息1000ms
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}) {
			public void run() {
				while (true) {
					System.out.println("sub is running ... "); // 打印当前线程的名字
					try {
						Thread.sleep(1000); // 休息1000ms
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			};
		}.start();


	}
}

运行结果如下:

sub is running ...
sub is running ...
sub is running ...

我们可以看到,其实是基于子类的执行了,为什么呢,其实很简单,我们先来看一下为什么不基于子类的时候 Runnable 的 run 方法可以执行。这个要从 Thread 的源码看起,下面是我截取的代码片段。

    public Thread(Runnable target)
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();

        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target; // 注意这里
        setPriority(priority);
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

其实上面的众多代码就是为了表现 this.target = target 那么 target 是什么呢,是 Thread 类的成员变量。那么在什么地方用到了 target 呢?下面是 run 方法的内容。

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

我们可以看到,如果通过上面的构造方法传入 target,那么就会执行 target 中的 run 方法。可能有朋友就会问了,我们同时继承 Thread 类和实现 Runnable 接口,target 不为空,那么为何不执行 target 的 run 呢。不要忘记了,我们在子类中已经重写了 Thread 类的 run 方法,因此 run 方法已经不在是我们看到的这样了。那当然也就不回执行 target 的 run 方法。

四、定时器

定时器可以说是一种基于线程的一个工具类。可以定时的来执行某个任务。比如要在凌晨的时候汇总一些数据,比如要每隔 10 分钟抓取一次某个网站上的数据等等,总之计时器无处不在。我们一般将需要定时完成的任务称之为计划任务,这在很多的系统中是非常常见的,比如 linux 的计划任务,比如 Windows 下的任务计划等等。我们自己的系统中也需要很多定时执行的也都需要计划任务。最简单的计划任务就可以通过 jdk 给我提供的 API 来实现,当然也有很多的计划任务的框架,比如 spring 的 schedule 以及著名的 quartz。我们这里不去讨论其他的计划任务框架,我们就来看一下 jdk 所给我们提供的 API 来实现定时任务。

  • 例 1:在 2017 年 10 月 11 日晚上 10 点执行任务。
package com.roocon.thread.t3;

import java.text.SimpleDateFormat;
import java.util.Timer;
import java.util.TimerTask;

/**
 * 定时器举例
 *
 */
public class TimerDemo {

	private static final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

	public static void main(String[] args) throws Exception {
		Timer timer = new Timer();
		timer.schedule(new TimerTask() {
			@Override
			public void run() {
				System.out.println("定时任务执行了....");
			}
		}, format.parse("2017-10-11 22:00:00"));
	}
}

  • 例 2: 每隔 5s 执行一次
package com.roocon.thread.t3;

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

public class TimerDemo2 {

	public static void main(String[] args) {
		Timer timer = new Timer();

		timer.schedule(new TimerTask() {

			@Override
			public void run() {
				System.out.println("Hello");
			}
		}, new Date(), 5000);
	}
}

关于 Spring 的定时任务,可以通过 spring 的教程来学习。

五:带返回值的线程

我们发现上面提到的不管是继承 Thread 类还是实现 Runnable 接口,发现有两个问题,第一个是无法抛出更多的异常,第二个是线程执行完毕之后并无法获得线程的返回值。那么下面的这种实现方式就可以完成我们的需求。这种方式的实现就是我们后面要详细介绍的 Future 模式,只是在 jdk5 的时候,官方给我们提供了可用的 API,我们可以直接使用。但是使用这种方式创建线程比上面两种方式要复杂一些,步骤如下。

  1. 创建一个类实现 Callable 接口,实现 call 方法。这个接口类似于 Runnable 接口,但比 Runnable 接口更加强大,增加了异常和返回值。
  2. 创建一个 FutureTask,指定 Callable 对象,做为线程任务。
  3. 创建线程,指定线程任务。
  4. 启动线程

代码如下:

package com.roocon.thread.t4;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

public class CallableTest {

	public static void main(String[] args) throws Exception {
		Callable<Integer> call = new Callable<Integer>() {

			@Override
			public Integer call() throws Exception {
				System.out.println("thread start .. ");
				Thread.sleep(2000);
				return 1;
			}
		};

		FutureTask<Integer> task = new FutureTask<>(call);
		Thread t =  new Thread(task);

		t.start();
		System.out.println("do other thing .. ");
		System.out.println("拿到线程的执行结果 : " + task.get());
	}
}

执行结果如下:

do other thing ..
thread start ..
拿到线程的执行结果 : 1

Callable 中可以通过范型参数来指定线程的返回值类型。通过 FutureTask 的 get 方法拿到线程的返回值。

六:基于线程池的方式

我们知道,线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。当然了,线程池也不需要我们来实现,jdk 的官方也给我们提供了 API。

代码如下:

package com.roocon.thread.t5;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolDemo {

	public static void main(String[] args) {

		// 创建线程池
		ExecutorService threadPool = Executors.newFixedThreadPool(10);

		while(true) {
			threadPool.execute(new Runnable() { // 提交多个线程任务,并执行

				@Override
				public void run() {
					System.out.println(Thread.currentThread().getName() + " is running ..");
					try {
						Thread.sleep(3000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			});
		}
	}
}

执行结果如下:

pool-1-thread-4 is running ..
pool-1-thread-1 is running ..
pool-1-thread-6 is running ..
pool-1-thread-2 is running ..
pool-1-thread-8 is running ..
pool-1-thread-3 is running ..
pool-1-thread-5 is running ..
pool-1-thread-9 is running ..
pool-1-thread-10 is running ..
pool-1-thread-7 is running ..

线程池的内容还有非常多,这里不再详细地讲解。

Spring 方式:使用 Spring 来实现多线程

在我们的应用系统中,经常会处理一些耗时任务,自然而然的会想到使用多线程。JDK 给我们提供了非常方便的操作线程的 API,JDK5 之后更是新增了 JUC 包的支持,并发编程大师 Doug Lea(JDK 并发的作者)也是一直在为我们使用线程做着不懈的努力。

为什么还要使用 Spring 来实现多线程呢?这是句废话!实际有两个原因,第一使用 Spring 比使用 JDK 原生的并发 API 更简单。第二我们的应用环境一般都会集成 Spring,我们的 Bean 也都交给 Spring 来进行管理,那么使用 Spring 来实现多线程更加简单,更加优雅。(更多的可能是在环境中就要这么用!!)

在 Spring3 之后,Spring 引入了对多线程的支持,如果你使用的版本在 3.1 以前,应该还是需要通过传统的方式来实现多线程的。从 Spring3 同时也是新增了 Java 的配置方式,而且 Java 配置方式也逐渐成为主流的 Spring 的配置方式,因此后面的例子都是以 Java 的配置进行演示。

废话有点多,下面具体说说该如何在 Spring 中实现多线程,其实非常简单,只需要在配置类中添加@EnableAsync 就可以使用多线程。在希望执行的并发方法中使用@Async 就可以定义一个线程任务。通过 spring 给我们提供的 ThreadPoolTaskExecutor 就可以使用线程池。下面举个例子来说明

首先定义配置类
package com.hy.spring.test7;

import java.util.concurrent.Executor;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
@ComponentScan("com.hy.spring.test7")
@EnableAsync  // 启用异步任务
public class ThreadConfig  {

     // 这里是声明一个bean,类似于xml中的<bean>标签。
     // Executor 就是一个线程池
     @Bean
     public Executor getExecutor() {
          ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
          executor.setCorePoolSize(5);
          executor.setMaxPoolSize(10);
          executor.setQueueCapacity(25);
          executor.initialize();
          return executor;
     }
}
定义要执行的任务
package com.hy.spring.test7;

import java.util.Random;
import java.util.UUID;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service // 注解的方式把AsyncService交给Spring来管理
public class AsynTaskService {

     // 这里可以注入spring中管理的其他bean,这也是使用spring来实现多线程的一大优势

     @Async    // 这里进行标注为异步任务,在执行此方法的时候,会单独开启线程来执行
     public void f1() {
          System.out.println("f1 : " + Thread.currentThread().getName() + "   " + UUID.randomUUID().toString());
          try {
              Thread.sleep(new Random().nextInt(100));
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
     }

     @Async
     public void f2() {
          System.out.println("f2 : " + Thread.currentThread().getName() + "   " + UUID.randomUUID().toString());
          try {
              Thread.sleep(100);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
     }
}
测试类
package com.hy.spring.test7;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {

     public static void main(String[] args) {
          AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ThreadConfig.class);
          AsynTaskService service = context.getBean(AsynTaskService.class);

          for (int i = 0; i < 10; i++) {
              service.f1(); // 执行异步任务
              service.f2();
          }
          context.close();
     }
}
输出结果
f1 : ThreadPoolTaskExecutor-5   20e6ba88-ae51-42b9-aac6-ed399419fe6d
f2 : ThreadPoolTaskExecutor-2   0d7b1da4-e045-4d58-9054-e793f931cae1
f2 : ThreadPoolTaskExecutor-4   17b8d7c7-24e3-4bcf-b4da-822650a8f0be
f1 : ThreadPoolTaskExecutor-3   a9b32322-1c9b-4fc7-9c2a-1f7a81f2b089
f1 : ThreadPoolTaskExecutor-1   13a85fde-73c7-4c9b-9bb2-92405d1d3ac4
f2 : ThreadPoolTaskExecutor-3   8896caaf-381c-4fc3-ab0f-a42fcc25e5fd
f1 : ThreadPoolTaskExecutor-5   48246589-f8e9-4e9c-b017-8586bf14c0b0
f2 : ThreadPoolTaskExecutor-1   291b03ea-154f-46ba-bc41-69a61d1dd4d5

可以看到我们两个任务是异步进行的。

下面关于线程池的配置还有一种方式,就是直接实现 AsyncConfigurer 接口,重写 getAsyncExecutor 方法即可,代码如下

package com.hy.spring.test7;

import java.util.concurrent.Executor;

import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
@ComponentScan("com.hy.spring.test7")
@EnableAsync
public class ThreadConfig implements AsyncConfigurer {

     @Override
     public Executor getAsyncExecutor() {
          ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
          executor.setCorePoolSize(5);
          executor.setMaxPoolSize(10);
          executor.setQueueCapacity(25);
          executor.initialize();
          return executor;
     }

     @Override
     public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
          return null;
     }
}

原文:java 多线程的 6 种实现方式详解,感谢博主的支持,谢谢 QAQ

微信公众号

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Tellsea

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值