java基础中一些值得聊的话题(并发篇)
来源:互联网 发布:c图形编程书籍 编辑:程序博客网 时间:2024/04/30 07:26
java的并发体系也是非常庞大,而且注意点非常多。这里可能不会面面俱到(否则就是写书了)。
之前写过一篇关于java并发方式的文章。
这里以一个从重到轻的方式来详细聊聊java的并发。
Synchronized和ReentrantLock
最常见的同步方式是synchronized,之所以叫synchronized是因为synchronized会将锁作用于整个对象或者类,而不是具体某个方法,有几种用法,加在方法上,同步代码块以及同步整个class,前两者作用于对象,最后一种方式作用于类。其次是ReentrantLock,理解起来更加容易,就是一个锁,持锁的线程能够进入执行,无锁的线程则阻塞等待.
使用锁容易犯的错误是没有搞清楚需要同步内容的范围。如下例,两个变量都已经作为原子变量来操作,然而,我们真正需要同步的语义是两个状态同时变化,因此实际上没有起到相应的作用。
class SynTest{private AtomicReference<String> stateA;private AtomicInteger stateB;public void syn(boolean b){if(b){stateA.set("aaa");stateB.set(10);return ;}stateA.set("bbb");stateB.set(5);}}
使用线程安全的数据结构
线程安全的数据结构有很多,如Vector,Hashtable,ConcurrentHashMap, ConcurrentLinkedList等等,关于数据结构,还得另外写文再讲讲。
一个常见的错误是,误以为只要使用了这些数据结构,就一定不会出现线程安全的问题。比如下面的例子,就有可能会出现一个ArrayIndexOutOfBoundsException。
public class VectorTest{private static Vector<Integer> v = new Vector<Integer>();public static void main(String [] args){while(true){for(int i = 0; i < 10; i++){v.add(i);}Thread t = new Thread(new Runnable(){public void run() {//synchronized(v){for(int i = 0; i < v.size(); i++)v.remove(i);//}}});Thread p = new Thread(new Runnable(){public void run() {//synchronized(v){for(int i = 0; i < v.size(); i++)System.out.print(v.get(i) + "\t");//}}});t.start();p.start();//while(Thread.activeCount() > 20);}}}
原因是什么呢?Vector内部使用数组保存对象,它对这个数据结构本身的操作如add, remove, get等做了同步,因此多个线程同时操作vector时,能保证vector内部操作数组的过程是安全的。但是如果仔细观察,会发现实际上这里的语义比较微妙,这里的v.size跟remove和get并没有原子性,举个例子,线程p首先调用v.size,得到10,此时线程t抢占处理器并调用了remove删除了所有元素,此时线程p抢占回来准备get(0),于是抛出错误ArrayIndexOutOfBoundsException。v.size和v.get语义上要求原子但实际上没有,这就是问题所在。因此,应该使用一个更大范围的封闭,在v上对整个for循环同步(见注释)。
java的内存模型与volatilevolatile是java中比较难理解的语义之一。要理解volatile,先理解java的内存模型。这里有一些深入讲解内存模型与volatile的文章,推荐http://www.ibm.com/developerworks/cn/java/j-jtp06197.html,http://ifeve.com/java-memory-model-4/
对于不同的线程有各自的工作内存,其操作都是先把数据装载进自己的工作内存中,然后同步回主存。这个过程并非原子性的,比如从主存中读内容需要两步read,load,向主存写内容也需要两步store,write。因此还有锁的语义lock, unlock。
java内存模型最主要揭示的一点就是操作的原子性跟可见性。原子性指的就是一件事被当做整体做完。可见性指一个线程的写操作能够立马被另一个线程看到。
而volatile最主要保证的就是内存的可见性和禁止指令重排。它并没有保证原子性,这点要注意,很多时候会以为用了volatile就线程安全了,其实不然,看下面的例子(来源网上)。
public class JoinThread extends Thread { public static volatile int n = 0; public void run() { for (int i = 0; i < 10; i++) try { n++; sleep(3); // 为了使运行结果更随机,延迟3毫秒 } catch (Exception e) { } } public static void main(String[] args) throws Exception { Thread threads[] = new Thread[100]; for (int i = 0; i < threads.length; i++) // 建立100个线程 threads[i] = new JoinThread(); for (int i = 0; i < threads.length; i++) // 运行刚才建立的100个线程 threads[i].start(); for (int i = 0; i < threads.length; i++) // 100个线程都执行完后继续 threads[i].join(); System.out.println("n=" + JoinThread.n); } }这个程序实际上总是打印出来小于1000的值,这说明对于++运算和n = n + 1这样的运算而言,volatile不能保证其原子性。要保证其原子性,只需要将volatile换为AtomicInteger,再将n++换为n.addAndSet(1)。从字节码来看,++指令实际上被翻译成了这么几个指令,而这几个指令的执行并没有上锁,因此++操作对于上层而言是一个操作,但是底层是多个操作,不具有原子性(《深入理解JVM》一书中更是提到,即便在字节码层面的单操作,其底层汇编有可能是多个操作)。
5 getstatic org.metro.core.JoinThread.n : int [10] 8 iconst_1 9 iadd 10 putstatic org.metro.core.JoinThread.n : int [10]对于指令重排,也是防不胜防,比如如下语句
boolean a=false;在一个线程中doSomeLoading();a=true;在另一个线程中if(a){ doSomeWork();}第一个线程先要做一些loading的工作,完成之后将标志设为true,这样第二个线程才能真正做事。但实际上a=true有可能被重排到doSomeLoading之前,这样另一个线程有可能会在没有loading的情况先做doSomeWork导致出错。
个人愚见,除非你很清楚在做什么,否则尽量使用Atomic变量代替volatile,因为其性能差异并不大。有意思的是,AtomicReference又在内部实现中用volatile来修饰被引用的对象,当然是希望用到volatile的可见性。
关于并发包的内容
一是ThreadPool;二是Callable与Future(关于future的其中一种用法,参考java超时控制);三是一些同步工具类,如CountDownLatch,Semaphore, Barrier
AQS http://blog.csdn.net/vernonzheng/article/details/8275624
深入锁机制 http://blog.csdn.net/chen77716/article/details/6641477
JUC并发类详解 http://my.oschina.net/foxeye/blog/625886
- java基础中一些值得聊的话题(并发篇)
- java基础中一些值得聊的话题(内存篇)
- java基础中一些值得聊的话题(加载篇)
- Java 基础中一些值得聊的话题( 加载篇 )
- java基础中一些值得聊的话题(可以当做面试题)
- JAVA基础复习:涉及并发的一些基础概念
- java中并发编程的一些总结
- java中一些值得注意的命名规范
- Java并发编程(二)--java线程安全的一些基础
- navigation导航中一些重要话题的来源和去路
- 正态分布中一些值得注意的量
- [Java]值得注意的一些语法点
- 关于自学的一些话题
- 关于c++的一些话题
- Java的话题层出不穷
- java并发的一些知识
- Java编程中“为了性能”尽量要做到的一些地方-值得学习的经验
- 漫谈并发编程(六):java中一些常用的并发构件的介绍
- 只要一直走,总会遇见新的风景
- java容器之Set常用方法
- coding----1wcods----坚持第9天----1737行
- ubuntu下配置VIM & VIM快捷键
- Linux KVM禁用virbr0 NAT接口
- java基础中一些值得聊的话题(并发篇)
- metaq使用memcache作为消费者去重的风险
- netperf在Android平台与windows的通信
- SQL*Net more data to client
- linux用户管理学习笔记
- Android编译详解之lunch命令
- codeforces-379B. New Year Present
- ZOJ-1086
- 一致性hash算法