Android 中java多线程编程及注意事项

373 阅读2分钟

开启线程方式:

//方式1
public class MyThread extends Thread{
    @Override
    public void run() {
        super.run();
        //do my work
    }
}
new MyThread().start();

//方式2:
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        //do my work
    }
}
new Thread(new MyRunnable()).start();

//方式3
class MyCallable implements Callable<String> {   
  @Override      
  public String call() throws Exception {      
    return "result";
  }
}
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
new Thread(futureTask).start();

当然还有一些 HandlerThread、ThreadPool、AsyncTask 等也可以开启新线程,都是基于Thread的封装


线程状态:

image-20210829004506607.png

等待和阻塞的区别:简单理解,等待是线程判断条件不满足,主动调用指令进入的一种状态;而阻塞是被动进入,而且只有synchronized关键字修饰时可能触发。

还一种说法:将IO、sleep、synchronized等进入的线程block状态称为阻塞,他们的共同点是让出cpu,但不释放已经拿到的锁,参考:blog.csdn.net/weixin_3104…


线程安全

不共享数据:ThreadLocal

        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocal.set("线程1数据");
                //do some work ...

                String data = threadLocal.get();
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocal.set("线程2数据");
                //do some work ...

                String data = threadLocal.get();
            }
        });

原理:

没个线程里都有一个threadlocalmap数组, 调用threadlocalset方法时,以threadlocal变量hash值做key,将对于value存入数组中。 image-20210829011508921.png

共享数据:

1.jvm关键字 synchronized, 注:非公平、可重入

    private List<Integer> shareData = new ArrayList<>();
    
    public void trySync(int data) {
        synchronized (this) {//锁TestSyncData对象, thread1, thread2 ...
            //操作共享数据
            shareData.add(data);
        }
        //do something later ...
    }

注意坑点:锁范围,存活周期不一样

    public void trySync1() {
        synchronized (this) {//锁TestSyncData对象
        }
    }
    //锁TestSyncData对象
    public synchronized void trySync2() {
    }

    //锁TestSyncData类
    public synchronized static void trySync3() {
    }
    
    //锁TestSyncData类
    public  void trySync4() {
        synchronized (TestSyncData.class){
        }
    }

有耗时任务获取锁,后续收尾处理一定要考虑锁释放耗时问题;比如单例对象在主线程释放时,一定要注意锁能否及时拿到。

如果不能确定,考虑将锁降级存活时长,比如用栈内锁,线程安全型bean

synchronized原理详见:blog.csdn.net/weixin_3960…

2.wait-notify 函数,java祖先类Object的成员函数

    public void testWait() {
        //1.创建同步对象
        Object lock = new Object();
      
        new Thread(new Runnable() {
            @Override
            public void run() {
                //2. do something hard ...
                shareData = new ShareData();
                
                //3. 唤醒原线程
                synchronized (lock){
                    lock.notify();
                }
                //5.some other logic
            }
        }).start();
        
        //4.原线程等待
        synchronized (lock){
            try {
                lock.wait(45000);
            } catch (InterruptedException e) {
                //6.线程中断触发
                e.printStackTrace();
            }
        }
        
        if (shareData != null) {
            //do something happy ...
        }
    }

坑点多多:

  1. notify不能定向唤醒,只能随机唤醒一个wait的线程(保证notify的数量>=wait数量),使用的时候一定要保证没有多个线程处于wait状态;如果想定向唤醒,考虑使用 ReentrantLock的Condition

  2. 标记5的地方最好不要做其他逻辑,可能不会执行到,尽量保证notify是耗时任务里的最后逻辑

  3. 标记6的地方注意wait会被线程中断,而跳出同步逻辑,如果需要可以使用抗扰动写法:

         synchronized (lock) {
             while (shareData == null) {//4 抗线程扰动写法
                 try {
                     lock.wait();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
         }
  1. 业务销毁的时候,如果不需要等数据返回,可以直接notifyAll,提前结束线程任务,释放对象

3.基于aqs(队列同步器)接口的一系列同步锁或组件 ReentrantLock、Semaphore、CountDownLantch;通过队列+计数+CAS

    private ReentrantLock lock = new ReentrantLock();
    private List<Integer> shareData = new ArrayList<>();

    public void testLock() {//非标准
        lock.lock();
        shareData.add(1);
        lock.unlock();
    }

坑点:注意手动释放,以免死锁; 同步逻辑会有exception,标准最好用try-catch-finally


线程安全的数据类型:StringBuffer、CopyOnWriteArrayList、concurrentxxx、BlockingQueue、Atomicxxx(CAS)

有些基于锁,有些基于无锁化的CAS(compare and swap),有些两者混合

cas原理参考:cloud.tencent.com/developer/a…

image-20210829040630653.png

坑点:

  1. CopyOnWriteArrayList:先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加删除元素,添加删除完元素之后,再将原容器的引用指向新的容器,整个过程加锁,保证了写的线程安全。然而我们还是出现了多线程的数组越界异常
        //不安全写法
        for (int i = 0; i < copyOnWriteArrayList.size(); i++) {
            copyOnWriteArrayList.get(i);
        }

				//安全写法
        Iterator iterator = copyOnWriteArrayList.iterator();
        while (iterator.hasNext()){
            iterator.next();
        }
  1. Atomicxxx 注意使用场景

    ABA问题   AtomicStampedReference,AtomicMarkableReference
    开销问题
    只能保证一个共享变量的原子操作  AtomicReference
    

阻塞队列(BlockingQueue): 生产者与消费者模式里,生产者与消费者解耦,生产者与消费者性能均衡问题

graph LR
A(BlockingQueue)-->B1(ArrayBlockingQueue)-->C1(数组结构的有界阻塞队列)
A-->B2(LinkedBlockingQueue)-->C2(链表结构的有界阻塞队列)
A-->B3(PriorityBlockingQueue)-->C3(优先级排序无界阻塞队列)
A-->B4(DelayQueue)-->C4(优先级队列无界阻塞队列)-->D4(应用--过期缓存)
A-->B5(SynchronousQueue)-->C5(不存储元素的阻塞队列)-->D5(应用--newCachedThreadPool)
A-->B6(LinkedTransferQueue)-->C6(链表结构无界阻塞队列)
A-->B7(LinkedBlockingDeque)-->C7(链表结构双向阻塞队列)

有界:定义最大容量 无界:不定义


线程池:线程管理与统一调度

创建:

ThreadPoolExecutor(int corePoolSize,    //核心线程数
                   int maximumPoolSize, //最大线程数
                   long keepAliveTime,  //空闲线程存活时间
                   TimeUnit unit,
                   BlockingQueue<Runnable> workQueue, //阻塞队列
                   ThreadFactory threadFactory,       //线程池工厂
                   RejectedExecutionHandler handler)  //拒绝策略

线程比较稀缺,需合理配置,根据任务特性

cpu密集型:内存中取数据计算 (最大线程数 <=Runtime.getRuntime().availableProcessors()+1) 注:+1 虚拟内存-页缺失

IO密集型:网络通信、读写磁盘 (最大线程数 <= cpu核心*2), 高阻塞低占用

混合型:以上两者,如果两者执行时间相当,拆分线程池;否则视权重大的分配。具体时长可以本地测试或者根据下面的经验表预估后选择 image-20210828234846390-0165731.png

执行:注意任务存放位置顺序 1、2、3、4(重点)

image-20210828225601263.png

拒绝策略(饱和策略):

graph LR

A(RejectedExecutionHandler)-->B1(AbortPolicy)-->C1(直接抛出异常-默认)
A-->B2(CallerRunsPolicy)-->C2(调用者线程执行)
A-->B3(DiscardOldestPolicy)-->C3(丢弃旧的)
A-->B4(DiscardPolicy)-->C4(直接丢弃)

关闭:

awaitTermination(long timeout, TimeUnit unit)//阻塞
shutDown()//中断没有执行任务的线程
shutdownNow()//中断所有线程,协作式处理,只是发出中断信号
isShutdown()//是否关闭