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

相关推荐
從南走到北2 分钟前
JAVA东郊到家按摩服务同款同城家政服务按摩私教茶艺师服务系统小程序+公众号+APP+H5
android·java·开发语言·微信小程序·小程序
alexhilton36 分钟前
学会用最优雅的姿式在Compose中显示富文本
android·kotlin·android jetpack
岁忧2 小时前
(LeetCode 面试经典 150 题 ) 155. 最小栈 (栈)
java·c++·算法·leetcode·面试·go
阿华的代码王国3 小时前
【Android】卡片式布局 && 滚动容器ScrollView
android·xml·java·前端·后端·卡片布局·滚动容器
风起云涌~3 小时前
【Android】桌面小组件开发
android·gitee
星眠4 小时前
学习低代码编辑器第四天
javascript·面试
NeverSettle1105745 小时前
手把手教你用nodejs + vue3 实现大文件上传、秒传、断点续传
前端·面试
双鱼大猫5 小时前
从传统播放器到AI智能体:Xplayer 2.0的技术革新之路
android
CYRUS_STUDIO5 小时前
动态篡改 so 函数返回值:一篇带你玩转 Android Hook 技术!
android·c++·逆向
Bohemian5 小时前
实现一个单机版令牌桶限流器(字节)
后端·面试