多线程:一项用于改进性能的技术。
概念及其作用:
概念: 多线程是指从软硬件上实现多条执行流程的技术。
作用和好处: 改进性能,意味着用更少的资源做更多的事情。
创建多线程的方式:
方式一:通过继承Thread类
优点: 编码简单
缺点: 线程类已经继承了Thread类无法继承其他类,不利于扩展
代码实现以及代码中的注意事项,会以注释的方式在代码中体现。
1.定义一个线程类去继承Thread类,重写run方法来说明让这个类去做什么事
//1.定义一个线程类继承Thread
public class MyThread extends Thread{
//2.重写run方法,里面定义线程做什么
//这里我们让这个线程去输出5次
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程执行输出数字:" + i);
}
}
}
2.在主方法中创建新的线程并且调用start方法
public class threadDemo1{
public static void main(String[] args) {
//3.new一个新的线程对象(这里用的是多态的写法)
Thread thread = new MyThread();
//4.调用start方法启动线程(执行的还是run方法)
//为什么不用run方法?
//如果调用run方法,主方法就会当run为一个普通方法,无法启动多线程,
//执行完成run方法后才会继续向下执行
//start方法是告诉我们操作系统我这又开了一条新的线程
thread.start();
//不要把主线程的任务放到子线程的前面去
//如果放到前面了,操作系统就会认为只有一个线程,主线程这时永远都会先跑完
//这里是主线程的任务
for (int i = 0; i < 5; i++) {
System.out.println("主线程输出数字:" + i);
}
}
}
执行结果:主线程与子线程同时进行
方式二:通过实现Runnable接口的方法
优点: 线程任务类只是实现了接口,可以继续继承类和实现接口,扩展性强
缺点: 编程多一层对象包装,如果线程有执行是不可以直接返回的
1.实现Runnable接口,并重写其中的run方法
//1.实现Runnable接口,并重写接口中的run方法
public class MyRunnable implements Runnable{
//重写run方法,这里的run方法中也是定义子线程执行的任务
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程输出数字:" + i);
}
}
}
2.创建任务对象,并且将任务对象交给线程对象启动start方法
public class ThreadDemo2 {
public static void main(String[] args) {
//2.创建一个任务对象
Runnable runnable = new MyRunnable();
//3.把runnable任务对象交给Thread处理
//这里需要通过Thread的构造器新创建一个线程对象并且调用start方法
Thread thread = new Thread(runnable);
//4,通过线程对象调用start方法
thread.start();
//这里是主线程执行的任务
for (int i = 0; i < 5; i++) {
System.out.println("主线程输出数字:" + i);
}
}
}
方式二的另一种写法(匿名内部类的方式)
这种方法就是方式二的变式,用了匿名内部类与lambda表达式
public class ThreadDemo2_other {
public static void main(String[] args) {
//1.创建匿名内部类的对象来重写run方法
Thread thread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("子线程输出数字:" + i);
}
});
//2.将任务对象交给线程对象,并且调用start方法
thread.start();
//主线程执行的任务
for (int i = 0; i < 5; i++) {
System.out.println("主线程输出数字:" + i);
}
}
}
方式三:JDK5.0新增:实现Callable接口
优点: 线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。可以在线程执行完毕后获取线程执行的结果
缺点: 编码复杂
1.定义任务类实现Callable接口
import java.util.concurrent.Callable;
/**
* 1.定义一个任务类实现Callable接口,应该声明线程任务执行完毕后的结果的数据类型
*/
//1.创建一个Callable的实现类,并且重写call方法
public class MyCallable implements Callable<String> {
private int n;
public MyCallable(int n) {
this.n = n;
}
@Override
public String call() throws Exception {
int sum = 0;
//重写call方法(线程的任务方法)
//这个任务为计算1-n的和
for (int i = 0; i < n; i++) {
sum += i;
}
return "子线程执行的结果是" + sum;
}
}
2.通过FutureTask对象将任务对象交给线程对象并启动任务
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadDemo3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//3.创建Callable任务对象
Callable<String> call = new MyCallable(100);
//4.将callable任务对象交给(封装为)FutureTask对象
//FutureTask继承Runnable,属于Runnable的对象
//因为Runnable对象能够交给线程对象,所以这里用FutureTask
//作用:是Runnalbe的对象兑现更可以交给Thread
// 可以在线程执行完毕之后通过调用其get方法得到线程的结果
FutureTask<String> futureTask = new FutureTask<>(call);
//5.交给线程对象,并且调用start方法
Thread thread = new Thread(futureTask);
//6.调用start方法让线程启动
thread.start();
//通过FutureTask对象的get方法来返回结果
System.out.println(futureTask.get());
for (int i = 0; i < 5; i++) {
System.out.println("主线程输出的数字为" + i);
}
}
}
多线程中常用的方法
这里setName() getName run() start() 方法不做演示
1.currentThread()方法
方法说明:可返回代码段正在被哪个线程调用的信息
public class Run{
public static void main(String[] args){
//调用currentThread()方法输出当前线程名称
System.out.println(Thread.currentThread().getName());
}
}
2.sleep(long time)方法
方法说明:让当前线程休眠指定的时间后再继续执行,单位为毫秒
import java.util.Date;
//这个例子用主线程任务来实现方法
public class ThreadSleepDemo {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
System.out.println("输出:" + i + new Date());
if (i == 3){
Thread.sleep(5000);
}
}
}
}
观察执行结果的时间可以发现,线程休眠了5秒
线程安全(了解就可以)
线程安全问题: 当多个线程同时操作同一个线程的时候可能会出现业务安全问题,称为线程安全问题。
例如取钱问题,如果发生线程安全问题,银行就会亏损等
线程同步:解决线程安全的法宝
方式一:同步代码块synchronized
作用: 把出现线程安全问题的核心代码给上锁
原理: 每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行
锁对象要求: 理论上,锁对象只要对于当前同时执行的线程来说是同一个对象即可,
规范上:建议使用共享资源作为锁对象。
同步代码块是如何实现线程安全的?
对于出现问题的核心代码使用synchronized进行加锁
每次之恩那个一个线程占锁进入访问
同步代码块的同步锁对象有什么要求?
对于实例方法建议使用this作为锁对象
对于静态方法建议使用字节码(类名.class)作为锁对象
public class SynchronizedDemo {
private double money;
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
//取钱业务
public void drawMoney(double money) {
//同步代码块
//this == 共享账户
synchronized (this) {
//1.判断账户是否够钱
if (this.getMoney() >= money){
//2.取钱
System.out.println("取钱成功,取出" + money);
this.money -= money;
}else {
System.out.println("余额不足");
}
}
}
}
方式二:同步方法
作用: 把出现线程安全问题的核心方法给上锁
原理: 每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行
底层原理: 同步方法其实底层也是有隐式锁对象的,只是所得范围是整个方法代码
//取钱业务
//使用同步方法,给方法加上synchronized关键字
public synchronized void drawMoney(double money) {
//1.判断账户是否够钱
if (this.getMoney() >= money){
//2.取钱
System.out.println("取钱成功,取出" + money);
this.money -= money;
}else {
System.out.println("余额不足");
}
}
方式三:Lock锁
JDK1.5后新增新一代的线程同步方式与采用synchronized相比,lock可提供多种锁方案,更灵活
//1.首先要拿来一把锁,创建锁对象
//final修饰后:锁对象是唯一和不可替换的
private final Lock lock = new ReentrantLock();
public void drawMoney(double money) {
//这里我们用try..finally...的方式,无论程序是否出错最后都把锁解开
try {
lock.lock();//2.上锁
//1.判断账户是否够钱
if (this.getMoney() >= money){
//2.取钱
System.out.println(name + "来取钱成功,吐出" + money);
this.money -= money;
}else {
System.out.println("余额不足");
}
} finally {
lock.unlock();//3.解锁
}
}
线程池(重点)
概念及其优势
概念: 线程池就是一个可以复用线程的技术
优势:
- 降低资源消耗:线程和任务分离,提高线程重用性
- 控制线程并发数量,降低服务器压力,统一管理所有线程
- 提高系统响应速度。假如创建线程用的时间为T1,执行任务的时间为T2,销毁线程的时间为T3,那么使用线程池就免去了T1和T3的时间。
如何获得线程池对象
通过线程池的实现类ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
类中参数的说明:
corePoolSize: 核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
maximumPoolSize: 线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
keepAliveTime: 线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
unit: 指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
workQueue: 任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
threadFactory: 线程工厂。用于指定为线程池创建新线程的方式。
handler: 拒绝策略。当达到最大线程数时需要执行的饱和策略。
线程池的使用
线程池执行Runnable任务
public class ThreadPoolDemo1 {
public static void main(String[] args) {
//1.创建线程池对象(多态)
ExecutorService threadPool = new ThreadPoolExecutor(3,10,
5, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
//2.给任务线程池处理
Runnable target = new MyRunnable();
//3.将任务通过线程池对象中的execute方法交给线程池执行任务
threadPool.execute(target);
}
// 关闭线程池
threadPool.shutdown(); // 立即关闭,即使任务没有完成,会丢失任务
threadPool.shutdownNow(); // 会等待全部任务执行完毕之后再关闭任务
}
线程池执行Callable任务
public class ThreadPoolDemo2 {
public static void main(String[] args) {
//1.创建线程池对象(多态)
ExecutorService threadPool = new ThreadPoolExecutor(3,10,
5, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
//2.将callable任务通过线程池对象的submit方法交给任务线程池处理
threadPool.submit(new MyCallable(100));
}
}
Executors工具类创建线程池对象
注意: Executors的底层其实也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的
Executors使用可能存在陷阱
大型并发系统环境中使用Executors如果不注意可能会出现系统风险
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//通过Executors工具类来创建线程池对象
public class ExecutorsPoolDemo {
public static void main(String[] args) {
//1.创建固定线程数据的线程池
ExecutorService pool = Executors.newFixedThreadPool(3);
//2.再通过线程池对象的execute方法可以提交执行Runnable任务
// 通过线程池对象的submit方法可以提交执行Callable任务
}
}
定时器 Timer ScheduledExecutorService
概念: 定时器是一种控制任务延时调用,或者周期调用的技术。
作用: 闹钟,定时邮件发送。
实现方式一:Timer
Timer定时器的特点和存在的问题:
- Timer是单线程,处理多个任务按照顺序执行,存在延时设置定时器的时间有出入
- 可能因为其中的某个任务的异常使Timer线程死掉,从而影响后续任务执行
Timer定时器的使用
import java.util.Timer;
import java.util.TimerTask;
public class TimerDemo1 {
public static void main(String[] args) {
//1.创建Timer定时器
Timer timer = new Timer();
//2.调用方法,处理定时任务(这里用了一个匿名内部类的方式创建一个延时任务)
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行一次");
}
},3000,2000);
}
}
实现方式二:ScheduledExecutorService
ScheduledExecutorService的优点:
- 基于线程池,某个任务的执行情况不会影响其他定时执行任务的执行
ScheduledExecutorService执行定时任务
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledExecutorServiceDemo {
public static void main(String[] args) {
//1.创建ScheduledExecutorService线程池,做定时器
ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
//2.开启定时任务
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
},3,2, TimeUnit.SECONDS);
}
}