Android的Java多线程和Synchronized学习总结

1,720 阅读16分钟
  • 线程的使用方式

1. 创建后台线程执行任务,大多数人(包括我)都会直接选择
new Thread()
//或者
new Thread(new Runnable())

之后用start()来启动线程。跟代码会发现start()会执行start0()这个native方法,虚拟机调用run方法。有Runnable就会调用传入的runnable的run()实现,否则就会执行Thread中的run()方法。

2. ThreadFactory:工厂模式,方便做一些统一的初始化操作:
ThreadFactory factory=new ThreadFactory(){
    @Override
    public Thread newThread(Runnable r){
        return new Thread(r);
    }
}

Runnable runnable =new Runnable(){
    //```
}

Thread thread=factory.newThread(runnable);
thread.start();
Thread thread1=factory.newThread(runnable);
thread1.start();
3. Executor:最常用到但是却很少用的方法:
Runnable runnable =new Runnable(){
    @Override
    public void run(){
        //```
    }
}

Executor executor=Executors.newCachedThreadPool();
executor.executor(runnable);
executor.executor(runnable);
(ExecutorService)executor.shutdown();

至少对于我,看起来是很少用,那为什么说最常用呢?
AsyncTask,Cursor,Rxjava其实也是使用Executor进行线程操作的。

可以来看一下Executor,只是一个接口来通过execute()来指定线程工作

public interface Executor {
    void execute(Runnable command);
}

对于Executor的扩展:ExecutorServive/Executors Executors.newCachedThreadPool()返回的又是一个ExecutorSevice extends Executor,对Executor做了一些扩展,主要关注的是shutdown()(保守型的结束)/shotdownNow()(立即结束),还有Future相关的submit(),这些后面会说。
newCachedThreadPool() 创建了一个带缓存的线程池,自动的进行线程的创建,缓存,回收操作。
还有几个别的方法:
newSingleThreadExecutor() 单一线程,用途较少。
newFixedThreadPool() 固定大小的线程池。 比如需要创建批量任务。 newScheduledThreadPool() 指定时间表,做个延时或者指定时间执行。
如果你想自定义线程池的时候就可以参考这几个方法在app初始化的时候直接new ThreadPoolExecutor了。 线程完成后结束:就可以直接添加任务后执行shutdown()。关于线程的结束,写在了后面的从及基本的启动/结束开始

ThreadPoolExecutor()的几个参数

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(
        0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,
        new SynchronousQueue<Runnable>());
    }
   
    public ThreadPoolExecutor(
    int corePoolSize,//初始线程池的大小/创建线程执行结束后回收到多少线程后不再回收
    int maximumPoolSize,//线程上线
    long keepAliveTime,//保持线程不被回收的等待时间
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,//阻塞队列
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler) {
    //```
    }

写到这里的时候,maximumPoolSize这个参数,想起之前在自己写一些图片加载、缓存的时候,开的线程总是会用CPU核心数来限制一下,比如2*CPU_CORE,以前不懂,会觉得大概是每个核分一个? 现在学到的:首先肯定不是为了一个核心来一个线程,毕竟一个cpu跑N多个线程,哪能就那么刚刚好一个核心一个。
大概是可以保证代码在不同的机器上的CPU调度积极性差不多,比如单核的,就创建两个线程,8核心的,就是16个线程。不然写8个线程,单核心机器运行可能就会比较卡,8核心机器运行又会太少。

4. callable 可以简单描述为有返回值的后台线程,安卓端比较少用,就简单记录下,毕竟AsyncTask、Handler、RxJava都比这个好用
     Callable callable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                try {
                    Thread.sleep(1500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return "找瓶子";
            }
        };
        ExecutorService executor = Executors.newCachedThreadPool();
        Future<String> future = executor.submit(callable);

        try {
            String result = future.get();
        } catch (ExecutionException | InterruptedException e) {
            e.printStackTrace();
        }

是不是看起来也不是很麻烦?甚至还有一丢丢好用? 但是这个Future会阻塞线程,如果在主线程中使用的话,就需要不停地来查看后台是否执行结束

     while (true) {
            if (future.isDone()){
                try {
                    String result = future.get();
                } catch (ExecutionException | InterruptedException e) {
                    e.printStackTrace();
                }
                try{
                    Thread.sleep(1000);//模拟主线程的任务,过一秒来看一看
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

ok,线程的常见使用方式就基本上说完了。使用的时候我们会经常遇到多个线程操作同一个资源的问题,比如A给B转账的同时,B,C又同时给A转账,那么应该怎么处理呢?这时候就要说到第二节的线程安全问题了。

  • 线程安全/同步问题:

1. 产生原因:

操作系统对于cpu使用的时间片机制,导致某段代码某个线程在执行中会被暂停,其他线程继续执行同一段代码/操作同一个数据(资源)的时候,可能带来的数据错误。
eg:

class Test{
    
    private int x=0;
    private int y=0;

    private void ifEquals(int val){
        x=val;
        y=val
        if(x!=y){
            System.out.println("x: "+x+",y: "+y);
        }
    }

    public void testThread(){
        new Thread(){
            public void run(){
                for(int i=0;i<1_000_000_000;i++){
                ifEquals(i);
                }
            }
        }.start();
        
        new Thread(){
            public void run(){
                for(int i=0;i<1_000_000_000;i++){
                    ifEquals(i);
                }
            }
            }.start();
        }

}

这还能不相等?
产生的原因其实简单,上面也简述过。就是在执行testThread的时候,两个线程同时操作ifEquals方法:

  1. 线程1在操作到i=10,当前x=val=10时候被切换线程,此时y=val还未被线程1进行赋值
  2. 线程2进行执行代码,当进行到x=100,y=100,然后线程再次切换
  3. 线程1执行y=10,此时x的值已经是线程2修改过的100了,就会导致x!=y。
2. 解决办法:Synchronized

那么知道了原因,解决的思路就变得简单,x,y这两步操作应该是变成一步操作即可。或者说一次ifEquals方法变成一步操作。所以JAVA提供了synchronized关键字,把这个关键字加在ifEquals这个方法,就使得其变成原子操作。 会对这个方法添加一个监视器Monitor这样在线程1未执行完毕的时候,monitor不会被释放,即使线程切换,线程2访问到这个方法的时候,由于monitor未被释放就会进入排队等待,不会执行这个方法。

关于Synchronized关键字:现在给ifEquals增加Synchronized关键以后,上述代码再增加下面这个delVal()方法在线程中调用,x,y的值会出现问题吗?依然会。
所以,看起来是对于方法的保护,实际上是对资源的保护,比如上面的例子,我们希望的其实并不是保护ifEquals方法,而是x,y的资源。

    private void delVal(int val){
        x=val-1;
        y=val-1;
    }
    
    String name;
    private Synchronized void setName(String val){
        name=val;
    }

3. 关于监视器Monitor

另一个问题就是在保护x/y的时候,同时也需要保护name的时候,如上对于两个方法都加上Synchronized的时候也不方便,此时会导致当一个线程只是访问到ifEquals方法的时候,另一个线程不能访问setName。这就与我们一个线程操作x、y,另一个线程同步操作name的预期不符。原因是对方法添加synchronized会把整个Test类对象当做Monitor进行监视,也就是这些方法都会被同一个monitor进行监视。

那为什么两个方法操作的不是同一个资源,还会被保护呢?因为monitor不会对方法进行检查,实际上我们synchronize方法的原因也不是为了让方法不能被另一个线程调用,而是为了保护资源。这时候,synchronize方法就不符合我们多线程操作的预期,就需要我们自己手动来进行操作。所以就需要引入Synchronized代码块。

    final Object numMonitor=new Object();
    final Object nameMonitor=new Object();
    //代码块
    private void ifEquals(int val){
        synchronize(this){
            x=val;
            y=val
            if(x!=y){
                System.out.println("x: "+x+",y: "+y);
            }
        }
    }
     //代码块
    private void delVal(int val){
         synchronize(this){
            x=val-1;
            y=val-1;
        }
    }
    
    //指定monitor
     private Synchronized void setName(String val){
     synchronize(nameMonitor){
            name=val;
        }
        
    }
    
   public void testThread(){
       //``````
   }

synchronize(Object object),允许你指定object作为monitor来进行监视,比如我们可以把上面的this,换成numMonitor,这个时候,name和x、y就是两个monitor进行,互不影响了。
synchronize的另一个作用:线程之间对于监视资源的数据同步

4.静态方法的synchronize

1.给方法加,默认的monitor就是当前的Test.Class,这个类,不是这个对象 2.代码块内部无法使用synchronize(this),应为这是静态方法并不存在这个this。可以使用synchronize(Test.Class)

5. volatile

private volatile int a;

  1. 比synchronized要轻,使得对的操作具有内存中的可见性(一个线程修改后会写入内存,使得其他线程可见),同步性。多个线程进行读写操作不会导致内存中的数据乱改
  2. 但是对于double、long,由于比较长,那么它本身不像int,其没有原子性
  3. 对于基本类型有效,对于对象,只有保证本身的赋值操作有效(Dog dog=new Dog(“wangwang”)有效,对于dog.name="miaomiao"无效 )
  4. 我们都知道(int)a++ 实际上就是两步操作,所以volatile 并不能保证a++的安全
    1. int temp=a+1;
    2. a=temp;

6. 保证a++使用AtomicXXX

AtomicInteger a=new ActomicInteger(0);
a.getAndIncrement();

7. Synchronize和运行速度

先解释下java虚拟机下面的数据操作:比如ifEquals这个方法,x、y读到内存中,并不是cpu直接操作内存中的数据,而是由cpu单独给线程一个存储空间进行操作。我们都知道,现在用的内存条速度,比起cpu的操作速度有着极大的速度差,就像硬盘和内存的巨大速度差一样,如果代码都是从硬盘执行,然后操作数据再写回硬盘,肯定无法忍受。对于cpu也是一样,ram的读写实在是太慢了,这时候就像使用内存来弥补速度差一样,使用cpu的高速cache,来弥补内存和cpu总线之间的速度差。 基于以上的描述

开始操作的时候,

  1. x=0,y=0;
  2. thread1进行:x=5,y=5结束(在线程的cpu cache中),还没有写入内存的时候,
  3. thread2进行数据读取,x=0,y=0;
synchronize关键字,就会保证cpu读取赋值以后,再写回内存中。来保证数据的正确

但是带来的结果就是,如果没有cpu缓存操作,这个x=5,y=5的操作会变得很慢。其实从上面的代码运行时间也能很明显的看出区别,testThread()方法的执行时间,在有没有对方法添加synchronize时会相差非常明显。所以虽然会带来线程之间的数据同步问题,当前的cache还是很有必要的。

8. 关于安全

很多时候我们在说安全,线程安全,网络安全,数据安全,但是其实这是不一样的“安全”:

Safety 保证不会被改错,改坏的安全,比如Thread Safety
Security 不被侵犯安全 https 的S

关于锁

死锁

死锁是我们最常遇到,或者是最常听到的一种锁。其实产生的原因也很简单,就是互相持有(对方的“钥匙”)导致互相等待:

    private Synchronized void setName(String val){
        synchronize(nameMonitor){
            name=val;
                synchronize(numMonitor){
                    x=val;
                    y=val
                    if(x!=y){
                        System.out.println("x: "+x+",y: "+y);
                    }
                } 
            }
        }
    }
    
    private void ifEquals(int val){
        synchronize(numMonitor){
            x=val;
            y=val
            if(x!=y){
            System.out.println("x: "+x+",y: "+y);
            
            synchronize(nameMonitor){
                    name="haha";
                } 
            }
        }
    }
    
     

Thread1 执行 ifEquals,numMonitor,然后cpu进行线程进行切换到Thread2 执行setName,持有nameMonitor,然后往下执行的时候,发现numMonitor被持有,Thread2进行等待,切换回Thread1,Thread1发现继续执行,但是nameMonitor被持有,进入等待,这样两个线程就变成了互相持有对方需要的monitor(钥匙)进入互相等待,也就是死锁。

乐观悲观锁

跟线程安全不是很相关的锁,更多的是,数据库相关,并不是线程相关。

比如数据库进行数据修改,需要先取出数据进行操作,再往里写,就会出现A操作写数据,B操作也写同一个数据,比如小明给我转账100,A操作出我的余额X+100,正要写入数据库:余额X+100,小王给我转账1并且先一步写入了X+1,此时如果A操作继续写入余额X+100就很明显是错误的了。
解决这个问题的方式两种:

  1. 悲观锁:对读写操作加锁,A操作进行结束之前,B操作进行等待。是不是跟synchronize操作看起来一样?
  2. 乐观锁:拿到数据的时候不加锁,A操作进行写入时,发现数据库数据已经跟取出时候有了变化,那么重新计算再写入。

读写锁lock

比如Test中,增加一个方法

    private Lock lock=new ReentrantLock();
    
    private void reset(){
        lock.lock();
        //```
        lock.unlock();
    }

但是如果在中间的方法中,出现了异常,后面的lock.unlock()无法执行,那就会导致一直被锁(Monitor遇到异常会自动解开),所以需要手动处理:

    private void reset(){
        lock.lock();
        try{
           //```  
        }finally{
            lock.unlock();  
        }
    }

看起来功能跟synchronized差不多?但是这么麻烦用你干啥?但是其实想一下,之前说线程同步的时候,都是在写数据的时候出现问题,单纯的读取数据并不会出现问题,只有在写入的时候,别人读写会导致出现问题,如果线程1读取数据中切换线程,线程2也不能读取,就会有性能的浪费,读写锁就可以解决这个问题:

    private ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
    private ReentrantReadWriteLock.ReadLock readLock=lock.readLock();
    private ReentrantReadWriteLock.WriteLock writeLock=lock.writeLock();
    
    private void ifEquals(int val){
        writeLock.lock();
        try{
            x=val;
            y=val;
        }finally{
            writeLock.unlock()
        }
    }
    
    private readData(){
        readLock.lock();
        try{
            System.out.println("x: "+x+",y: "+y);
        }finally{
            readLock.unlock();
    }
        
    }

这样,有线程在调用ifEquals()的时候,别人不能读也不能写,readData()的时候,别人能跟我一起读取数据。就有利于性能的提升。

  • 线程之间的交互

1. 从及基本的启动/结束开始:
    Thread thread = new Thread() {
        @Override
        public void run() {
            for (int i = 0; i < 1_000_000_000; i++) {// 模拟一段耗时操作
                Log.d("========>", "找汤圆");
            }
        }
    };
    thread.start();
    Thread.sleep(100);
    thread.stop();

这样就在主线程完成了子线程的开始和终止,就基本的交互就是这样。但是用的时候会发现,stop()。喵喵喵?不是很好用吗?为什么划线不建议用了呢?
因为不可控,Thread.stop会直接结束,而不管你内部正在进行什么操作(实际上主线程也确实不知道子线程在做什么),这样就带来了不可控性。
但是比如我明知道进行A操作以后,后面的线程做的工作已经无意义了,需要节省资源终止线程要怎么做呢? 用thread.interrupt(),但是这个interrupt只是做了一个标记,如果仅仅使用interrupt是没有任何作用的,还需要子线程自己根据这个中断状态进行操作:

    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 1_000_000_000; i++) {
                if (isInterrupted()) {//不改变interrupt状态
                // if(Thread.interrupted()) //这个方法会在使用之后重置interrupt的状态
                //做线程结束的收尾工作
                    return;
                }
                Log.d("========>", "找汤圆"+i);
            }
        }
    });
    thread.start();
    try {
        Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    thread.interrupt();

看到这个interrupt是不是会觉得这个词在线程操作中有点熟悉?哎(二声),这不是刚好是上面两行sleep的时候要catch的InterruptedException么?为什么我就想让线程sleep一下,要加入个try/catch呢?
原因有两个:

  1. sleep方法会检测当前的interrupt的状态,如果当前线程已经被interrupt,则会抛InterruptedException
  2. 因为我们在使用interrupt的时候,需要注意interrupt会直接唤醒sleep,重置interrupt的状态
    //对于Thread.interrupt()方法的部分注释
     * If this thread is blocked in an invocation of the wait() or join() or sleep(),
     * methods of this class, then its interrupt status will be cleared and it
     * will receive an {@link InterruptedException}.
     

所以中断线程的时候需要考虑一下进行处理:

    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 1_000_000_000; i++) {
                if (Thread.interrupted()) {//false,100毫秒后才会变成ture
                    //进行自己的interrupt处理
                    return;
                }
                try {
                  Thread.sleep(2000);//睡得时候被执行interrupt,会直接唤醒进入中断异常,
                    //如果不在下面catch进行处理,interrupt的值又会被重置,导致外部调用的interrupt相当于没有发生
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    //收尾工作
                    return;
                }
                Log.d("========>", "找汤圆"+i);
            }
        }
    });
    thread.start();
    try {
        Thread.sleep(100);//这里是主线程睡了,
        //跟上面的子线程的sleep不一样哦,只是为了模拟start()和interrupt之间中间有个时间差
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    thread.interrupt();

那所以安卓中我只是想单纯的让线程sleep一下,不需要外部interrupt时候提供支持,不想写try/catch不行吗?

SystemClock.sleep(100);//小哥哥,了解一下这个
2. wait(),join(),yield()的使用(Android较少用到):
  1. wait(),比如上面讲到的死锁中,存在的情况,双方互相需要操作资源的时候发现monitor不在手里,wait总是要和个synchronized一起出现,因为wait出现就是为了使用共同的资源
String name=null;

private synchronized void setName(){
    name="汤圆";
}

private synchronized String getName(){
    return name;
}

private void main(){
    new Thread() {
        @Override
        public void run() {
        //一些操作
        setName();
        }
    }.start();    
    new Thread() {
        @Override
        public void run() {
        //一些操作
        getname();
        }
    }.start();   
    
}

由于两个Thread不知道谁先执行完,所以可能出现getName先执行,但是getName获取空的话又没法进行操作,这时候怎么办呢?

private synchronized String getName(){
    while(name==null){}//一直干等着,直到name不为空
    return name;
}

但是这是个synchronized方法又会持有跟setname一样的monitor,setName也被锁住了成了死锁,那怎么做?

private synchronized String getName(){
    while(name==null){//使用wait的标准配套就是while判断,而不是if,因为wait会被interrupt唤醒
    try{
        wait();//object方法
    }catch(InterruptedException e){
        //
        e.printStackTrace();
    }
    }//干等着,直到不为空
    return name;
}
private synchronized void setName(){
    name="汤圆";
    notifyAll();//object方法,把monitor上的所有在等待的线程全部唤醒去看一下是否满足执行条件了
}

或者是进入页面,需要请求多个接口,根据接口的数据来设置页面设置数据,也是类似的情况。不过实际上一个Rxjava的zip操作就能解决大多数问题了。 实际上写了这么多,这种需求Rxjava的zip操作就解决了...

  1. join() 可能会存在一个thread1在执行的过程中,需要thread0来执行,等完全结束后再继续执行该线程
private void main(){
    new Thread() {
        @Override
        public void run() {
        //一些操作
        try{
            thread0.join();//相当于自动notify的wait
            //Thread.yield();
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        
        //一些操作
        getname();
        }
    };   
}

  1. yield() 极少用,了解就行了,参考上面注释掉的那行代码,让出本次的cpu执行时间片给同优先级的线程。

感谢&参考:扔物线