java基础中一些值得聊的话题(并发篇)

时间:2022-02-02 17:33:45

   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,ConcurrentHashMapConcurrentLinkedList等等,关于数据结构,还得另外写文再讲讲。

  一个常见的错误是,误以为只要使用了这些数据结构,就一定不会出现线程安全的问题。比如下面的例子,就有可能会出现一个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的内存模型与volatile
   volatile是java中比较难理解的语义之一。要理解volatile,先理解java的内存模型。这里有一些深入讲解内存模型与volatile的文章,推荐 http://www.ibm.com/developerworks/cn/java/j-jtp06197.htmlhttp://ifeve.com/java-memory-model-4/

java基础中一些值得聊的话题(并发篇)

   对于不同的线程有各自的工作内存,其操作都是先把数据装载进自己的工作内存中,然后同步回主存。这个过程并非原子性的,比如从主存中读内容需要两步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