Java线程与多线程
1.线程的定义
1.1概述
1)进程Process:是执行程序的一次执行过程。
2)线程Thread:是CPU调度和执行的单位。main()为主线程,为系统的主入口,用于执行整个程序。在程序运行的过程中,即使没有手动创建线程,后台也会有多个线程(如主线程,gc线程)。
3)进程与线程的区别:
在一个进程中包含若干个线程,但一个进程中至少包含一个线程。线程的运行由调度器安排调度,其先后顺序不能人为干预。
多线程就多个线程同时执行的过程。
1.2线程的状态
线程共有5大状态,可通过getState()方法获取当前线程的状态。如下图
1)新建状态(New):新创建了一个线程对象
2)就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法,就会进入就绪状态,但此时并不意味着会立即执行
3)运行状态(Running):就绪状态的线程获取了CPU使用权,在执行线程中的代码块
4)阻塞状态(Blocked):线程因为某种原因放弃CPU使用权(如调用sleep,wait或同步锁定),暂时停止运行。
5)死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。死亡状态的线程无法再次使用,需要重新创建。在程序中需要停止线程时,建议使用标志变量让线程自己停止。
1.3线程状态变化的几种方式
1)线程休眠:sleep(毫秒)
指定当前线程阻塞的时间,单位是毫秒。执行完sleep后的线程会进入就绪状态,可用于倒计时及延时等场景。每一个对象都有一个锁,但sleep不会释放锁。
Thread.sleep(2000);
上述代码是让当前线程休眠2秒。
2)线程礼让:yield
让当前正在执行的线程暂停,进入就绪状态,让CPU重新调度。
Thread.yield();
上述代码是让当前线程礼让,但不一定礼让成功。原因是此转入就绪状态的线程也同样可以再次被CPU进行调度。
3)线程插队:join
当前线程A在运行过程中,另一个的线程B插队进来执行,B执行完成后当前线程A再继续执行。
1.4线程的优先级
在Java中提供了一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照线程的优先级来调度谁先执行。优先级使用数据表示,范围在1~10。
线程的最小优先级是Thread.MIN_PRIORITY=1,最大优先级是Thread.MAX_PRIORITY=10,默认的优先级是Thread.NORM_PRIORITY=5。通过getPriority()获取优先级,通过setPriority()设置优先级。
当然不一定优先级越高就越先执行。如main线程的优先级是5,其会先执行,再按优先级执行。
2.线程的创建方式
2.1 继承Thread
2.1.1 创建线程的步骤
1)自定义类继承Thread类
2)重写run方法,编写线程执行体
3)在main()方法中创建自定义的线程对象,调用start()方法启动线程
2.1.2 创建的代码
package com.zys.example; public class MyThread1 extends Thread { @Override public void run() { for (int i = 1; i <= 200; i++) { System.out.println("我是自定义创建的线程1:" + i); } } public static void main(String[] args) { MyThread1 thread1 = new MyThread1(); thread1.start(); for (int i = 1; i <= 1000; i++) { System.out.println("我是主线程:" + i); } } }
分析:执行main方法后,观察打印结果,发现main方法和自定义的线程打印的结果是交叉的,说明main方法运行的过程中自定义的线程也在运行,也就是说这两个线程是并行交替执行的。
注意:线程开启后不一定会立即执行,而是由CPU调度执行的。
2.2 实现Runnable接口
2.2.1 创建线程的步骤
1)自定义类实现Runnable类
2)重写run方法,编写线程执行体
3)在main()方法中创建线程对象,传入Runnable的实现类对象,调用start()方法启动线程
2.2.2 创建的代码
package com.zys.example; public class MyThread2 implements Runnable { @Override public void run() { for (int i = 1; i <= 200; i++) { System.out.println("我是自定义创建的线程2:" + i); } } public static void main(String[] args) { Thread thread = new Thread(new MyThread2()); thread.start(); for (int i = 1; i <= 1000; i++) { System.out.println("我是主线程:" + i); } } }
分析:执行main方法后,观察打印结果大概和实现Thread类的线程打印结果类似。
3.线程并发与同步问题
3.1线程并发的案例
1)买火车票案例
package com.zys.example; public class MyThread3 implements Runnable { private Integer ticketNums = 10; @Override public void run() { while (ticketNums > 0) { try { //模拟延迟操作,相当于网络延时 Thread.sleep(200); //买票。票数减少 System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNums-- + "张票"); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { MyThread3 myThread3 = new MyThread3(); Thread thread = new Thread(myThread3, "小红"); Thread thread2 = new Thread(myThread3, "小李"); Thread thread3 = new Thread(myThread3, "小张"); thread.start(); thread2.start(); thread3.start(); } }
执行后会发现,总会有人取到第0张票,这是不符合正常逻辑的。
2)Arraylist加元素案例
package com.zys.example; import java.util.ArrayList; import java.util.List; public class MyThread4 { public static void main(String[] args) throws InterruptedException { List<String> list = new ArrayList<>(); for (int i = 0; i < 10000; i++) { //动态把线程名称加入集合 new Thread(() -> list.add(Thread.currentThread().getName())).start(); } Thread.sleep(5000); System.out.println(list.size()); } }
此案例是动态创建1万个线程,并把线程名称加入集合。但在运行时,发现集合中的元素个数并不一定都是1万,也有少于一万的结果。
3.2问题分析
在多个线程同时操作同一个对象时,由于线程的执行顺序是由CPU决定的,不能为人干预,因此是线程不安全的,会造成数据混乱。有效的解决方案就是使用线程同步。
3.3线程同步
线程同步:实际是一种等待机制,多个需要同时访问同一个对象的线程进行此对象的等待池形成队列并加锁,当前面的线程执行完成释放此对象锁,下一个线程才能使用,从而保证数据的安全性。
用法很简单,加入同步块synchronized(obj){}即可,当然也可以直接对方法锁定。其中obj是需要被监视的对象,对象的操作放到块内部。当第一个线程访问时,锁定同步监视器,执行块中代码;当第二个线程访问时,发现同步监视器被锁定,无法访问。当第一个线程访问完毕后第二个线程按照线程一的方式访问并锁定同步监视器。
1)对买票案例加入同步锁
package com.zys.example; public class MyThread3 implements Runnable { private Integer ticketNums = 10; @Override public void run() { //加入同步锁 synchronized (ticketNums) { while (ticketNums > 0) { try { //模拟延迟操作,相当于网络延时 Thread.sleep(200); //买票。票数减少 System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNums-- + "张票"); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) { MyThread3 myThread3 = new MyThread3(); Thread thread = new Thread(myThread3, "小红"); Thread thread2 = new Thread(myThread3, "小李"); Thread thread3 = new Thread(myThread3, "小张"); thread.start(); thread2.start(); thread3.start(); } }
2)买票案例优化(推荐使用)
当然,也可以将锁加在方法上,对上述代码进行改造
package com.zys.example; public class MyThread3 implements Runnable { private Integer ticketNums = 10; //线程停止的标志 private Boolean flag = true; @Override public void run() { while (flag) { buy(); } } //加入同步锁 private synchronized void buy() { try { if (ticketNums <= 0) { flag = false; return; } //模拟延迟操作,相当于网络延时 Thread.sleep(200); //买票。票数减少 System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNums-- + "张票"); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { MyThread3 myThread3 = new MyThread3(); Thread thread = new Thread(myThread3, "小红"); Thread thread2 = new Thread(myThread3, "小李"); Thread thread3 = new Thread(myThread3, "小张"); thread.start(); thread2.start(); thread3.start(); } }
将买票的动作抽成一个方法,对此方法进行上锁。另外,使用了自定义的标志来终止线程,从而保证了线程的高效性。
3)对ArrayList进行同步锁
package com.zys.example; import java.util.ArrayList; import java.util.List; public class MyThread4 { public static void main(String[] args) throws InterruptedException { List<String> list = new ArrayList<>(); for (int i = 0; i < 10000; i++) { //动态把线程名称加入集合 new Thread(() -> { synchronized (list) { list.add(Thread.currentThread().getName()); } }).start(); } Thread.sleep(5000); System.out.println(list.size()); } }
ArrayList本身就是不安全的,故多个线程对其操作时,必填使用同步锁。
3.4 Lock锁
JDK也提供了一种线程同步机制,显示的定义同步锁对象来实现同步。使用ReentrantLock(其实现了Lock接口)对对象加锁,和synchronized具有相同的并发性。
对上述的买票案例进行显示加锁:
package com.zys.example; import java.util.concurrent.locks.ReentrantLock; public class MyThread3 implements Runnable { private Integer ticketNums = 10; //线程停止的标志 private Boolean flag = true; //定义锁 private final ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (flag) { buy(); } } private void buy() { try { //加锁 lock.lock(); if (ticketNums <= 0) { flag = false; return; } //模拟延迟操作,相当于网络延时 Thread.sleep(200); //买票。票数减少 System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNums-- + "张票"); } catch (InterruptedException e) { e.printStackTrace(); } finally { //释放锁,一般均在finally中释放 lock.unlock(); } } public static void main(String[] args) { MyThread3 myThread3 = new MyThread3(); Thread thread = new Thread(myThread3, "小红"); Thread thread2 = new Thread(myThread3, "小李"); Thread thread3 = new Thread(myThread3, "小张"); thread.start(); thread2.start(); thread3.start(); } }
先定义锁,然后在需要操作的对象前加锁,操作完成后释放锁。
4.线程池
在使用多个线程时,频繁的创建、销毁线程,其实是比较耗费资源的,那么便可以提前创建好多个线程,放入一个池子中,使用时直接从池中获取,使用完毕后再放入池中,实现重复利用,节省资源。
4.1在SpringBoot中使用线程池
1)线程池配置
使用线程池时,需先配置线程池的主要参数。同时也需要添加注解 @EnableAsync 来开启线程池异步
package com.zys.example.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; import java.util.concurrent.ThreadPoolExecutor; /** * @Auther: zxh * @Date: 2022/3/20 09:17 * @Description: 配置线程池 */ @Configuration @EnableAsync public class TaskExecutePool { //核心线程数 private static final Integer CORE_POOL_SIZE = 20; //最大线程数 private static final Integer MAX_POOL_SIZE = 20; //缓存队列容量 private static final Integer QUEUE_CAPACITY = 200; //线程活跃时间(秒) private static final Integer KEEP_ALIVE = 60; //默认线程名称前缀 private static final String THREAD_NAME_PREFIX = "MyExecutor-"; @Bean("MyAsyncTaskExecutor") public Executor myTaskAsyncPool() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(CORE_POOL_SIZE); executor.setMaxPoolSize(MAX_POOL_SIZE); executor.setQueueCapacity(QUEUE_CAPACITY); executor.setKeepAliveSeconds(KEEP_ALIVE); executor.setThreadNamePrefix(THREAD_NAME_PREFIX); //拒绝策略 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } }
2)使用线程池
在需要使用线程池的方法上加上注解@Async,但需要指定线程池注入Spring时Bean的名称。
先创建接口,用于访问:
package com.zys.example.controller; import com.zys.example.service.TestService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api") @Slf4j public class TestController { @Autowired private TestService testService; @GetMapping("/test") public void test() { for (int i = 0; i < 100; i++) { testService.test(i); } } }
创建实现类,使用线程池:
package com.zys.example.service; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @Service @Slf4j public class TestService { @Async("MyAsyncTaskExecutor") public void test(Integer i) { if (i % 2 == 0) { log.info("是偶数"); } else { log.info("是奇数"); } } }
此接口及实现类的作用是使用线程池判断100内的数字是奇数还是偶数。
注意事项(@Async注解不生效的原因):
①在使用@Async注解时,需要指定配置线程池时注入Spring的Bean名称,否则不会使用配置的线程池。
②调用此注解修饰的方法时,必须在另一个类中调用,不能在声明此方法的类中调用。原因是此注解的底层使用的是AOP动态代理,调用的是代理对象中增加的方法。如果在本类中调用,相当于调用的是this的方法,并不是代理对象的方法。
③此方法返回值不是void或者Future。在源码的注释上已经说明。
④方法不被public修饰。上面说到是使用AOP,那么必须使用public来修饰方法。
⑤方法不能用static/final修饰。被static/final修饰不能被重写,而AOP可能会重写方法。
⑥异步的类要注入spring(添加@Service、@Component注解)
⑦不能使用new的方法调用。原因是自己new的对象,不会被Spring管理。
3)测试
启动项目,访问http://localhost:8080/api/test,即可看到控制台打印的结果,其中线程的名称是配置类中定义的。