您的位置:首页 > 科技 > 能源 > day12 多线程

day12 多线程

2024/12/23 6:17:16 来源:https://blog.csdn.net/zhangnobill/article/details/140855846  浏览:    关键词:day12 多线程

目录

1.概念相关

1.1什么是线程

1.2什么是多线程

2.创建线程

2.1方式一:继承Thread类

2.1.1实现步骤

2.1.2优缺点

2.1.3注意事项

2.2方式二:实现Runnable接口

2.2.1实现步骤

2.2.2优缺点

2.2.3匿名内部类写法

2.3方式三:实现callable接口

2.3.1callable接口解决了什么问题

2.3.2实现步骤

2.3.3FutureTask的API

2.3.4优缺点

3.线程的常用方法

4.线程安全

4.1什么是线程安全

4.2线程安全问题出现的原因

4.3模拟线程安全问题场景 :取钱

5.线程同步

5.1什么是线程同步

5.2线程同步的核心思想

5.3常见方案

5.4方式一:同步代码块

5.4.1作用:

5.4.2原理:

5.4.3注意事项:

5.4.4锁对象的使用规范 

5.4.5如何实现线程安全的

5.5方式二:同步方法

5.5.1作用

5.5.2原理

5.5.3底层原理

5.5.4同步代码块和同步方法哪种好

5.6lock锁

5.6.1lock锁是什么

5.6.2lock锁的构造器、常用方法

5.6.3锁对象建议加上什么修饰

5.6.4释放锁的操作建议放到哪里

6.线程池

6.1认识线程池

6.2不使用线程池的后果

6.3创建线程池

6.4任务拒绝策略

6.5线程池的注意事项

6.6处理runnable任务

6.4.1ExecutorService的常用方法

6.7处理callable

6.8通过Executors创建线程池

6.8.1方法

6.8.2Executors使用可能存在的陷阱

7.并发、并行

7.1进程

7.2并发的含义

7.3并行的含义


 

1.概念相关

1.1什么是线程

线程(Thread)是一个程序内部的一条执行流程。程序中如果只有一条执行流程,那这个程序就是单线程的程序。

1.2什么是多线程

多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)。

2.创建线程

2.1方式一:继承Thread类

2.1.1实现步骤

【1】定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法

【2】创建MyThread类的对象

【3】调用线程对象的start()方法启动线程(启动后还是执行run方法的)

2.1.2优缺点

【1】优点:编码简单

【2】缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展

2.1.3注意事项

【1】启动线程必须是调用start方法,不是调用run方法。

【2】不要把主线程任务放在启动子线程之前。

public class Test {public static void main(String[] args) {Thread t1=new MyThread();t1.start();for (int i = 0; i < 5; i++) {System.out.println("主线程"+i);}}}class MyThread extends Thread{@Overridepublic void run(){for (int i = 0; i < 10; i++){System.out.println("子线程" + i);}}
}

2.2方式二:实现Runnable接口

2.2.1实现步骤

【1】定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法

【2】创建MyRunnable任务对象

【3】把MyRunnable任务对象交给Thread处理

【4】调用线程对象的start()方法启动线程

2.2.2优缺点

【1】优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。

【2】缺点:需要多一个Runnable对象。

2.2.3匿名内部类写法

【1】可以创建Runnable的匿名内部类对象。

【2】再交给Thread线程对象。

【3】再调用线程对象的start()启动线程。

    public static void main(String[] args) {new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println("子线程"+i);}}}).start();new Thread(()->{for (int i = 0; i < 5; i++) {System.out.println("子线程2"+i);}}).start();for (int i = 0; i < 5; i++) {System.out.println("主线程"+i);}}

2.3方式三:实现callable接口

2.3.1callable接口解决了什么问题

假如线程执行完毕后有一些数据需要返回,前两种重写的run方法均不能直接返回结果。使用Callable接口和FutureTask类来实现创建,可以返回线程执行完毕后的结果。

2.3.2实现步骤

【1】定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。

【2】把Callable类型的对象封装成FutureTask(线程任务对象)。

【3】把线程任务对象交给Thread对象。

【4】调用Thread对象的start方法启动线程。

【5】线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果。

2.3.3FutureTask的API

FutureTask提供的构造器说明
public FutureTask<>(Callable call)把Callable对象封装成FutureTask对象。
FutureTask提供的方法说明
public V get() throws Exception获取线程执行call方法返回的结果。

2.3.4优缺点

【1】优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果。

【2】缺点:编码复杂一点。

public class ThreadDemo3 {public static void main(String[] args) {// 目标:掌握多线程的创建方式三:实现Callable接口,方式三的优势:可以获取线程执行完毕后的结果的。// 3、创建一个Callable接口的实现类对象。Callable<String> c1 = new MyCallable(100);// 4、把Callable对象封装成一个真正的线程任务对象FutureTask对象。/*** 未来任务对象的作用?*    a、本质是一个Runnable线程任务对象,可以交给Thread线程对象处理。*    b、可以获取线程执行完毕后的结果。*/FutureTask<String> f1 = new FutureTask<>(c1); // public FutureTask(Callable<V> callable)// 5、把FutureTask对象作为参数传递给Thread线程对象。Thread t1 = new Thread(f1);// 6、启动线程。t1.start();Callable<String> c2 = new MyCallable(50);FutureTask<String> f2 = new FutureTask<>(c2); // public FutureTask(Callable<V> callable)Thread t2 = new Thread(f2);t2.start();// 获取线程执行完毕后返回的结果try {// 如果主线程发现第一个线程还没有执行完毕,会让出CPU,等第一个线程执行完毕后,才会往下执行!System.out.println(f1.get());} catch (Exception e) {e.printStackTrace();}try {// 如果主线程发现第二个线程还没有执行完毕,会让出CPU,等第一个线程执行完毕后,才会往下执行!System.out.println(f2.get());} catch (Exception e) {e.printStackTrace();}}
}// 1、定义一个实现类实现Callable接口
class MyCallable implements Callable<String> {private int n;public MyCallable(int n) {this.n = n;}// 2、实现call方法,定义线程执行体public String call() throws Exception {int sum = 0;for (int i = 1; i <= n; i++) {sum += i;}return "子线程计算1-" + n + "的和是:"  + sum;}
}

3.线程的常用方法

Tread提供的常用方法说明
public void run()线程的任务方法
public void start()启动线程
public String getName()获取当前线程的名称,线程名称默认是Thread-索引
public void setName(String name)为线程设置名称
public static Thread currentThread()获取当前执行的线程对象
public static void sleep(long time)让当前执行的线程休眠多少毫秒后,再继续执行
public final void join()...让调用当前这个方法的线程先执行完!
public class ThreadApiDemo1 {public static void main(String[] args) {// 目标:搞清楚线程的常用方法。Thread t1 = new MyThread("1号线程");// t1.setName("1号线程");t1.start();System.out.println(t1.getName()); // 线程默认名称是:Thread-索引Thread t2 = new MyThread("2号线程");// t2.setName("2号线程");t2.start();System.out.println(t2.getName()); // 线程默认名称是:Thread-索引// 哪个线程调用这个代码,这个代码就拿到哪个线程Thread m = Thread.currentThread(); // 主线程m.setName("主线程");System.out.println(m.getName()); // main}
}// 1、定义一个子类继承Thread类,成为一个线程类。
class MyThread extends Thread {public MyThread(String name) {super(name); // public Thread(String name)}// 2、重写Thread类的run方法@Overridepublic void run() {// 3、在run方法中编写线程的任务代码(线程要干的活儿)for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread().getName() +"子线程输出:" + i);}}
}
public static void main(String[] args) {// 目标:搞清楚Thread类的Sleep方法(线程休眠)for (int i = 1; i <= 10; i++) {System.out.println(i);try {// 让当前执行的线程进入休眠状态,直到时间到了,才会继续执行。// 项目经理让我加上这行代码,如果用户交钱了,我就注释掉。Thread.sleep(1000); // 1000ms = 1s} catch (Exception e) {e.printStackTrace();}}}
public class ThreadApiDemo3 {public static void main(String[] args) {// 目标:搞清楚线程的join方法:线程插队:让调用这个方法线程先执行完毕。MyThread2 t1 = new MyThread2();t1.start();for (int i = 1; i <= 5; i++) {System.out.println(Thread.currentThread().getName() +"线程输出:" + i);if(i == 1){try {t1.join(); // 插队 让t1线程先执行完毕,然后继续执行主线程} catch (Exception e) {e.printStackTrace();}}}}
}class MyThread2 extends Thread {@Overridepublic void run() {for (int i = 1; i <= 5; i++) {System.out.println(Thread.currentThread().getName() +"子线程输出:" + i);}}
}

4.线程安全

4.1什么是线程安全

多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。

4.2线程安全问题出现的原因

【1】存在多个线程在同时执行

【2】同时访问一个共享资源

【3】存在修改该共享资源

4.3模拟线程安全问题场景 :取钱

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {private String cardId; // 卡号private double money; // 余额// 小明和小红都到这里来了取钱public synchronized void drawMoney(double money) {// 拿到当前谁来取钱。String name = Thread.currentThread().getName();// 判断余额是否足够if (this.money >= money) {// 余额足够,取钱System.out.println(name + "取钱成功,吐出了" + money + "元成功!");// 更新余额this.money -= money;System.out.println(name + "取钱成功,取钱后,余额剩余" + this.money + "元");} else {// 余额不足System.out.println(name + "取钱失败,余额不足");}}
}
// 取钱线程类
public class DrawThread extends Thread{private Account acc; // 记住线程对象要处理的账户对象。public DrawThread(String name, Account acc) {super(name);this.acc = acc;}@Overridepublic void run() {// 小明 小红 取钱acc.drawMoney(100000);}
}
    public static void main(String[] args) {// 目标:模拟线程安全问题。// 1、设计一个账户类:用于创建小明和小红的共同账户对象,存入10万。Account acc = new Account("ICBC-110", 100000);// 2、设计线程类:创建小明和小红两个线程,模拟小明和小红同时去同一个账户取款10万。new DrawThread("小明", acc).start();new DrawThread("小红", acc).start();}

5.线程同步

5.1什么是线程同步

线程同步是线程安全问题的解决方案

5.2线程同步的核心思想

让多个线程先后依次访问共享资源,这样就可以避免出现线程安全问题。

5.3常见方案

加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。

5.4方式一:同步代码块

5.4.1作用:

把访问共享资源的核心代码给上锁,以此保证线程安全。

5.4.2原理:

每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行

5.4.3注意事项:

对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。

5.4.4锁对象的使用规范 

【1】建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象。

【2】对于静态方法建议使用字节码(类名.class)对象作为锁对象。

5.4.5如何实现线程安全的

【1】对出现问题的核心代码使用synchronized进行加锁

【2】每次只能一个线程占锁进入访问

// 小明和小红都到这里来了取钱public void drawMoney(double money) {// 拿到当前谁来取钱。String name = Thread.currentThread().getName();// 判断余额是否足够synchronized (this) {if (this.money >= money) {// 余额足够,取钱System.out.println(name + "取钱成功,吐出了" + money + "元成功!");// 更新余额this.money -= money;System.out.println(name + "取钱成功,取钱后,余额剩余" + this.money + "元");} else {// 余额不足System.out.println(name + "取钱失败,余额不足");}}}

5.5方式二:同步方法

5.5.1作用

把访问共享资源的核心方法给上锁,以此保证线程安全。

5.5.2原理

每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行

5.5.3底层原理

【1】同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。

【2】如果方法是实例方法:同步方法默认用this作为的锁对象。

【3】如果方法是静态方法:同步方法默认用类名.class作为的锁对象。

5.5.4同步代码块和同步方法哪种好

同步代码块锁的范围更小,同步方法锁的范围更大。同步方法可读性更好

    // 小明和小红都到这里来了取钱public synchronized void drawMoney(double money) {// 拿到当前谁来取钱。String name = Thread.currentThread().getName();// 判断余额是否足够if (this.money >= money) {// 余额足够,取钱System.out.println(name + "取钱成功,吐出了" + money + "元成功!");// 更新余额this.money -= money;System.out.println(name + "取钱成功,取钱后,余额剩余" + this.money + "元");} else {// 余额不足System.out.println(name + "取钱失败,余额不足");}}

5.6lock锁

5.6.1lock锁是什么

Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。

5.6.2lock锁的构造器、常用方法

构造器说明
public ReentrantLock​()获得Lock锁的实现类对象
方法名称说明
void lock()获得锁
void unlock()释放锁

5.6.3锁对象建议加上什么修饰

建议使用final修饰,防止被别人篡改

5.6.4释放锁的操作建议放到哪里

建议将释放锁的操作放到finally代码块中,确保锁用完了一定会被释放

    private final Lock lk = new ReentrantLock(); // 保护锁对象// 小明和小红都到这里来了取钱public void drawMoney(double money) {// 拿到当前谁来取钱。String name = Thread.currentThread().getName();lk.lock(); // 上锁try {// 判断余额是否足够if (this.money >= money) {// 余额足够,取钱System.out.println(name + "取钱成功,吐出了" + money + "元成功!");// 更新余额this.money -= money;System.out.println(name + "取钱成功,取钱后,余额剩余" + this.money + "元");} else {// 余额不足System.out.println(name + "取钱失败,余额不足");}} finally {lk.unlock();// 解锁}}

6.线程池

6.1认识线程池

线程池就是一个可以复用线程的技术。

6.2不使用线程池的后果

用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的,  创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能。

6.3创建线程池

通过ThreadPoolExecutor创建线程池。使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。

【1】参数一:corePoolSize : 指定线程池的核心线程的数量。

【2】参数二:maximumPoolSize:指定线程池的最大线程数量。

【3】参数三:keepAliveTime :指定临时线程的存活时间。

【4】参数四:unit:指定临时线程存活的时间单位(秒、分、时、天)

【5】参数五:workQueue:指定线程池的任务队列。

【6】参数六:threadFactory:指定线程池的线程工厂。

【7】参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)

6.4任务拒绝策略

策略说明
ThreadPoolExecutor.AbortPolicy()丢弃任务并抛出RejectedExecutionException异常。是默认的策略
ThreadPoolExecutor. DiscardPolicy()丢弃任务,但是不抛出异常,这是不推荐的做法
ThreadPoolExecutor. DiscardOldestPolicy()抛弃队列中等待最久的任务 然后把当前任务加入队列中
ThreadPoolExecutor. CallerRunsPolicy()由主线程负责调用任务的run()方法从而绕过线程池直接执行

6.5线程池的注意事项

// 1、定义一个线程任务类实现Runnable接口
public class MyRunnable implements Runnable {// 2、重写run方法,设置线程任务@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread().getName() + "输出:" + i);try {Thread.sleep(Integer.MAX_VALUE);} catch (Exception e) {e.printStackTrace();}}}
}
    public static void main(String[] args) {// 目标:创建线程池对象来使用。// 1、使用线程池的实现类ThreadPoolExecutor声明七个参数来创建线程池对象。ExecutorService pool = new ThreadPoolExecutor(3, 5,10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3),Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());// 2、使用线程池处理任务!看会不会复用线程?Runnable target = new MyRunnable();pool.execute(target); // 提交第1个任务 创建第1个线程 自动启动线程处理这个任务pool.execute(target); // 提交第2个任务 创建第2个线程 自动启动线程处理这个任务pool.execute(target); // 提交第2个任务 创建第3个线程 自动启动线程处理这个任务pool.execute(target);pool.execute(target);pool.execute(target);pool.execute(target); // 到了临时线程的创建时机了pool.execute(target); // 到了临时线程的创建时机了pool.execute(target); // 到了任务拒绝策略了,忙不过来// 3、关闭线程池 :一般不关闭线程池。// pool.shutdown(); // 等所有任务执行完毕后再关闭线程池!
//        pool.shutdownNow(); // 立即关闭,不管任务是否执行完毕!}

6.6处理runnable任务

6.4.1ExecutorService的常用方法

方法名称说明
void execute(Runnable command)执行 Runnable 任务
Future<T> submit(Callable<T> task)执行 Callable 任务,返回未来任务对象,用于获取线程返回的结果
void shutdown()等全部任务执行完毕后,再关闭线程池!
List<Runnable> shutdownNow()立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务
        // 2、使用线程池处理任务!看会不会复用线程?Runnable target = new MyRunnable();pool.execute(target); // 提交第1个任务 创建第1个线程 自动启动线程处理这个任务pool.execute(target); // 提交第2个任务 创建第2个线程 自动启动线程处理这个任务pool.execute(target); // 提交第2个任务 创建第3个线程 自动启动线程处理这个任务pool.execute(target);pool.execute(target);pool.execute(target);pool.execute(target); // 到了临时线程的创建时机了pool.execute(target); // 到了临时线程的创建时机了pool.execute(target); // 到了任务拒绝策略了,忙不过来// 3、关闭线程池 :一般不关闭线程池。pool.shutdown(); // 等所有任务执行完毕后再关闭线程池!pool.shutdownNow(); // 立即关闭,不管任务是否执行完毕!

6.7处理callable

线程池如何处理Callable任务,并得到任务执行完后返回的结果?

Future<T> submit(Callable<T> command)

6.8通过Executors创建线程池

6.8.1方法

是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象

方法名称说明
public static ExecutorService newFixedThreadPool​(int nThreads)创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。
public static ExecutorService newSingleThreadExecutor()创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。
public static ExecutorService newCachedThreadPool()线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了60s则会被回收掉。
public static ScheduledExecutorService newScheduledThreadPool​(int corePoolSize)创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。

6.8.2Executors使用可能存在的陷阱

【1】大型并发系统环境中使用Executors如果不注意可能会出现系统风险。

【2】不适合做大型互联网场景的线程池方案

【3】建议使用ThreadPoolExecutor来指定线程池参数,这样可以明确线程池的运行规则,规避资源耗尽的风险。

7.并发、并行

7.1进程

【1】正在运行的程序(软件)就是一个独立的进程。

【2】线程是属于进程的,一个进程中可以同时运行很多个线程。

【3】进程中的多个线程其实是并发和并行执行的。

7.2并发的含义

进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。

7.3并行的含义

在同一个时刻上,同时有多个线程在被CPU调度执行。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com