Java并发合集

来源:互联网 发布:淘宝上的电器是正品吗 编辑:程序博客网 时间:2024/04/27 14:44
22.1 可重入内置锁
    每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁或监视器锁。线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时会自动释放锁。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
    重入的概念:当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。“重入”意味着获取锁的操作的粒度是“线程”,而不是调用。重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程所持有,当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1,如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放。
    重入进一步提升了加锁行为的封装性,因此简化了面向对象并发代码的开发。分析如下程序:

public class Father  
{  
    public synchronized void doSomething(){  
        ......  
    }  
}  
 
public class Child extends Father  
{  
    public synchronized void doSomething(){  
        ......  
        super.doSomething();  
    }  
}  
    子类覆写了父类的同步方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码件产生死锁。
     由于father和Child中的doSomething方法都是synchronized方法,因此每个doSomething方法在执行前都会获取Child对象实例上的锁。如果内置锁不是可重入的,那么在调用super.doSomething时将无法获得该Child对象上的互斥锁,因为这个锁已经被持有,从而线程会永远阻塞下去,一直在等待一个永远也无法获取的锁。重入则避免了这种死锁情况的发生。
    同一个线程在调用本类中其他synchronized方法/块或父类中的synchronized方法/块时,都不会阻碍该线程地执行,因为互斥锁时可重入的。
    在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁。
    可重入锁实例
    public class Test implements Runnable{
    public synchronized void get(){
        System.out.println(Thread.currentThread().getId());
        set();
    }
 
    public synchronized void set(){    // set方法在被自己调用的时候重入了是允许的操作
        System.out.println(Thread.currentThread().getId());
    }
    @Override
    public void run() {
        get();
    }
    public static void main(String[] args) {
        Test ss=new Test();
        new Thread(ss).start();
        new Thread(ss).start();
        new Thread(ss).start();
    }
}
public class Test implements Runnable {
    ReentrantLock lock = new ReentrantLock();
    public void get() {
        lock.lock();
        System.out.println(Thread.currentThread().getId());
        set();
        lock.unlock();
    }
    public void set() {
        lock.lock();
        System.out.println(Thread.currentThread().getId());    // set方法被重入了
        lock.unlock();
    }
    @Override
    public void run() {
        get();
    }
    public static void main(String[] args) {
        Test ss = new Test();
        new Thread(ss).start();
        new Thread(ss).start();
        new Thread(ss).start();
    } 
}

可重入锁最大的作用是避免死锁
我们以自旋锁作为例子,这种就是不可重入的
public class SpinLock {
    private AtomicReference<Thread> owner =new AtomicReference<>();
    public void lock(){
        Thread current = Thread.currentThread();
        while(!owner.compareAndSet(null, current)){    // 如果当前等于所有者不为空,所以线程一直执行,产生了死锁
        }
    }
    public void unlock (){
        Thread current = Thread.currentThread();
        owner.compareAndSet(current, null);
    }
}

对于自旋锁来说,
1、若有同一线程两调用lock() ,会导致第二次调用lock位置进行自旋,产生了死锁
说明这个锁并不是可重入的。(在lock函数内,应验证线程是否为已经获得锁的线程)
2、若1问题已经解决,当unlock()第一次调用时,就已经将锁释放了。实际上不应释放锁。
(采用计数次进行统计)
修改之后,如下:
public class SpinLock1 {
 private AtomicReference<Thread> owner =new AtomicReference<>();
 private int count =0;
 public void lock(){
  Thread current = Thread.currentThread();
  if(current==owner.get()) {
   count++;
   return ;
  }

  while(!owner.compareAndSet(null, current)){

  }
 }
 public void unlock (){
  Thread current = Thread.currentThread();
  if(current==owner.get()){
   if(count!=0){
    count--;
   }else{
    owner.compareAndSet(current, null);
   }

  }

 }
}
   
22.2 线程中断interrupt()
    当一个线程运行时,另一个线程可以调用对应的Thread对象的interrupt()方法来中断它,该方法只是在目标线程中设置一个标志,表示它已经被中断,并立即返回。这里需要注意的是,如果只是单纯的调用interrupt()方法,线程并没有实际被中断,会继续往下执行。
public class SleepInterrupt extends Object implements Runnable{
 public void run(){
  try{
   System.out.println("in run() - about to sleep for 20 seconds");
   Thread.sleep(20000);
   System.out.println("in run() - woke up");
  }catch(InterruptedException e){
   System.out.println("in run() - interrupted while sleeping");
   //处理完中断异常后,返回到run()方法人口,
   //如果没有return,线程不会实际被中断,它会继续打印下面的信息
   return;  
  }
  System.out.println("in run() - leaving normally");
 }


 public static void main(String[] args) {
  SleepInterrupt si = new SleepInterrupt();
  Thread t = new Thread(si);
  t.start();
  //主线程休眠2秒,从而确保刚才启动的线程有机会执行一段时间
  try {
   Thread.sleep(2000); 
  }catch(InterruptedException e){
   e.printStackTrace();
  }
  System.out.println("in main() - interrupting other thread");
  //中断线程t
  t.interrupt();
  System.out.println("in main() - leaving");
 }
}

    请注意:由于不确定的线程规划,上图运行结果的后两行可能顺序相反,这取决于主线程和新线程哪个先消亡。但前两行信息的顺序必定如上图所示。
    另外,如果将catch块中的return语句注释掉,则线程在抛出异常后,会继续往下执行,而不会被中断,从而会打印出”leaving normally“信息。

待决中断
    在上面的例子中,sleep()方法的实现检查到休眠线程被中断,它会相当友好地终止线程,并抛出InterruptedException异常。另外一种情况,如果线程在调用sleep()方法前被中断,那么该中断称为待决中断,它会在刚调用sleep()方法时,立即抛出InterruptedException异常。
 public class PendingInterrupt extends Object {
 public static void main(String[] args){
  //如果输入了参数,则在mian线程中中断当前线程(亦即main线程)
  if( args.length > 0 ){
   Thread.currentThread().interrupt();
  } 
  //获取当前时间
  long startTime = System.currentTimeMillis();
  try{
   Thread.sleep(2000);
   System.out.println("was NOT interrupted");
  }catch(InterruptedException x){
   System.out.println("was interrupted");
  }
  //计算中间代码执行的时间
  System.out.println("elapsedTime=" + ( System.currentTimeMillis() - startTime));
 }
}

    这种模式下,main线程中断它自身。除了将中断标志(它是Thread的内部标志)设置为true外,没有其他任何影响。线程被中断了,但main线程仍然运行,main线程继续监视实时时钟,并进入try块,一旦调用sleep()方法,它就会注意到待决中断的存在,并抛出InterruptException。于是执行跳转到catch块,并打印出线程被中断的信息。最后,计算并打印出时间差。

使用isInterrupted()方法判断中断状态
   可以在Thread对象上调用isInterrupted()方法来检查任何线程的中断状态。这里需要注意:线程一旦被中断,isInterrupted()方法便会返回true,而一旦sleep()方法抛出异常,它将清空中断标志,此时isInterrupted()方法将返回false。

使用Thread.interrupted()方法判断中断状态
    可以使用Thread.interrupted()方法来检查当前线程的中断状态(并隐式重置为false)。又由于它是静态方法,因此不能在特定的线程上使用,而只能报告调用它的线程的中断状态,如果线程被中断,而且中断状态尚不清楚,那么,这个方法返回true。与isInterrupted()不同,它将自动重置中断状态为false,第二次调用Thread.interrupted()方法,总是返回false,除非中断了线程。

yield和join的区别
    join方法用线程对象调用,如果在一个线程A中调用另一个线程B的join方法,线程A将会等待线程B执行完毕后再执行。
    yield可以直接用Thread类调用,yield让出CPU执行权给同等级的线程,如果没有相同级别的线程在等待CPU的执行权,则该线程继续执行。

22.3 挂起和恢复线程
    Thread 的API中包含两个被淘汰的方法,它们用于临时挂起和重启某个线程,这些方法已经被淘汰,因为它们是不安全的,不稳定的。如果在不合适的时候挂起线程(比如,锁定共享资源时),此时便可能会发生死锁条件——其他线程在等待该线程释放锁,但该线程却被挂起了,便会发生死锁。另外,在长时间计算期间挂起线程也可能导致问题。
    public class AlternateSuspendResume extends Object implements Runnable {

 private volatile int firstVal;
 private volatile int secondVal;
 //增加标志位,用来实现线程的挂起和恢复
 private volatile boolean suspended;

 public boolean areValuesEqual() {
  return ( firstVal == secondVal );
 }

 public void run() {
  try {
   suspended = false;
   firstVal = 0;
   secondVal = 0;
   workMethod();
  } catch ( InterruptedException x ) {
   System.out.println("interrupted while in workMethod()");
  }
 }

 private void workMethod() throws InterruptedException {
  int val = 1;

  while ( true ) {
   //仅当贤臣挂起时,才运行这行代码
   waitWhileSuspended(); 

   stepOne(val);
   stepTwo(val);
   val++;

   //仅当线程挂起时,才运行这行代码
   waitWhileSuspended(); 

   Thread.sleep(200);  
  }
 }

 private void stepOne(int newVal) 
     throws InterruptedException {

  firstVal = newVal;
  Thread.sleep(300);  
 }

 private void stepTwo(int newVal) {
  secondVal = newVal;
 }

 public void suspendRequest() {
  suspended = true;
 }

 public void resumeRequest() {
  suspended = false;
 }

 private void waitWhileSuspended() 
    throws InterruptedException {

  //这是一个“繁忙等待”技术的示例。
  //它是非等待条件改变的最佳途径,因为它会不断请求处理器周期地执行检查, 
  //更佳的技术是:使用Java的内置“通知-等待”机制
  while ( suspended ) {
   Thread.sleep(200);
  }
 }

 public static void main(String[] args) {
  AlternateSuspendResume asr = 
    new AlternateSuspendResume();

  Thread t = new Thread(asr);
  t.start();

  //休眠1秒,让其他线程有机会获得执行
  try { Thread.sleep(1000); } 
  catch ( InterruptedException x ) { }

  for ( int i = 0; i < 10; i++ ) {
   asr.suspendRequest();

   //让线程有机会注意到挂起请求
   //注意:这里休眠时间一定要大于
   //stepOne操作对firstVal赋值后的休眠时间,即300ms,
   //目的是为了防止在执行asr.areValuesEqual()进行比较时,
   //恰逢stepOne操作执行完,而stepTwo操作还没执行
   try { Thread.sleep(350); } 
   catch ( InterruptedException x ) { }

   System.out.println("dsr.areValuesEqual()=" + 
     asr.areValuesEqual());

   asr.resumeRequest();

   try { 
    //线程随机休眠0~2秒
    Thread.sleep(
      ( long ) (Math.random() * 2000.0) );
   } catch ( InterruptedException x ) {
    //略
   }
  }

  System.exit(0); //退出应用程序
 }
}

终止线程
当调用Thread的start()方法,执行完run()方法后,或在run()方法中return,线程便会自然消亡。另外Thread API中包含了一个stop()方法,可以突然终止线程。但它在JDK1.2后便被淘汰了,因为它可能导致数据对象的崩溃。
    1. 没有机会执行清理工作
    2. 突然释放锁会导致某些对象中数据不一致

22.4 守护线程与线程阻塞的四种情况
    线程分类:
    1. User Thread(用户线程),即运行在前台的线程
    2. Daemon Thread(守护线程) ,即运行在后台的线程。当VM检测仅剩一个守护线程,而用户线程都已经退出运行时,VM就会退出,因为没有如果没有了被守护这,也就没有继续运行程序的必要了。如果有非守护线程仍然存活,VM就不会退出。
    
    Thread的setDaemon(true)方法设置当前线程为守护线程。
    虽然守护线程可能非常有用,但必须小心确保其他所有非守护线程消亡时,不会由于它的终止而产生任何危害。因为你不可能知道在所有的用户线程退出运行前,守护线程是否已经完成了预期的服务任务。一旦所有的用户线程退出了,虚拟机也就退出运行了。 因此,不要在守护线程中执行业务逻辑操作(比如对数据的读写等)
    
    注意:
     1、setDaemon(true)必须在调用线程的start()方法之前设置,否则会跑出IllegalThreadStateException异常。
    2、在守护线程中产生的新线程也是守护线程。  
    3、 不要认为所有的应用都可以分配给守护线程来进行服务,比如读写操作或者计算逻辑。 

    线程阻塞
    线程可以阻塞于四种状态:
    1、当线程执行Thread.sleep()时,它一直阻塞到指定的毫秒时间之后,或者阻塞被另一个线程打断;
    2、当线程碰到一条wait()语句时,它会一直阻塞到接到通知(notify())、被中断或经过了指定毫秒时间为止(若制定了超时值的话)
    3、线程阻塞与不同I/O的方式有多种。常见的一种方式是InputStream的read()方法,该方法一直阻塞到从流中读取一个字节的数据为止,它可以无限阻塞,因此不能指定超时时间;
    4、线程也可以阻塞等待获取某个对象锁的排他性访问权限(即等待获得synchronized语句必须的锁时阻塞)。
    注意,并非所有的阻塞状态都是可中断的,以上阻塞状态的前两种可以被中断,后两种不会对中断做出反应。

22.5 volatile变量修饰符—意料之外的问题
    volatile用处说明
    在当前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
    Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
    Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才将私有拷贝与共享内存中的原始值进行比较。
    volatile是一种稍弱的同步机制,在访问volatile变量时不会执行加锁操作,也就不会执行线程阻塞,因此volatilei变量是一种比synchronized关键字更轻量级的同步机制。
    建议:
    1. 在两个或者更多的线程需要访问的成员变量上使用volatile。
    2. 当要访问的变量已在synchronized代码块中,或者为常量时,没必要使用volatile。

下面给出一段代码,通过其运行结果来说明使用关键字volatile产生的差异,但实际上遇到了意料之外的问题:

public class Volatile extends Object implements Runnable {
 //value变量没有被标记为volatile
 private int value;  
 //missedIt变量被标记为volatile
 private volatile boolean missedIt;
 //creationTime不需要声明为volatile,因为代码执行中它没有发生变化
 private long creationTime;
 public Volatile() {
  value = 10;
  missedIt = false;
  //获取当前时间,亦即调用Volatile构造函数时的时间
  creationTime = System.currentTimeMillis();
 }
 public void run() {
  print("entering run()");
  //循环检查value的值是否不同
  while ( value < 20 ) {
   //如果missedIt的值被修改为true,则通过break退出循环
   if  ( missedIt ) {
    //进入同步代码块前,将value的值赋给currValue
    int currValue = value;
    //在一个任意对象上执行同步语句,目的是为了让该线程在进入和离开同步代码块时,
    //将该线程中的所有变量的私有拷贝与共享内存中的原始值进行比较,
    //从而发现没有用volatile标记的变量所发生的变化
    Object lock = new Object();
    synchronized ( lock ) {
     //不做任何事
    }
    //离开同步代码块后,将此时value的值赋给valueAfterSync
    int valueAfterSync = value;
    print("in run() - see value=" + currValue +", but rumor has it that it changed!");
    print("in run() - valueAfterSync=" + valueAfterSync);
    break;
   }
  }
  print("leaving run()");
 }
 public void workMethod() throws InterruptedException {
  print("entering workMethod()");
  print("in workMethod() - about to sleep for 2 seconds");
  Thread.sleep(2000);
  //仅在此改变value的值
  value = 50;
  print("in workMethod() - just set value=" + value);
  print("in workMethod() - about to sleep for 5 seconds");
  Thread.sleep(5000);
  //仅在此改变missedIt的值
  missedIt = true;
  print("in workMethod() - just set missedIt=" + missedIt);
  print("in workMethod() - about to sleep for 3 seconds");
  Thread.sleep(3000);
  print("leaving workMethod()");
 }
/*
*该方法的功能是在要打印的msg信息前打印出程序执行到此所化去的时间,以及打印msg的代码所在的线程
*/
 private void print(String msg) {
  //使用java.text包的功能,可以简化这个方法,但是这里没有利用这一点
  long interval = System.currentTimeMillis() - creationTime;
  String tmpStr = "    " + ( interval / 1000.0 ) + "000";
  int pos = tmpStr.indexOf(".");
  String secStr = tmpStr.substring(pos - 2, pos + 4);
  String nameStr = "        " + Thread.currentThread().getName();
  nameStr = nameStr.substring(nameStr.length() - 8, nameStr.length());
  System.out.println(secStr + " " + nameStr + ": " + msg);
 }
 public static void main(String[] args) {
  try {
   //通过该构造函数可以获取实时时钟的当前时间
   Volatile vol = new Volatile();
   //稍停100ms,以让实时时钟稍稍超前获取时间,使print()中创建的消息打印的时间值大于0
   Thread.sleep(100);  
   Thread t = new Thread(vol);
   t.start();
   //休眠100ms,让刚刚启动的线程有时间运行
   Thread.sleep(100);  
   //workMethod方法在main线程中运行
   vol.workMethod();
  } catch ( InterruptedException x ) {
   System.err.println("one of the sleeps was interrupted");
  }
 }
}

    对于非volatile修饰的变量,尽管jvm的优化,会导致变量的可见性问题,但这种可见性的问题也只是在短时间内高并发的情况下发生,CPU执行时会很快刷新Cache,一般的情况下很难出现,而且出现这种问题是不可预测的,与jvm, 机器配置环境等都有关。

22.6 Runnable和Thread实现多线程的区别
runnable接口好处如下:

     1、可以避免由于Java的单继承特性而带来的局限;

    2、增强程序的健壮性,代码能够被多个线程共享,代码与数据是独立的;

    3、适合多个相同程序代码的线程区处理同一资源的情况。


下面以典型的买票程序(基本都是以这个为例子)为例,来说明二者的区别。

    首先通过继承Thread类实现,代码如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. class MyThread extends Thread{  
  2.     private int ticket = 5;  
  3.     public void run(){  
  4.         for (int i=0;i<10;i++)  
  5.         {  
  6.             if(ticket > 0){  
  7.                 System.out.println("ticket = " + ticket--);  
  8.             }  
  9.         }  
  10.     }  
  11. }  
  12.   
  13. public class ThreadDemo{  
  14.     public static void main(String[] args){  
  15.         new MyThread().start();  
  16.         new MyThread().start();  
  17.         new MyThread().start();  
  18.     }  
  19. }  
    某次的执行结果如下:

    从结果中可以看出,每个线程单独卖了5张票,即独立地完成了买票的任务,但实际应用中,比如火车站售票,需要多个线程去共同完成任务,在本例中,即多个线程共同买5张票。

    下面是通过实现Runnable接口实现的多线程程序,代码如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. class MyThread implements Runnable{  
  2.     private int ticket = 5;  
  3.     public void run(){  
  4.         for (int i=0;i<10;i++)  
  5.         {  
  6.             if(ticket > 0){  
  7.                 System.out.println("ticket = " + ticket--);  
  8.             }  
  9.         }  
  10.     }  
  11. }  
  12.   
  13. public class RunnableDemo{  
  14.     public static void main(String[] args){  
  15.         MyThread my = new MyThread();  
  16.         new Thread(my).start();  
  17.         new Thread(my).start();  
  18.         new Thread(my).start();  
  19.     }  
  20. }  
    某次的执行结果如下:


    从结果中可以看出,三个线程一共卖了5张票,即它们共同完成了买票的任务,实现了资源的共享。

针对以上代码补充三点: 


    1、在第二种方法(Runnable)中,ticket输出的顺序并不是54321,这是因为线程执行的时机难以预测,ticket--并不是原子操作。

    2、在第一种方法中,我们new了3个Thread对象,即三个线程分别执行三个对象中的代码,因此便是三个线程去独立地完成卖票的任务;而在第二种方法中,我们同样也new了3个Thread对象,但只有一个Runnable对象,3个Thread对象共享这个Runnable对象中的代码,因此,便会出现3个线程共同完成卖票任务的结果。如果我们new出3个Runnable对象,作为参数分别传入3个Thread对象中,那么3个线程便会独立执行各自Runnable对象中的代码,即3个线程各自卖5张票。 

    3、在第二种方法中,由于3个Thread对象共同执行一个Runnable对象中的代码,因此可能会造成线程的不安全,比如可能ticket会输出-1(如果我们System.out....语句前加上线程休眠操作,该情况将很有可能出现),这种情况的出现是由于,一个线程在判断ticket为1>0后,还没有来得及减1,另一个线程已经将ticket减1,变为了0,那么接下来之前的线程再将ticket减1,便得到了-1。这就需要加入同步操作(即互斥锁),确保同一时刻只有一个线程在执行每次for循环中的操作。而在第一种方法中,并不需要加入同步操作,因为每个线程执行自己Thread对象中的代码,不存在多个线程共同执行同一个方法的情况。


22.7 使用synchronized获取互斥锁的几点说明
 在并发编程中,多线程同时并发访问的资源叫做临界资源,当多个线程同时访问对象并要求操作相同资源时,分割了原子操作就有可能出现数据的不一致或数据不完整的情况,为避免这种情况的发生,我们会采取同步机制,以确保在某一时刻,方法内只允许有一个线程。
      采用synchronized修饰符实现的同步机制叫做互斥锁机制,它所获得的锁叫做互斥锁。每个对象都有一个monitor(锁标记),当线程拥有这个锁标记时才能访问这个资源,没有锁标记便进入锁池。任何一个对象系统都会为其创建一个互斥锁,这个锁是为了分配给线程的,防止打断原子操作。每个对象的锁只能分配给一个线程,因此叫做互斥锁。
 
      这里就使用同步机制获取互斥锁的情况,进行几点说明:
      1、如果同一个方法内同时有两个或更多线程,则每个线程有自己的局部变量拷贝。
      2、类的每个实例都有自己的对象级别锁。当一个线程访问实例对象中的synchronized同步代码块或同步方法时,该线程便获取了该实例的对象级别锁,其他线程这时如果要访问synchronized同步代码块或同步方法,便需要阻塞等待,直到前面的线程从同步代码块或方法中退出,释放掉了该对象级别锁。
      3、访问同一个类的不同实例对象中的同步代码块,不存在阻塞等待获取对象锁的问题,因为它们获取的是各自实例的对象级别锁,相互之间没有影响。
      4、持有一个对象级别锁不会阻止该线程被交换出来,也不会阻塞其他线程访问同一示例对象中的非synchronized代码。当一个线程A持有一个对象级别锁(即进入了synchronized修饰的代码块或方法中)时,线程也有可能被交换出去,此时线程B有可能获取执行该对象中代码的时间,但它只能执行非同步代码(没有用synchronized修饰),当执行到同步代码时,便会被阻塞,此时可能线程规划器又让A线程运行,A线程继续持有对象级别锁,当A线程退出同步代码时(即释放了对象级别锁),如果B线程此时再运行,便会获得该对象级别锁,从而执行synchronized中的代码。
     5、持有对象级别锁的线程会让其他线程阻塞在所有的synchronized代码外。例如,在一个类中有三个synchronized方法a,b,c,当线程A正在执行一个实例对象M中的方法a时,它便获得了该对象级别锁,那么其他的线程在执行同一实例对象(即对象M)中的代码时,便会在所有的synchronized方法处阻塞,即在方法a,b,c处都要被阻塞,等线程A释放掉对象级别锁时,其他的线程才可以去执行方法a,b或者c中的代码,从而获得该对象级别锁。
     6、使用synchronized(obj)同步语句块,可以获取指定对象上的对象级别锁。obj为对象的引用,如果获取了obj对象上的对象级别锁,在并发访问obj对象时时,便会在其synchronized代码处阻塞等待,直到获取到该obj对象的对象级别锁。当obj为this时,便是获取当前对象的对象级别锁。
     7、类级别锁被特定类的所有示例共享,它用于控制对static成员变量以及static方法的并发访问。具体用法与对象级别锁相似。
    8、互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。synchronized关键字经过编译后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁,如果获得了锁,把锁的计数器加1,相应地,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁便被释放了。由于synchronized同步块对同一个线程是可重入的,因此一个线程可以多次获得同一个对象的互斥锁,同样,要释放相应次数的该互斥锁,才能最终释放掉该锁。

22.8 多线程环境中安全使用集合API(含代码)
    在Collections类 中有多个静态方法,它们可以获取通过同步方法封装非同步集合而得到的集合:

     public static Collection synchronizedCollention(Collection c)
     public static List synchronizedList(list l)
     public static Map synchronizedMap(Map m)
     public static Set synchronizedSet(Set s)
     public static SortedMap synchronizedSortedMap(SortedMap sm)
     public static SortedSet synchronizedSortedSet(SortedSet ss)

 下面给出一段多线程中安全遍历集合元素的示例。我们使用Iterator逐个扫描List中的元素,在多线程环境中,当遍历当前集合中的元素时,一般希望阻止其他线程添加或删除元素。安全遍历的实现方法如下:
[java] view plaincopy在CODE上查看代码片派生到我的代码片
import java.util.*;  
 
public class SafeCollectionIteration extends Object {  
    public static void main(String[] args) {  
        //为了安全起见,仅使用同步列表的一个引用,这样可以确保控制了所有访问  
        //集合必须同步化,这里是一个List  
        List wordList = Collections.synchronizedList(new ArrayList());  
 
        //wordList中的add方法是同步方法,会获取wordList实例的对象锁  
        wordList.add("Iterators");  
        wordList.add("require");  
        wordList.add("special");  
        wordList.add("handling");  
 
        //获取wordList实例的对象锁,  
        //迭代时,阻塞其他线程调用add或remove等方法修改元素  
        synchronized ( wordList ) {  
            Iterator iter = wordList.iterator();  
            while ( iter.hasNext() ) {  
                String s = (String) iter.next();  
                System.out.println("found string: " + s + ", length=" + s.length());  
            }  
        }  
    }  
}  
     这里需要注意的是:在Java语言中,大部分的线程安全类都是相对线程安全的,它能保证对这个对象单独的操作时线程安全的,我们在调用的时候不需要额外的保障措施,但是对于一些特定的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。例如Vector、HashTable、Collections的synchronizedXxxx()方法包装的集合等。

22.9 死锁
    当线程需要同时持有多个锁时,有可能产生死锁。考虑如下情形:
    线程A当前持有互斥所锁lock1,线程B当前持有互斥锁lock2。接下来,当线程A仍然持有lock1时,它试图获取lock2,因为线程B正持有lock2,因此线程A会阻塞等待线程B对lock2的释放。如果此时线程B在持有lock2的时候,也在试图获取lock1,因为线程A正持有lock1,因此线程B会阻塞等待A对lock1的释放。二者都在等待对方所持有锁的释放,而二者却又都没释放自己所持有的锁,这时二者便会一直阻塞下去。这种情形称为死锁。
    大部分代码并不容易产生死锁,死锁可能在代码中隐藏相当长的时间,等待不常见的条件地发生,但即使是很小的概率,一旦发生,便可能造成毁灭性的破坏。避免死锁是一件困难的事,遵循以下原则有助于规避死锁:
     1、只在必要的最短时间内持有锁,考虑使用同步语句块代替整个同步方法
     2、尽量编写不在同一时刻需要持有多个锁的代码,如果不可避免,则确保线程持有第二个锁的时间尽量短暂;
     3、创建和使用一个大锁来代替若干小锁,并把这个锁用于互斥,而不是用作单个对象的对象级别锁;
    
22.10 使用wait/notify/notifyAll实现线程间通信的几点重要说明
    Object对象:
    wait():置入休眠状态,直到接到通知或被中断为止。
    notify():通知其他线程从wait()方法处返回,随机某个线程
    notifyAll():全部返回

    1. wait()
     public final void wait()  throws InterruptedException,IllegalMonitorStateException
    该方法用来将当前线程置入休眠状态,直到接到通知或被中断为止。在调用wait()之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法。进入wait()方法后,当前线程释放锁在从wait()返回前,线程与其他线程竞争重新获得锁。如果调用wait()时,没有持有适当的锁,则抛出IllegalMonitorStateException,它是RuntimeException的一个子类,因此,不需要try-catch结构
    2、notify()
    public final native void notify() throws IllegalMonitorStateException
     该方法也要在同步方法或同步块中调用,即在调用前,线程也必须要获得该对象的对象级别锁,的如果调用notify()时没有持有适当的锁,也会抛出IllegalMonitorStateException。
    该方法用来通知那些可能等待该对象的对象锁的其他线程。如果有多个线程等待,则线程规划器任意挑选出其中一个wait()状态的线程来发出通知,并使它等待获取该对象的对象锁(notify后,当前线程不会马上释放该对象锁,wait所在的线程并不能马上获取该对象锁,要等到程序退出synchronized代码块后,当前线程才会释放锁,wait所在的线程也才可以获取该对象锁),但不惊动其他同样在等待被该对象notify的线程们。当第一个获得了该对象锁的wait线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,会继续阻塞在wait状态,直到这个对象发出一个notify或notifyAll。这里需要注意:它们等待的是被notify或notifyAll,而不是锁。这与下面的notifyAll()方法执行后的情况不同。 
    3、notifyAll()
     public final native void notifyAll() throws IllegalMonitorStateException
      该方法与notify()方法的工作方式相同,重要的一点差异是:
      notifyAll使所有原来在该对象上wait的线程统统退出wait的状态(即全部被唤醒,不再等待notify或notifyAll,但由于此时还没有获取到该对象锁,因此还不能继续往下执行),变成等待获取该对象上的锁,一旦该对象锁被释放(notifyAll线程退出调用了notifyAll的synchronized代码块的时候),他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出synchronized代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。
4、wait(long)和wait(long,int)
     显然,这两个方法是设置等待超时时间的后者在超值时间上加上ns,精度也难以达到,因此,该方法很少使用。对于前者,如果在等待线程接到通知或被中断之前,已经超过了指定的毫秒数,则它通过竞争重新获得锁,并从wait(long)返回。另外,需要知道,如果设置了超时时间,当wait()返回时,我们不能确定它是因为接到了通知还是因为超时而返回的,因为wait()方法不会返回任何相关的信息。但一般可以通过设置标志位来判断,在notify之前改变标志位的值,在wait()方法后读取该标志位的值来判断,当然为了保证notify不被遗漏,我们还需要另外一个标志位来循环判断是否调用wait()方法。
    
  深入理解:
   如果线程调用了对象的wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁
   当有线程调用了对象的notifyAll()方法(唤醒所有wait线程)或notify()方法(只随机唤醒一个wait线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
   优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了synchronized代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

22.11 线程间通信中notify通知的遗漏(含代码)
notify通知的遗漏很容易理解,即threadA还没开始wait的时候,threadB已经notify了,这样,threadB通知是没有任何响应的,当threadB退出synchronized代码块后,threadA再开始wait,便会一直阻塞等待,直到被别的线程打断。
public class WaitNotify {
    public static void main(String[] args) throws InterruptedException {
        ThreadA a = new ThreadA();
        ThreadB b = new ThreadB(a);
        new Thread(a).start();
        new Thread(b).start();
        Thread.sleep(3000);
        new Thread(b).start();
    }
}
class ThreadA implements Runnable{
    @Override
    public void run() {
        try {
            System.out.println("threada sleep 2s!");
            Thread.sleep(2000);// 2s
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (this){
            try {
                System.out.println("threadA is waiting!");
                wait();
                System.out.println("threadA is notified!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class ThreadB implements Runnable{
    private ThreadA a;
    public ThreadB(ThreadA a) {
        this.a=a;
    }
    @Override
    public void run() {
        synchronized (a){    // 需要拿到a的对象
            a.notifyAll();
            System.out.println("already notifyAll a!");
    }
    }
}


修正后的代码
    为了修正MissedNotify,需要添加一个boolean指示变量,该变量只能在同步代码块内部访问和修改。修改后的代码如下:
public class MissedNotifyFix extends Object {
 private Object proceedLock;
 //该标志位用来指示线程是否需要等待
 private boolean okToProceed;

 public MissedNotifyFix() {
  print("in MissedNotify()");
  proceedLock = new Object();
  //先设置为false
  okToProceed = false;
 }

 public void waitToProceed() throws InterruptedException {
  print("in waitToProceed() - entered");

  synchronized ( proceedLock ) {
   print("in waitToProceed() - entered sync block");
   //while循环判断,这里不用if的原因是为了防止早期通知
   while ( okToProceed == false ) {
    print("in waitToProceed() - about to wait()");
    proceedLock.wait();
    print("in waitToProceed() - back from wait()");
   }

   print("in waitToProceed() - leaving sync block");
  }

  print("in waitToProceed() - leaving");
 }

 public void proceed() {
  print("in proceed() - entered");

  synchronized ( proceedLock ) {
   print("in proceed() - entered sync block");
   //通知之前,将其设置为true,这样即使出现通知遗漏的情况,也不会使线程在wait出阻塞
   okToProceed = true;
   print("in proceed() - changed okToProceed to true");
   proceedLock.notifyAll();
   print("in proceed() - just did notifyAll()");

   print("in proceed() - leaving sync block");
  }

  print("in proceed() - leaving");
 }

 private static void print(String msg) {
  String name = Thread.currentThread().getName();
  System.out.println(name + ": " + msg);
 }

 public static void main(String[] args) {
  final MissedNotifyFix mnf = new MissedNotifyFix();

  Runnable runA = new Runnable() {
    public void run() {
     try {
      //休眠1000ms,大于runB中的500ms,
      //是为了后调用waitToProceed,从而先notifyAll,后wait,
      Thread.sleep(1000);
      mnf.waitToProceed();
     } catch ( InterruptedException x ) {
      x.printStackTrace();
     }
    }
   };

  Thread threadA = new Thread(runA, "threadA");
  threadA.start();

  Runnable runB = new Runnable() {
    public void run() {
     try {
      //休眠500ms,小于runA中的1000ms,
      //是为了先调用proceed,从而先notifyAll,后wait,
      Thread.sleep(500);
      mnf.proceed();
     } catch ( InterruptedException x ) {
      x.printStackTrace();
     }
    }
   };

  Thread threadB = new Thread(runB, "threadB");
  threadB.start();

  try { 
   Thread.sleep(10000);
  } catch ( InterruptedException x ) {}

  print("about to invoke interrupt() on threadA");
  threadA.interrupt();
 }
}

注意代码中加了注释的部分,在threadB进行通知之前,先将okToProceed置为true,这样如果threadA将通知遗漏,那么就不会进入while循环,也便不会执行wait方法,线程也就不会阻塞。如果通知没有被遗漏,wait方法返回后,okToProceed已经被置为true,下次while循环判断条件不成立,便会退出循环。
    这样,通过标志位和wait、notifyAll的配合使用,便避免了通知遗漏而造成的阻塞问题。
   总结:在使用线程的等待/通知机制时,一般都要配合一个boolean变量值(或者其他能够判断真假的条件),在notify之前改变该boolean变量的值,让wait返回后能够退出while循环(一般都要在wait方法外围加一层while循环,以防止早期通知),或在通知被遗漏后,不会被阻塞在wait方法处。这样便保证了程序的正确性。

22.12 进程间听信中notifyAll造成早通知的问题

    

2.13 生产者消费者模型
    生产者消费者问题是线程模型中的经典问题:生产者和消费者在同一时间段内共用同一存储空间,生产者向空间里生产数据,而消费者取走数据。
    这里实现如下情况的生产--消费模型:
    生产者不断交替地生产两组数据“姓名--1 --> 内容--1”,“姓名--2--> 内容--2”,消费者不断交替地取得这两组数据,这里的“姓名--1”和“姓名--2”模拟为数据的名称,“内容--1 ”和“内容--2 ”模拟为数据的内容。
    由于本程序中牵扯到线程运行的不确定性,因此可能会出现以下问题:
     1、假设生产者线程刚向数据存储空间添加了数据的名称,还没有加入该信息的内容,程序就切换到了消费者线程,消费者线程将把信息的名称和上一个信息的内容联系在一起;
     2、生产者生产了若干次数据,消费者才开始取数据,或者是,消费者取完一次数据后,还没等生产者放入新的数据,又重复取出了已取过的数据。
     问题1很明显要靠同步来解决,问题2则需要线程间通信,生产者线程放入数据后,通知消费者线程取出数据,消费者线程取出数据后,通知生产者线程生产数据,这里用wait/notify机制来实现。

class Info{ // 定义信息类
 private String name = "name";//定义name属性,为了与下面set的name属性区别开
 private String content = "content" ;// 定义content属性,为了与下面set的content属性区别开
 private boolean flag = true ; // 设置标志位,初始时先生产
 public synchronized void set(String name,String content){
  while(!flag){
   try{
    super.wait() ;
   }catch(InterruptedException e){
    e.printStackTrace() ;
   }
  }
  this.setName(name) ; // 设置名称
  try{
   Thread.sleep(300) ;
  }catch(InterruptedException e){
   e.printStackTrace() ;
  }
  this.setContent(content) ; // 设置内容
  flag  = false ; // 改变标志位,表示可以取走
  super.notify();
 }
 public synchronized void get(){
  while(flag){
   try{
    super.wait() ;
   }catch(InterruptedException e){
    e.printStackTrace() ;
   }
  }
  try{
   Thread.sleep(300) ;
  }catch(InterruptedException e){
   e.printStackTrace() ;
  }
  System.out.println(this.getName() + 
   " --> " + this.getContent()) ;
  flag  = true ; // 改变标志位,表示可以生产
  super.notify();
 }
 public void setName(String name){
  this.name = name ;
 }
 public void setContent(String content){
  this.content = content ;
 }
 public String getName(){
  return this.name ;
 }
 public String getContent(){
  return this.content ;
 }
}
class Producer implements Runnable{ // 通过Runnable实现多线程
 private Info info = null ; // 保存Info引用
 public Producer(Info info){
  this.info = info ;
 }
 public void run(){
  boolean flag = true ; // 定义标记位
  for(int i=0;i<10;i++){
   if(flag){
    this.info.set("姓名--1","内容--1") ; // 设置名称
    flag = false ;
   }else{
    this.info.set("姓名--2","内容--2") ; // 设置名称
    flag = true ;
   }
  }
 }
}
class Consumer implements Runnable{
 private Info info = null ;
 public Consumer(Info info){
  this.info = info ;
 }
 public void run(){
  for(int i=0;i<10;i++){
   this.info.get() ;
  }
 }
}
public class ThreadCaseDemo03{
 public static void main(String args[]){
  Info info = new Info(); // 实例化Info对象
  Producer pro = new Producer(info) ; // 生产者
  Consumer con = new Consumer(info) ; // 消费者
  new Thread(pro).start() ;
  //启动了生产者线程后,再启动消费者线程
  try{
   Thread.sleep(500) ;
  }catch(InterruptedException e){
   e.printStackTrace() ;
  }

  new Thread(con).start() ;
 }
}
     另外,在run方法中,二者循环的次数要相同,否则,当一方的循环结束时,另一方的循环依然继续,它会阻塞在wait()方法处,而等不到对方的notify通知。

22.14 内存可见性
     加锁(synchronized同步)的功能不仅仅局限于互斥行为,同时还存在另外一个重要的方面:内存可见性。我们不仅希望防止某个线程正在使用对象状态而另一个线程在同时修改该状态,而且还希望确保当一个线程修改了对象状态后,其他线程能够看到该变化。而线程的同步恰恰也能够实现这一点。
    内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果。为了确保所有的线程都能看到共享变量的最新值,可以在所有执行读操作或写操作的线程上加上同一把锁。下图示例了同步的可见性保证。
        当线程A执行某个同步代码块时,线程B随后进入由同一个锁保护的同步代码块,这种情况下可以保证,当锁被释放前,A看到的所有变量值(锁释放前,A看到的变量包括y和x)在B获得同一个锁后同样可以由B看到。换句话说,当线程B执行由锁保护的同步代码块时,可以看到线程A之前在同一个锁保护的同步代码块中的所有操作结果。如果在线程A unlock M之后,线程B才进入lock M,那么线程B都可以看到线程A unlock M之前的操作,可以得到i=1,j=1。如果在线程B unlock M之后,线程A才进入lock M,那么线程B就不一定能看到线程A中的操作,因此j的值就不一定是1。
           现在考虑如下代码:
[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public class  MutableInteger  
  2. {  
  3.     private int value;  
  4.   
  5.     public int get(){  
  6.         return value;  
  7.     }  
  8.     public void set(int value){  
  9.         this.value = value;  
  10.     }  
  11. }  


     以上代码中,get和set方法都在没有同步的情况下访问value。如果value被多个线程共享,假如某个线程调用了set,那么另一个正在调用get的线程可能会看到更新后的value值,也可能看不到。

     通过对set和get方法进行同步,可以使MutableInteger成为一个线程安全的类,如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public class  SynchronizedInteger  
  2. {  
  3.     private int value;  
  4.   
  5.     public synchronized int get(){  
  6.         return value;  
  7.     }  
  8.     public synchronized void set(int value){  
  9.         this.value = value;  
  10.     }  
  11. }  


     对set和get方法进行了同步,加上了同一把对象锁,这样get方法可以看到set方法中value值的变化,从而每次通过get方法取得的value的值都是最新的value值。


22.15 并发编程中实现内存可见的两种方法比较:加锁和volatile变量

    1、volatile变量是一种稍弱的同步机制在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。

    2、从内存可见性的角度看,写入volatile变量相当于退出同步代码块,而读取volatile变量相当于进入同步代码块

    3、在代码中如果过度依赖volatile变量来控制状态的可见性,通常会比使用锁的代码更脆弱,也更难以理解。仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它。一般来说,用同步机制会更安全些。

    4、加锁机制(即同步机制)既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性,原因是声明为volatile的简单变量如果当前值与该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作:“count++”、“count = count+1”。

当且仅当满足以下所有条件时,才应该使用volatile变量:

     1、对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值

     2、该变量没有包含在具有其他变量的不变式中

总结:在需要同步的时候,第一选择应该是synchronized关键字,这是最安全的方式,尝试其他任何方式都是有风险的。尤其在、jdK1.5之后,对synchronized同步机制做了很多优化,如:自适应的自旋锁、锁粗化、锁消除、轻量级锁等,使得它的性能明显有了很大的提升。

22.16 深入Java内存模型——happen-before规则及其对DCL的分析(含代码)
happen—before规则介绍
    Java语言中有一个“先行发生”(happen—before)的规则,它是Java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,其意思就是说,在发生操作B之前,操作A产生的影响都能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等,它与时间上的先后发生基本没有太大关系。这个原则特别重要,它是判断数据是否存在竞争、线程是否安全的主要依据。
    举例来说,假设存在如下三个线程,分别执行对应的操作:
---------------------------------------------------------------------------
线程A中执行如下操作:i=1
线程B中执行如下操作:j=i
线程C中执行如下操作:i=2
---------------------------------------------------------------------------
    假设线程A中的操作”i=1“ happen—before线程B中的操作“j=i”,那么就可以保证在线程B的操作执行后,变量j的值一定为1,即线程B观察到了线程A中操作“i=1”所产生的影响;现在,我们依然保持线程A和线程B之间的happen—before关系,同时线程C出现在了线程A和线程B的操作之间,但是C与B并没有happen—before关系那么j的值就不确定了,线程C对变量i的影响可能会被线程B观察到,也可能不会,这时线程B就存在读取到不是最新数据的风险,不具备线程安全性。
    下面是Java内存模型中的八条可保证happen—before的规则,它们无需任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们进行随机地重排序。
    1、程序次序规则:在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作happen—before(时间上)后执行的操作。
    2、管理锁定规则:一个unlock操作happen—before后面(时间上的先后顺序,下同)对同一个锁的lock操作。
    3、volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。
    4、线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。
    5、线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
    6、线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。
    7、对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。
    8、传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。

时间上先后顺序和happen—before原则

   1、首先来看操作A在时间上先与操作B发生,是否意味着操作A happen—before操作B?

    一个常用来分析的例子如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. private int value = 0;  
  2.   
  3. public int get(){  
  4.     return value;  
  5. }  
  6. public void set(int value){  
  7.     this.value = value;  
  8. }  
    假设存在线程A和线程B,线程A先(时间上的先)调用了setValue(3)操作,然后(时间上的后)线程B调用了同一对象的getValue()方法,那么线程B得到的返回值一定是3吗?

    对照以上八条happen—before规则,发现没有一条规则适合于这里的value变量,从而我们可以判定线程A中的setValue(3)操作与线程B中的getValue()操作不存在happen—before关系。因此,尽管线程A的setValue(3)在操作时间上先于操作B的getvalue(),但无法保证线程B的getValue()操作一定观察到了线程A的setValue(3)操作所产生的结果,也即是getValue()的返回值不一定为3(有可能是之前setValue所设置的值)。这里的操作不是线程安全的。

    因此,”一个操作时间上先发生于另一个操作“并不代表”一个操作happen—before另一个操作“。

    解决方法:可以将setValue(int)方法和getValue()方法均定义为synchronized方法,也可以把value定义为volatile变量(value的修改并不依赖value的原值,符合volatile的使用场景),分别对应happen—before规则的第2和第3条。注意,只将setValue(int)方法和getvalue()方法中的一个定义为synchronized方法是不行的,必须对同一个变量的所有读写同步,才能保证不读取到陈旧的数据,仅仅同步读或写是不够的 。

    2、其次来看,操作A happen—before操作B,是否意味着操作A在时间上先与操作B发生?

    看有如下代码:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. x = 1;  
  2. y = 2;  

    假设同一个线程执行上面两个操作:操作A:x=1和操作B:y=2。根据happen—before规则的第1条,操作A happen—before 操作B,但是由于编译器的指令重排序(Java语言规范规定了JVM线程内部维持顺序化语义,也就是说只要程序的最终结果等同于它在严格的顺序化环境下的结果,那么指令的执行顺序就可能与代码的顺序不一致。这个过程通过叫做指令的重排序。指令重排序存在的意义在于:JVM能够根据处理器的特性(CPU的多级缓存系统、多核处理器等)适当的重新排序机器指令,使机器指令更符合CPU的执行特点,最大限度的发挥机器的性能。在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整)等原因,操作A在时间上有可能后于操作B被处理器执行,但这并不影响happen—before原则的正确性。

    因此,”一个操作happen—before另一个操作“并不代表”一个操作时间上先发生于另一个操作“。


    最后,一个操作和另一个操作必定存在某个顺序,要么一个操作或者是先于或者是后于另一个操作,或者与两个操作同时发生。同时发生是完全可能存在的,特别是在多CPU的情况下。而两个操作之间却可能没有happen-before关系,也就是说有可能发生这样的情况,操作A不happen-before操作B,操作B也不happen-before操作A,用数学上的术语happen-before关系是个偏序关系。两个存在happen-before关系的操作不可能同时发生,一个操作A happen-before操作B,它们必定在时间上是完全错开的,这实际上也是同步的语义之一(独占访问)。


利用happen—before规则分析DCL

   DCL即双重检查加锁,关于单例模式的DCL机制,可以参看:http://blog.csdn.net/ns_code/article/details/17359719一文,这里不再详细介绍。下面是一个典型的在单例模式中使用DCL的例子:
[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public class LazySingleton {  
  2.     private int someField;  
  3.       
  4.     private static LazySingleton instance;  
  5.       
  6.     private LazySingleton() {  
  7.         this.someField = new Random().nextInt(200)+1;         // (1)  
  8.     }  
  9.       
  10.     public static LazySingleton getInstance() {  
  11.         if (instance == null) {                               // (2)  
  12.             synchronized(LazySingleton.class) {               // (3)  
  13.                 if (instance == null) {                       // (4)  
  14.                     instance = new LazySingleton();           // (5)  
  15.                 }  
  16.             }  
  17.         }  
  18.         return instance;                                      // (6)  
  19.     }  
  20.       
  21.     public int getSomeField() {  
  22.         return this.someField;                                // (7)  
  23.     }  
  24. }  

    这里得到单一的instance实例是没有问题的,问题的关键在于尽管得到了Singleton的正确引用,但是却有可能访问到其成员变量的不正确值。具体来说Singleton.getInstance().getSomeField()有可能返回someField的默认值0。如果程序行为正确的话,这应当是不可能发生的事,因为在构造函数里设置的someField的值不可能为0。为也说明这种情况理论上有可能发生,我们只需要说明语句(1)和语句(7)并不存在happen-before关系。

   假设线程Ⅰ是初次调用getInstance()方法,紧接着线程Ⅱ也调用了getInstance()方法和getSomeField()方法,我们要说明的是线程Ⅰ的语句(1)并不happen-before线程Ⅱ的语句(7)。线程Ⅱ在执行getInstance()方法的语句(2)时,由于对instance的访问并没有处于同步块中,因此线程Ⅱ可能观察到也可能观察不到线程Ⅰ在语句(5)时对instance的写入,也就是说instance的值可能为空也可能为非空。我们先假设instance的值非空,也就观察到了线程Ⅰ对instance的写入,这时线程Ⅱ就会执行语句(6)直接返回这个instance的值,然后对这个instance调用getSomeField()方法,该方法也是在没有任何同步情况被调用,因此整个线程Ⅱ的操作都是在没有同步的情况下调用 ,这时我们便无法利用上述8条happen-before规则得到线程Ⅰ的操作和线程Ⅱ的操作之间的任何有效的happen-before关系(主要考虑规则的第2条,但由于线程Ⅱ没有在进入synchronized块,因此不存在lock与unlock锁的问题),这说明线程Ⅰ的语句(1)和线程Ⅱ的语句(7)之间并不存在happen-before关系,这就意味着线程Ⅱ在执行语句(7)完全有可能观测不到线程Ⅰ在语句(1)处对someFiled写入的值,这就是DCL的问题所在。很荒谬,是吧?DCL原本是为了逃避同步,它达到了这个目的,也正是因为如此,它最终受到惩罚,这样的程序存在严重的bug,虽然这种bug被发现的概率绝对比中彩票的概率还要低得多,而且是转瞬即逝,更可怕的是,即使发生了你也不会想到是DCL所引起的。

    前面我们说了,线程Ⅱ在执行语句(2)时也有可能观察空值,如果是种情况,那么它需要进入同步块,并执行语句(4)。在语句(4)处线程Ⅱ还能够读到instance的空值吗?不可能。这里因为这时对instance的写和读都是发生在同一个锁确定的同步块中,这时读到的数据是最新的数据。为也加深印象,我再用happen-before规则分析一遍。线程Ⅱ在语句(3)处会执行一个lock操作,而线程Ⅰ在语句(5)后会执行一个unlock操作,这两个操作都是针对同一个锁--Singleton.class,因此根据第2条happen-before规则,线程Ⅰ的unlock操作happen-before线程Ⅱ的lock操作,再利用单线程规则,线程Ⅰ的语句(5) -> 线程Ⅰ的unlock操作,线程Ⅱ的lock操作 -> 线程Ⅱ的语句(4),再根据传递规则,就有线程Ⅰ的语句(5) -> 线程Ⅱ的语句(4),也就是说线程Ⅱ在执行语句(4)时能够观测到线程Ⅰ在语句(5)时对Singleton的写入值。接着对返回的instance调用getSomeField()方法时,我们也能得到线程Ⅰ的语句(1) -> 线程Ⅱ的语句(7)(由于线程Ⅱ有进入synchronized块,根据规则2可得),这表明这时getSomeField能够得到正确的值。但是仅仅是这种情况的正确性并不妨碍DCL的不正确性,一个程序的正确性必须在所有的情况下的行为都是正确的,而不能有时正确,有时不正确。

    对DCL的分析也告诉我们一条经验原则:对引用(包括对象引用和数组引用)的非同步访问,即使得到该引用的最新值,却并不能保证也能得到其成员变量(对数组而言就是每个数组元素)的最新值。

   解决方案:
    1、最简单而且安全的解决方法是使用static内部类的思想,它利用的思想是:一个类直到被使用时才被初始化,而类初始化的过程是非并行的,这些都有JLS保证。
如下述代码:
[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public class Singleton {  
  2.   
  3.   private Singleton() {}  
  4.   
  5.   // Lazy initialization holder class idiom for static fields  
  6.   private static class InstanceHolder {  
  7.    private static final Singleton instance = new Singleton();  
  8.   }  
  9.   
  10.   public static Singleton getSingleton() {   
  11.     return InstanceHolder.instance;   
  12.   }  
  13. }  

 
    2、另外,可以将instance声明为volatile,即
private volatile static LazySingleton instance; 
 

    这样我们便可以得到,线程Ⅰ的语句(5) -> 语线程Ⅱ的句(2),根据单线程规则,线程Ⅰ的语句(1) -> 线程Ⅰ的语句(5)和语线程Ⅱ的句(2) -> 语线程Ⅱ的句(7),再根据传递规则就有线程Ⅰ的语句(1) -> 语线程Ⅱ的句(7),这表示线程Ⅱ能够观察到线程Ⅰ在语句(1)时对someFiled的写入值,程序能够得到正确的行为。


   注:

    1、volatile屏蔽指令重排序的语义在JDK1.5中才被完全修复,此前的JDK中及时将变量声明为volatile,也仍然不能完全避免重排序所导致的问题(主要是volatile变量前后的代码仍然存在重排序问题),这点也是在JDK1.5之前的Java中无法安全使用DCL来实现单例模式的原因。

    2、把volatile写和volatile读这两个操作综合起来看,在读线程B读一个volatile变量后,写线程A在写这个volatile变量之前,所有可见的共享变量的值都将立即变得对读线程B可见。

 

   3、 在java5之前对final字段的同步语义和其它变量没有什么区别,在java5中,final变量一旦在构造函数中设置完成(前提是在构造函数中没有泄露this引用),其它线程必定会看到在构造函数中设置的值。而DCL的问题正好在于看到对象的成员变量的默认值,因此我们可以将LazySingleton的someField变量设置成final,这样在java5中就能够正确运行了。


22.17-19 并发新特性—Executor框架与线程池(含代码)

     Executor在Java5中引入的,在java.util.cocurrent 包下,来控制线程的启动、执行和关闭。

    优点:

        1. 简化并发编程的操作

        2. 有助于避免this逃逸的问题,如果我们在构造器中启动一个线程,因为另一个任务可能会在构造器结束之前开始执行,此时可能会访问到初始化了一半的对象。

    Executor框架包括:线程池,Executor,Executors,ExecutorService,CompletionService,Future,Callable等。

        Executor接口:定义一个execute(Runnable command)方法接收runnable实例执行一个线程





newCachedThreadPool()               

-缓存型池子,先查看池中有没有以前建立的线程,如果有,就 reuse.如果没有,就建一个新的线程加入池中
-缓存型池子通常用于执行一些生存期很短的异步型任务
 因此在一些面向连接的daemon型SERVER中用得不多。但对于生存期短的异步任务,它是Executor的首选。
-能reuse的线程,必须是timeout IDLE内的池中线程,缺省     timeout是60s,超过这个IDLE时长,线程实例将被终止及移出池。
  注意,放入CachedThreadPool的线程不必担心其结束,超过TIMEOUT不活动,其会自动被终止。



newFixedThreadPool(int)               

-newFixedThreadPool与cacheThreadPool差不多,也是能reuse就用,但不能随时建新的线程

-其独特之处:任意时间点,最多只能有固定数目的活动线程存在,此时如果有新的线程要建立,只能放在另外的队列中等待直到当前的线程中某个线程终止直接被移出池子
-和cacheThreadPool不同,FixedThreadPool没有IDLE机制(可能也有,但既然文档没提,肯定非常长,类似依赖上层的TCP或UDP IDLE机制之类的),所以FixedThreadPool多数针对一些很稳定很固定的正规并发线程,多用于服务器
-从方法的源代码看,cache池和fixed 池调用的是同一个底层 池,只不过参数不同:
fixed池线程数固定,并且是0秒IDLE(无IDLE)    
cache池线程数支持0-Integer.MAX_VALUE(显然完全没考虑主机的资源承受能力),60秒IDLE  


newScheduledThreadPool(int)

-调度型线程池
-这个池子里的线程可以按schedule依次delay执行,或周期执行

SingleThreadExecutor()

-单例线程,任意时间池中只能有一个线程
-用的是和cache池和fixed池相同的底层池,但线程数目是1-1,0秒IDLE(无IDLE)

22.20 lock详解
    synchronized:互斥同步主要的问题就是线程阻塞和线程换新所带来的性能问题,又称为阻塞同步。它是一种悲观的并发策略,即独占锁。在多任务的时候会引起CPU频繁上下文切换效率很低。是语法层面的互斥锁。
    ReetrantLock:基于冲突检测的乐观并发策略,如果没有其他线程征用共享数据,操作成功,如果共享数据被征用,产生了冲突,在进行补偿(最常见的补偿措施就是不断地重拾,直到试成功为止)。API层面的互斥锁。
    
悲观并发策略和乐观并发策略比较:
悲观锁:假定访问某个对象概率很高,于是在访问对象之前就将该对象锁住,知道提交工作之后才释放锁。
    缺陷: 1. 加锁时间可能会很长,会长时间限制其他用户访问
                2. 并发性不高
乐观锁:假定访问某个对象概率很小,直到要提交更改的时候才将对象锁住,读取的时候并不加锁。
    优点: 1. 加锁时间短,能有较好的并发访问性能。
    缺点: 1. 第二个用户在第一个用户提交之前读取了该对象,那么当第二个要提交更改的时候会发现对象已经变化,于是会增加用户读取对象的次数。

ReentrantLock相对synchronized而言还是增加了一些高级功能,主要有以下三项:
1. 等待可中断:当持有锁的线程长期不释放锁时,正在等待的线程可以选择放弃等待,改为处理其他事情,它对处理执行时间非常上的同步块很有帮助。而在等待由synchronized产生的互斥锁时,会一直阻塞,是不能被中断的。
2、可实现公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序排队等待,而非公平锁则不保证这点,在锁释放时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁时非公平锁,ReentrantLock默认情况下也是非公平锁,但可以通过构造方法ReentrantLock(ture)来要求使用公平锁。
3、锁可以绑定多个条件:ReentrantLock对象可以同时绑定多个Condition对象(名曰:条件变量或条件队列),而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含条件,但如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无需这么做,只需要多次调用newCondition()方法即可。而且我们还可以通过绑定Condition对象来判断当前线程通知的是哪些线程(即与Condition对象绑定在一起的其他线程)。

可中断锁
ReetrantLock锁类型:
    1. 忽略中断锁:和sychronize一样不能响应中断
    2. 相应中断锁:可以响应中断

    如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,如果此时ReetrantLock提供的是忽略中断锁,则它不会去理会该中断,而是让线程B继续等待,而如果此时ReetrantLock提供的是响应中断锁,那么它便会处理中断,让线程B放弃等待,转而去处理其他事情。
[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. ReentrantLock lock = new ReentrantLock();  
  2. ...........  
  3. lock.lockInterruptibly();//获取响应中断锁  
  4. try {  
  5.       //更新对象的状态  
  6.       //捕获异常,必要时恢复到原来的不变约束  
  7.       //如果有return语句,放在这里  
  8. }finally{  
  9.     lock.unlock();        //锁必须在finally块中释放  
  10. }  

读写锁
    由于sychronized会将读读操作同样锁定,会降低性能,所以lock提供了读写锁
[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. ReadWriteLock rwl = new ReentrantReadWriteLock();      
  2. rwl.writeLock().lock()  //获取写锁  
  3. rwl.readLock().lock()  //获取读锁  
    使用lock的条件当你需要以下高级特性时,才应该使用:可定时的、可轮询的与可中断的锁获取操作,公平队列,或者非块结构的锁。否则,请使用synchronized。     

22.21 阻塞队列和阻塞栈
    阻塞队列是Java5中的新特性,接口是java.util.concurrent.BlockingQueue。
    实现类:ArrayBlockingQueue、DelayQueue、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等。

    ArrayBlockingQueue:实现了一个有界队列,当队列满时,便会阻塞等待,直到有元素出队,后续的元素才可以被加入队列。

    阻塞栈:Java6中加入的新特性,接口java.util.concurrent.BlockingDeque,其使用方法很类似。

    例子:
  1. import java.util.concurrent.BlockingDeque;   
  2. import java.util.concurrent.LinkedBlockingDeque;   
  3.   
  4. public class BlockingDequeTest {   
  5.     public static void main(String[] args) throws InterruptedException {   
  6.             BlockingDeque<String> bDeque = new LinkedBlockingDeque<String>(20);   
  7.             for (int i = 0; i < 30; i++) {   
  8.                 //将指定元素添加到此阻塞栈中  
  9.                 bDeque.putFirst("" + i);   
  10.                 System.out.println("向阻塞栈中添加了元素:" + i);   
  11.             }   
  12.             System.out.println("程序到此运行结束,即将退出----");   
  13.     }   
  14. }  
    

22.22 并发新特性—障碍器CyclicBarrier(含代码)
    CyclicBarrier(又叫障碍器):Java5中的新特性,导入包java.util.concurrent.CylicBarrier
    它适用于这样一种情况:你希望创建一组任务,它们并发地执行工作,另外的一个任务在这一组任务并发执行结束前一直阻塞等待,直到该组任务全部执行结束,这个任务才得以执行。这非常像CountDownLatch,只是CountDownLatch是只触发一次的事件,而CyclicBarrier可以多次重用
    
public class CyclicBarrierTest {
public static void main(String[] args){
CyclicBarrier cb = new CyclicBarrier(5,new MainThread());
new Thread(new SubThread("1",cb)).start();
new Thread(new SubThread("2",cb)).start();
new Thread(new SubThread("3",cb)).start();
new Thread(new SubThread("4",cb)).start();
new Thread(new SubThread("5",cb)).start();
}
}

class MainThread implements Runnable{

@Override
public void run() {
System.out.println("这是最后一个程序!");
}
}

class SubThread implements Runnable{
private String name;
private CyclicBarrier cb;

public SubThread(String name, CyclicBarrier cb) {
this.name = name;
this.cb = cb;
}

@Override
public void run() {
System.out.println("子线程:" + name + " 开始执行!");
for (int i=0;i<999999;i++);
System.out.println("子线程:"+name+" 执行完毕!");
try {
cb.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}


22.23 并发新特性—信号量Semaphore(含代码)
    Semaphore是功能完毕的计数信号量,他维护了一个许可集合,对控制一定资源的昂费与回收有着重要的意义。Semaphore可以控制某个资源被同时反问的任务数,通过acquire()获取一个许可,release()释放一个许可。如果同时被访问的任务数已满,则其他acquire的任务进入等待状态,直到有个任务呗releaseIAO,它才能得到许可。
    
public class SemaphoreTest {
public static void main(String[] args){
Semaphore sm = new Semaphore(5);
ExecutorService executorService = Executors.newCachedThreadPool();

for (int i=0;i<50;i++){
SubThreadTest subThreadTest = new SubThreadTest(sm,"线程名"+i); // 创建多个并行线程
executorService.execute(subThreadTest);
}
}
}

class SubThreadTest implements Runnable{
private Semaphore sm;
private String name;

public SubThreadTest(Semaphore sm,String name) {
this.sm = sm;
this.name = name;
}

@Override
public void run() {
try {
sm.acquire();
System.out.println("进入子线程:"+name);
for (int i=0;i<99999;i++)
for (int j=0;j<99999;j++);
System.out.println("子线程运算结束!"+name);
sm.release();
System.out.println("当前允许任务数:"+sm.availablePermits());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
进入子线程:线程名0
进入子线程:线程名2
进入子线程:线程名1
子线程运算结束!线程名2
子线程运算结束!线程名1
子线程运算结束!线程名0
当前允许任务数:4
当前允许任务数:5
当前允许任务数:4
进入子线程:线程名5
子线程运算结束!线程名5
当前允许任务数:4
进入子线程:线程名4



























































0 0
原创粉丝点击