【多线程与并发】Android线程基础

  • 线程与进程
  • 线程的几种创建方式
  • 线程的优先级
  • 线程的几种状态与常用方法
  • 线程间消息通讯
  • 线程安全
Android Thread.png

线程与进程

  • 一个进程至少一个线程
  • 进程可以包含多个线程
  • 进程在执行过程中拥有独立的内存空间,而线程运行在进程内

线程的几种创建方式

  • new Thread:可复写Thread#run方法。也可传递Runnable对象,更加灵活。
    • 缺点:缺乏统一管理,可能无限制新建线程,相互之间竞争,极可能占用过多系统资源导致死机或oom
// 传递Runnable对象
1.new Thread(new Runnable(){
 ...
}).start()

//复写Thread#run方法
2.class MyThread extends Thread{
  public void run(){
    
  }
}
new MyThread().start()
  • AysncTask:轻量级的异步任务工具类,提供任务执行的进度回调给UI线程
    • 场景:需要知晓任务执行的速度,多个任务串行执行
    • 缺点:生命周期和宿主的生命周期不同步,有可能发生内存泄漏,默认情况所有任务串行执行
class AsyncTaskTest extends AsyncTask<String,Integer,String> {
   private static final String TAG = "AsyncTaskTest";
    @Override
    protected String doInBackground(String... params) {
        for (int i = 0; i < 10; i++) {
            publishProgress(i * 10);
        }
        return params[0];
    }

    @Override
    protected void onPostExecute(String result) {
        Log.e(TAG,"result:" + result);
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        Log.e(TAG,"onProgressUpdate:" + values[0].intValue());
    }

    public static void main(String[] args) {
        //1.子类复写方法
        AsyncTaskTest asyncTaskTest = new AsyncTaskTest();
        asyncTaskTest.execute("execute asyncTaskTest");
           //or
        asyncTaskTest.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,"execute AsyncTaskTest");

        //2.使用execute方法,同样串行执行
        AsyncTask.execute(new Runnable() {
            @Override
            public void run() {
                .....
            }
        });
        //3.使用内置THREAD_POOL_EXECUTOR线程池,并发执行
        AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
            @Override
            public void run() {

            }
        });
    }
}
  • HandlerThread:适用于主线程需要和工作线程通信,适用于持续性任务,比如轮训的场景,所有任务串行执行。
    • 缺点:不会像普通线程一样主动销毁资源,会一直运行下去,所以可能会造成内存溢出。
 HandlerThread thread = new HandlerThread("concurrent-thread");
        thread.start();
        ThreadHandler handler = new ThreadHandler(thread.getLooper()) {
            @Override
            public void handleMessage(@NonNull Message msg) {
                switch ((msg.what)) {
                    case MSG_WHAT_FLAG_1:
                        break;
                }

            }
        };

 handler.sendEmptyMessage(MSG_WHAT_FLAG_1);
 thread.quitSafely();
//定义成静态,防止内存泄漏
 static class ThreadHandler extends Handler {
        public ThreadHandler(Looper looper) {
            super(looper);
        }
    }
  • IntentService:适用于我们的任务需要跨页面读取任务执行的进度,结果。比如后台上传页面,批量操作数据库等,任务执行完成功后,就会自我结束,所以不需要手动stopService,这是它跟Service的区分。
class MyIntentService extends IntentService{
  @Override
  protected void onHandleIntent(@Nullable Intent intent){
    int conmmand = intent.getInt("command")
      ·····
  }
}
//启动IntentService
context.startService(new Intent())
  • ThreadPoolExecutor:适用于快速处理大量耗时较短的任务场景
Excutors.newCachedThreadPool();//线程可复用线程池
Excutors.newFixedThreadPool();//固定线程数量的线程池
Excutors.newScheduledThreadPool();//可指定定时任务的线程池
Excutors.newSingleThreadExecutor();//线程数量为1的线程池

线程优先级

public static void main(String[] args){
  Tread thread = new Thread();
  thread.start();
  int ui_proi = Process.getThreadPriority(0);
  int th_proi = thread.getPriority();
  
  //输出结果
  ui_proi = 5
  th_proi = 5
}
  • 线程的优先级具有继承性,在某线程中创建的线程会继承此线程的优先级,那么我们在UI线程中创建了线程,则线程优先级是和UI线程优先级一样,平等的和UI抢占CPU时间片资源。
  • JDK Api,限制了新设置的线程的优先级必须为[1~10],优先级priority的值越高,获取CPU时间片的概率越高。UI线程优先级为5
java.lang.Thread.setPriority(int newPriority)
  • Android Api,可以为线程设置更为精细的优先级(-20~19),优先级priority的值越低,获取CPU时间片的概率越高。UI线程优先级为-10
android.os.Process.setThreadPriority(int newPriority)

线程的几种状态与常用方法

方法名 说明
NEW 初始状态,线程被新建,还没调用start方法
RUNNABLE 运行状态,把“运行中”和“就绪”统称为运行状态
BLOCKED 阻塞状态,表示线程阻塞于锁
WAITING 等待状态,需要其他线程通知唤醒
TIME_WAITING 超时等待状态,表示可以在指定的时间超时后自行返回
TERMINATED 终止状态,表示当前线程已经执行完毕
image-20230723210346165.png
方法名 说明
wait 进入等待池,释放资源对象锁,可使用notify.notifyAll,或等待超时来唤醒
join 等待目标线程执行完成后再执行此线程
yield 暂停当前正在执行的线程对象,不会释放资源锁,使同优先级或更高优先级的线程有执行的机会
sleep 使调用线程进入休眠状态,但在一个synchronized块中执行sleep,线程虽然会休眠,但不会释放资源资源对象锁

线程间消息通讯

  • 主线程向子线程发送消息
//自定义一个looper子线程
class LooperThread extends Thread {
    private Looper looper;

    public Looper getLooper(){
        synchronized (this){
            if(looper == null && isAlive()){
                try {
                    wait();
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
        return looper;
    }

    @Override
    public void run() {
        Looper.prepare();
        synchronized (this){
            looper = Looper.myLooper();
            notifyAll();
        }
        Looper.loop();
    }
}

//主线程向子线程发送消息
LooperThread looperThread = new LooperThread("looper-thread");
looperThread.start();
Handler handler = new Handler(looperThread.getLooper()){
  @Override
  public void handleMessage(@NonNull Message msg){
    super.handleMessage(msg);
    Log.e(TAG,"handleMessage:" + msg.what);
    Log.e(TAG,"handleMessage:" + Thread.currentThread.getName);
  }
};
handler.sendEmptyMessage(MSG_WHAT_1);
  

  • 子线程向主线程发送消息(很熟悉,这里就省略了)

线程安全

什么是线程并发安全

线程安全的本质是能够让并发线程有序的运行(这个有序有可能是先来后到的排队,有可能有人插队,但不管怎么样,同一时刻只能一个线程有权访问同步资源),线程执行的结果,能够对其他线程可见。

线程安全的几种分类

  • synchronized关键字
  • ReentrantLock锁
  • AtomicInteger...原子类


    image.png
  • synchronized,ReentrantLock-锁


    image-20230729222013984.png
  • 原子类-自旋


    image-20230729222203332.png
  • 锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
  • 原子类适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升

如何保证线程安全

  • AtomicInteger原子包装类

    • AtomicInteger原子包装类,CAS(Compare-And-Swap)实现无锁数据更新。自旋的设计能够有效避免线程因阻塞-唤醒带来的系统资源开销。
    • 使用场景:多线程计数,原子操作,并发数量小的场景。
    //1、构建对象
    AtomicInteger atomicInteger = new AtomicInteger(1);
    //2.调用 Api
    atomicInteger.getAndIncrement();
    atomicInteger.getAndAdd(2);
    
    atomicInteger.getAndDecrement();
    atomicInteger.getAndAdd(-2);
    
  • volatile 可见性修饰

    volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存重新读取该成员的值,而且,当成员变量值发生变化时,强迫变化的值重新写入共享内存。

    不能解决非原子操作的线程安全。性能不及原子类高。

    volatile int count;
    public void increment(){
      //其他线程可见
      count = 5;
      //非原子操作,其他线程不可见
      count = count + 1;
      count ++;
    
  • synchronized

    锁java对象,锁Class对象,锁代码块

    • 锁方法(本质是锁java对象),加在方法上,未获取到对象锁的其他线程都不可以访问该方法。
    synchronized void printThreadName(){
      
    }
    
    • 锁Class对象,加在static方法上相当于给Class对象加锁,哪怕是不同的java对象实例,也需要排队执行。
    static synchronized void printThreadName(){
      
    }
    
    • 锁代码块,未获取到对象锁的其他线程可以执行同步块之外的代码
    void printThreadName(){
      String name = Thread.currentThread.getName();
      System.out.println("线程:" + name + "准备好了...");
      synchronized(this){
        
      }
        
    }
    

    synchronized的优势是什么呢?

    • 哪怕我们一个同步方法中出现了异常,那么JVM也能够为我们自动释放锁,能主动从而规避死锁,不需要开发者手动释放锁。

    劣势是什么呢?

    • 必须要等到获取锁对象的线程执行完成,或者出现异常,才能释放掉。不能中途中途释放锁,不能中断一个正在试图获得锁的线程。
    • 另外咱们也不知道多线程竞争锁的时候,获取锁成功与否,所以不够灵活。
    • 每个锁仅有单一的条件(某个对象),不能设定超时。
  • ReentrantLock悲观锁,可重入锁,公平锁,非公平锁

    • 基本用法
    ReentrantLock lock = new ReentrantLock();
    try{
      lock.lock
        ...
    }finally{
      lock.unLock()
    }
    
    void lock();//获取不到会阻塞
    boolean tryLock();//尝试获取锁,成功返回true
    boolean tryLock(3000,TimeUnit.MILLISECONDS);//在一定时间内去不断尝试获取锁
    void lockInterruptibly();//可使用Thread.interrupt()打断阻塞状态,退出竞争,让给其他线程
    
    • 可重入,避免死锁
    ReentrantLock lock = new ReentrantLock();
    public void doWork(){
      try{
        lock.lock();
        doWork();//递归调用,使得统一线程多次获得锁
      }finally{
        lock.unLock();
      }
    }
    
    • 公平锁与非公平锁

      • 公平锁,所有进入阻塞的线程排队依次均有机会执行
      • 默认非公平锁,允许线程插队,避免每一个线程都进入阻塞,在唤醒,性能高。因为线程可以插队,导致队列中可能会存在线程饿死的情况,一直得不到锁,一直得不到执行。
    • ReentrantLock进阶用法——Condition条件对象

      可使用它的await-singnal指定唤醒一个(组)线程。相比于wait-notify要么全部唤醒,要么只能唤醒一个,更加灵活可控

      ReentrantLock lock = new ReentrantLock();
      Condition worker1 = lock.newCondition();
      Condition worker2 = lock.newCondition();
      class Worker1{
        ...
          worker1.await();//进入阻塞,等待唤醒
        ...  
      }
      
      class Worker2{
        ...
          worker2.await();//进入阻塞,等待唤醒
      }
      
      class Boss{
        if(...){
          worker1.signal();//指定唤醒线程1
        }else{
          worker2.signal();//指定唤醒线程2
        }
      }
      
    • ReentrantReadWriteLock共享锁,排他锁

      • 共享锁,所有线程均可同时获得,并发量高,比如在线文档查看。
      • 排他锁,同一时刻只有一个线程有权修改资源,比如在线文档编辑。
    ReentrantReadWriteLock reentrantReadWriteLock;
    ReentrantReadWriteLock.ReadLock readLock;
    ReentrantReadWriteLock.WriteLock writeLock;
    

    如何正确的使用锁&原子类

    • 减少持锁时间

      尽管锁在同一时间只能允许一个线程持有,其他想要占用锁的线程都在临界区外等待锁的释放,这个等待的时间我们希望尽可能的短。

      public void syncMethod(){
        noneLockedCode1();//2s
        synchronized(this){
          needLockedMethed();//2s
        }
        noneLockedCode2();//2s
      }
      
    • 锁分离

      读读,读写,写读。只要有写锁进入才需要做同步处理,但是对于大多数应用来说,读的场景要远大于写的场景,因此一旦使用读写锁,在读多写少的场景中,就可以很好的提高系统的性能。

      读锁 写锁
      读锁 可以访问 不可访问
      写锁 不可访问 不可访问
    • 锁粗化

      多次加锁,释放锁合并成一次

      public void doSomethingMethod(){
        synchronized(lock){
          //do some thing
        }
        ...
          //这里还有一些代码,做其它不需要同步的工作,但能很快执行完毕
        ...
        synchronized(lock){
          //do other thing
        }
      }
      
      public void doSomethingMethod(){
        //进行锁粗化:整合成一次锁请求、释放
        synchronized(lock){
          //do something
          //做其它不需要同步的工作,但能很快执行完毕
          //do other thing
        }
      }
      
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容