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

相关推荐
前端小崔41 分钟前
前端面试题之ES6保姆级教程
开发语言·前端·javascript·面试·职场和发展·ecmascript·es6
像风一样自由1 小时前
【001】frida API分类 总览
android·frida
casual_clover1 小时前
Android 之 kotlin 语言学习笔记四(Android KTX)
android·学习·kotlin
安妮的心动录2 小时前
人是习惯的结果
面试·程序员·求职
前端小巷子2 小时前
Promise 静态方法:轻松处理多个异步任务
前端·面试·promise
工呈士2 小时前
Context API 应用与局限性
前端·react.js·面试
移动开发者1号2 小时前
Android 大文件分块上传实战:突破表单数据限制的完整方案
android·java·kotlin
移动开发者1号3 小时前
单线程模型中消息机制解析
android·kotlin
异常君3 小时前
@Bean 在@Configuration 中和普通类中的本质区别
java·spring·面试
sss191s5 小时前
校招 java 面试基础题目及解析
java·开发语言·面试