写点什么

JAVA-Android- 多线程实现方式及并发与同步,写给 1-3 年 Android 程序员的几点建议

用户头像
Android架构
关注
发布于: 刚刚

}


};


HandlerThread 的好处是代码看起来没前面的版本那么乱,相对简洁一点。还有一个好处就是通过 handlerThread.quit()或者 quitSafely()使线程结束自己的生命周期。####4、AsyncTask:具体的使用代码就不贴上来了,可以去看我的一篇博文。但值得一说的是,上面说过 HandlerThread 只开一条线程,任务都被阻塞在一个队列中,那么就会使阻塞的任务延迟了,而 AsyncTask 开启线程的方法 asyncTask.execute()默认是也是开启一个线程和一个队列的,不过也可以通过 asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, 0)开启一个含有 5 个新线程的线程池,也就是说有个 5 个队列了,假如说你执行第 6 个耗时任务时,除非前面 5 个都还没执行完,否则任务是不会阻塞的,这样就可以大大减少耗时任务延迟的可能性,这也是它的优点所在。当你想多个耗时任务并发的执行,那你更应该选择 AsyncTask。####5、IntentService:最后是 IntentService,相信很多人也不陌生,它是 Service 的子类,用法跟 Service 也差不多,就是实现的方法名字不一样,耗时逻辑应放在 onHandleIntent(Intent intent)的方法体里,它同样有着退出启动它的 Activity 后不会被系统杀死的特点,而且当任务执行完后会自动停止,无须手动去终止它。例如在 APP 里我们要实现一个下载功能,当退出页面后下载不会被中断,那么这时候 IntentService 就是一个不错的选择了。阅读全文文末领取免费高阶 Android 学习资料及高清思维脑图!


###线程状态 1、wait()。使一个线程处于等待状态,并且释放所有持有对象的 lock 锁,直到 notify()/notifyAll()被唤醒后放到锁定池(lock blocked pool ),释放同步锁使线程回到可运行状态(Runnable)。


2、sleep()。使一个线程处于睡眠状态,是一个静态方法,调用此方法要捕捉 Interrupted 异常,醒来后进入 runnable 状态,等待 JVM 调度。


3、notify()。使一个等待状态的线程唤醒,注意并不能确切唤醒等待状态线程,是由 JVM 决定且不按优先级。


4、allnotify()。使所有等待状态的线程唤醒,注意并不是给所有线程上锁,而是让它们竞争。


5、join()。使一个线程中断,IO 完成会回到 Runnable 状态,等待 JVM 的调度。


6、Synchronized()。使 Running 状态的线程加同步锁使其进入(lock blocked pool ),同步锁被释放进入可运行状态(Runnable)。


注意:当线程在 runnable 状态时是处于被调度的线程,此时的调度顺序是不一定的。Thread 类中的 yield 方法可以让一个 running 状态的线程转入 runnable。


###基础概念


1、 并行。多个 cpu 实例或多台机器同时执行一段代码,是真正的同时。2、并发。通过 cpu 调度算法,让用户看上去同时执行,实际上从 cpu 操作层面不是真正的同时。3、线程安全。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。线程不安全就意味着线程的调度顺序会影响最终结果,比如某段代码不加事务去并发访问。4、线程同步。指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。如某段代码加入 @synchronized 关键字。线程安全的优先级高于性能优化。5、原子性。一个操作或者一系列操作,要么全部执行要么全部不执行。数据库中的“事物”就是个典型的院子操作。6、可见性。当一个线程修改了共享属性的值,其它线程能立刻看到共享属性值的更改。比如 JMM 分为主存和工作内存,共享属性的修改过程是在主存中读取并复制到工作内存中,在工作内存中修改完成之后,再刷新主存中的值。若线程 A 在工作内存中修改完成但还来得及刷新主存中的值,这时线程 B 访问该属性的值仍是旧值。这样可见性就没法保证。7、有序性。程序运行时代码逻辑的顺序在实际执行中不一定有序,为了提高性能,编译器和处理器都会对代码进行重新排序。前提是,重新排序的结果要和单线程执行程序顺序一致。


###Synchronized 同步


由于 java 的每个对象都有一个内置锁,当用此关键字修饰方法时,?内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。补充: synchronized 关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。


1、方法同步。给方法增加 synchronized 修饰符就可以成为同步方法,可以是静态方法、非静态方法,但不能是抽象方法、接口方法。小示例:


public synchronized void aMethod() {// do something}


public static synchronized void anotherMethod() {// do something}


使用详解:线程在执行同步方法时是具有排它性的。当任意一个线程进入到一个对象的任意一个同步方法时,这个对象的所有同步方法都被锁定了,在此期间,其他任何线程都不能访问这个对象的任意一个同步方法,直到这个线程执行完它所调用的同步方法并从中退出,从而导致它释放了该对象的同步锁之后。在一个对象被某个线程锁定之后,其他线程是可以访问这个对象的所有非同步方法的。


2、块同步。同步块是通过锁定一个指定的对象,来对块中的代码进行同步;同步方法和同步块之间的相互制约只限于同一个对象之间,静态同步方法只受它所属类的其它静态同步方法的制约,而跟这个类的实例没有关系。如果一个对象既有同步方法,又有同步块,那么当其中任意一个同步方法或者同步块被某个线程执行时,这个对象就被锁定了,其他线程无法在此时访问这个对象的同步方法,也不能执行同步块。


3、使用方法同步保护共享数据。示例:


public class ThreadTest implements Runnable{


public synchronized void run(){for(int i=0;i<10;i++) {System.out.print(" " + i);}}


public static void main(String[] args) {Runnable r1 = new ThreadTest();Runnable r2 = new ThreadTest();Thread t1 = new Thread(r1);Thread t2 = new Thread(r2);t1.start();t2.start();}}


示例详解:代码中可见,run()被加上了 synchronized?关键字,但保护的并不是共享数据。因为程序中两个线程对象 t1、t2 其实是另外两个线程对象 r1、r2 的线程,这个听起来绕,但是一眼你就能看明白;因为不同的线程对象的数据是不同的,即 r1,r2?有各自的 run()方法,所以输出结果就无法预知。这时使用 synchronized 关键字可以让某个时刻只有一个线程可以访问该对象 synchronized 数据。每个对象都有一个“锁标志”,当这个对象的一个线程访问这个对象的某个 synchronized?数据时,这个对象的所有被 synchronized?修饰的数据将被上锁(因为“锁标志”被当前线程拿走了),只有当前线程访问完它要访问的 synchronized?数据时,当前线程才会释放“锁标志”,这样同一个对象的其它线程才有机会访问 synchronized?数据。


接下来,我们把 r2 给注释掉, 即只保留一个 r 对象。如下:


public class ThreadTest implements Runnable{


public synchronized void run(){for(int i=0;i<10;i++){System.out.print(" " + i);}}


public static void main(String[] args){Runnable r = new ThreadTest();Thread t1 = new Thread(r);Thread t2 = new Thread(r);t1.start();t2.start();}}


示例详解:如果你运行 1000?次这个程序,它的输出结果也一定每次都是:0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9。因为这里的 synchronized?保护的是共享数据。t1,t2?是同一个对象(r)的两个线程,当其中的一个线程(例如:t1)开始执行 run()方法时,由于 run()受 synchronized 保护,所以同一个对象的其他线程(t2)无法访问 synchronized?方法(run?方法)。只有当 t1 执行完后 t2?才有机会执行。


4、使用块同步,示例:


public class ThreadTest implements Runnable{public void run(){synchronized(this){ //与上面示例不同于关键字使用 for(int i=0;i<10;i++){System.out.print(" " + i);}}}public static void main(String[] args){Runnable r = new ThreadTest();Thread t1 = new Thread(r);Thread t2 = new Thread(r);t1.start();t2.start();}}


示例详解:这个与上面示例的运行结果也一样的。这里是把保护范围缩到最小,this?代表 ‘这个对象’ ? 。没有必要把整个 run()保护起来,run()中的代码只有一个 for 循环,所以只要保护 for?循环就可以了。


最后,再看一个示例:


public class ThreadTest implements Runnable{


public void run(){for(int k=0;k<5;k++){System.out.println(Thread.currentThread().getName()+ " : for loop : " + k);}


synchronized(this){for(int k=0;k<5;k++) {System.out.println(Thread.currentThread().getName()+ " : synchronized for loop : " + k);}} }


public static void main(String[] args){Runnable r = new ThreadTest();Thread t1 = new Thread(r,"t1_name");Thread t2 = new Thread(r,"t2_name");t1.start();t2.start();} }


//运行结果:t1_name : for loop : 0t1_name : for loop : 1t1_name : for loop : 2t2_name : for loop : 0t1_name : for loop : 3t2_name : for loop : 1t1_name : for loop : 4t2_name : for loop : 2t1_name : synchronized for loop : 0t2_name : for loop : 3t1_name : synchronized for loop : 1t2_name : for loop : 4t1_name : synchronized for loop : 2t1_name : synchronized for loop : 3t1_name : synchronized for loop : 4t2_name : synchronized for loop : 0t2_name : synchronized for loop : 1t2_name : synchronized for loop : 2t2_name : synchronized for loop : 3t2_name : synchronized for loop : 4


示例详解:第一个 for?循环没有受 synchronized?保护。对于第一个 for?循环,t1,t2?可以同时访问。运行结果表明 t1?执行到了 k=2?时,t2?开始执行了。t1?首先执行完了第一个 for?循环,此时 t2 还没有执行完第一个 for?循环(t2?刚执行到 k=2)。t1?开始执行第二个 for?循环,当 t1 的第二个 for?循环执行到 k=1?时,t2?的第一个 for?循环执行完了。t2?想开始执行第二个 for?循环,但由于 t1?首先执行了第二个 for?循环,这个对象的锁标志自然在 t1?手中(synchronized?方法的执行权也就落到了 t1?手中),在 t1?没执行完第二个 for?循环的时候,它是不会释放锁标志的。所以 t2?必须等到 t1?执行完第二个 for?循环后,它才可以执行第二个 for?循环。


###Volatile?同步


a.volatile 关键字为域变量的访问提供了一种免锁机制 b.使用 volatile 修饰域相当于告诉虚拟机该域可能


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


会被其他线程更新 c.因此每次使用该域就要重新计算,而不是使用寄存器中的值?d.volatile 不会提供任何原子操作,它也不能用来修饰 final 类型的变量?例如:?在上面的例子当中,只需在 account 前面加上 volatile 修饰,即可实现线程同步。?代码实例:?


//只给出要修改的代码,其余代码与上同 class Bank {//需要同步的变量加上 volatileprivate volatile int account = 100;


public int getAccount() {return account;}//这里不再需要 synchronizedpublic void save(int money) {account += money;}}


注:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。?用 final 域,有锁保护的域和 volatile 域可以避免非同步的问题。?


###重入锁同步


在?JavaSE5.0 中?新增了一个 java.util.concurrent 包来支持同步。?ReentrantLock 类是可重入、互斥、实现了 Lock 接口的锁,它与使用 synchronized 方法和快具有相同的基本行为和语义,并且扩展了其能力。?ReenreantLock 类的常用方法有:


ReentrantLock() : 创建一个 ReentrantLock 实例 lock() : 获得锁 unlock() : 释放锁


注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用 例如:?


class Bank {private int account = 100;//需要声明这个锁 private Lock lock = new ReentrantLock();public int getAccount() {return account;}//这里不再需要 synchronizedpublic void save(int money) {lock.lock();try{account += money;}finally{lock.unlock();}}}


注:关于 Lock 对象和 synchronized 关键字的选择:?a.最好两个都不用,使用一种 java.util.concurrent 包提供的机制,能够帮助用户处理所有与锁相关的代码。?b.如果 synchronized 关键字能满足用户的需求,就用 synchronized,因为它能简化代码?

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
JAVA-Android-多线程实现方式及并发与同步,写给1-3年Android程序员的几点建议