当前位置: 首页 > news >正文

android/java中主线程和子线程的详解

一、UI线程(主线程)介绍

1、UI线程是什么?

UI线程,也称为主线程(Main Thread),是Android应用启动时由系统自动创建的第一个线程,是一个特殊的、预先创建好的单线程消息循环。你的应用入口点(如 MainActivityonCreate 方法)就是运行在这个线程上的,系统为每个应用分配了且仅有一个UI线程,是唯一有权限直接修改UI的线程。

它的核心身份是:

  1. 一个普通的线程:它是 java.lang.Thread 的一个实例。
  2. 一个特殊的消息循环线程:它内部运行着一个 LooperMessageQueue,不断地处理消息(Message)或 Runnable 对象。

2、UI线程的核心职责

UI线程被设计为承担所有与用户交互相关的敏感操作,主要原因是为了保证线程安全操作的有序性

  1. UI绘制与更新

    • 测量、布局、绘制视图树中的所有View(onMeasure(), onLayout(), onDraw())。
    • 更新任何UI组件的属性,例如 TextView.setText(), ImageView.setImageBitmap()
  2. 处理用户输入事件

    • 分发和处理屏幕触摸事件(onTouchEvent)、按键事件(onKeyDown)。
  3. 执行生命周期回调

    • Activity、Fragment、Service等组件的生命周期方法(onCreate, onResume, onPause 等)都在UI线程中被调用。
  4. 执行通过 HandlerView.post() 提交的 Runnable 任务


3、UI线程的工作原理:消息循环机制(Message Loop)

这是理解UI线程如何工作的关键。其核心组件包括:

组件作用
MessageQueue(消息队列)一个单链表结构的优先级队列,用于存放由 Handler 发送过来的 MessageRunnable。它是一个阻塞队列,当没有消息时,线程会进入休眠状态以节省CPU。
Looper(循环器)UI线程的“引擎”。它在一个无限循环中工作,不断地从 MessageQueue 中取出(MessageQueue.next())消息。如果队列为空,它就阻塞;如果有消息,它就将其分发给对应的目标。一个线程只能有一个Looper
Handler(处理器)消息的“发送者”和“处理者”。它被绑定到创建它的线程(及其Looper)上。开发者通过 HandlerMessageRunnable 发送(post/send) 到消息队列中,也通过它在其绑定的线程上处理(handleMessage) 取出的消息。

4. 正确的使用方法:线程池 + UI线程协作

标准的做法是让线程池UI线程各司其职,协同工作:

  1. 在线程池中执行耗时任务:使用线程池(如 AsyncTask 的旧方式,或现在推荐的 ExecutorServiceCoroutine + Dispatchers.IO)在后台执行耗时操作。
  2. 将结果发送回UI线程:当后台任务完成并得到结果后,使用专门的方法将结果从工作线程发送(Post) 到UI线程,然后在UI线程上更新界面。

5.如何从后台线程更新UI?

有多种方法可以将代码切回UI线程执行:

1. Activity.runOnUiThread(Runnable action)

// 在后台线程中(例如线程池的线程)
executorService.execute(() -> {// 执行耗时操作,比如网络请求String result = doNetworkRequest();// 操作完成后,切回UI线程更新界面runOnUiThread(() -> {textView.setText(result); // 这是在UI线程中安全执行的});
});

2. View.post(Runnable action)

// 任何View都可以
executorService.execute(() -> {String result = doNetworkRequest();myTextView.post(() -> {myTextView.setText(result);});
});

3. Handler(Looper.getMainLooper())

Handler mainHandler = new Handler(Looper.getMainLooper());
executorService.execute(() -> {String result = doNetworkRequest();mainHandler.post(() -> {textView.setText(result);});
});

二、什么时候需要开启子线程?

核心原则:所有可能阻塞主线程(UI 线程)的耗时操作都必须在子线程中执行。

具体包括以下情况:

1. 网络请求

// 必须在子线程中执行
new Thread(new Runnable() {@Overridepublic void run() {try {URL url = new URL("https://api.example.com/data");HttpURLConnection connection = (HttpURLConnection) url.openConnection();// 读取数据...} catch (Exception e) {e.printStackTrace();}}
}).start();

2. 文件读写操作

// 文件操作需要在子线程
new Thread(() -> {try {File file = new File(getFilesDir(), "data.txt");FileOutputStream fos = new FileOutputStream(file);fos.write("Hello World".getBytes());fos.close();} catch (IOException e) {e.printStackTrace();}
}).start();

3. 数据库操作(特别是大量数据)

// 大量数据库查询
new Thread(() -> {SQLiteDatabase db = dbHelper.getReadableDatabase();Cursor cursor = db.query("table_name", null, null, null, null, null, null);// 处理数据...cursor.close();
}).start();

4. 复杂计算或数据处理

// 复杂计算
new Thread(() -> {double result = 0;for (int i = 0; i < 1000000; i++) {result += Math.sin(i) * Math.cos(i);}// 计算完成后需要更新UI的话要回到主线程
}).start();

5. 图片处理/压缩

// 图片压缩处理
new Thread(() -> {Bitmap compressedBitmap = Bitmap.createScaledBitmap(originalBitmap, targetWidth, targetHeight, true);// 处理完成后需要回到主线程显示图片
}).start();

三、什么时候需要回到主线程执行?

核心原则:所有UI更新操作都必须在主线程中执行。

具体包括以下情况:

1. 更新UI组件

// 在子线程中获取数据后,回到主线程更新UI
new Thread(() -> {// 模拟网络请求try {Thread.sleep(2000);final String result = "获取的数据";// 回到主线程更新UIrunOnUiThread(new Runnable() {@Overridepublic void run() {textView.setText(result); // UI更新必须在主线程progressBar.setVisibility(View.GONE);}});} catch (InterruptedException e) {e.printStackTrace();}
}).start();

2. 显示Toast

new Thread(() -> {// 耗时操作...// 显示Toast需要主线程runOnUiThread(() -> {Toast.makeText(MainActivity.this, "操作完成", Toast.LENGTH_SHORT).show();});
}).start();

3. 操作Adapter和RecyclerView/ListView

new Thread(() -> {List<Data> newData = fetchDataFromServer();runOnUiThread(() -> {adapter.setData(newData);adapter.notifyDataSetChanged(); // 必须在主线程调用});
}).start();

4. 启动Activity或Fragment事务

// 任何界面跳转都应在主线程
runOnUiThread(() -> {Intent intent = new Intent(MainActivity.this, DetailActivity.class);startActivity(intent);
});

四、需要/不需要确认线程的情况

1. 不需要确认线程的情况

不需要确认每一行代码,但需要确认每一段有特定线程要求的代码。**
1、不需要确认每一行代码的情况
大部分普通的、无副作用的计算代码不需要关心线程。比如:

int a = 10;
int b = 20;
int result = a + b; // 这个加法在任何线程执行结果都一样
String name = "Hello"; // 字符串赋值
final User user = new User("John"); // 创建对象(如果构造函数很简单)

这些代码是"线程安全"的,因为它们:

  1. 只操作局部变量
  2. 不访问共享资源
  3. 不修改外部状态
  4. 没有特定的线程要求

2. 必须确认线程情况的代码(关键!)

  1. UI操作 - 必须主线程
textView.setText("Hello"); // ✅ 必须在主线程
button.setEnabled(false);  // ✅ 必须在主线程
recyclerView.notifyDataSetChanged(); // ✅ 必须在主线程
  1. Android系统组件生命周期方法 - 通常在主线程
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState); // 在主线程setContentView(R.layout.activity_main); // 在主线程
}@Override
public void onResume() {super.onResume(); // 在主线程
}
  1. 耗时操作 - 必须工作线程
// 网络请求
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); // ❌ 不要在主线程
InputStream response = connection.getInputStream(); // ❌ 不要在主线程// 文件读写
FileOutputStream fos = new FileOutputStream(file); // ❌ 不要在主线程
fos.write(data); // ❌ 不要在主线程// 大量数据库操作
SQLiteDatabase db = dbHelper.getWritableDatabase(); // ❌ 简单查询可以,大量操作不要在主线程
db.insert(...); // ❌
  1. 访问共享资源/状态 - 需要线程安全考虑
// 静态变量
public static List<String> cachedData = new ArrayList<>();// 单例对象中的状态
public class DataManager {private static DataManager instance;private int requestCount = 0; // 需要同步访问public void incrementCount() {requestCount++; // ❌ 非原子操作,需要同步}
}

3. 实用的思维模式

不要想着:“我需要检查每行代码的线程”
而应该想:“我知道这几类代码有线程要求,当我写这些代码时要特别注意”

4. 如何培养这种意识(开发习惯)

  1. 建立清单:记住哪些操作必须在主线程(所有UI操作),哪些必须在工作线程(所有I/O操作)

  2. 使用代码结构提示

    // 看到这些方法,就要意识到可能在工作线程
    public void fetchData() {// 这里应该切换线程或确保已在工作线程
    }private void onDataReceived(String data) {// 收到数据后要更新UI?那需要切回主线程
    }
    
  3. 善用工具和注解

    @WorkerThread    // 标记该方法应在工作线程调用
    public void loadFromDatabase() { ... }@MainThread      // 标记该方法应在主线程调用  
    public void updateUI() { ... }@AnyThread       // 标记该方法线程安全,可在任何线程调用
    public synchronized int getCount() { ... }
    
  4. 运行时检查(调试时非常有用)

    // 在怀疑的地方添加检查
    if (Looper.myLooper() != Looper.getMainLooper()) {Log.w("ThreadCheck", "这段UI代码在后台线程执行!");// 或者直接抛出异常throw new IllegalStateException("必须在主线程调用");
    }
    
情况是否需要确认线程原因
简单的局部变量操作❌ 不需要线程安全
UI更新✅ 必须确认必须在主线程
网络/文件/数据库操作✅ 必须确认必须在工作线程
访问共享状态✅ 必须确认需要线程同步
系统生命周期方法✅ 应该知道通常在主线程

最终建议:不需要过度焦虑地检查每一行代码,但要培养对关键代码段的线程敏感性。

五、推荐的线程管理方式

使用Handler

Handler handler = new Handler(Looper.getMainLooper());new Thread(() -> {// 子线程执行耗时操作String result = doWork();// 通过Handler回到主线程handler.post(() -> {updateUI(result);});
}).start();

使用现代并发工具(推荐)

// 使用ExecutorService管理线程池
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.execute(() -> {// 子线程工作String result = fetchData();runOnUiThread(() -> {// 回到主线程更新UItextView.setText(result);});
});

六、总结

场景执行线程原因
网络请求子线程避免阻塞UI线程导致ANR
文件操作子线程磁盘IO可能很慢
数据库操作子线程特别是大量数据查询
复杂计算子线程占用CPU资源,会卡顿UI
图片处理子线程解码和处理可能很耗时
UI更新主线程Android的UI系统不是线程安全的
Toast显示主线程属于UI操作
Adapter更新主线程避免并发修改异常

记住这个简单规则:

    1. 做工作:在子线程(后台线程)
    1. 展示结果:在主线程(UI线程)
    1. 永远不要阻塞UI线程,也不要从非UI线程操作UI。正确的做法是使用线程池处理耗时任务,任何可能超过几毫秒的操作都必须移到后台线程。然后通过 runOnUiThread(), view.post() 或协程等方式将结果传回UI线程进行更新。
http://www.dtcms.com/a/354096.html

相关文章:

  • Nano Banana揭秘:Google Gemini 2.5 Flash Image正式发布 | AI图像编辑新时代
  • 内网应用如何实现外网访问?外地通过公网地址访问内网服务器的设置方法
  • 动态规划:青蛙跳台阶实践
  • H20 性能表现之 Kimi-K2
  • 【git】:gitee项目管理vs2019
  • 装饰器进阶与设计模式
  • Linux入门教程 第十五章 Linux 系统调优工具
  • 【工具篇】github/huggingface 镜像源总结
  • 嵌入式系统学习Day24(线程)
  • Custom SRP - Shadow Masks
  • Axure:如何将SVG转换为形状
  • leetcode 155 官方golang标准答案错误
  • Java Lambda 处理日期时间 根据区间找出区间内集合
  • Linux程序与进程:核心概念与管理全解析
  • Class45循环神经网络RNN
  • “互联网 +”时代下开源 AI 大模型 AI 智能名片 S2B2C 商城小程序:行业变革与未来展望
  • 基于 Ultralytics YOLO11与 TrackZone 的驱动的高效区域目标跟踪方案实践
  • Python Imaging Library (PIL) 全面指南:PIL基础入门-Python图像处理实战
  • 多版本兼容的golang客服系统
  • 稀土:从“稀有”到“命脉”的科技核心
  • 通过概率正 - 未标记网络从医学图像的特定感兴趣区域中学习|文献速递-深度学习人工智能医疗图像
  • 【底层机制】thread_local 变量的初始化时机和生命周期
  • Spring Retry Spring 生态系统优雅的重试组件
  • 浏览器网页路径扫描器(脚本)
  • SQL优化:SQL模拟Split二维数组
  • Linux 基础开发工具
  • django-redis 使用类实现和使用
  • React(面试)
  • JUC之异步编程理论总结
  • 实现基于数据库 flag 状态的消息消费控制