注册

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

开启线程方式:

//方式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会被线程中断,而跳出同步逻辑,如果需要可以使用抗扰动写法:

            while (shareData == null) {//4 抗线程扰动写法
    synchronized (lock) {
    try {
    lock.wait();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
  4. 业务销毁的时候,如果不需要等数据返回,可以直接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): 生产者与消费者模式里,生产者与消费者解耦,生产者与消费者性能均衡问题

BlockingQueue
ArrayBlockingQueue
数组结构的有界阻塞队列
LinkedBlockingQueue
链表结构的有界阻塞队列
PriorityBlockingQueue
优先级排序无界阻塞队列
DelayQueue
优先级队列无界阻塞队列
应用--过期缓存
SynchronousQueue
不存储元素的阻塞队列
应用--newCachedThreadPool
LinkedTransferQueue
链表结构无界阻塞队列
LinkedBlockingDeque
链表结构双向阻塞队列

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


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

创建:

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

拒绝策略(饱和策略):

RejectedExecutionHandler
AbortPolicy
直接抛出异常-默认
CallerRunsPolicy
调用者线程执行
DiscardOldestPolicy
丢弃旧的
DiscardPolicy
直接丢弃

关闭:

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

0 个评论

要回复文章请先登录注册