您的当前位置:首页正文

Java线程及线程池入门(使用篇)

2024-11-20 来源:个人技术集锦

0.引言

基于之前的多线程理论之后,本片将会带来简单的使用篇。我会由浅入深慢慢向大家展示并发编程之美。并发编程有其不确定性,然而正是这些不确定性,才使得它值得深入研究。当你融会贯通之后,又会觉得它如此的美。

诚然对于初学者,或者多数时间都在项目中做日常CRUD操作的小伙伴很少有机会接触多线程编程。不过这不是问题,我会慢慢解解释尽量做到详实。所谓门槛,既是门也是槛。对于跨不过去的人是槛,而对于跨过去的人则是门,一扇进入高阶Java编程的门,一扇助你研习开源框架的门。

好了,话不多说,我们直接进入正题吧。

1.线程基础

1.1 创建线程

1.1.1 继承并重写 run 方法

import org.junit.jupiter.api.Test;

public class ThreadDemo {
	
	@Test
	void test_create() {
		Thread thread = new MyThread();
		thread.start();
	}

	private static class MyThread extends Thread {
		@Override
		public void run() {
			System.out.println("My Thread");
		}
	}
}

执行以上代码输出结果:

1.1.2 Thread匿名内部类

public class ThreadDemo {

	@Test
	void test_create() {
		Thread thread = new Thread() {
			@Override
			public void run() {
				System.out.println("Thread 匿名内部类创建法");
			}
		};
		thread.start();
	}
}

运行以上代码输出:

1.1.3 实现Runnable接口

import org.junit.jupiter.api.Test;

public class ThreadDemo {

	@Test
	void test_create() {
		Thread thread = new Thread(new MyTask());
		thread.start();
	}
	
	static class MyTask implements Runnable {
		@Override
		public void run() {
			System.out.println("Runnable 接口实现法");
		}
	}
}

运行以上代码输出结果:

1.1.4 匿名内部类Runnable接口

import org.junit.jupiter.api.Test;

public class ThreadDemo {

	@Test
	void test_create() {
		Thread thread = new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println("匿名内部类实现Runnable接口");
			}
		});
		thread.start();
	}
}

运行以上代码输出结果:

1.1.5 Lambda表达式

import org.junit.jupiter.api.Test;

public class ThreadDemo {

	@Test
	void test_create() {
		Thread thread = new Thread(() -> System.out.println("Lambda表达式实现Runnable接口"));
		thread.start();
	}
}

运行以上代码输出结果:

1.1.6 小结

这里给大家列举了5种创建Thread的方法,不过我日常项目常用的是 实现Runnable接口法,对于task内容较少的可以使用Lambda表达式法。这样既可以做到业务task与主方法隔离,也可以在task内容较少时减少过多创建class文件,使得整个项目更加容易维护。

1.2 线程API使用

1.2.1 基本属性

import org.junit.jupiter.api.Test;

public class ThreadDemo {

	@Test
	void test_create() {
		Thread thread = new Thread(() -> {
			System.out.println("id: " + Thread.currentThread().getId());
			System.out.println("name: " + Thread.currentThread().getName());
			System.out.println("priority: " + Thread.currentThread().getPriority());
			System.out.println("status: " + Thread.currentThread().getState());
			System.out.println("thread-group: " + Thread.currentThread().getThreadGroup());
		});
		thread.setName("My-Thread");
		thread.setPriority(4);
		thread.start();
	}
}

运行以上代码输出:

线程的setName可以给当前线程设置名称,这一点很有用,当在多线程运行时打印线程名称,何以帮助开发人员很快定位到问题。

线程的setPriority被评为最废的方法,因为这个方法基本不会有用。这个方法只是设置了一个线程优先级的参考值,那么操作系统在执行时就可以参考也可以不参考。假如你想要通过一个线程设置为2,一个设置为8的优先级来做负载均衡那基本是不能达到的。之所以觉得废,我认为还是使用者不了解其机制而想要达到自己想当然的结果了。毕竟线程任务怎么执行,执行多久不是JVM说了算的了。

id - 系统自动生成的线程编号

status - 当前线程的状态(生命周期)

thread-group: 所属线程池, 默认是main, 这个属性在区分不同线程池时有用

1.2.2 多个线程联动

在这里主要为大家介绍线程自带的两个用来协调多线程联动的API。

Thread.yield(): 这个方法在操作系统层面是有语义的,它会告诉cpu我主动放弃了此次cpu时间片,但是cpu下一次调度时会不会继续调度当前前程还是其他线程时是未知的或者换句话说都有可能。

Thread.join(): 当一个线程调用另一个线程的join方法时,当前线程会进入等待状态,等待被调用join操作的线程任务执行结束。

> yield

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

import org.junit.jupiter.api.Test;

public class ThreadDemo {
	
	private ExecutorService es = Executors.newFixedThreadPool(2);
	
	@Test
	void test_yield() {
		
		Runnable yieldTask = () -> {
			System.out.println(Thread.currentThread() + "yield task start");
			Thread.yield();
			System.out.println(Thread.currentThread() + "yield task end");
		};
		
		Runnable normalTask = () -> {
			System.out.println(Thread.currentThread() + "normal task start");
		};
		
		for(int i = 0; i < 10; i++) {
			es.submit(yieldTask);
			es.submit(normalTask);
		}
		
	}
}

输出结果(截取):

从上面的结果(虽然不怎么严谨,但足以说明问题了)不难看出,yield所产生的作用与我们分析的一致。Thread.yield() 只是给cpu建议一下,至于cpu人家要不要采纳要不要执行Java无法决定(这也许就是Java的痛点,无法直接操控底层系统资源)。

> join

import org.junit.jupiter.api.Test;

public class ThreadDemo {

	@Test
	void test_join() throws InterruptedException {

		Runnable joinTask = () -> {
			for(int i = 0; i<100; i++) {				
				System.out.println(Thread.currentThread() + "join task start");
			}
		};
		Thread joinThread = new Thread(joinTask);
		joinThread.start();
		joinThread.join();
		
		System.out.println("test_join end");
	}
}

输出结果:

无论执行多少次,test_join总是最后打印,这个结果也符合我们对join方法作用的分析。按这个思路join能不能帮我们做到流程控制呢?我们继续试一下就知道了。

import org.junit.jupiter.api.Test;

public class ThreadDemo {

	@Test
	void test_join() throws InterruptedException {

		Runnable joinTask1 = () -> {
			for(int i = 0; i<10; i++) {				
				System.out.println(Thread.currentThread() + "join task 1 start");
			}
		};
		
		Runnable joinTask2 = () -> {
			for(int i = 0; i<10; i++) {				
				System.out.println(Thread.currentThread() + "join task 2 start");
			}
		};
		
		Thread joinThread1 = new Thread(joinTask1);
		Thread joinThread2 = new Thread(joinTask2);
		joinThread1.start();
		joinThread2.start();
		joinThread1.join();
		joinThread2.join();
		
		System.out.println("test_join end");
	}
}
Thread[Thread-1,5,main]join task 1 start
Thread[Thread-1,5,main]join task 1 start
Thread[Thread-1,5,main]join task 1 start
Thread[Thread-1,5,main]join task 1 start
Thread[Thread-1,5,main]join task 1 start
Thread[Thread-1,5,main]join task 1 start
Thread[Thread-1,5,main]join task 1 start
Thread[Thread-1,5,main]join task 1 start
Thread[Thread-1,5,main]join task 1 start
Thread[Thread-1,5,main]join task 1 start
Thread[Thread-2,5,main]join task 2 start
Thread[Thread-2,5,main]join task 2 start
Thread[Thread-2,5,main]join task 2 start
Thread[Thread-2,5,main]join task 2 start
Thread[Thread-2,5,main]join task 2 start
Thread[Thread-2,5,main]join task 2 start
Thread[Thread-2,5,main]join task 2 start
Thread[Thread-2,5,main]join task 2 start
Thread[Thread-2,5,main]join task 2 start
Thread[Thread-2,5,main]join task 2 start
test_join end

不管次数改的多大, 都是1先完然后再是2,按照我们的预期应该时1,2同时执行才可以的,看来理想和现实还是有很大的差距的。想想也是join会使当前线程等待,join几次就等待几次的,其实各个join进来的task是按次序一个个来的(就如同插队,不管前面插进来几个人,过闸口时还是一个个来的),所以如果想要做到并发执行一些任务再等待结果然后再做一些事情,对于这样的需求我们应该借助于CountDownLaunch或者CompletableFuture来实现,把希望寄托在原生的线程API上,可能是我们想多了哦。

1.2.3 小结

线程自带的一些联动方法,基本都是属于给cpu提建议的,至于cpu参考还是不参考不是由线程自身决定的。所以想要做到可控的线程联动,我们得借助其他方法而并非线程自带的一些API。

2. 线程池基础

JDK提供了几种线程池,Spring也提供了自己的线程池实现。这里我将为大家展示这几种线程池的使用案例。

2.1 SingleThreadExecutor

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

import org.junit.jupiter.api.Test;

public class ThreadDemo {

	@Test
	void test_thread_pool() throws InterruptedException {

		ExecutorService es = Executors.newSingleThreadExecutor();

		Runnable task1 = () -> {
			for (int i = 0; i < 100; i++) {
				System.out.println(Thread.currentThread() + "task 1 start");
			}
		};

		Runnable task2 = () -> {
			for (int i = 0; i < 100; i++) {
				System.out.println(Thread.currentThread() + "task 2 start");
			}
		};

		es.submit(task1);
		es.submit(task2);
        // 这里要让主线程等待下,不然主线程执行完成方法就退出了
		Thread.sleep(10000);
	}
}

输出结果:

无论循环次数设置的多么大,输出结果一直是按照提交次序来执行的,原因是只有一个线程在干活,它会先执行完第一个task再来执行第二个task, 至于第二个task被放在哪里,这个后面会有专门的文章来讨论,这里只是展示使用上的效果。

2.2 FixedThreadPool

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

import org.junit.jupiter.api.Test;

public class ThreadDemo {

	@Test
	void test_thread_pool() throws InterruptedException {

		ExecutorService es = Executors.newFixedThreadPool(2);

		Runnable task1 = () -> {
			for (int i = 0; i < 100; i++) {
				System.out.println(Thread.currentThread() + "task 1 start");
			}
		};

		Runnable task2 = () -> {
			for (int i = 0; i < 100; i++) {
				System.out.println(Thread.currentThread() + "task 2 start");
			}
		};

		es.submit(task1);
		es.submit(task2);
		// 主线程在此等待,不然输出结果不全
		Thread.sleep(10000);
	}
}

输出结果:

从以上结果,不难看出输出结果次序与提交顺序无关(因为有两个线程一起干活了,从输出的线程名称也可以看出来),而且输出是乱序的。这已经符合我们对多线程的预期,多个task一起跑,各跑各的(线程通信?暂不考虑了先),对外有一种错觉多个task是一起跑的(只是cpu切换的比较快我们感知不到而已)。

2.3 CachedThreadPool

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

import org.junit.jupiter.api.Test;

public class ThreadDemo {

	@Test
	void test_thread_pool() throws InterruptedException {

		ExecutorService es = Executors.newCachedThreadPool();

		Runnable task1 = () -> {
			for (int i = 0; i < 100; i++) {
				System.out.println(Thread.currentThread() + "task 1 start");
			}
		};

		Runnable task2 = () -> {
			for (int i = 0; i < 100; i++) {
				System.out.println(Thread.currentThread() + "task 2 start");
			}
		};

		es.submit(task1);
		es.submit(task2);
		// 主线程在此等待,不然输出结果不全
		es.awaitTermination(10, TimeUnit.SECONDS);
	}
}

输出结果:

这个结果与2.2中的输出结果没有什么区别。

2.4 Spring - ThreadPoolTaskExecutor

2.4.1 SingleThreadPool

package test01;

import org.junit.jupiter.api.Test;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

public class ThreadDemo {

	@Test
	void test_thread_pool() throws InterruptedException {

		ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor();
		threadPool.setCorePoolSize(1);
		threadPool.afterPropertiesSet();

		Runnable task1 = () -> {
			for (int i = 0; i < 100; i++) {
				System.out.println(Thread.currentThread() + "task 1 start");
			}
		};

		Runnable task2 = () -> {
			for (int i = 0; i < 100; i++) {
				System.out.println(Thread.currentThread() + "task 2 start");
			}
		};

		threadPool.submit(task1);
		threadPool.submit(task2);
		// 主线程在此等待,不然输出结果不全
		Thread.sleep(10000);
	}
}

这里的输出结果与2.1输出一致,不再展示。

2.4.2 FixedThreadPool

package test01;

import org.junit.jupiter.api.Test;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

public class ThreadDemo {

	@Test
	void test_thread_pool() throws InterruptedException {

		ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor();
		threadPool.setCorePoolSize(2);
		threadPool.afterPropertiesSet();

		Runnable task1 = () -> {
			for (int i = 0; i < 100; i++) {
				System.out.println(Thread.currentThread() + "task 1 start");
			}
		};

		Runnable task2 = () -> {
			for (int i = 0; i < 100; i++) {
				System.out.println(Thread.currentThread() + "task 2 start");
			}
		};

		threadPool.submit(task1);
		threadPool.submit(task2);
		// 主线程在此等待,不然输出结果不全
		Thread.sleep(10000);
	}
}

这里的输出结果与2.2一致,就不再展示了。

2.5 小结

这里想大家展示了常用的几种的线程池的创建和使用案例,就项目而言,一般推荐使用提供的线程池因为它可以帮我们管理很多线程池底层的组件(后面会详细展开说的)。不过对于一般的应用场景FixedThreadPool也是个很好的选择。

3. 总结

本篇文章向大家展示了Java线程及线程池的基本使用场景,由于本文仅涉及基本使用所以所有用例都未涉及线程竞争,所以也就无需线程同步,关于线程同步及线程池底层实现原理,我会在后续文章中详细分析,这里也希望大家持续关注了。

显示全文