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不能物尽其用。多线程中的线程安全是一个很重要的话题。希望文章对您有所帮助,如有错误,请不吝指出。

相关推荐
Cloud_Shy6188 分钟前
解读《Effective Python 3rd Edition》:从练气到老魔(第六章 Item 40 - 43)
android·开发语言·人工智能·笔记·python·学习方法
AFinalStone1 小时前
Android12 U盘插拔链路源码全解析(五):Framework层(下) StorageManagerService
android·frameworks
黄昏回响1 小时前
信息系统基础知识(八):典型信息系统架构模型详解
程序人生·面试·系统架构·改行学it
林九生2 小时前
【实用技巧】MySQL 绿色版一键路径更新脚本详解 —— update_path.bat 深度解析
android·数据库·mysql
故渊at3 小时前
第十三板块:Android 综合架构与未来演进 | 第三十一篇:Android 架构演进与 Fuchsia OS 的挑战
android·架构·宏内核·微内核·fuchsia·ipc 性能博弈
aqi003 小时前
一文速览 HarmonyOS 6.1.1 推出的十个新特性
android·华为·harmonyos·鸿蒙·harmony
触底反弹4 小时前
从 JS 引擎执行原理理解数据类型:栈内存、堆内存与作用域
javascript·数据结构·面试
matrixmind14 小时前
aiomysql:异步场景下的 MySQL 驱动
android·数据库·mysql·其他
随遇丿而安4 小时前
第8周:弹窗 / 提示组件全功能与弹窗优化
android
zh_xuan4 小时前
诡异Bug:输入框删除字符,却越删越多
android·bug