并发基础深入

并发基础深入

join()的替代品

注意javaSE5的java.util.concurrent类库包含诸如CyclicBarrier这样的工具,他们可能会比最初的线程类库的join()更加合适.

捕获异常

由于线程的本质特性,使得你不能捕获从线程中逃逸的异常,一旦异常逃出任务的run()方法,他就会向外传播到控制台,除非你采取特殊的步骤捕获这种错误的异常,在JavaSE5之前,你可以使用线程组来捕获这些异常,但是有了javaSE5,就可以用Executor来解决这个问题.

下面的任务总是会抛出一个异常,该异常会传播到其run()方法外部也就是控制台.

public class ExceptionThread implements Runnable {
  public void run() {
    throw new RuntimeException();
  }
  public static void main(String[] args) {
    ExecutorService exec = Executors.newCachedThreadPool();
    exec.execute(new ExceptionThread());
  }
}

即使将main的主体放到try-catch语句块中也是没有作用的:这将产生与上面的实例相同的结果:未捕获的异常.

public class NaiveExceptionHandling {
  public static void main(String[] args) {
    try {
      ExecutorService exec =
        Executors.newCachedThreadPool();
      exec.execute(new ExceptionThread());
    } catch(RuntimeException ue) {
      // This statement will NOT execute!
      System.out.println("Exception has been handled!");
    }
  }
} 

为了解决这个问题,我么要修改Executor产生线程的方式.Thread.UncaughtExceptionHandler是JavaSE5中的新接口,它允许你在每个Thread对象上都附着一个异常处理器.Thread.UnCaughtExceptionHandler.uncaughtException()会在线程因为未捕获的异常而临近死亡的时候被调用.**为了使用它,我们创建了一个新类型的ThreadFactory,他将在每个新创建的Thread对象上附着一个Thread.UncaughtExceptionHandler.我们将这个工厂传递给Executor穿件新的ExecutorService的方法.**代码如下所示:

import java.util.concurrent.*;

class ExceptionThread2 implements Runnable {
  public void run() {
    Thread t = Thread.currentThread();
    System.out.println("run() by " + t);
    System.out.println(
      "eh = " + t.getUncaughtExceptionHandler());
    throw new RuntimeException();
  }
}

class MyUncaughtExceptionHandler implements
Thread.UncaughtExceptionHandler {
  public void uncaughtException(Thread t, Throwable e) {
    System.out.println("caught " + e);
  }
}

class HandlerThreadFactory implements ThreadFactory {
  public Thread newThread(Runnable r) {
    System.out.println(this + " creating new Thread");
    Thread t = new Thread(r);
    System.out.println("created " + t);
    t.setUncaughtExceptionHandler(
      new MyUncaughtExceptionHandler());
    System.out.println(
      "eh = " + t.getUncaughtExceptionHandler());
    return t;
  }
}

public class CaptureUncaughtException {
  public static void main(String[] args) {
    ExecutorService exec = Executors.newCachedThreadPool(
      new HandlerThreadFactory());
    exec.execute(new ExceptionThread2());
  }
} /* Output: (90% match)
HandlerThreadFactory@de6ced creating new Thread
created Thread[Thread-0,5,main]
eh = MyUncaughtExceptionHandler@1fb8ee3
run() by Thread[Thread-0,5,main]
eh = MyUncaughtExceptionHandler@1fb8ee3
caught java.lang.RuntimeException
*///:~

共享资源竞争

同步的时机

如果你正在写一个变量,它可能接下来被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且,读写线程都必须用相同的监视器同步.

如果在你的类中有超过一个方法在处理临界数据,那么你必须同步所有的相关的方法.如果只同步一个方法,那么其他方法将会随意的忽略这个对象锁,并且可以在无任何惩罚的情况下被调用.这是很重要的一点:每个访问临界共享资源的方法都必须被同步,否则他们就不会正确的工作.

同步方式简介
  • 内建的锁,synchronized关键字
  • 使用显式的Lock对象.java.util.concurrent中的Lock对象必须被显式的创建,锁定和释放.因此它和内建的锁相比,代码缺乏优雅性,但是对于解决某些类型的问题来说,他更加灵活.对于lock的调用,你必须放在finally子句中带有unlock的try-finally语句中.注意,return语句必须在try子句中出现,以确保unlock()不会过早的发生,从而将数据暴露给第二个任务.

Lock的优势:尽管try-finally所需的代码比sychronized关键字要多,但是这也代表了显式的Lock对象的优点之一.如果在使用Synchronized关键字的时候,某些事物失败了,那么就会抛出一个异常.但是你没有任何机会去做任何清理工作,以维护系统使其处于良好状态.有了显式的Lock对象,你就可以使用finally语句将系统维护在正确的状态了.

可见性与原子性详解.

内存可见性
public class Test {
	boolean status = false;
	
	public void changeStatus() {
		status=true;
	}
	
	public void run() {
		if(status) {
			System.out.println("running...");
		}
	}
}

上面这个例子,在多线程环境里,假设线程A执行changeStatus()方法后,线程B运行run()方法,可以保证输出"running…"吗?

**答案是NO!**这个结果可能会让人有所疑惑,可以理解.在单线程中,当线程A执行了changeStatus()方法后,再执行run()方法,确实可以输出"running…".但是在多线程模型中,是没办法做出这种保证的.因为对于共享变量status来说,线程A的修改对于线程B来说是不可见的.也就是说线程B此时可能无法观测到status已经被修改为true.那么什么是可见性呢?

所谓可见性,是指当一条线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的.很显然,上述的例子中是没有办法做到内存可见性的.在Java中 volatile, synchronized和final实现可见性

为什么会出现这种情况呢.我们需要先了解一下JMM(java内存模型).

Java内存模型

Java虚拟机有自己的内存模型JMM,JMM可以屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都达到一致的内存访问效果.

​ JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用道德主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量.

大概了解了JMM的简单定义后,问题就很容易理解了,对于普通的共享变量来说,比如我们上文中的status,线程A将其修改为A这个操作发生在线程A的本地内存中,而此时还未同步到主内中去,而线程B缓存了status的初始值为false,此时可能还没有观测到status的值被修改了,所以就导致了上述的问题.

那么这种共享变量在多线程中不可见性如何解决呢,比较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了,有点炮打蚊子的意思.比较合理的方式其实就是volatile.

所说只需将上面的代码中的status声明为volatile,即可保证在线程A将其修改为true时,线程B可以立即知道

volatile boolean status = false;

Volatile特性:

  • 当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去,保证共享变量对线程的所有可见性.;
  • 这个写操作会导致其他线程的缓存无效.
  • 禁止指令重排序优化(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理).

Volatile与Synchronized区别:

​ **我们总是在拿volatile和synchronized作比较,但是其实volatile并不能完全代替synchronized,他依然是个轻量级的锁,**再很多场景下,volatile并不能胜任.看下面的例子.

import java.util.concurrent.CountDownLatch;

public class Counter {
    public static volatile int num = 0;
    //使用CountDownLatch来等待计算线程执行完
    static CountDownLatch countDownLatch = new CountDownLatch(30);
    public static void main(String []args) throws InterruptedException {
        //开启30个线程进行累加操作
        for(int i=0;i<30;i++){
            new Thread(){
                public void run(){
                    for(int j=0;j<10000;j++){
                        num++;//自加操作
                    }
                    countDownLatch.countDown();
                }
            }.start();
        }
        //等待计算线程执行完
        countDownLatch.await();
        System.out.println(num);
    }
}

//执行结果:224291

针对这个示例,一些同学可能会觉得疑惑,如果用volatile修饰的共享变量可以保证可见性,那么结果不应该是300000么?

问题就出在num++这个操作上,因为num++不是个原子性的操作,而是个复合操作。我们可以简单讲这个操作理解为由这三步组成:

1.读取

2.加一

3.赋值

所以,在多线程环境下,有可能线程A将num读取到本地内存中,此时其他线程可能已经将num增大了很多,线程A依然对过期的num进行自加,重新写到主存中,最终导致了num的结果不合预期,而是小于30000。

上面这个例子必须用synchronized锁才不会出错.但是为什么volatile不能胜任呢?这就要说到原子性了.

原子性

原子是世界上最小的单位,具有不可分割性.原子操作是不能被线程调度机制中断的操作.一旦操作开始,那么他一定可以在可能发生的"上下文切换"之前(切换到其它线程)执行完毕.

原子性可以应用于除long和double之外的所有基本类型之上的"简单操作".对于读取和写入long和double之外的基本类型变量这样的操作,可以保证他们会被当做不可分的操作来操作内存.但是JVM可以将64位(long和double变量)的读取和写入当做两个分离的32位操作来执行,这就产生了一个读取和写入操作中间发生上下文切换,从而导致不同的任务可以看到不正确结果的可能性.

但是当你定义long或double变量时,如果使用volatile关键字,就会获得原子性.

什么才属于原子操作呢?对域中的值做赋值和返回操作通常都是原子性的,但在Java中下面的操作肯定不是原子性的:

// {Exec: javap -c Atomicity}

public class Atomicity {
  int i;
  void f1() { i++; }
  void f2() { i += 3; }
} /* Output: (Sample)
...
void f1();
  Code:
   0:        aload_0
   1:        dup
   2:        getfield        #2; //Field i:I
   5:        iconst_1
   6:        iadd
   7:        putfield        #2; //Field i:I
   10:        return

void f2();
  Code:
   0:        aload_0
   1:        dup
   2:        getfield        #2; //Field i:I
   5:        iconst_3
   6:        iadd
   7:        putfield        #2; //Field i:I
   10:        return
*///:~

每条指令都会产生一个get和put,他们之间还有一些其他的指令.因此在获取和放置之间,另一个任务可能会修改这个域,所以,这些操作不是原子性的.

如果你盲目的应用原子性概念,那么就会看到在下面程序中的getValue()符合上面的描述

import java.util.concurrent.*;

public class AtomicityTest implements Runnable {
  private int i = 0;
  public int getValue() { return i; }
  private synchronized void evenIncrement() { i++; i++; }
  public void run() {
    while(true)
      evenIncrement();
  }
  public static void main(String[] args) {
    ExecutorService exec = Executors.newCachedThreadPool();
    AtomicityTest at = new AtomicityTest();
    exec.execute(at);
    while(true) {
      int val = at.getValue();
      if(val % 2 != 0) {
        System.out.println(val);
        System.exit(0);
      }
    }
  }
} 
/* Output: (Sample)
191583767
*///:~

但是,该程序将找到奇数值并终止.尽管return i确实是原子操作,但是缺少同步使得其数值可以在处于不稳定的中间状态时被读取.除此之外,由于i也不是volatile的,因此还存在可视性问题.getValue()和evenIncrement()必须是sychronized的.

如下面的例子,虽然seriaNumber被设置成了volatile,nextSerialNumber也确保了不会有重复的数值,但是由于serialNumber不是一个原子操作,所以最终还是得到重复序列的数.

public class SerialNumberGenerator {
  private static volatile int serialNumber = 0;
  public static int nextSerialNumber() {
    return serialNumber++; // Not thread-safe
  }
} 

class CircularSet {
  private int[] array;
  private int len;
  private int index = 0;
  public CircularSet(int size) {
    array = new int[size];
    len = size;
    // Initialize to a value not produced
    // by the SerialNumberGenerator:
    for(int i = 0; i < size; i++)
      array[i] = -1;
  }
  public synchronized void add(int i) {
    array[index] = i;
    // Wrap index and write over old elements:
    index = ++index % len;
  }
  public synchronized boolean contains(int val) {
    for(int i = 0; i < len; i++)
      if(array[i] == val) return true;
    return false;
  }
}

public class SerialNumberChecker {
  private static final int SIZE = 10;
  private static CircularSet serials =
    new CircularSet(1000);
  private static ExecutorService exec =
    Executors.newCachedThreadPool();
  static class SerialChecker implements Runnable {
    public void run() {
      while(true) {
        int serial =
          SerialNumberGenerator.nextSerialNumber();
        if(serials.contains(serial)) {
          System.out.println("Duplicate: " + serial);
          System.exit(0);
        }
        serials.add(serial);
      }
    }
  }
  public static void main(String[] args) throws Exception {
    for(int i = 0; i < SIZE; i++)
      exec.execute(new SerialChecker());
    // Stop after n seconds if there's an argument:
    if(args.length > 0) {
      TimeUnit.SECONDS.sleep(new Integer(args[0]));
      System.out.println("No duplicates detected");
      System.exit(0);
    }
  }
} /* Output: (Sample)
Duplicate: 8468656
*///:~

对基本类型的读取和赋值操作被认为是安全的原子性操作.但是正如你在Atomicity.java中看到的,当对象处于不稳定状态时,仍旧很有可能使用原子性操作来访问他们,对这个问题作出假设是相当棘手且危险的,最明智的做法就是遵循同步规则.

原子类

临界区

请看Synchronized详解和本章的Lock锁.

线程本地存储

防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享.线程本地存储是一种自动化机制,可以为使用相同变量的每个不同线程都创建不同的存储.因此如果你有5个线程都要使用变量X所表示的对象,那线程本地存储就会生成5个用于x的不同的存储块.主要是它们使得你可以将状态和线程关联起来.

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值