JAVA线程的概念和多线程的实现方式
一. 线程的概念
二. 线程的六种状态
三. Thread类
四.多线程的实现方式
五. 线程安全
一.线程的概念
线程是操作系统进行资源调度的最小单位,一个进程中包含多个线程,而这多个线程之间有些可以并发执行,提高效率.
在JAVA中,线程是和主线程main()并发执行的一条路径,而这条路径实际上可以完成多个任务.所有的线程创建方式实际上都是依靠Thread类去创建的.
二. 线程的状态
JAVA中的线程一共有六种状态,在Thread类中的State:
- NEW 新建 线程刚刚创建,还没有启动
- RUNNABLE可运行 .java将就绪和运行状态统称为可运行,就绪是指具备了运行条件还没有获得CPU的时间片
- WAITING 等待,称为无限期等待,比如调用了Join()方法后,会进入无限期等待
- TIMED_WAITING 超时等待,在特定时间后会返回到运行状态,比如调用sleep()方法睡眠一段时间
- BLOCKED 阻塞 ,线程没有获得需要的锁会发生阻塞,只有没获得锁才会阻塞
-
TERMINATED 终止, 线程执行完毕
三. Thread类
java.lang.Thread包下的线程类,是所有创建线程的核心类;
1.构造方法
构造方法 | Value |
---|---|
Thread() | 创建一个线程对象 |
Thread(String name) | 创建一个线程对象并命名 |
Thread(Runnable target) | 创建一个线程对象,并为其分配Runnable任务 |
Thread(Runnable target, String name) | 创建一个线程对象,为其分配Runnable任务 ,并命名 |
2.常用成员方法
构造方法 | Value |
---|---|
public final String getName() | 获取该线程的名字 |
public final void setName(String name) | 修改线程的名字 |
public final int getPriority() | 获取该线程的优先级 |
public final void setPriority(int newPriority) | 更改线程的优先级 |
public void run() | 分线程中的开始方法,分线程从这里运行 |
public void start() | 启动分线程后,分线程会自动调用run()方法 |
public final void join() | 只有调用这个方法的线程执行完毕后才会执行后面的语句 |
3.常用静态方法
静态方法 | Value |
---|---|
public static void yield() | 暂停当前线程,放弃时间片,等待下一次分配 |
public static void sleep(long millis) | 休眠当前线程,参数为毫秒,1000毫秒=1秒 |
public static Thread currentThread() | 获取当前线程的引用,相当与this |
四. 多线程的实现方式
多线程的实现方式一共有三种,继承Thread类,实现Runnable接口,以及线程池,线程池提交任务的方式有两种.
多线程的实现方式,在功能上是依次递进的,线程池的功能无疑是最强大的.
1. 继承Thread类
这是最基本的多线程实现方式,继承Thread类,重写run方法,在run方法中设置想要完成的任务.实现步骤如下:
-
创建一个资源类继承Thread类
-
重写run方法
-
在main方法中创建资源类对象
-
调用start()方法启动分线程
代码如下:
package gw; class MyThread extends Thread {// 1. 创建一个资源类继承Thread类 @Override public void run() {// 2. 重写run方法 // 遍历数字的任务 for (int i = 0; i < 100; i++) { System.out.println(getName() + ":" + i); } } } public class Test { public static void main(String[] args) { // 3. 在main方法中创建资源类对象 Thread t1 = new MyThread(); t1.setName("线程1"); Thread t2 = new MyThread(); t2.setName("线程2"); // 4. 调用start()方法启动分线程 t1.start(); t2.start(); } }
2.实现Runnable接口
由于在JAVA中子类只可以继承一个父类,继承Thread的方式实现多线程的方式具有很大的局限性,因此实现Runnable接口的方式更加常用一些.实现步骤如下:
- 创建一个资源类实现Runnable接口
- 重写run方法
- 在main方法中创建资源类对象
- 将资源类对象作为Thread有参构造的参数创建线程对象
- 调用start()方法启动分线程
代码如下:
package gw; class MyRunnable implements Runnable {// 1. 创建一个资源类实现Runnable接口 @Override public void run() {// 2. 重写run方法 // 遍历数字的任务 for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } } } public class Test2 { public static void main(String[] args) { // 3. 在main方法中创建资源类对象 Runnable r = new MyRunnable(); // 4. 将资源类对象作为Thread有参构造的参数创建线程对象 Thread t1 = new Thread(r); Thread t2 = new Thread(r); t1.setName("MyRunnable1"); t2.setName("MyRunnable2"); // 5. 调用start()方法启动分线程 t1.start(); t2.start(); } }
实现Runnable接口的方式,实际上是将线程与任务分离,可以提升程序的灵活性与扩展性.
在这种模式中,实际上是用到了静态代理的设计模式.
静态代理,是创建一个代理角色,来完成真实角色无法完成的功能;
具体实现步骤是:
- 创建代理角色的类,与真实角色实现同一个接口;
- 在真实角色类中增加一个代理角色实例的成员属性,并增加有参构造
- 真实角色无法完成的方法,由代理角色进行重写,并创建与真实角色方法的链接
- 在main方法中创建代理角色实例
- 使用有参构造创建真实角色实例,将代理角色实例作为参数传入
在实现Runnable接口创建多线程的方式中,自己创建的资源类就是代理角色,而Thread才是真实角色,他们共同实现了Runnable接口,代理类重写了run方法,并将自己的实例作为参数传入Thread类,这样在调用Thread中的run方法时,就会调用代理角色的run方法.
3.线程池
线程的创建和销毁对系统来说是很大的开销,在需要频繁的创建线程的程序中,线程池方式可以节省系统的资源.
线程池是存放的线程的数组,线程在使用完毕后,并不会销毁,而是返回到线程池中,等待再次分配任务.
3.1 线程池的相关接口
线程池的超级接口是Executor,位于java.util.concurrent .
而我们常用的是Executor的子接口,java.util.concurrent. ExecutorService, ExecutorService可以提交Runnable任务和Callable任务,功能更加强大;
而线程池的实例需要通过Executors工厂获得.Executors是一个操作Executor、ExecutorService、ScheduledExecutorService和 Callable 类的工具类;
另外除了Executors工厂可以获得线程池,还有java.util.concurrent.ThreadPoolExecutor可以获得一个线程池,两者的效率相差不多,但是ThreadPoolExecutor可以设置更多的参数.
3.2 线程池的相关方法
- ExecutorService接口
方法 | Value |
---|---|
Future<?> submit(Runnable task) | 提交一个Runnable任务,在任务完成后返回null通过特殊手段获得返回值 |
Future submit(Callable task) | 提交一个Callable任务,返回值是任务执行结果,通过特殊手段获得返回值 |
void shutdown() | 调用此方法后线程池不会再接受新任务,但是已经提交的还会继续执行 |
boolean isTerminated() | 判断当前线程池的任务是否全部完成,一般与shutdown()一起使用,否则永不为true |
- Executors工具类
方法 | Value |
---|---|
public static ExecutorService newCachedThreadPool() | 创建一个动态的线程池 |
public static ExecutorService newFixedThreadPool(int nThreads) | 创建一个固定数量的线程池 |
- ThreadPoolExecutor
ThreadPoolExecutor的构造方法有很多参数
参数 | Value |
---|---|
corepoolsize | 最小线程数,一般设置为服务器核心数量 |
maxpoolsiz | 最大线程数,当队列满后会扩容到最大线程数 |
keepalivetime | 当线程池中线程数大于corepoolsize 并且无新增任务时,销毁等待的最大时间。 |
unit | 销毁时间单位 |
workQueue | 工作队列 |
handler | 饱和策略。当现在同时进行的任务数量大于最大线程数量并且队列也已经放满后,线程池采取的一些策略 |
AbortPolicy | 直接抛出异常,RejectedExecutionException. |
CallerRunsPolicy | 调用执行自己的线程来完成任务,当您的任务不能抛弃时采取这个策略,这个策略会给系统带来额外的开销,影响性能。 |
DiscardPolicy | 直接抛弃掉 |
DiscardOldestPolicy | 丢弃掉最老的任务。 |
-
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long
keepAliveTime, TimeUnit unit, BlockingQueue workQueue)
ThreadPoolExecutor创建一个线程池实例:
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 6, 3, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(3)
3.3 线程池实现多线程方式1
创建一个线程池,将Runnable任务提交到线程池会在线程池有线程空闲时自动执行,如果提交到的线程时,线程池的线程都被分配到了任务,会等到有线程完成任务空闲时,再去执行分配的任务.
采用线程池方式实现多线程,不仅可以让线程与任务分离,还可以让线程多次复用,执行多个任务,减少系统的资源分配.线程池实现多线程的步骤如下:
- 创建一个资源类实现Runnable接口
- 重写run方法
- 创建任务对象
- 创建一个线程池
- 将任务提交到线程池会自动执行
package gw; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; class MyRunExec implements Runnable {// 1. 创建一个资源类实现Runnable接口 @Override public void run() {// 2. 重写run方法 // 遍历数字的任务 for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } } } public class Test3 { public static void main(String[] args) { // 3.创建任务对象 MyRunExec re = new MyRunExec(); // 4.创建一个线程池 ExecutorService e = Executors.newFixedThreadPool(2); for (int i = 0; i < 3; i++) { // 5.将任务提交到线程池会自动执行 e.submit(re); } e.shutdown();// 关闭线程池 } }
3.4 线程池实现多线程方式2
线程池不仅仅可以提交Runnable任务,还可以提交Callable任务,不同之处在于Callable可以返回线程的执行结果,并且可以抛出异常,而提交Runnable只可以在线程执行完毕过后获得一个null,也不可以抛出异常.
线程池提交Callable任务实现多线程方式如下:
- 创建资源对象实现Callable接口
- 重写call方法布置任务
- 创建一个线程池
- 创建任务类的实例对象
- 将任务提交到线程池
package gw; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; class MyCallable implements Callable<Integer> { // 1.创建资源对象实现Callable接口 @Override public Integer call() throws Exception {// 2.重写call方法布置任务 for (int i = 0; i < 100; i++) { System.out.println(i); } return null; } } public class Test4 { public static void main(String[] args) { // 3.创建一个动态的线程池.(固定的也可以) ExecutorService es = Executors.newCachedThreadPool(); // 4.创建任务类的实例对象 MyCallable c = new MyCallable(); // 5.将任务提交到线程池 es.submit(c); es.shutdown(); } }
3.5 线程池异步任务的应用
提交Callable任务到线程池实现多线程的方式,可以获取线程的执行结果.那我们是不是可以使用这种方式去将一个任务分成几个部分,分配给不同的线程去计算,从而加快效率.
依据这种思想,我们可以将一个计算和1-1000的任务,分成2个部分,分配给不同的线程去执行.
要将一个求和的任务分配到三个线程,肯定要从三个线程中拿到执行结果才可以,不然分配任务也就没有了意义.
而我们的任务是存放在call方法中的,call方法是JVM调用的,我们不是call方法的调用者,理论上根本无法拿到call方法的返回值.
回到线程池,我们看到提交线程的方法有一个返回值,是Future类型的,我们完全可以在提交任务的同时来接受这个Future方法.
Future submit(Callable task)
实际上,JAVA提供了一个接口获得call方法的执行结果.Callable接口的call方法执行任务,Futrue接口的get方法获得计算结果,
submit方法是如何获得任务结果的呢?
通过submit()方法提交的任务,会自动的去执行call()方法,然后call()的返回值自然就在submit方法中,但是由于submit()每次提交的任务不一定,其计算结果的类型也不一定,也可能包括自定义类型的数据,为了使得submit()方法可以接受任意类型的数据,JAVA就提供了一个Future接口去封装这个返回值,不论返回值是什么类型,都将他封装成一个Future对象.然后通过其公共访问方法去获得这个值.不得不去佩服前人的智慧.
根据上面的知识,我们来创建一个异步任务,计算1-1000的累加和.
步骤如下:
- 创建两个任务类实现Callable接口
- 分别重写call方法去布置任务
- 创建线程池
- 创建任务类实例对象
- 将任务提交到线程池
- 同时接受返回的Future
- 获取返回值
- 打印结果
代码如下: package gw; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; //1. 创建两个任务类实现Callable接口 class MyCallable1 implements Callable<Integer> { // 2. 重写call方法计算1-500累加和 @Override public Integer call() throws Exception { int sum = 0; for (int i = 0; i <= 500; i++) { sum += i; } return sum; } } // 1. 创建两个任务类实现Callable接口 class MyCallable2 implements Callable<Integer> { // 2. 重写call方法计算501-1000累加和 @Override public Integer call() throws Exception { int sum = 0; for (int i = 501; i <= 1000; i++) { sum += i; } return sum; } } public class Test5 { public static void main(String[] args) throws InterruptedException, ExecutionException { // 3. 创建线程池 ExecutorService es = Executors.newCachedThreadPool(); // 4. 创建任务类实例对象 Callable<Integer> c1 = new MyCallable1(); Callable<Integer> c2 = new MyCallable2(); // 5. 将任务提交到线程池 6. 同时接受返回的Future Future<Integer> f1 = es.submit(c1); Future<Integer> f2 = es.submit(c2); // 7. 获取返回值 //get()方法会自动的阻塞main方法,只有等到call方法执行完毕 //,得到结果以后才会执行get()方法. int sum = f1.get() + f2.get(); // 8. 打印结果 System.out.println(sum); } }
Callable接口的泛型,Future的泛型,call()方法的泛型,与计算结果的类型都要保持一致.
五. 线程安全
本文地址:https://blog.csdn.net/gwgw0621/article/details/108018551