许多java开发,都是刚刚接触多线程开发。但即使是有经验的开发,也会陷入很多 多线程
的陷阱。本篇内容,基本上都是一些反例,有些很低级但常见。当你的程序没有得相应的期望,希望本文能帮你了解到其中的微妙之处。
当然,面试时拿来装逼用,也是极好的。
先来10个。
现象:系统资源耗尽,进程僵死。
原因:每次方法执行,都new一个线程池。代码示例。
小姐姐味道解决方式:共用一个线程池即可。
作死等级:五颗星
脑残等级:五颗星
void doJob(){ ThreadPoolExecutor exe = new ThreadPoolExecutor(...); exe.submit(new Runnable(){...}) }
现象:某个线程一直持有锁而不释放,造成锁泄漏。
原因:未知异常或逻辑导致unlock函数未执行。
小姐姐味道解决方式:始终将unlock函数放在finally中。
作死等级:三颗星
脑残等级:四颗星
private final Lock lock = new ReentrantLock(); void doJob(){ try{ lock.lock(); //do. sth lock.unlock(); }catch(Exception e){ } }
现象:在某个条件下,抛出IllegalMonitorStateException。
原因:调用wait、notify等,忘记synchronized,或者同步了错误的变量。
小姐姐味道解决方式:调用这些函数之前,要使用同步关键字同步它。
作死等级:两颗星
脑残等级:四颗星
Object condition = new Object(); condition.wait();
现象:cpu占用高,发生死循环,使用jstack查看是阻塞在get方法上。
原因:在某种条件下,进行rehash时,会形成环形链。某些get请求会走到这个环上。
小姐姐味道解决方式:多线程环境下,使用ConcurrentHashMap,别犹豫。
作死等级:四颗星
脑残等级:四颗星
现象:不能够达到同步效果,结果是错误的。
原因:非基本类型被重新赋值,会改变锁的指向,不同线程持有的锁可能不一样。
小姐姐味道解决方式:把锁对象声明为final类型。
作死等级:四颗星
脑残等级:三颗星
List listeners = new ArrayList(); void add(Listener listener, boolean upsert){ synchronized(listeners){ List results = new ArrayList(); for(Listener ler:listeners){ ... } listeners = results; } }
现象:线程作业无法继续运行,不明终止。
原因:未捕获循环中的异常,造成线程退出。
小姐姐味道解决方式:习惯性捕获所有异常。
作死等级:三颗星
脑残等级:三颗星
volatile boolean run = true; void loop(){ while(run){ //do . sth int a = 1/0; } }
现象:多线程计数结果有误。
原因:volatile保证可见性,不保证原子性,多线程操作并不能保证其正确性。
小姐姐味道解决方式:直接使用Atomic类。
作死等级:三颗星
脑残等级:两颗星
volatile count = 0; void add(){ ++count; }
现象:虽然使用了线程安全的集合,但达不到同步效果。
原因:操作要修改多个线程安全的集合,但操作本身不是原子的。
小姐姐味道解决方式:弄明白要保护的代码逻辑域。
作死等级:三颗星
脑残等级:四颗星
private final ConcurrentHashMap<String,Integer> nameToNumber; private final ConcurrentHashMap<Integer,Salary> numberToSalary; public int geBonusFor(String name) { Integer serialNum = nameToNumber.get(name); Salary salary = numberToSalary.get(serialNum); return salary.getBonus(); }
再比如下面的错误代码。
Map<String, String> map = Collections.synchronizedMap(new HashMap<String, String>()); if(!map.containsKey("foo")) map.put("foo", "bar");
现象:使用全局的Calendar,SimpleDateFormat等进行日期处理,发生异常或者数据不准确。
原因:这俩东西不是线程安全的,并发调用会有问题。
小姐姐味道解决方式:放在ThreadLocal中,建议使用线程安全的DateTimeFormatter。
作死等级:三颗星
脑残等级:三颗星
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); Date dododo(String str){ return format(str); }
现象:代码产生死锁和相互等待。
原因:代码满足了下面四个条件:互斥;不可剥夺;请求和保持;循环等待。
小姐姐味道解决方式:破坏这四个条件。或者少用同步。
作死等级:两颗星
脑残等级:一颗星
下面是一段简单的死锁代码。
final Object lock1 = new Object(); final Object lock2 = new Object(); new Thread(new Runnable() { @Override public void run() { sleep(1000); synchronized (lock1) { synchronized (lock2) { } } } }).start(); new Thread(new Runnable() { @Override public void run() { synchronized (lock2) { sleep(1000); synchronized (lock1) { } } } }).start();
现象:会读取到非设置的值。
原因:long变量读写不是原子的,可能会读到1个变量的高32位和另一个变量的低32位字节。
小姐姐味道解决方式:确保long和double变量的数据正确,可以加上volatile关键字。
作死等级:一颗星
脑残等级:没有星
扩展阅读(jdk10): https://docs.oracle.com/javase/specs/jls/se10/html/jls-17.html#jls-17.7
多线程的使用是及其复杂的,使用低级api出错的概率会成倍增加,对技能要求也较高。所幸,concurrent包使得这个过程方便了很多,但依然存在资源规划和同步失效的问题。小姐姐味道这里一个比较浅显但全面的总结: JAVA多线程使用场景和注意事项简版 ,但健壮的代码还要靠你自己去实践呀。
更多精彩文章。
《微服务不是全部,只是特定领域的子集》
《“分库分表" ?选型和流程要慎重,否则会失控》
这么多监控组件,总有一款适合你
《Linux生产环境上,最常用的一套“vim“技巧》
《使用Netty,我们到底在开发些什么?》
Linux五件套之类的。
《Linux之《荒岛余生》(一)准备篇》
《Linux之《荒岛余生》(二)CPU篇》
《Linux之《荒岛余生》(三)内存篇》
《Linux之《荒岛余生》(四)I/O篇》
《Linux之《荒岛余生》(五)网络篇》
更多请关注。当然也可以关注公众号。