深入探究Java多线程并发编程的要点

关键字synchronized
synchronized关键可以修饰函数、函数内语句。无论它加上方法还是对象上,它取得的锁都是对象,而不是把一段代码或是函数当作锁。
1,当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一段时间只能有一个线程得到执行,而另一个线程只有等当前线程执行完以后才能执行这块代码。
2,当一个线程访问object中的一个synchronized(this)同步代码块时,其它线程仍可以访问这个object中是其它非synchronized (this)代码块。
3,这里需要注意的是,当一个线程访问object的一个synchronized(this)代码块时,其它线程对这个object中其它synchronized (this)同步代码块的访问将被阻塞。
4,以上所述也适用于其它的同步代码块,也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,这个线程就获得了object的对象锁。而且每个对象(即类实例)对应着一把锁,每个synchronized(this)都必须获得调用该代码块儿(可以函数,也可以是变量)的对象的锁才能执行,否则所属线程阻塞,方法一旦执行就会独占该锁,直到从方法返回时,也释放这个锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个对象,其所有声明为synchronized的成员函数中至多只有一个处于可执行状态(因为至多只有一个线程可以获取该对象的锁),从而避免了类成员变量的访问冲突。
synchronized方式的缺点:
由于synchronized锁定的是调用这个同步方法的对象,也就是说,当一个线程P1在不同的线程中执行这个方法时,它们之间会形成互斥,从而达到同步的效果。但这里需要注意的是,这个对象所性的Class的另一个对象却可以任意调用这个被加了synchronized关键字的方法。同步方法的实质是将synchronized作用于object reference,对于拿到了P1对象锁的线程才可以调用这个synchronized方法,而对于P2来说,P1与它毫不相干,程序也可能在这种情况下摆脱同步机制的控制,造成数据混乱。以下我们将对这种情况进行详细地说明:
首先我们先介绍synchronized关键字的两种加锁对象:对象和类——synchronized可以为资源加对象锁或是类锁,类锁对这个类的所有对象(实例)均起作用,而对象锁只是针对该类的一个指定的对象加锁,这个类的其它对象仍然可以使用已经对前一个对象加锁的synchronized方法。
在这里我们主要讨论的一个问题就是:“同一个类,不同实例调用同一个方法,会产生同步问题吗?”
同步问题只和资源有关系,要看这个资源是不是静态的。同一个静态数据,你相同函数分属不同线程同时对其进行读写,CPU也不会产生错误,它会保证你代码的执行逻辑,而这个逻辑是否是你想要的,那就要看你需要什么样的同步了。即便你两个不同的代码,在CPU的不同的两个core里跑,同时写一个内存地址,Cache机制也会在L2里先锁定一个。然后更新,再share给另一个core,也不会出错,不然intel,amd就白养那么多人了。
因此,只要你没有两个代码共享的同一个资源或变量,就不会出现数据不一致的情况。而且同一个类的不同对象的调用有完全不同的堆栈,它们之间完全不相干。
以下我们以一个售票过程举例说明,在这里,我们的共享资源就是票的剩余张数。

package com.test;

public class ThreadSafeTest extends Thread implements Runnable {

  private static int num = 1;

  public ThreadSafeTest(String name) {
    setName(name);
  }

  public void run() {
    sell(getName());
  }

  private synchronized void sell(String name){
    if (num > 0) {
      System. out.println(name + ": 检测票数大于0" );
      System. out.println(name + ": \t正在收款(大约5秒完成)。。。" );
      try {
        Thread. sleep(5000);
        System. out.println(name + ": \t打印票据,售票完成" );
        num--;
        printNumInfo();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    } else {
      System. out.println(name+": 没有票了,停止售票" );
    }
  }

  private static void printNumInfo() {

    System. out.println("系统:当前票数:" + num);
    if (num < 0) {
      System. out.println("警告:票数低于0,出现负数" );
    }
  }

  public static void main(String args[]) {
    try {
      new ThreadSafeTest("售票员李XX" ).start();
      Thread. sleep(2000);
      new ThreadSafeTest("售票员王X" ).start();

    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

运行上述代码,我们得到的输出是:

售票员李XX: 检测票数大于0
售票员李XX:    正在收款(大约5秒完成)。。。
售票员王X: 检测票数大于0
售票员王X: 正在收款(大约5秒完成)。。。
售票员李XX:    打印票据,售票完成
系统:当前票数:0
售票员王X: 打印票据,售票完成
系统:当前票数:-1
警告:票数低于0,出现负数

根据输出结果,我们可以发现,剩余票数为-1,出现了同步错误的问题。之所以出现这种情况的原因是,我们建立的两个实例对象,对共享的静态资源static int num = 1同时进行了修改。那么我们将上面代码中方框内的修饰词static去掉,然后再运行程序,可以得到:

售票员李XX: 检测票数大于0
售票员李XX:    正在收款(大约5秒完成)。。。
售票员王X: 检测票数大于0
售票员王X: 正在收款(大约5秒完成)。。。
售票员李XX:    打印票据,售票完成
系统:当前票数:0
售票员王X: 打印票据,售票完成
系统:当前票数:0

对程度修改之后,程序运行貌似没有问题了,每个对象拥有各自不同的堆栈,分别独立运行。但这样却违背了我们希望多线程同时对共享资源的处理(去static后,num就从共享资源变成了每个实例各自拥有的成员变量),这显然不是我们想要的。
在以上两种代码中,采取的主要是对对象的锁定。由于我之前谈到的原因,当一个类的两个不同的实例对同一共享资源进行修改时,CPU为了保证程序的逻辑会默认这种做法,至于是不是想要的结果,这个只能由程序员自己来决定。因此,我们需要改变锁的作用范围,若作用对象只是实例,那么这种问题是无法避免的;只有当锁的作用范围是整个类的时候,才可能排除同一个类的不同实例对共享资源同时修改的问题。

package com.test;

public class ThreadSafeTest extends Thread implements Runnable {
  private static int num = 1;

  public ThreadSafeTest(String name) {
    setName(name);
  }

  public void run() {
    sell(getName());
  }  

  private synchronized static void sell(String name){

    if (num > 0) {
      System. out.println(name + ": 检测票数大于0" );
      System. out.println(name + ": \t正在收款(大约5秒完成)。。。" );
      try {
        Thread. sleep(5000);
        System. out.println(name + ": \t打印票据,售票完成" );
        num--;
        printNumInfo();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    } else {
      System. out.println(name+": 没有票了,停止售票" );
    }
  }

  private static void printNumInfo() {
    System. out.println("系统:当前票数:" + num);
    if (num < 0) {
      System. out.println("警告:票数低于0,出现负数" );
    }
  }

  public static void main(String args[]) {
    try {
      new ThreadSafeTest("售票员李XX" ).start();
      Thread. sleep(2000);
      new ThreadSafeTest("售票员王X" ).start();

    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

将程序做如上修改,可以得到运行结果:

售票员李XX: 检测票数大于0
售票员李XX:    正在收款(大约5秒完成)。。。
售票员李XX:    打印票据,售票完成
系统:当前票数:0
售票员王X: 没有票了,停止售票

对sell()方法加上了static修饰符,这样就将锁的作用对象变成了类,当该类的一个实例对共享变量进行操作时将会阻塞这个类的其它实例对其的操作。从而得到我们如期想要的结果。
总结:
1,synchronized关键字有两种用法:synchronized方法和synchronized块。
2,在Java中不单是类实例,每一个类也可以对应一把锁
在使用synchronized关键字时,有以下几点儿需要注意:
1,synchronized关键字不能被继承。虽然可以用synchronized来定义方法,但是synchronized却并不属于方法定义的一部分,所以synchronized关键字并不能被继承。如果父类中的某个方法使用了synchronized关键字,而子类中也覆盖了这个方法,默认情况下子类中的这个方法并不是同步的,必须显示的在子类的这个方法中加上synchronized关键字才可。当然,也可以在子类中调用父类中相应的方法,这样虽然子类中的方法并不是同步的,但子类调用了父类中的同步方法,也就相当子类方法也同步了。如,
在子类中加synchronized关键字:

class Parent {
  public synchronized void method() {  }
}
class Child extends Parent {
  public synchronized void method () {  }
}

调用父类方法:

class Parent {
  public synchronized void method() {  }
}
class Child extends Parent {
  public void method() { super.method();  }
}

2,在接口方法定义时不能使用synchronized关键字。
3,构造方法不能使用synchronized关键字,但可以使用synchronized块来进行同步。
4,synchronized位置可以自由放置,但是不能放置在方法的返回类型后面。
5,synchronized关键字不可以用来同步变量,如下面代码是错误的:

public synchronized int n = 0;
public static synchronized int n = 0;

6,虽然使用synchronized关键字是最安全的同步方法,但若是大量使用也会造成不必要的资源消耗以及性能损失。从表面上看synchronized锁定的是一个方法,但实际上锁定的却是一个类,比如,对于两个非静态方法method1()和method2()都使用了synchronized关键字,在执行其中的一个方法时,另一个方法是不能执行的。静态方法和非静态方法情况类似。但是静态方法和非静态方法之间不会相互影响,见如下代码:

public class MyThread1 extends Thread {
  public String methodName ; 

  public static void method(String s) {
    System. out .println(s);
    while (true );
  }
  public synchronized void method1() {
    method( "非静态的method1方法" );
  }
  public synchronized void method2() {
    method( "非静态的method2方法" );
  }
  public static synchronized void method3() {
    method( "静态的method3方法" );
  }
  public static synchronized void method4() {
    method( "静态的method4方法" );
  }
  public void run() {
    try {
      getClass().getMethod( methodName ).invoke( this);
    }
    catch (Exception e) {
    }
  }
  public static void main(String[] args) throws Exception {
    MyThread1 myThread1 = new MyThread1();
    for (int i = 1; i <= 4; i++) {
      myThread1. methodName = "method" + String.valueOf (i);
      new Thread(myThread1).start();
      sleep(100);
    }
  }
}

运行结果为:

非静态的method1方法
静态的method3方法

从上面的运行结果可以看出,method2和method4在method1和method3运行完之前是不会运行的。因此,可以得出一个结论,如查在类中使用synchronized来定义非静态方法,那么将影响这个类中的所有synchronized定义的非静态方法;如果定义的静态方法,那么将影响这个类中所有以synchronized定义的静态方法。这有点儿像数据表中的表锁,当修改一条记录时,系统就将整个表都锁住了。因此,大量使用这种同步方法会使程序的性能大幅度地下降。
对共享资源的同步访问更加安全的技巧:
1,定义private的instance变量+它的get方法,而不要定义public/protected的instance变量。如果将变量定义为public,对象可以在外界绕过同步方法的控制而直接取得它,并且改动它。这也是JavaBean的标准实现之一。
2,如果instance变量是一个对象,如数组或ArrayList等,那上述方法仍然不安全,因为当外界通过get方法拿到这个instance对象的引用后,又将其指向另一个对象,那么这个private变量也就变了,岂不是很危险。这个时候就需要将get方法也加上synchronized同步,并且只返回这个private对象的clone()。这样,调用端得到的就只是对象副本的一个引用了。

wait()与notify()获取对象监视器(锁)的三种方式
在某个线程方法中对wait()和notify()的调用必须指定一个Object对象,而且该线程必须拥有该Object对象的monitor。而获取对象monitor最简单的办法就是,在对象上使用synchronized关键字。当调用wait()方法以后,该线程会释放掉对象锁,并进入sleep状态。而在其它线程调用notify()方法时,必须使用同一个Object对象,notify()方法调用成功后,所在这个对象上的相应的等侍线程将被唤醒。
对于被一个对象锁定的多个方法,在调用notify()方法时将会任选其中一个进行唤醒,而notifyAll()则是将其所有等待线程唤醒。

package net.mindview.util;

import javax.swing.JFrame;

public class WaitAndNotify {
    public static void main(String[] args) {
      System. out.println("Hello World!" );
      WaitAndNotifyJFrame frame = new WaitAndNotifyJFrame();
      frame.setDefaultCloseOperation(JFrame. EXIT_ON_CLOSE);
       // frame.show();
      frame.setVisible( true);
   }
}

@SuppressWarnings("serial" )
class WaitAndNotifyJFrame extends JFrame {

    private WaitAndNotifyThread t ;

    public WaitAndNotifyJFrame() {
      setSize(300, 100);
      setLocation(250, 250);
      JPanel panel = new JPanel();
      JButton start = new JButton(new AbstractAction("Start") {
          public void actionPerformed(ActionEvent event) {
             if (t == null) {
                t = new WaitAndNotifyThread(WaitAndNotifyJFrame.this);
                t.start();
            } else if (t .isWait ) {
                t. isWait = false ;
                t.n();
                // t.notify();
            }
         }
      });
      panel.add(start);
      JButton pause = new JButton(new AbstractAction("Pause") {
          public void actionPerformed(ActionEvent e) {
             if (t != null) {
                t. isWait = true ;
            }
         }
      });
      panel.add(pause);
      JButton end = new JButton(new AbstractAction("End") {
          public void actionPerformed(ActionEvent e) {
             if (t != null) {
                t.interrupt();
                t = null;
            }
         }
      });
      panel.add(end);
      getContentPane().add(panel);
   }

}

@SuppressWarnings("unused" )
class WaitAndNotifyThread extends Thread {

    public boolean isWait ;
    private WaitAndNotifyJFrame control ;
    private int count ;

    public WaitAndNotifyThread(WaitAndNotifyJFrame f) {
       control = f;
       isWait = false ;
       count = 0;
   }

    public void run() {
       try {
          while (true ) {
             synchronized (this ) {
               System. out.println("Count:" + count++);
                sleep(100);
                if (isWait )
                  wait();
            }
         }
      } catch (Exception e) {
      }
   }

   public void n() {
       synchronized (this ) {
         notify();
      }
   }

}

如上面例子方框中的代码,若去掉同步代码块,执行就会抛出java.lang.IllegalMonitorStateException异常。
查看JDK,我们可以看到,出现此异常的原因是当前线程不是此对象监视器的所有者。
此方法只应由作为此对象监视器的所有者的线程来调用,通过以下三种方法之一,可以使线程成为此对象监视器的所有者:
1,通过执行此对象的同步实例方法,如:

  public synchronized void n() {
     notify();
   }

2,通过执行在此对象上进行同步的synchronized语句的正文,如:

 public void n() {
     synchronized (this ) {
       notify();
     }
   }

3,对于Class类型的对象,可以通过执行该类的同步静态方法。
在调用静态方法时,我们并不一定创建一个实例对象。因此,就不能使用this来同步静态方法,所以必须使用Class对象来同步静态方法,由于notify()方法不是静态方法,所以我们无法将n()方法设置成静态方法,所以采用另外一个例子加以说明:

public class SynchronizedStatic implements Runnable {

    private static boolean flag = true;

//类对象同步方法一:
   // 注意static修饰的同步方法,监视器:SynchronizedStatic.class
    private static synchronized void testSyncMethod() {
       for (int i = 0; i < 100; i++) {
          try {
            Thread. sleep(100);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
         System. out.println("testSyncMethod:" + i);
      }
   }

//类对象同步方法二:
      private void testSyncBlock() {
       // 显示使用获取class做为监视器.它与static synchronized method隐式获取class监视器一样.
       synchronized (SynchronizedStatic. class) {
          for (int i = 0; i < 100; i++) {
             try {
               Thread. sleep(100);
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
            System. out.println("testSyncBlock:" + i);
         }
      }
   }

    public void run() {
       // flag是static的变量.所以,不同的线程会执行不同的方法,只有这样才能看到不同的锁定效果.
       if (flag ) {
          flag = false ;
          testSyncMethod();
      } else {
          flag = true ;
         testSyncBlock();
      }
   }

    public static void main(String[] args) {
      ExecutorService exec = Executors. newFixedThreadPool(2);
      SynchronizedStatic rt = new SynchronizedStatic();
      SynchronizedStatic rt1 = new SynchronizedStatic();
      exec.execute(rt);
      exec.execute(rt1);
      exec.shutdown();
   }
}

以上代码的运行结果是,让两个同步方法同时打印从0到99这100个数,其中方法一是一个静态同步方法,它的作用域为类;方法二显示的声明了代码块的作用域是类。这两个方法的异曲同工的。由于方法一和方法二的作用域同为类,所以它们两个方法间是互斥的,也就是说,当一个线程调用了这两个方法中的一个,剩余没有调用的方法也会对其它线程形成阻塞。因此,程序的运行结果会是:

testSyncMethod:0
testSyncMethod:1
... ...
testSyncMethod:99
testSyncBlock:0
... ...
testSyncBlock:99

但是,如果我们将方法二中的SynchronizedStatic. class替换成this的话,由于作用域的没,这两个方法就不会形成互斥,程序的输出结果也会交替进行,如下所示:

testSyncBlock:0
testSyncMethod:0
testSyncBlock:1
testSyncMethod:1
... ...
testSyncMethod:99
testSyncBlock:99

锁(lock)的作用域有两种,一种是类的对象,另一种的类本身。在以上代码中给出了两种使锁的作用范围为类的方法,这样就可以使同一个类的不同对象之间也能完成同步。
总结以上,需要注意的有以下几点:
1,wait()、notify()、notifyAll()都需要在拥有对象监视器的前提下执行,否则就会抛出java.lang.IllegalMonitorStateException异常。
2,多个线程可以同时在一个对象上等待。
3,notify()是随机唤醒一个在对象上等待的线程,若没有等待的线程,则什么也不做。
4,notify()唤醒的线程,并不是在notify()执行以后就立即唤醒,而是在notify()线程释放了对象监视器之后才真正执行被唤醒的线程。
5,Object的这些方法与Thread的sleep、interrupt方法相差还是很远的,不要混为一谈。

(0)

相关推荐

  • Java并发编程示例(十):线程组

    对线程分组是Java并发API提供的一个有趣功能.我们可以将一组线程看成一个独立单元,并且可以随意操纵线程组中的线程对象.比如,可以控制一组线程来运行同样的任务,无需关心有多少线程还在运行,还可以使用一次中断调用中断所有线程的执行. Java提供了ThreadGroup类来控制一个线程组.一个线程组可以通过线程对象来创建,也可以由其他线程组来创建,生成一个树形结构的线程. 根据<Effective Java>的说明,不再建议使用ThreadGroup.建议使用Executor. --D瓜哥特此

  • Java并发编程示例(六):等待线程执行终止

    在某些场景下,我们必须等待线程执行完成才能进行下一步工作.例如,某些程序在开始执行之前,需要先初始化一些资源.这时,我们可以启动一个线程专门来做初始化任务,等到线程任务完成后,再去执行其他部分. 为此,Thread类为我们提供了join()方法.当我们使用线程对象调用此方法时,正在掉调用的线程对象将被推迟到被调用对象执行完成后再开始执行. 在本节,示例程序演示等待初始化方法完成后,再去执行其他任务. 知其然 按照下面所示步骤,完成示例程序. 1.创建一个名为DataSourcesLoader的类

  • Java并发编程示例(七):守护线程的创建和运行

    Java有一种特殊线程,守护线程,这种线程优先级特别低,只有在同一程序中的其他线程不执行时才会执行. 由于守护线程拥有这些特性,所以,一般用为为程序中的普通线程(也称为用户线程)提供服务.它们一般会有一个无限循环,或用于等待请求服务,或用于执行任务等.它们不可以做任何重要的工作,因为我们不确定他们什么时才能分配到CPU运行时间,而且当没有其他线程执行时,它们就会自动终止.这类线程的一个典型应用就是Java的垃圾回收. 在本节示例中,我们将创建两个线程,一个是普通线程,向队列中写入事件:另外一个是

  • Java并发编程示例(五):线程休眠与恢复

    有时,我们需要在指定的时间点中断正在执行的线程.比如,每分钟检查一次传感器状态的线程,其余时间,线程不需要做任何事情.在此期间,线程不需要使用计算机的任何资源.过了这段时间之后,并且当Java虚拟机调度了该线程,则该线程继续执行.为此,你可以使用Thread类的sleeep()方法.该方法以休眠的方式来推迟线程的执行,而且整数类型的参数则指明休眠的毫秒数.当调用sleep()方法,休眠时间结束后,Java虚拟机分配给线程CPU运行时间,线程就会继续执行. 另一种是用sleep()方法的方式是通过

  • Java并发编程示例(二):获取和设置线程信息

    Thread类包含几个属性,这些属性所表示的信息能帮助我们识别线程.观察其状态.控制其优先级等.这些线程包括如下几种: ID: 该属性表示每个线程的唯一标识: Name: 该属性存储每个线程的名称: Priority: 该属性存储每个Thread对象的优先级.线程优先级分1到10十个级别,1表示最低优先级,10表示最高优先级.并不推荐修改线程的优先级,但是如果确实有这方面的需求,也可以尝试一下. Status: 该属性存储线程的状态.线程共有六种不同的状态:新建(new).运行(runnable

  • Java并发编程之栅栏(CyclicBarrier)实例介绍

    栅栏类似闭锁,但是它们是有区别的. 1.闭锁用来等待事件,而栅栏用于等待其他线程.什么意思呢?就是说闭锁用来等待的事件就是countDown事件,只有该countDown事件执行后所有之前在等待的线程才有可能继续执行;而栅栏没有类似countDown事件控制线程的执行,只有线程的await方法能控制等待的线程执行. 2.CyclicBarrier强调的是n个线程,大家相互等待,只要有一个没完成,所有人都得等着. 场景分析:10个人去春游,规定达到一个地点后才能继续前行.代码如下 复制代码 代码如

  • Java并发编程之显示锁ReentrantLock和ReadWriteLock读写锁

    在Java5.0之前,只有synchronized(内置锁)和volatile. Java5.0后引入了显示锁ReentrantLock. ReentrantLock概况 ReentrantLock是可重入的锁,它不同于内置锁, 它在每次使用都需要显示的加锁和解锁, 而且提供了更高级的特性:公平锁, 定时锁, 有条件锁, 可轮询锁, 可中断锁. 可以有效避免死锁的活跃性问题.ReentrantLock实现了 Lock接口: 复制代码 代码如下: public interface Lock {  

  • Java并发编程之显式锁机制详解

    我们之前介绍过synchronized关键字实现程序的原子性操作,它的内部也是一种加锁和解锁机制,是一种声明式的编程方式,我们只需要对方法或者代码块进行声明,Java内部帮我们在调用方法之前和结束时加锁和解锁.而我们本篇将要介绍的显式锁是一种手动式的实现方式,程序员控制锁的具体实现,虽然现在越来越趋向于使用synchronized直接实现原子操作,但是了解了Lock接口的具体实现机制将有助于我们对synchronized的使用.本文主要涉及以下一些内容: 接口Lock的基本组成成员 可重入锁Re

  • Java并发编程示例(九):本地线程变量的使用

    共享数据是并发程序最关键的特性之一.对于无论是继承Thread类的对象,还是实现Runnable接口的对象,这都是一个非常周重要的方面. 如果创建了一个实现Runnable接口的类的对象,并使用该对象启动了一系列的线程,则所有这些线程共享相同的属性.换句话说,如果一个线程修改了一个属性,则其余所有线程都会受此改变的影响. 有时,我们更希望能在线程内单独使用,而不和其他使用同一对象启动的线程共享.Java并发接口提供了一种很清晰的机制来满足此需求,该机制称为本地线程变量.该机制的性能也非常可观.

  • Java并发编程示例(一):线程的创建和执行

    开门见山 在IT圈里,每当我们谈论并发时,必定会说起在一台计算机上同时运行的一系列线程.如果这台电脑上有多个处理器或者是一个多核处理器,那么这时是实实在在的"同时运行":但是,如果计算机只有一个单核处理器,那么这时的"同时运行"只是表象而已. 所有的现代操作系统全部支持任务的并发执行.你可以边听音乐,边上网看新闻,还不耽误首发电子邮件.我们可以说,这种并发是 进程级并发 .在进程内部,我也可以看到有许许多多的并发任务.我们把运行在一个进程里面的并发任务称 线程. 和

随机推荐