Android多线程开发之线程安全

Android多线程

一般情况下,Android UI线程和主线程是一回事,文章的以后的内容中我都会用主线程代表UI线程。主线城主要是运行和UI页面相关的业务逻辑,动画绘制,页面布局以及响应屏幕的触摸事件。Android为了保证60fps,也就是每秒60帧的屏幕刷新率,每一帧的平均时间为16.67ms,这是一个相当短的时间。我们就必须保证主线程禁止执行耗时操作,比如说网络请求,数据库的读写,以及耗CPU的数据计算操作,否则引起页面刷新应该分得的时间被占用,导致丢帧表现出卡顿,甚至出现ANR(Application Not Response),导致用户体验欠佳,用户会关闭应用甚至卸载应用。

内存模型

先了解一下JMM(Java Memory Model)Java内存模型。我只讲和线程安全相关的内存模型,线程共用的部分是Heap,线程独占的部分是线程的栈帧,如下图所示:

VM创建线程之后,从Heap中读数据,然后对数据进行操作然后把数据写回Heap。

线程安全产生的原因

线程安全主要是数据安全。数据安全也就是数据的一致性,数据存在Heap中,同时线程也会从Heap中读取一份,多个线程对同一份数据进行读写,数据会有多份数据。如何保证保证多份数据的一致性,会是一个很有挑战性的问题。

线程安全的方法一(不可变)

数据不可变是能够保证线程安全的方法之一。例如像java.lang.String设计的那样,类被设计成public final(不能被继承), 类里面的相关属性设计成private final(属性不可变)。

线程安全的方法二(独占不分享)

如果数据只在线程内使用,不与Heap分享,这也能保证数据安全。ThreadLocal第一眼很容易让人误以为这是一个Thread,其实并不是,它是在JDK 1.2中引入,为每个线程提供一个独立的本地变量副本,用来解决变量并发访问的冲突问题。所有的线程可以共享一个ThreadLocal对象,但是每一个线程只能访问自己所存储的变量,线程之间互不影响。 例如,下面的类为每个线程生成唯一的本地标识符。线程的 ID 在第一次调用 ThreadId.get() 时分配,并在后续调用中保持不变。 示例代码如下:

java 复制代码
 import java. util. concurrent. atomic. AtomicInteger;
 
  public class ThreadId {
      // Atomic integer containing the next thread ID to be assigned
      private static final AtomicInteger nextId = new AtomicInteger(0);
 
      // Thread local variable containing each thread's ID
      private static final ThreadLocal<Integer> threadId =
          new ThreadLocal<Integer>() {
              @Override protected Integer initialValue() {
                  return nextId. getAndIncrement();
          }
      };
 
      // Returns the current thread's unique ID, assigning it if necessary
      public static int get() {
          return threadId. get();
      }
  }

线程安全的方法三(CPU cache无效)

volatile关键字主要有两个作用‌。1.内存可见性‌:volatile关键字确保当一个线程修改了该变量的值,其他线程能立即感知到这种变化。这是因为volatile变量在写操作时会立即同步到主内存中,而在读操作时,其他线程会从主内存中读取最新的值。这种机制保证了多线程环境下变量的值对所有线程都是可见的。2.‌禁止指令重排序‌:volatile关键字还确保变量的读写操作不会因为编译器或CPU的优化而被重排序。这意味着,即使编译器或CPU试图优化代码执行顺序,volatile变量的读写操作也会保持原有的顺序,从而避免了因指令重排序导致的并发问题。volatile修饰属性就是使用作用1,这样就能让CPU缓存cache中的数据无效,每次读数据都是从Heap读取;写数据时候,直接写到Heap。CPU cache读写速度是主内存千百倍的速度差异。

线程安全的方法三(synchronized)

Java中内置了语言级的同步原语--synchronized。Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。synchronized锁独占,互斥,请求并保持直到方法执行完毕。synchronized有两种类型的锁,一种是对象锁,另一种是类锁。对象锁的示例代码如下:

java 复制代码
    /**
     * Returns the number of keys in this hashtable.
     *
     * @return  the number of keys in this hashtable.
     */
    public synchronized int size() {
        return count;
    }

    /**
     * Tests if this hashtable maps no keys to values.
     *
     * @return  {@code true} if this hashtable maps no keys to values;
     *          {@code false} otherwise.
     */
    public synchronized boolean isEmpty() {
        return count == 0;
    }

上述的代码是java.util.HashTable中的两个方法,两个方法都用synchronized关键字修饰,代表是对象锁。如果同一个对象在两个不同的线程A和B中同时执行size方法和isEmpty方法,这样就不能同时执行。比如线程A执行方法的时候拿到了对象锁,线程B就需要等到线程A释放对象锁,获取对象锁才能执行。但是synchronized锁是可重入的,线程A执行方法的时候获取到了对象B的对象锁,再调用对象B的相应的synchronized对象方法时候能够直接获取相应的对象锁,并执行相应的方法。

线程安全的方法四(Lock)

需要通过显式创建Lock对象,并调用lock()和unlock()方法来加锁和解锁,通常放在 finally块中以确保无论发生何种情况都能释放锁避免死锁。Lock提供了更大的灵活性,支持可中断的加锁lockInterruptibly(),尝试获取锁tryLock()和设置超时等操作。示例代码如下所示:

java 复制代码
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final Lock lock = new ReentrantLock();
    private int counter = 0;

    public void increment() {
        lock.lock();
        try {
            counter++;
        } finally {
            lock.unlock();
        }
    }
}

总结

Android设备中CPU和内存资源是相对比较有限的,创建一个进程资源消耗的内存大概是2M,创建一个线程消耗内存大概是64K。创建线程的数量应该平衡,创建线程过多,线程之间切换次数过多导致效率低下;创建线程过少,线程不能跑满CPU,导致CPU不能物尽其用。多线程中的线程安全是一个很重要的话题。希望文章对您有所帮助,如有错误,请不吝指出。

相关推荐
TeleostNaCl9 分钟前
如何安装 Google 通用的驱动以便使用 ADB 和 Fastboot 调试(Bootloader)设备
android·经验分享·adb·android studio·android-studio·android runtime
小林学习编程32 分钟前
2025年最新AI大模型原理和应用面试题
人工智能·ai·面试
月阳羊36 分钟前
【硬件-笔试面试题-69】硬件/电子工程师,笔试面试题(知识点:电机驱动电路的反馈电路)
java·经验分享·嵌入式硬件·面试
fatiaozhang95271 小时前
中国移动浪潮云电脑CD1000-系统全分区备份包-可瑞芯微工具刷机-可救砖
android·网络·电脑·电视盒子·刷机固件·机顶盒刷机
uhakadotcom1 小时前
DuckDB相比于ClickHouse有什么不同点和优势?
后端·面试·github
2501_915918412 小时前
iOS 开发全流程实战 基于 uni-app 的 iOS 应用开发、打包、测试与上架流程详解
android·ios·小程序·https·uni-app·iphone·webview
lichong9512 小时前
【混合开发】vue+Android、iPhone、鸿蒙、win、macOS、Linux之dist打包发布在Android工程asserts里
android·vue.js·iphone
Android出海2 小时前
Android 15重磅升级:16KB内存页机制详解与适配指南
android·人工智能·新媒体运营·产品运营·内容运营
一只修仙的猿2 小时前
毕业三年后,我离职了
android·面试
编程乐学3 小时前
安卓非原创--基于Android Studio 实现的新闻App
android·ide·android studio·移动端开发·安卓大作业·新闻app