资讯专栏INFORMATION COLUMN

Java多线程之线程安全与异步执行

taoszu / 341人阅读

摘要:同步包装器任何集合类使用同步包装器都会变成线程安全的,会将集合的方法使用锁加以保护,保证线程的安全访问。线程池中的线程执行完毕并不会马上死亡,而是在池中准备为下一个请求提供服务。

多线程并发修改一个数据结构,很容易破坏这个数据结构,如散列表。锁能够保护共享数据结构,但选择线程安全的实现更好更容易,如阻塞队列就是线程安全的集合。

线程安全的集合

VectorHashTable类提供了线程安全的动态数组和散列表,而ArrayListHashMap却不是线程安全的。

java.util.concurrent包提供了映射表、有序集、队列的高效实现,如:

ConcurrentLinkedQueue:多线程安全访问,无边界,非阻塞,队列;

ConcurrentHashMap:多线程安全访问,散列映射表,初始容量默认16,调整因子默认0.75。

并发的散列映射表ConcurrentHashMap提供原子性的关联插入putIfAbsent(key, value)和关联删除removeIfPresent(key, value)。写数组的拷贝CopyOnWriteArrayListCopyOnWriteArraySet是线程安全的集合,所有的修改线程会对底层数组进行复制。对于经常被修改的数据列表,使用同步的ArrayList性能胜过CopyOnWriteArrayList

对于线程安全的集合,返回的是弱一致性的迭代器:

迭代器不一定能反映出构造后的所有修改;

迭代器不会将同一个值返回两次;

迭代器不会抛出ConcurrentModificationException异常。

通常线程安全的集合能够高效的支持大量的读者和一定数量的写者,当写者线程数目大于设定值时,后来的写者线程会被暂时阻塞。而对于大多数线程安全的集合,size()方法一般无法在常量时间完成,一般需要遍历整个集合才能确定大小。

同步包装器

任何集合类使用同步包装器都会变成线程安全的,会将集合的方法使用锁加以保护,保证线程的安全访问。使用同步包装器时要确保没有任何线程通过原始的非同步方法访问数据结构,也可以说确保不存在任何指向原始对象的引用,可以采用下面构造一个集合并立即传递给包装器的方法定义。

List synchArrayList = Collections.synchronizedList(new ArrayList());
Map synchHashMap = Collections.synchronizedMap(new HashMap());

当然最好使用java.util.concurrent包中定义的集合,同步包装器并没有太多安全和性能上的优势。

Callable与Future

CallableRunnable类似,都可以封装一个异步执行的任务,但是Callable有返回值。Callabele接口是一个参数化的类型,只有一个方法call(),类型参数就是返回值的类型。Future用来保存异步计算的结果,用get()方法获取结果。get()方法的调用会被阻塞,直到计算完成。有超时参数的get()方法超时时会抛出TimeoutException异常。

FutureTask可将Callable转换成FutureRunnable,实现了两者的接口。

Callable myComputation = new MyComputationCallable();
FutureTask task = new FutureTask(myComputation);
Thread t = new Thread(task);  // it"s a Runnable
t.start();
Integer result = task.get();  // it"s a Future

这里有一个计算指定目录及其子目录下与关键字匹配的文件数目的例子,涉及到CallableFutureTaskFuture的使用。

public Integer call() {
    count = 0;
    try {
        File [] files = directory.listFiles();
        List> results = new ArrayList<>();

        for (File file : files) {
            if (file.isDirectory()) {
                MatchCounter counter = new MatchCounter(file, keyword);
                FutureTask task = new FutureTask<>(counter);
                results.add(task);
                Thread t = new Thread(task);
                t.start();
            } else {
                if (search(file)) {
                    count++;
                }
            }
        }

        for (Future result : results) {
            try {
                count += result.get();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    } catch (InterruptedException e) {
        ;
    }
    return count;
}
线程池

构建一个新的线程是有代价的,涉及到与操作系统的交互。对于程序中需要创建大量生命期很短的线程,应该使用线程池。线程池中的线程执行完毕并不会马上死亡,而是在池中准备为下一个请求提供服务。当然使用线程池还可以限制并发线程的数目。

需要调用执行器Executors的静态工厂方法来构建线程池,下面的方法返回的是ExecutorService接口的ThreadPoolExecutor类的对象。

Executors.newCachedThreadPool:线程空闲60秒后终止,若有空闲线程立即执行任务,若无则创建新线程。

Executors.newFixedThreadPool:池中线程数由参数指定,固定大小,剩余任务放置在队列。

使用submit()方法,将Runnable对象或Callable对象提交给线程池ExecutorService,任务何时执行由线程池决定。调用submit()方法,会返回一个Future对象,用来查询任务状态或结果。当用完线程池时,要记得调用shutdown()关闭,会在所有任务执行完后彻底关闭。类似的调用shutdownNow,可取消尚未开始的任务并试图终端正在运行的线程。

线程池的使用步骤大致如下:

调用Executors类的静态方法newCachedThreadPool()newFixedThreadPool()

调用submit()提交RunnableCallable对象;

如果提交Callable对象,就要保存好返回的Future对象;

线程池用完时,调用shutdown()

对于之前提到的计算文件匹配数的例子,需要产生大量生命期很多的线程,可以使用一个线程池来运行任务,完整代码在这里。

public Integer call() {
    count = 0;
    try {
        File [] files = directory.listFiles();
        List> results = new ArrayList<>();
        for (File file : files) {
            if (file.isDirectory()) {
                MatchCounter counter = new MatchCounter(file, keyword, pool);
                Future result = pool.submit(counter);
                results.add(result);
            } else {
                if (search(file)) {
                    count++;
                }
            }
        }
        for (Future result : results) {
            try {
                count += result.get();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    } catch (InterruptedException e) {
        ;
    }
    return count;
}
Fork-Join框架

对于多线程程序,有些应用使用了大量线程,但其中大多数都是空闲的。还有些应用需要完成计算密集型任务,Fork-Join框架专门用来支持这类任务。使用Fork-Join框架解决思路大致是分治的思想,采用递归计算再合并结果。只需继承RecursiveTask类,并覆盖compute()方法。invokeAll()方法接收很多任务并阻塞,直到这些任务完成,join()方法将生成结果。

对于问题,统计数组中满足某特性的元素个数,使用Fork-Join框架是很合适的。

import java.util.concurrent.*;

public class ForkJoinTest {
    public static void main(String [] args) {
        final int SIZE = 10000000;
        double [] numbers = new double[SIZE];
        for (int i = 0; i < SIZE; i++) {
            numbers[i] = Math.random();
        }
        Counter counter = new Counter(numbers, 0, numbers.length, new Filter() {
            public boolean accept(double x) {
                return x > 0.5;
            }
        });
        ForkJoinPool pool = new ForkJoinPool();
        pool.invoke(counter);
        System.out.println(counter.join());
    }
}

interface Filter {
    boolean accept(double t);
}

class Counter extends RecursiveTask {
    private final int THRESHOLD = 1000;
    private double [] values;
    private int from;
    private int to;
    private Filter filter;

    public Counter(double [] values, int from, int to, Filter filter) {
        this.values = values;
        this.from = from;
        this.to = to;
        this.filter = filter;
    }

    public Integer compute() {
        if (to - from < THRESHOLD) {
            int count = 0;
            for (int i = from; i < to; i++) {
                if (filter.accept(values[i])) {
                    count++;
                }
            }
            return count;
        } else {
            int mid = (from + to) / 2;
            Counter first = new Counter(values, from, mid, filter);
            Counter second = new Counter(values, mid, to, filter);
            invokeAll(first, second);
            return first.join() + second.join();
        }
    }
}

另外,Fork-Join框架使用工作密取来平衡可用线程的工作负载,比手工多线程强多了。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/65766.html

相关文章

  • Java 并发编程系列带你了解线程

    摘要:的内置锁是一种互斥锁,意味着最多只有一个线程能持有这种锁。使用方式如下使用显示锁之前,解决多线程共享对象访问的机制只有和。后面会陆续的补充并发编程系列的文章。 早期的计算机不包含操作系统,它们从头到尾执行一个程序,这个程序可以访问计算机中的所有资源。在这种情况下,每次都只能运行一个程序,对于昂贵的计算机资源来说是一种严重的浪费。 操作系统出现后,计算机可以运行多个程序,不同的程序在单独...

    Elle 评论0 收藏0
  • 后端好书阅读推荐

    摘要:后端好书阅读与推荐这一两年来养成了买书看书的习惯,陆陆续续也买了几十本书了,但是一直没有养成一个天天看书的习惯。高级程序设计高级程序设计第版豆瓣有人可能会有疑问,后端为啥要学呢其实就是为了更好的使用做铺垫。 后端好书阅读与推荐 这一两年来养成了买书看书的习惯,陆陆续续也买了几十本书了,但是一直没有养成一个天天看书的习惯。今天突然想要做个决定:每天至少花1-3小时用来看书。这里我准备把这...

    clasnake 评论0 收藏0
  • 后端好书阅读推荐

    摘要:后端好书阅读与推荐这一两年来养成了买书看书的习惯,陆陆续续也买了几十本书了,但是一直没有养成一个天天看书的习惯。高级程序设计高级程序设计第版豆瓣有人可能会有疑问,后端为啥要学呢其实就是为了更好的使用做铺垫。 后端好书阅读与推荐 这一两年来养成了买书看书的习惯,陆陆续续也买了几十本书了,但是一直没有养成一个天天看书的习惯。今天突然想要做个决定:每天至少花1-3小时用来看书。这里我准备把这...

    Juven 评论0 收藏0
  • 高并发 - 收藏集 - 掘金

    摘要:在中一般来说通过来创建所需要的线程池,如高并发原理初探后端掘金阅前热身为了更加形象的说明同步异步阻塞非阻塞,我们以小明去买奶茶为例。 AbstractQueuedSynchronizer 超详细原理解析 - 后端 - 掘金今天我们来研究学习一下AbstractQueuedSynchronizer类的相关原理,java.util.concurrent包中很多类都依赖于这个类所提供的队列式...

    levius 评论0 收藏0
  • 高并发 - 收藏集 - 掘金

    摘要:在中一般来说通过来创建所需要的线程池,如高并发原理初探后端掘金阅前热身为了更加形象的说明同步异步阻塞非阻塞,我们以小明去买奶茶为例。 AbstractQueuedSynchronizer 超详细原理解析 - 后端 - 掘金今天我们来研究学习一下AbstractQueuedSynchronizer类的相关原理,java.util.concurrent包中很多类都依赖于这个类所提供的队列式...

    fantix 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<