目录
线程相关概念
并发
并行
继承Thread类
实现Runnable接口
实现Callable接口
使用ExecutorService 和线程池
多线程卖手机
非同步
同步机制卖手机
锁方法
锁代码块
编辑锁静态方法
锁静态代码块
线程常用方法
用户线程和守护线程
线程状态
线程池
自定义线程池
java内置线程池
newCacheThreadPool
newFixedThreadPool
newSingleThreadExecutor
newScheduledThreadPool(int corePoolSize)
Future
模拟案例场景
手机秒杀
银行取款
练习源码
线程相关概念
Java多线程是Java编程语言中一个重要的特性,它允许程序同时执行多个线程,从而实现并发执行任务,提高程序的性能和响应性。
基础概念
-
线程: 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。Java中,每一个线程都是一个独立的执行流,拥有自己的堆栈和局部变量。
-
并发与并行: 并发指的是在同一时间段内,多个任务交替执行,看起来像是同时执行的;并行则是指真正的同时执行多个任务,这通常需要多核处理器的支持。
并发
并发是在同一个时刻,多个任务在交替执行,一种"貌似同时"在执行的错觉,因为计算机执行速度太快,所以会产生这样的错觉,其实就是 单核cpu实现的多任务就是并发
并行
在同一个时刻,多个任务同时进行,多核cpu可以实现并行
继承Thread类
- 创建一个新的类继承
Thread
类,并重写run()
方法。在run()
方法中编写线程要执行的代码。 - 优点:可以直接访问并扩展
Thread
类的方法和属性,如interrupt()
或getState()
。 - 缺点:如果想要扩展其他类,就不能使用此方法,因为Java不支持多重继承。
java中普通类继承Thread可以实现多线程,Thread类其实也是实现了Runnable接口来实现多线程的
启动查看效果
可以看到打印的日志信息线程名字是在子线程和main线程之间来回进行切换,从而得出并发的效果
而run方法其实只是一个普通方法,使用start()方法才会启动线程的并发
如果使用的是run方法看下是怎样的效果
启动查看
可以看到并没有交替执行,且子线程打印的线程名也一直是main的名字,run方法只是一个普通方法,并不会启动多线程
实现Runnable接口
- 创建一个新的类实现
Runnable
接口,并实现run()
方法。 - 将这个
Runnable
实例传递给Thread
构造函数创建Thread
对象,然后调用start()
方法启动线程。 - 优点:可以让你的类继承自其他类,同时实现多线程功能。
- 缺点:相比直接继承
Thread
类,你必须额外创建一个Thread
对象。
由于java是单继承多实现,当我们使用了java中某一个类,而当前类又继承了某一个父类,此时如果想再用该类去实现多线程就不能再去继承Thread类来进行实现多线程了,因为java是单继承多实现的
可以采用实现Runnable接口来实现多线程
查看效果
实现Callable接口
Callable
和Runnable
类似,但是Callable
的call()
方法可以返回一个结果并且可以抛出异常。- 需要创建一个
Callable
接口的实现类,然后将其封装进FutureTask
中,最后将FutureTask
传给Thread
构造函数或直接交给ExecutorService
执行。 - 优点:提供了一种可以获取线程执行结果的方式。
- 缺点:实现比
Runnable
更复杂。
import java.util.concurrent.*;
public class IphoneCallable implements Callable<Integer> {
// 定义手机数量为100台
private static int count = 100;
private static final Object lock = new Object();
@Override
public Integer call() throws Exception {
int sold = 0;
while (true) {
synchronized (lock) {
if (count <= 0) {
System.out.println(Thread.currentThread().getName()
+ " 库存为0, 停止售卖");
return sold;
}
System.out.println(Thread.currentThread().getName()
+ " Callable 窗口卖手机,剩余手机数量:" + --count);
sold++;
Thread.sleep(10);
}
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(3);
Future<Integer>[] futures = new Future[3];
for (int i = 0; i < 3; i++) {
futures[i] = executor.submit(new IphoneCallable());
}
int totalSold = 0;
for (Future<Integer> future : futures) {
totalSold += future.get();
}
System.out.println("总共售出手机:" + totalSold);
executor.shutdown();
}
}
可以看到获取了线程最后的返回值
使用ExecutorService
和线程池
ExecutorService
是 Java 提供的一种用于管理和控制线程的高级工具,它可以复用一组线程来执行任务。- 通过
Executors
工厂方法创建ExecutorService
实例,然后使用submit()
方法提交Runnable
或Callable
任务。 - 优点:可以更有效地管理线程资源,避免了频繁创建和销毁线程的开销,提高了系统性能。
- 缺点:相比前三种方法,使用
ExecutorService
的代码更为复杂,且需要理解线程池的工作原理。 -
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class IphoneRunnable implements Runnable { // 定义手机数量为100台 private static int count = 100; private static final Object lock = new Object(); private static final int THREAD_COUNT = 3; private static volatile boolean shouldRun = true; @Override public void run() { while (shouldRun) { synchronized (lock) { if (count <= 0) { System.out.println(Thread.currentThread().getName() + " 库存为0, 停止售卖"); shouldRun = false; return; } System.out.println(Thread.currentThread().getName() + " Runnable 窗口卖手机,剩余手机数量:" + --count); try { Thread.sleep(10); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } } public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT); for (int i = 0; i < THREAD_COUNT; i++) { executor.execute(new IphoneRunnable()); } executor.shutdown(); } }
多线程卖手机
非同步
下面以多个窗口卖手机为例,查看下多线程情况下卖手机会有什么问题
启动查看结果
发现当手机库存为0后Thread-2和Thread-0还在进行卖,从而出现了-1的超卖现象,这种情况下肯定是不允许的
下面以实现Runnable接口查看现象看下
也出现了超卖现象
同步机制卖手机
锁方法
启动测试
多次测试没有出现超卖现象
锁代码块
synchronoized不仅可以写在方法上,还可以单独包裹代码块
或者定义一个对象,然后锁该对象,要保证锁的是同一个对象即可
锁静态方法
如果是静态方法,则锁的是当前的类本身
锁静态代码块
锁静态方法的代码块,也是锁定当前类本身
线程常用方法
start()
: 启动线程,使其开始执行线程的任务。run()
: 线程的任务代码,定义在线程中要执行的操作。join()
: 在一个线程中调用另一个线程的join()方法,会让当前线程等待被调用线程执行完毕后再继续执行。sleep(long milliseconds)
: 使线程暂停执行指定的时间,以毫秒为单位。yield()
: 使当前线程让出CPU执行权,让同优先级的线程有机会执行。interrupt()
: 中断线程,发送一个中断信号给线程,使其退出阻塞状态。isAlive()
: 检测线程是否还存活。setPriority(int priority)
: 设置线程的优先级,优先级越高,被执行的可能性越大。- getPriority():获取线程优先级
用户线程和守护线程
用户线程: 也叫工作线程,当线程的任务执行完成或通知方式结束
守护线程: 一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束
常见的守护线程:垃圾回收机制
先来看下没有守护线程的情况下主线程和子线程的执行关系
启动查看
可以看到当主线程停止了辛苦的工作后,子线程还在执行愉快的玩耍,没有和主线程共同进退,此时就需要使用守护进程了
再次启动查看效果
可以看到当设置子线程为守护线程后,主线程停止后,子线程也自动停止了执行
线程状态
- NEW(新建): 当线程对象被创建时,它处于新建状态。此时线程尚未开始执行。
- RUNNABLE(可运行 可细分为 ready(准备) 和runing(运行)): 线程对象创建后,其他线程调用了该对象的start()方法,线程进入可运行状态。处于可运行状态的线程可能正在等待CPU的调度执行,也可能正在执行。
- BLOCKED(阻塞): 当线程在等待获取同步锁时,如果获取不到(因为其他线程已经持有了锁),该线程会进入阻塞状态。
- WAITING(等待): 线程进入等待状态,等待其他线程的通知或者唤醒。
- TIMED_WAITING(计时等待): 线程进入计时等待状态,等待一定的时间后自动唤醒。
- TERMINATED(终止): 线程执行完任务或者因异常退出后,进入终止状态
看下线程状态演化图
代码打印其各个状态
Waitiing状态没有打印出来,可以自己尝试下
线程池
线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。这里的线程就是前面的线程,任务就是实现了Runnable或Callable接口的实例对象;
使用线程池最大的原因就是可以根据系统的需求和硬件环境灵活的控制线程的数量且可以对所有线程进行统一的管理和控制,从而提高系统的运行效率,降低系统运行运行压力;
线程和任务分离,提升线程重用性;
控制线程并发数量,降低服务器压力,统一管理所有线程;
提升系统响应速度,假如创建线程用的时间为T1,执行任务用的时间为T2,销毁线程用的时间为T3,那么使用线程池就免去了T1和T3的时间
自定义线程池
自定义线程类
import java.util.List;
/*
需求:
编写一个线程类,需要继承Thread类,设计一个属性,用于保存线程的名字;
设计一个集合,用于保存所有的任务;
*/
public class MyWorker extends Thread{
private String name;//保存线程的名字
private List<Runnable> tasks;
//利用构造方法,给成员变量赋值
public MyWorker(String name, List<Runnable> tasks) {
super(name);
this.tasks = tasks;
}
@Override
public void run() {
//判断集合中是否有任务,只要有,就一直执行任务
while (tasks.size()>0){
Runnable r = tasks.remove(0);
r.run();
}
}
}
自定义任务类
/*
需求:
自定义线程池练习,这是任务类,需要实现Runnable;
包含任务编号,每一个任务执行时间设计为0.2秒
*/
public class MyTask implements Runnable{
private int id;
//由于run方法是重写接口中的方法,因此id这个属性初始化可以利用构造方法完成
private static boolean loop = true;
private static int count=100;
public void setId(int id){
this.id = id;
}
@Override
public void run() {
// 判断手机数量是否大于0
while (loop) {
extracted();
}
}
private synchronized static void extracted() {
if(count<=0){
System.out.println(Thread.currentThread().getName()
+ "库存为0,停止售卖");
loop = false;
return;
}
System.out.println(Thread.currentThread().getName()
+ "Runnable窗口卖手机,剩余手机数量:" + --count);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public String toString() {
return "MyTask{" +
"id=" + id +
'}';
}
}
自定义线程池
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/*
这是自定义的线程池类;
成员变量:
1:任务队列 集合 需要控制线程安全问题
2:当前线程数量
3:核心线程数量
4:最大线程数量
5:任务队列的长度
成员方法
1:提交任务;
将任务添加到集合中,需要判断是否超出了任务总长度
2:执行任务;
判断当前线程的数量,决定创建核心线程还是非核心线程
*/
public class MyThreadPool {
// 1:任务队列 集合 需要控制线程安全问题
private List<Runnable> tasks = Collections.synchronizedList(new LinkedList<>());
//2:当前线程数量
private int num;
//3:核心线程数量
private int corePoolSize;
//4:最大线程数量
private int maxSize;
//5:任务队列的长度
private int workSize;
public MyThreadPool(int corePoolSize, int maxSize, int workSize) {
this.corePoolSize = corePoolSize;
this.maxSize = maxSize;
this.workSize = workSize;
}
//1:提交任务;
public void submit(Runnable r){
//判断当前集合中任务的数量,是否超出了最大任务数量
if(tasks.size()>=workSize){
System.out.println("任务:"+r+"被丢弃了...");
}else {
tasks.add(r);
//执行任务
execTask(r);
}
}
//2:执行任务;
private void execTask(Runnable r) {
//判断当前线程池中的线程总数量,是否超出了核心数,
if(num < corePoolSize){
new MyWorker("核心线程:"+num,tasks).start();
num++;
}else if(num < maxSize){
new MyWorker("非核心线程:"+num,tasks).start();
num++;
}else {
System.out.println("任务:"+r+" 被缓存了...");
}
}
}
测试
/*
测试类:
1: 创建线程池类对象;
2: 提交多个任务
*/
public class MyTest {
public static void main(String[] args) {
//1:创建线程池类对象;
MyThreadPool pool = new MyThreadPool(2,4,20);
//2: 提交多个任务
MyTask my = new MyTask();
for (int i = 0; i <30 ; i++) {
//3:创建任务对象,并提交给线程池
my.setId(i);
pool.submit(my);
}
}
}
启动测试:
可以看到自定义的线程池也没有出现超卖
java内置线程池
java中使用ExecutorService可以利用jdk中的Executors类中的静态方法获取线程池
newCacheThreadPool
- 作用: 创建一个可缓存的线程池,线程数动态调整。
- 优点: 当任务完成后,空闲线程会被回收,当需要时,又可以迅速恢复线程,适用于执行大量短时间的任务。
创建一个默认的线程池对象 里面的线程可重用
还可以传递自定义的线程工厂来实现
newFixedThreadPool
- 作用: 创建一个固定大小的线程池。
- 优点: 线程数量固定,可以有效控制资源使用,避免大量线程创建和销毁带来的性能开销。
创建可重用固定线程数的线程池
同样也可以指定线程数
测试
newSingleThreadExecutor
- 作用: 创建一个单线程化的线程池。
- 优点: 确保所有任务按照指定顺序执行,可以用于需要保持任务执行顺序的场景。
创建一个使用的那个worker线程的Executor,以误界队列方式以来运行该线程
也可以指定线程工厂来进行创建任务
也是只有一个线程
newScheduledThreadPool(int corePoolSize)
- 作用: 创建一个定时的线程池,支持定时和周期性任务执行。
- 优点: 可以执行定时任务,非常适合于需要定期执行的任务场景。
启动查看效果
可以看到程序先输出了over,有了2秒延迟后才开始执行任务
Future
Future
是 Java 中用于表示异步计算结果的一个接口,它通常与 ExecutorService
和 Callable
结合使用
当我们在执行异步任务时,有时会遇到需要拿取任务返回值的情况,此时就需要用Future和Callable接口来进行任务书写
模拟案例场景
手机秒杀
创建线程任务
/*
任务类:
包含了商品数量,客户名称,送手机的行为;
*/
public class Mytask implements Runnable {
//设计一个变量,用于表示商品的数量
private static int id = 10;
//表示客户名称的变量
private String userName;
public Mytask(String userName) {
this.userName = userName;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(userName+"正在使用"+name+"参与秒杀任务...");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Mytask.class){
if(id>0){
System.out.println(userName+"使用"+name+"秒杀:"+id-- +"号商品成功啦!");
}else {
System.out.println(userName+"使用"+name+"秒杀失败啦!");
}
}
}
}
创建测试入口
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/*
主程序类,测试任务类
*/
public class Mytest {
public static void main(String[] args) {
//1:创建一个线程池对象
ThreadPoolExecutor pool = new ThreadPoolExecutor(3,5,1,
TimeUnit.MINUTES,new LinkedBlockingQueue<>(15));
//2:循环创建任务对象
for (int i = 1; i <=20 ; i++) {
Mytask myTask = new Mytask("客户"+i);
pool.submit(myTask);
}
//3:关闭线程池
pool.shutdown();
}
}
启动测试
银行取款
编写线程任务
public class MyTask implements Runnable {
//用户姓名
private String userName;
//取款金额
private double money;
//总金额
private static double total = 1000;
public MyTask(String userName, double money) {
this.userName = userName;
this.money = money;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(userName+"正在准备使用"+name+"取款:"+money+"元");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (MyTask.class){
if(total-money>0){
System.out.println(userName+"使用"+name+"取款:"+money+"元成功,余额:"+(total-money));
total-=money;
}else {
System.out.println(userName+"使用"+name+"取款:"+money+"元失败,余额:"+total);
}
}
}
}
启动入口
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
public class MyTest {
public static void main(String[] args) {
//1:创建线程池对象
ExecutorService pool = Executors.newFixedThreadPool(2, new ThreadFactory() {
int id = 1;
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "ATM" + id++);
}
});
//2:创建两个任务并提交
for (int i = 1; i <=2 ; i++) {
MyTask myTask = new MyTask("客户" + i, 800);
pool.submit(myTask);
}
//3:关闭线程池
pool.shutdown();
}
}
练习源码
练习源码