Android 多线程同步

    技术2022-07-10  127

    序言:Android开发,对高并发等并没有太高的要求,所以本文介绍主要针对安卓开发过程中的同步问题。

    目录: 1.synchronized(重点讲解); 2.Lock锁; 3.CountDownLatch和Semaphore; 4.java.util.concurrent包下面的集合; 5.开发过程中遇到的问题

    一、.synchronized原理及使用 使用: 1.用于同步方法:

    // 同步普通方法 public synchronized String getBusNumber(){ return ""; } // 同步静态方法 public synchronized static String getBusTime(){ return ""; } // 用于同步代码块 public String getBusColor(){ synchronized (this){ } return ""; }

    关于synchronized的使用,刚接触时经常会遇到同步不了的情况,这里要注意所谓的synchronized是针对对象而言,比如说同步的是对象还是类对象。**静态方法是属于类的,所以修饰静态方法时是同步的类对象。同步的本质就是要是多个线程执行的同步方法是一个对象。下面从原理来分析: ** 2.实现原理: synchronized是针对于对象(Object)来实现的,每个Object都有一个monitor(监视器,底层实现的),我们把问题抽象出来,这样理解: 现在A公司准备招聘(被synchronized的Object),有甲、乙、丙三个面试人员(这里相当于有3个线程要),进行面试的人要获得进入面试房间的唯一一把钥匙(要获取Object的monitor),当甲先拿到钥匙进入房间后,此时乙和丙也来了(有一个线程先获得了monitor进入同步方法或同步代码块),这个时候只能等甲面试结束,放出钥匙才能进入房间面试(后面来的线程进入block状态,会把这些线程放入Object的等待对列中)。如果这个时候甲面试完成,面试官感觉还不错,想再考虑一下,此时让甲先别走,等待结果(该线程执行Object的waite()方法),此时甲释放出钥匙(该线程交出monitor),在等待队列的乙和丙此时抢占模式,先拿到钥匙的进入房间面试(拿到monitor),假定进入房间的是乙,此时乙在面试过程中如果让面试官认为是否可以录用在隔壁等候结果的甲(及此时第二个线程调用Object的notify()方法,唤醒在等结果队列的线程),此时就可以决定让甲离开(线程进入Runable状态,等待CPU分配时间片进入运行状态),或者乙也和甲一样进入等待结果(该线程执行Object的waite()方法,释放monitor进入等待状态)… 总结一下:Object持有一个monitor。Object还有两个集合,一个是存放阻塞的线程,另一个是存放执行了waite()方法等待唤醒的形成。执行synchronized(Object)需要获取该Object的monitor,没有获取到monitor的线程状态变为BLOCKED,被加入Object阻塞的集合里面。获取到monitor的线程,如果该线程执行了Object的waite()方法,该线程状态变为WAITING被加入等待集合里面,并释放monitor。只有当其他线程获取到该Object的monitor并调用Object的notify()或者notifyAll()时,会把Object的等待集合里面的线程从WAITING变为RUNNABLE,并从该集合移除该线程(notifyAll针对时集合里面的所有线程)。 这里说一下线程的几种状态:NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED。具体的解释可以看Thread的源码。 就Android开发而言,synchronized比较适合,并发量没有Java后台那么大,而且维护很简单,不用担心死锁之类的问题(因为在编译的时候会把synchronized修饰的代码块和方法前面和结尾增加获取和释放monitor的指令)。

    二、Lock锁 这里网上讲Lock的比较多,android开发中用的场景不多。简单介绍一下Lock和synchronized的区别: 优点:并发量大的时候,Lock在性能上有优势,synchronized性能消耗较大; 缺点:Lock需要手动维护,处理不适当会造成死锁;

    三、CountDownLatch和Semaphore; 1.CountDownLatch,可以看做一个倒计时器,具有原子性,适合多线程协调完成某项工作;但是CountDownLatch只能倒计时,倒计时到0时,不能重用。 用法:

    private void startThreads(){ // 等待任务执行完成 new Thread(new Runnable() { @Override public void run() { try { countDownLatch.await(); System.out.println("------任务执行完了-----"); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); for (int i = 0; i < 5; i ++){ new Thread("第" + i + "线程"){ @Override public void run() { super.run(); System.out.println("-----正在执行任务------"); countDownLatch.countDown(); } }.start(); } } **2.Semaphore信号量 Semaphore其实就是一个原子计数器,也可以用信号量Semaphore来做同步。** Semaphore semaphore = new Semaphore(1); private void doSemaphore(){ try { semaphore.acquire(); System.out.println("----我现在可以快乐的玩耍拉---------"); semaphore.release(); } catch (Exception e) { e.printStackTrace(); } }

    把Semaphore 初始值设为1,当有线程执行方法时先acquire(),获取到之后Semaphore 的值变为了0,当此时其他线程也执行该方法时,就只能等Semaphore 的值大于0,及需要执行semaphore.release();

    四、java.util.concurrent包下面的集合 在生产者消费者模型解决同步问题时,简单的话可以使用Java已经实现的同步机制的集合等,在java.util.concurrent包下面,之前看实现原理主要是CAS算法来实现同步,执行效率还可以(但也不能滥用,要看场景,毕竟性能比非同步的集合等要高)里面提供原子操作的类,如下图

    五、开发过程中遇到的奇葩问题 之前在开发中使用RxJava里面线程来执行对文件的操作,比如把内容保存到指定文件。通过File.exsit()方法来判断文件是否存在,写个伪代码:

    public synchronized void saveToFile(String filePath, String content){ File file = new File(filePath); if (file.exists()) return; // 因为之前保存的数据耗时较长,模拟耗时的操作 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 执行保存内容 writeToFile(filePath, content); // 对于复用线程,比如线程池时,必须要加上 file = null; }

    如果不加上file = null的话,文件保存进去了,file.exists())依然会是false。反复排除synchronized 用的也没啥问题,为什么会这样呢?后来把线程id打出来,发现是同一个线程执行file.exists()会false。个人理解是线程内部的ThreadLocal会缓存成员变量file,如果不置null的吧,会认为是一个对象,从而不会去磁盘里面看文件是否存在。

    Processed: 0.015, SQL: 9