`
randy.chen
  • 浏览: 8598 次
  • 性别: Icon_minigender_1
  • 来自: 上海
最近访客 更多访客>>
社区版块
存档分类
最新评论

多线程开发注意事项

    博客分类:
  • JAVA
阅读更多
设计线程安全的类需要 考虑哪些因素?
1) 找出哪些变量 属于对象的状态
2) 找出哪些不变量 属于对象的状态
3) 使用合适的并发策略来管理对状态的访问

考虑线程安全的需求
1) 同步范围多大? 整个方法? 一个大块? 小块?
2) 有哪些限制和先决条件?

java内建的监视器模型, 通过锁定, 即在锁对象添加监视器, 一旦锁定释放, 监视器通知其他等待的线程. Object.wait/notity/notifyAll

"代理"线程安全性
如果一个类合理的使用了一个已经实现线程安全的类, 就是把线程安全性交给内部对象"代理"了.
JDK提供一些线程安全的类, 如Vector, HashTable, ConcurrentHashMap, Collections.concurrentXXX(Obj) , BlockingQueue等. 例子:

public class DelegatingVehicleTracker {
    private final ConcurrentMap<String, Point> locations;
    private final Map<String, Point> unmodifiableMap;

    public DelegatingVehicleTracker(Map<String, Point> points) {
        locations = new
ConcurrentHashMap <String, Point>(points);
        unmodifiableMap =
Collections.unmodifiableMap (locations);
    }

    public Map<String, Point> getLocations() {
        return unmodifiableMap;
    }

    public Point getLocation(String id) {
        return locations.get(id);
    }

    public void setLocation(String id, int x, int y) {
        if (locations.replace(id, new Point(x, y)) == null)
            throw new IllegalArgumentException(
                "invalid vehicle name: " + id);
    }
}


对于只有单个需要代理的对象, 可以很方便的应用上述的"代理"方式, 对于多个相互独立的对象而已, 也可以使用
public class VisualComponent {
    private final List<KeyListener> keyListeners = new CopyOnWriteArrayList<KeyListener>();
    private final List<MouseListener> mouseListeners = new CopyOnWriteArrayList<MouseListener>();
    public void add(..);
    public void remove(...);
}


但是如果这些对象有相互关系, 就出问题了
public class NumberRange {
    // INVARIANT: lower <= upper

    private final AtomicInteger lower = new AtomicInteger(0);
    private final AtomicInteger upper = new AtomicInteger(0);

    public void setLower(int i) {
        // Warning -- unsafe check-then-act

        if (i > upper.get())
            throw new IllegalArgumentException(
                    "can't set lower to " + i + " > upper");
        lower.set(i);
    }

    public void setUpper(int i) {
        // Warning -- unsafe check-then-act

        if (i < lower.get())
            throw new IllegalArgumentException(
                    "can't set upper to " + i + " < lower");
        upper.set(i);
    }

    public boolean isInRange(int i) {
        return (i >= lower.get() && i <= upper.get());
    }
}

}
显然setLower, setUpper, isInRange三个方法的调用没有原子性保障, 而且isInRange同时依赖到两个相关的值, 很容易导致结果错误. 需要增加某些锁定来保证. 或者运行的情况下, 把两个对象合并为一个

如何给线程安全的类增加方法
1) 修改源代码, 最好. 需了解原来的线程安全的策略
2) 继承类, 并增加synchronized方法.
3) 用Decorator模式, 把类包起来, 增加新方法, 然而需要注意锁定的对象. 比如下面是不对的
public class ListHelper<E> {
   
//锁定对象为返回的list
    public List<E> list = Collections.synchronizedList(new ArrayList<E>());
    ...
    public synchronized boolean putIfAbsent(E x) {
//锁定对象为ListHelper对象
        boolean absent = !list.contains(x);
        if (absent)
            list.add(x);
        return absent;
    }
}

因为两个同步的锁定不是同一个对象, 因此不同的线程可以分别操作putIfAbsent方法和list对象固有的方法. 如果要让这个类正确运行, 需要修正如下
public class ListHelper<E> {
   
//锁定对象为list
    public List<E> list =Collections.synchronizedList(new ArrayList<E>());
    ...
    public boolean putIfAbsent(E x) {
        synchronized (list) {
//锁定对象为list
            boolean absent = !list.contains(x);
            if (absent)
                list.add(x);
            return absent;
        }
    }
}


使用synchronized collection的原子性的问题
比如下面的代码:
public class VectorHelper{
public static Object getLast(Vector list) {
    int lastIndex = list.size() - 1;
    return list.get(lastIndex);
}

public static void deleteLast(Vector list) {
    int lastIndex = list.size() - 1;
    list.remove(lastIndex);
}
}
非常简单, 很多代码都是这样写的, 但是, 这段代码如果要正常运行, 必须是在假设Vertor对象是不被多个线程共享的情况下. 因为虽然Vector本身是线程安全的, 但VectorHelper不是, 并且getLast和deleteLast同时依赖于Vector.size()来判断最后一个元素. 很容易造成ArrayIndexOutOfBoundsException. 如果Vector被多个线程共享, 最简单的就是加上同步, 然后对一个集合的同步会带来很大的性能代价, 因为阻止了其他线程对集合的访问, 特别是当集合很大并且处理的任务非常大的情况下.

另一种变通的方法是, 在遍历集合之前, 复制一份集合的引用. 当然集合复制也有细微的性能代价.

被隐藏的Iterator
public class HiddenIterator {
    @GuardedBy("this")
    private final Set<Integer> set = new HashSet<Integer>();

    public synchronized void add(Integer i) { set.add(i); }
    public synchronized void remove(Integer i) { set.remove(i); }

    public void addTenThings() {
        Random r = new Random();
        for (int i = 0; i < 10; i++)
            add(r.nextInt());
        System.out.println("DEBUG: added ten elements to " + set);

   }
}
能想象这么简单的代码会在什么地方出错么? 答案是最后一行, System.out.println, 因为用到的set.toString(), 而toString()内部是会用Iterator来访问set的, 一旦此过程中set被修改, 就抛出 ConcurrentModificationException .

JDK5的并发集合
ConcurrentMap (接口)和 ConcurrentHashMap, 并发环境下HashMap的替代品
CopyOnWriteArrayList, 并发环境下List的替代品
Queue:
ConcurrentLinkedQueue , PriorityQueue(非并发) . 不阻塞, 如果队列为空, get()返回null
BlockingQueue: 它的方法是阻塞的, 当队列为空, get()会阻塞直到队列有元素加入, 当队列满脸, insert()也会阻塞

ConcurrentHashMap使用一种
lock striping 的机制来实现并发控制, 并且提供了弱一致性的Iterator, 允许在迭代期间被修改而不抛出ConcurrentModificationException(例 如,忽略被删除的元素). 然而作为代价, size()返回的值不是精确的元素的数量, 不过size()在并发环境的作用比get, put, containsKey的用途小得多

看一小段程序
        ConcurrentHashMap<String, String> m = new ConcurrentHashMap<String, String>();
        m.put("1", "one");
        m.put("2", "two");
        m.put("3", "three");
       
        Iterator keyIt = m.keySet().iterator();
       
        // !! remove some element, remove-if-equals 风格的操作
        m.remove("1", "one");
       
        while (keyIt.hasNext()) {
            String k = (String)keyIt.next();
            System.out.println("key: " + k + ", value: " + m.get(k));
        }

输出结果:
key: 1, value: null
key: 3, value: three
key: 2, value: two

很短的代码, 一下执行完了, 没有再报错, 可以发现remove操作没有影响到遍历抛出异常. 然而, key: 1, value: null 这行输出说明了我们还留着对这个key的引用.

ConcurrentHashMap有几种新的风格的方法, put-if-absent, remove-if-equal, replace-if-equal, 比如上面的 m.remove("1", "one"); 就是, 如果改为 m.remove("1", "two"); 那么["1","one"]这一对就被保留下来了

CopyOnWriteArrayList, 顾名思义, 就能猜到每次都这个类做了改动, 就会copy一份原来的数组, 在改动后替换原来的数组, 再返回给调用者

BlockingQueue是基于 生产者-消费者 的模式构建的, 包括LinkedBlockingQueue, ArrayBlockingQueue, PriorityBlockingQueue,SynchronousQueue 4个实现类. 看一点示例代码:
    public static void putAndOffer() throws InterruptedException {
        BlockingQueue<String> q = new ArrayBlockingQueue<String>(1);
        q.offer("1");
        System.out.println("offer A done");
        q.offer("2");
        System.out.println("offer B done");
       
        q.clear();
        q.put("A");
        System.out.println("put A done");
        q.put("B");
        System.out.println("put B done");
    }

输出结果:
offer A done
offer B done
put A done

我们指定了BlockingQueue的容量为1, offer方法是不阻塞的, 所以q.offer("2");直接返回false, 而put方法是阻塞的, 所以System.out.println("put B done");一直没有执行.

PriorityBlockingQueue: 对于加入队列的元素是可排序的
SynchronousQueue : 不能算是queue的实现, 但是模仿了queue的行为, 更像是worker模式的实现. 因为它内部维护了一组用于处理元素的线程, 并且直接把加入队列的元素分配给处理该元素的线程. 如果没有可用线程, 就阻塞了.

BlockingQueue实例代码
下面是摘录的代码, 使用BlockingQueue, 分配N个线程执行文件索引的任务, 很经典:
public class FileCrawler implements Runnable
 { //这个是producer角色
    private final BlockingQueue<File> fileQueue
;
    private final FileFilter fileFilter;
    private final File root;
    ...
    public FileCrawler(File root, BlockingQueue queue, FileFilter filter){ ... }
    public void run() {
        try {
            crawl(root);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    /*把该目录下所有的文件夹都放进一个queue, 如果满了, 就阻塞*/
    private void crawl(File root) throws InterruptedException { 
        File[] entries = root.listFiles(fileFilter);
        if (entries != null) {
            for (File entry : entries)
                if (entry.isDirectory())
                    crawl(entry);
                else if (!alreadyIndexed(entry))
                    fileQueue.put(entry)
; //把找到的目录放入队列
        }
    }
}

public class Indexer implements Runnable
 { //这个是consumer角色
    private final BlockingQueue<File> queue;

    public Indexer(BlockingQueue<File> queue
) { // queue在indexer和FileCrawler之间共享
        this.queue = queue;
    }

    public void run() {
        try {
            while (true)
                indexFile(queue.take()); //从queue中获取一个File对象进行索引
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

//这个是客户端的方法, 可以假设roots是在界面设置的几个目录
public static void startIndexing(File[] roots) {
BlockingQueue<File> queue = new LinkedBlockingQueue<File>(BOUND);
    FileFilter filter = new FileFilter() {
        public boolean accept(File file) { return true; }
    };

    for (File root : roots)
        new Thread(new FileCrawler(queue
, filter, root)).start(); 
        //此处是否可以考虑使用线程池更有效? 而且, 如果有些目录层次非常深, 
        //就会有某些线程运行时间非常长, 相反有些线程非常快就执行完毕.
        //最恶劣的情况是可能其他线程都完成了, 而退化到只有一个线程中运行,
        //成为"单线程"程序?


    for (int i = 0; i < N_CONSUMERS; i++)
        new Thread(new Indexer(queue
)).start();  //此处是否可以考虑使用线程池更有效?

}

基于上述这种"单线程"的考虑, 不谋而合的, JDK6引入了一种"Work Stealing"的模式, 即N个线程中运行, 
每个线程处理自己的事情, 一旦自己手头的事情处理, 那么就去尝试"偷"来其他线程的任务来运行. 这样一来, 
系统就会时刻保持多个线程中处理任务, 而不是出现"一人忙活,大家凉快"的情况
分享到:
评论

相关推荐

    易语言-多线程开发框架 1.1.1版本

    在这个"易语言-多线程开发框架 1.1.1版本"中,我们关注的是如何在易语言中实现多线程编程,以及该框架提供的功能和组件。 多线程技术是现代软件开发中的一个重要概念,它允许程序同时执行多个任务,提高了程序的...

    spring boot中多线程开发的注意事项总结

    在Spring Boot中,多线程开发是提升应用性能和并发能力的重要手段。Spring Boot通过`TaskExecutor`接口提供了一种方便的方式来实现多线程和并发编程。`TaskExecutor`允许我们定义一个线程池,有效地管理和调度线程...

    易语言多线程传递多参数

    通过阅读源码,我们可以学习到具体的操作步骤和注意事项,例如线程同步、异常处理等。 总之,易语言多线程传递多参数是一项关键的编程技能,它涉及到线程的创建、管理和参数传递。理解并掌握这些知识,能够帮助...

    易语言源码多线程类源码.rar

    6. 多线程的挑战与注意事项:虽然多线程带来了性能提升,但同时也引入了复杂性,比如数据一致性问题、竞态条件、死锁等。开发者在使用多线程时需要注意避免这些问题,合理规划线程的生命周期,以及合理使用同步机制...

    Java开发注意事项

    在深入探讨Java开发注意事项之前,我们首先应当澄清,给定的部分内容似乎包含了非文本或乱码信息,这可能源于文档转换或编码问题。不过,基于标题“Java开发注意事项”及描述“Java开发注意事项,编程人员可以看看哦...

    delphi2010多线程编程

    Delphi2010多线程编程教程旨在帮助开发者快速掌握多线程编程的要领,包括基础知识、使用TThread类和CreateThread函数实现多线程、注意事项和实例代码分析等内容。本教程适合初学者和有经验的开发者,旨在帮助他们...

    12.1 Qt5多线程:多线程及简单实例

    在编程领域,尤其是在开发高效、响应迅速的应用程序时,多线程技术扮演着至关重要的角色。Qt5框架提供了一种方便的方式来实现多线程,它允许开发者在不同的线程中执行任务,从而避免主线程(GUI线程)因处理耗时操作...

    Visual Basic 6.0建立多线程程序就是这么简单

    在软件开发中,多线程技术对于提高程序的执行效率及用户体验具有重要作用。Visual Basic 6.0 (以下简称VB6)作为一种广泛使用的编程语言,支持通过多种方式创建多线程应用程序。本文将详细介绍如何在VB6中实现多线程...

    WEB开发注意事项

    在WEB开发过程中,需要注意多个方面的细节来确保代码的性能和稳定性。以下是一些重要的实践和技巧: 1. **集合和映射对象初始化**:在创建`Collection`和`Map`对象时,应预估所需容量并正确初始化。例如,创建`...

    Android多线程文件上传

    在Android应用开发中,文件上传是一项常见的任务,尤其是在处理大文件或者需要提高用户交互体验时,多线程技术显得尤为重要。本主题聚焦于"Android多线程文件上传",我们将探讨如何利用多线程技术来优化文件上传过程...

    Linux多线程编程的高效开发经验.

    本文将深入探讨在Linux环境中进行多线程开发时的一些高效实践和注意事项。 首先,对于互斥锁的使用,Linux的Pthread库默认并不支持递归互斥锁。这意味着,如果一个线程已经持有了某个互斥锁,再次尝试加锁会引发...

    PB多线程例子,PB源码

    在描述中反复提到了“PB实现多线程开发,PB源码”,这表明提供的压缩包内容包含了如何在PowerBuilder中设计和实现多线程应用的源代码实例。多线程技术在PB中可以用于并发处理,比如在后台执行耗时操作,如数据查询、...

    MFCACE多线程demo

    MFC是微软提供的一个C++类库,用于简化Windows应用程序开发,而ACE则是一个跨平台的网络通信框架,支持多种并发模型,包括多线程。 **1. MFC中的多线程** MFC提供了CWinThread类来支持多线程。CWinThread是线程的...

    python selenium chrome 多开 多线程

    4. **注意事项**: 每个浏览器实例需要独立的Chromedriver,并且在多线程环境下,要注意同步问题,防止不同线程间的操作相互干扰。 总结起来,"python selenium chrome 多开 多线程"这个主题涉及到使用Python的...

    易语言超级列表框多线程源码

    通常,这种文本文件会包含源码的注释、使用方法、注意事项等内容。如果要深入理解并运用这段源码,需要打开此文件查看具体实现和指导。 总的来说,这个易语言的示例程序为我们提供了一个在处理大型数据集合时如何...

    C# 多线程资料集合

    ASP.NET多线程编程则将多线程技术应用到Web开发中,可能会涵盖在ASP.NET环境中创建和管理线程的注意事项,因为Web服务器环境通常对线程有特殊的管理方式。开发者需要了解如何在ASP.NET中正确地使用线程,以避免影响...

    Windows多线程编程技术与实例(C++)(PDF)

    《Windows多线程编程技术与实例(C++)》是一本深入探讨Windows环境下多线程编程的书籍,特别适合正在学习或已经从事C++多线程开发的人员阅读。本书通过丰富的实例,详细讲解了如何在Windows操作系统中利用C++进行...

    VB 实现多线程

    多线程编程技术在现代软件开发中扮演着至关重要的角色。它可以帮助开发者构建出更高效、响应更快的应用程序。尽管Visual Basic (VB) 被认为是一种面向对象且易于学习的语言,但许多人对其支持多线程的能力持怀疑态度...

    IOS两个精典的关于多线程的例子代码

    在iOS开发中,多线程是一项至关重要的技术,它使得应用程序可以同时执行多个任务,提升用户体验,优化系统资源的利用。本主题将深入探讨两个经典iOS多线程的例子,主要涉及`NSThread`类的使用。 一、多线程基础 在...

    在PB中实现 多线程的例子

    注意事项** - 线程安全:确保在多线程环境中访问共享资源时进行适当的锁定。 - 资源管理:谨慎处理线程的创建和销毁,避免资源泄露。 - 性能优化:合理设计线程数量,过多的线程可能导致系统资源过度消耗,影响性能...

Global site tag (gtag.js) - Google Analytics