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

时间:2022-02-02 15:26:04

关键字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就白养那么多人了。
因此,只要你没有两个代码共享的同一个资源或变量,就不会出现数据不一致的情况。而且同一个类的不同对象的调用有完全不同的堆栈,它们之间完全不相干。
以下我们以一个售票过程举例说明,在这里,我们的共享资源就是票的剩余张数。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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();
    }
  }
}

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

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

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

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

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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();
    }
  }
}

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

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

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

?
1
2
3
4
5
6
class Parent {
  public synchronized void method() {  }
}
class Child extends Parent {
  public synchronized void method () {  }
}

调用父类方法:

?
1
2
3
4
5
6
class Parent {
  public synchronized void method() {  }
}
class Child extends Parent {
  public void method() { super.method();  }
}

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

?
1
2
public synchronized int n = 0;
public static synchronized int n = 0;

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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);
    }
  }
}

运行结果为:

?
1
2
非静态的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()则是将其所有等待线程唤醒。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
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,通过执行此对象的同步实例方法,如:
        

?
1
2
3
public synchronized void n() {
   notify();
 }

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

?
1
2
3
4
5
public void n() {
    synchronized (this ) {
      notify();
    }
  }

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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个数,其中方法一是一个静态同步方法,它的作用域为类;方法二显示的声明了代码块的作用域是类。这两个方法的异曲同工的。由于方法一和方法二的作用域同为类,所以它们两个方法间是互斥的,也就是说,当一个线程调用了这两个方法中的一个,剩余没有调用的方法也会对其它线程形成阻塞。因此,程序的运行结果会是:

?
1
2
3
4
5
6
7
testSyncMethod:0
testSyncMethod:1
... ...
testSyncMethod:99
testSyncBlock:0
... ...
testSyncBlock:99

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

?
1
2
3
4
5
6
7
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方法相差还是很远的,不要混为一谈。