Java中的多线程
1、程序、进程、线程的概念:
程序:是为完成特定任务、用某种程序语言编写的一组指令的集合。即:一段静态的代码,静态对象。
进程:是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程。有它自身的产生、存在和消亡的过程(生命周期)。
线程:是进程的进一步细化,是一个程序内部的某一条执行路径。
2、单核与多核CPU的任务执行-并行与并发:
单核CPU:是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。
多核CPU:才能更好的发挥多线程的效率。
注:一个Java程序中,其实至少有3个线程:主线程(main())、垃圾回收线程(gc())、异常处理线程。若发生异常,会影响主线程。
并行:多个CPU同时执行多个任务。
并发:一个CPU(采用时间片)同时执行多个任务。
3、多线程的优点:
(1)提高应用程序的响应。对图形化界面更有意义。可增强用户体验。
(2)提高计算机系统CPU的利用率。
(3)改善程序结构,将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
(4)何时需要多线程:
① 程序需要同时执行2个或多个任务;
② 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等;
③ 需要一些后台运行的程序时。
4、创建多线程方式:
(1)继承Thread类:
① 创建一个继承于Thread类的子类;
② 重写Thread类的run()方法,将此线程执行的操作声明在run()方法中;
③ 创建Thread类的子类对象;
④ 通过此对象调用start()方法。
start()方法作用:① 启动当前线程;② 调用当前线程的run()方法。
注意:创建多线程过程中的问题:
① 不能通过直接调用run()方法的方式启动线程。
② 再启动一个线程时,不能让已经start(开启)的线程去执行,会报 IllegalThreadStateException 异常。此时,需要重新创建一个线程的对象。
另外,可以通过创建匿名子类的方式创建两个不同任务的线程:
new thread(){ //重写run方法 @Override public void run(){ //需要执行的代码 } }.start();
(2)实现Runnable接口:
① 创建一个实现了Runnable接口的类;
② 实现类去实现Runnable接口中的抽象方法:run()方法;
③ 创建实现类的对象;
④ 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象;
⑤ 通过thread类的对象调用start()方法。
实例如下:
实现类:MyThread.java
//创建一个实现了Runnable接口的类,并实现Runnable接口中的抽象run()方法 public class MyThread implements Runnable{ @Override public void run() { for (int i = 0; i < 100; i++) { if (i %2 == 0) { System.out.println(i); } } } }
测试类:TestMyThread.java
public class TestMyThread { public static void main(String[] args) { //创建实现类的对象 MyThread mt = new MyThread(); //将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象 Thread t1 = new Thread(mt); //通过Thread类的对象调用start()方法:① 启动线程;②调用当前线程的run() → 调用了Runnable类型的target t1.start(); } }
(3)线程的两种创建方式对比:
开发中,优先选择使用 实现Runnable接口 的方式。原因:
① 实现的方式没有类的单继承的局限性;
② 实现的方式更适合来处理多个线程有共享数据的情况。
联系:public class Thread implements Runnable
相同点:两种方式都需要重写run()方法,将线程要执行的逻辑声明在run()方法中。
5、线程的常用方法:
① start():启动当前线程,调用当前线程的run()方法。
② run():通常需要重写类中的此方法,将创建的线程的要执行的操作,声明在此方法中。
③ currentThread():静态方法,返回执行当前代码的线程。
④ getName():获取当前线程名称。
⑤ setName():设置当前线程名称。
⑥ yield():释放当前CPU的执行权。
⑦ join():在线程A中调用线程B的join()方法,此时线程A就进入阻塞状态,直到线程B完全执行完以后,线程A才结束阻塞状态。
⑧ stop():(已过时)当执行此方法时,强制结束当前线程。
⑨ sleep(long millitime):让当前线程“休眠”指定的millitime(毫秒数)。在指定的millitime(毫秒)时间内,当前线程是阻塞状态。
⑩ isAlive():判断当前线程是否存活。
6、线程优先级的设置:
(1)优先级划分:
最大优先级(MAX_PRIORITY):10
最小优先级(MIN_PRIORITY):1
默认优先级(NORM_PRIORITY):5
(2)获取和设置当前线程的优先级:
getPriority():获取线程的优先级
setPriority(int p):设置线程的优先级
说明:高优先级的线程要抢占低优先级的线程的CPU执行权。只是从概率上来讲,高优先级的线程有很大几率会被先执行。但并不意味着,只有当高优先级的线程完全执行完后,低优先级的线程才执行。
7、线程的生命周期:
① 新建:当一个thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
② 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源。
③ 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能。
④ 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态。
⑤ 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束。
8、线程安全问题:
(1)场景举例:卖票过程中,出现了重票、错票,此时就是线程安全问题。
(2)出现原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
(3)如何解决:当一个线程A在操作ticket的时候,其他线程不能参与进来。直到线程A操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程A出现了阻塞,也不能被改变。
(4)Java中解决线程安全问题:
① 方式1:同步(synchronized)代码块
synchronized(同步监视器){ //需要被同步的代码 }
说明:
Ⅰ.操作共享数据的代码,即为需要被同步的代码(不能包含代码过多,也不能包含代码过少)。
Ⅱ.共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
Ⅲ.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。(要求:多个线程必须要共用同一把锁。)
补充:
Ⅰ.在 “实现Runnable接口创建多线程” 的方式中,可以考虑使用 this 充当同步监视器。
Ⅱ.在 “继承Thread类创建多线程” 的方式中,慎用 this 充当同步监视器,考虑使用当前类充当同步监视器。
② 方式2:同步方法
含义:如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步方法。
权限修饰符 synchronized 返回值类型 方法名(){ //需要被同步的代码 }
总结:
Ⅰ.同步方法仍然涉及到同步监视器,只是不需要我们显示的声明;
Ⅱ.非静态的同步方法,同步监视器是:this;
Ⅲ.静态的同步方法,同步监视器是:当前类本身。
(5)线程同步方式的优缺点:
优点:同步的方式,解决了线程安全的问题;
缺点:操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。
(6)线程安全的单例模式之懒汉式:
使用同步机制将单例模式中的懒汉式改为线程安全的方式(关键代码如下):
方法1:使用同步方法
class Bank{ private Bank(){}; private static Bank instance = null; //----方法1:直接添加 “synchronized” 变为同步方法 public static synchronized Bank getInstance(){ if (instance == null){ instance = new Bank(); } return instance; } }
方法2:使用同步代码块
class Bank{ private Bank(){}; private static Bank instance = null; public static Bank getInstance(){ //----方法2:在原方法体内添加同步代码块 //-------方法2的方式①:效率稍差------ // synchronized (Bank.class) { // if (instance == null) { // instance = new Bank(); // } // return instance; // } //-------方法2的方式②:效率更高------ if (instance == null){ synchronized (Bank.class) { if (instance == null) { instance = new Bank(); } } } return instance; } }
(7)线程的死锁问题:
理解:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
说明:
① 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续;
② 我们使用同步时,要避免出现死锁。
解决方法:
① 专门的算法、原则;
② 尽量减少同步资源的定义;
③ 尽量避免嵌套同步。
(8)使用Lock锁(接口)方式解决线程安全问题(JDK5.0新增):
实现代码如下:
package com.xrf.java; import java.util.concurrent.locks.ReentrantLock; /** *@author XiaoRuoFeng *@create 2022-03-22 17:05 */ public class LockTest { public static void main(String[] args) { Window w = new Window(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } } class Window implements Runnable{ private int ticket = 100; //使用Lock接口的实现类:ReentrantLock 来制造锁 private ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (true){ try{ //调用锁定方法:lock() lock.lock(); if(ticket>0){ try{ Thread.sleep(100); }catch(InterruptedException e){ e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":售票,票号为:"+ ticket); ticket--; }else{ break; } }finally{ //调用解锁方法:unlock() lock.unlock(); } } } }
面试题1:Synchronized 与 Lock 的异同?
相同:二者都可以解决线程安全问题。
不同:Synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器,而Lock需要手动的启动同步(lock()方法)监视器 和 结束同步(unlock()方法)监视器。
★★★建议使用顺序:Lock → 同步代码块(已经进入了方法体,分配了相应资源) → 同步方法(在方法体之外)
面试题2:如何解决线程安全问题?有几种方式?
Lock(锁)、同步代码块、同步方法(同步代码块、同步方法都属于 Synchronized 方法)
9、线程的通信(即:多个线程交替使用):
(1)涉及到的3个方法:
wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
(2)说明:
1.wait()、notify()、notifyAll()三个方法必须使用在同步代码块或同步方法中。
2.wait()、notify()、notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则会出现IllegalMonitorStateException异常。
3.wait()、notify()、notifyAll()三个方法是定义在java.lang.Object类中。
面试题3:sleep() 方法 和 wait() 方法的异同?
相同点:一旦执行此方法,都可以使得当前的线程进入阻塞状态。
不同点:
-
声明的位置不同:Thread类中声明sleep()方法,Object类中声明wait()方法;
-
调用的要求不同:sleep()方法可以在任何需要的场景下调用。wait()方法必须使用在同步代码块或同步方法中;
-
关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()方法不会释放锁,wait()方法会释放锁。
(3)经典例题:生产者/消费者问题
10、创建多线程的方式3 — 实现Callable接口(JDK5.0新增):
与使用Runnable接口相比,Callable接口功能更强大:
① 相比run()方法,call()方法可以有返回值;
② call()方法可以抛出异常,被外面的操作捕获,获取异常信息;
③ Callable是支持泛型的返回值;
④ 需要借助FutureTask类,比如:获取返回结果。
具体步骤:
(1)创建一个实现 Callable 接口的实现类;
(2)实现call()方法,将此线程需要执行的操作声明在call()方法中;
(3)创建callable接口实现类对象;
(4)将此Callable接口实现类对象作为参数,传递到FutureTask构造器中,创建FutureTask的对象;
(5)将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法;
(6)获取Callable中call()方法的返回值。
实现代码如下:
package com.xrf.java; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; /** *@author XiaoRuoFeng *@create 2022-03-24 14:33 */ public class CallableTest { public static void main(String[] args) { //3.创建callable接口实现类对象 NumThread numThread = new NumThread(); //4.将此Callable接口实现类对象作为参数,传递到FutureTask构造器中,创建FutureTask的对象 FutureTask futureTask = new FutureTask(numThread); //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法 new Thread(futureTask).start(); try{ //6.获取Callable中call()方法的返回值 //get()方法返回值 即为:FutureTask构造器参数Callable实现类重写的call()方法的返回值 Object sum = futureTask.get(); System.out.println("总和为:"+sum); }catch (InterruptedException e){ e.printStackTrace(); }catch (ExecutionException e){ e.printStackTrace(); } } } //1.创建一个实现 Callable 接口的实现类 class NumThread implements Callable{ //2.实现call()方法,将此线程需要执行的操作声明在call()方法中 @Override public Object call() throws Exception{ int sum = 0; for (int i = 1; i <= 100 ; i++){ if (i % 2 == 0){ System.out.println(i); sum += i; } } return sum; } }
11、创建多线程的方式4 — 线程池(JDK5.0新增):
(1)使用线程池优点:
① 提高响应速度:减少了创建新线程的时间
② 降低资源消耗:重复利用线程池中线程,不需要每次都创建
③ 便于线程管理:
corePoolSize:核心池的大小
maxmumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止
......
(2)创建线程池:
代码如下:
package com.xrf.java; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; /** *@author XiaoRuoFeng *@create 2022-03-24 15:15 */ public class ThreadPool { public static void main(String[] args) { //1.提供指定线程数量的线程池 ExecutorService service = Executors.newFixedThreadPool(10); //额外:设置线程池属性(需要在ExecutorService接口的实现类中才能设置) ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) service; threadPoolExecutor.setCorePoolSize(15); //threadPoolExecutor.setMaximumPoolSize(); //2.执行指定线程的操作。需要提供实现Runnable接口或Callable接口的实现类对象 service.execute(new NumberThread()); //适用于Runnable接口 //service.submit(Callable callable); //适用于Callable接口 //3.关闭线程池 service.shutdown(); } } class NumberThread implements Runnable{ @Override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 == 0) { System.out.println( Thread.currentThread().getName() +":"+ i); } } } }
面试题4:创建多线程有几种方式?
四种:Thread类、Runnable接口、Callable接口、线程池。
本文来自博客园,作者:萧若風,转载请注明原文链接:https://www.cnblogs.com/XiaoRuoFeng/p/16403062.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律