Java 多线程 之 线程的同步机制

一、线程的安全问题

  1、问题的发现

    当有多个线程同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,这就是线程安全的。

    下面通过一个案例来演示线程的安全问题。

    模拟电影票买票的过程,其中,一共有100张票。下面来模拟电影票的售票窗口,实现多个窗口同时卖票,采用线程对象来模拟,通过实现 Runnable 接口子类来模拟。

    示例:

 1 public class WindowTest1 {
 2     public static void main(String[] args) {
 3         Window w = new Window();
 4 
 5         Thread t1 = new Thread(w, "窗口1");
 6         Thread t2 = new Thread(w, "窗口2");
 7         Thread t3 = new Thread(w, "窗口3");
 8 
 9         //同时卖票
10         t1.start();
11         t2.start();
12         t3.start();
13     }
14 }
15 
16 class Window implements Runnable {
17 
18     private int ticket = 100;
19 
20     @Override
21     public void run() {
22         
23         while (true) {
24             //有票,可以出售
25             if (ticket > 0) {
26 
27                 //出票操作,使用 sleep 模拟一下出票时间
28                 try {
29                     Thread.sleep(100);
30                 } catch (InterruptedException e) {
31                     e.printStackTrace();
32                 }
33                 //获取当前线程对象的名字
34                 System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
35 
36 
37                 ticket--;
38             } else {
39                 break;
40             }
41         }
42     }
43 }

  运行结果,发现会有这样的现象发生:

  (1)错票

    

  (2)重票

    

     在运行结果中可以看到会有两个问题发生:

      ① 相同的票数,比如100这张票被卖了两次;

      ② 不存在的票,比如 0 和 -1 票,是不存在的;

  2、分析问题

    针对于上面的售票现象,为什么会出现这样的情况呢?

    当只有一个窗口售票或多个窗口分别出售自己的票是没有问题的。但是当三个窗口,同时访问共享的资源,就会导致线程不同步,这种问题称为 线程不安全

  线程安全问题的产生的原理:

      可以发现多个线程执行的不确定性引起执行结果的不稳定;

      多个线程对公共的数据处理,会造成操作的不完整性,会破坏数据。

    注意:线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量,静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

  

  3、问题的总结

    问题出现的原因当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。

    如何解决对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

         当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。

二、同步机制

  Java 对于多线程的安全问题提供了专业的解决方式:同步机制

  针对上面的售票案例简单描述一下同步机制:

当窗口1线程进入操作的时候,窗口2和窗口3线程只能在外面等着,窗口1操作结束,窗口1、窗口2和窗口3才有机会进入代码去执行。
也就是说某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

  同步方式的分析:

    1、同步的方式,解决了线程的安全问题(好处)

    2、操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。(局限性)

  同步的注意项:

    (1)操作共享数据的代码,即为需要被同步的代码加锁;

        不能包含的代码少了(同步将不起作用),也不能包含多了(可能造成死锁或与逻辑混乱)

    (2)共享数据:多个线程共同操作的变量。例如:ticket就是共享数据。

    (3)同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。

        要求:多个线程必须要共用同一把锁。

  

三、方式一:同步代码块

  1、语法格式

synchronized (对象){
    // 需要被同步的代码 / 可能会出现线程安全问题的代码(访问共享数据的代码)
}

  

  2、实现方式使用同步代码块

    代码示例:

 1 public class WindowTest1 {
 2     public static void main(String[] args) {
 3         Window w = new Window();
 4 
 5         Thread t1 = new Thread(w, "窗口1");
 6         Thread t2 = new Thread(w, "窗口2");
 7         Thread t3 = new Thread(w, "窗口3");
 8 
 9         //同时卖票
10         t1.start();
11         t2.start();
12         t3.start();
13     }
14 }
15 
16 class Window implements Runnable {
17 
18     private int ticket = 100;
19     //创建一个 同步监视器,锁对象
20     private Object obj = new Object();
21 
22     @Override
23     public void run() {
24         
25         while (true) {
26             synchronized (obj) {  //使用 synchronized 给操作共享数据的地方加锁
27                 //有票,可以出售
28                 if (ticket > 0) {
29 
30                     //出票操作,使用 sleep 模拟一下出票时间
31                     try {
32                         Thread.sleep(100);
33                     } catch (InterruptedException e) {
34                         e.printStackTrace();
35                     }
36                     //获取当前线程对象的名字
37                     System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
38 
39 
40                     ticket--;
41                 } else {
42                     break;
43                 }
44             }
45         }
46     }
47 }

  如果加锁都需要创建一个对象,我们可以改进一下:

 1 public class WindowTest1 {
 2     public static void main(String[] args) {
 3         Window w = new Window();
 4 
 5         Thread t1 = new Thread(w, "窗口1");
 6         Thread t2 = new Thread(w, "窗口2");
 7         Thread t3 = new Thread(w, "窗口3");
 8 
 9         //同时卖票
10         t1.start();
11         t2.start();
12         t3.start();
13     }
14 }
15 
16 class Window implements Runnable {
17 
18     private int ticket = 100;
19 
20     @Override
21     public void run() {
22         
23         while (true) {
24             synchronized (this) {  //方式二:此时的this:唯一的Window1的对象
25                 //有票,可以出售
26                 if (ticket > 0) {
27 
28                     //出票操作,使用 sleep 模拟一下出票时间
29                     try {
30                         Thread.sleep(100);
31                     } catch (InterruptedException e) {
32                         e.printStackTrace();
33                     }
34                     //获取当前线程对象的名字
35                     System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
36 
37 
38                     ticket--;
39                 } else {
40                     break;
41                 }
42             }
43         }
44     }
45 }

  注意:这里的 this 指的就是当前的 Window 对象,因为在main方法中多个 Thread 共用了同一个 Window 对象,所以这里的 this 是公共的锁。

  3、继承方式使用同步代码块

 1 public class WindowTest2 {
 2     public static void main(String[] args) {
 3         Window2 t1 = new Window2();
 4         Window2 t2 = new Window2();
 5         Window2 t3 = new Window2();
 6 
 7         t1.setName("窗口1");
 8         t2.setName("窗口2");
 9         t3.setName("窗口3");
10 
11         t1.start();
12         t2.start();
13         t3.start();
14     }
15 }
16 
17 class Window2 extends Thread {
18     private static int ticket = 100;
19     private static Object obj = new Object();
20 
21     @Override
22     public void run() {
23 
24         while (true) {
25             synchronized (obj) {
26                 if (ticket > 0) {
27 
28                     try {
29                         Thread.sleep(100);
30                     } catch (InterruptedException e) {
31                         e.printStackTrace();
32                     }
33 
34                     System.out.println(getName() + ":卖票,票号为:" + ticket);
35                     ticket--;
36                 } else {
37                     break;
38                 }
39             }
40         }
41     }
42 }

  对于继承的方式,如果我们也想避免创建的繁琐,可以这样写:

 1 public class WindowTest2 {
 2     public static void main(String[] args) {
 3         Window2 t1 = new Window2();
 4         Window2 t2 = new Window2();
 5         Window2 t3 = new Window2();
 6 
 7         t1.setName("窗口1");
 8         t2.setName("窗口2");
 9         t3.setName("窗口3");
10 
11         t1.start();
12         t2.start();
13         t3.start();
14     }
15 }
16 
17 class Window2 extends Thread {
18     private static int ticket = 100;
19 
20     @Override
21     public void run() {
22 
23         while (true) {
24             synchronized (Window2.class) {  //方式2
25                 if (ticket > 0) {
26 
27                     try {
28                         Thread.sleep(100);
29                     } catch (InterruptedException e) {
30                         e.printStackTrace();
31                     }
32 
33                     System.out.println(getName() + ":卖票,票号为:" + ticket);
34                     ticket--;
35                 } else {
36                     break;
37                 }
38             }
39         }
40     }
41 }

  注意这里是不能使用 this 的,因为在 main 中创建了多个 Window2对象,它们各不一样。但是我们可以使用当前类的对象,全局唯一的类对象来充当锁。因为类对象只会加载一次,是全局唯一的。

  4、小结

    (1)操作共享数据的代码,即为需要被同步的代码加锁;

        不能包含的代码少了(同步将不起作用),也不能包含多了(可能造成死锁或与逻辑混乱)

    (2)共享数据:多个线程共同操作的变量。例如:ticket就是共享数据。

    (3)同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。

        要求:多个线程必须要共用同一把锁。

      注意:(具体情况还要具体分析!

      ① 在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。

      ② 在继承Thread类创建多线程的方式中,慎用 this 充当同步监视器,考虑使用当前类充当同步监视器

    

四、方式二:同步方法

  1、语法格式

public static synchronized void show (String name){
   可能会产生线程安全问题的代码 / 可能会出现线程安全问题的代码(访问了共享数据的代码)
}

    synchronized还可以放在方法声明中,表示整个方法为同步方法

   如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。

  2、实现方式使用同步方法

    代码示例:

 1 public class WindowTest3 {
 2     public static void main(String[] args) {
 3         Window3 w = new Window3();
 4 
 5         Thread t1 = new Thread(w);
 6         Thread t2 = new Thread(w);
 7         Thread t3 = new Thread(w);
 8 
 9         t1.setName("窗口1");
10         t2.setName("窗口2");
11         t3.setName("窗口3");
12 
13         t1.start();
14         t2.start();
15         t3.start();
16     }
17 }
18 
19 class Window3 implements Runnable {
20     private int ticket = 100;
21 
22 
23     @Override
24     public void run() {
25         while (true) {
26             show();
27         }
28     }
29 
30     private synchronized void show() {  //同步监视器:this
31         if (ticket > 0) {
32 
33             try {
34                 Thread.sleep(100);
35             } catch (InterruptedException e) {
36                 e.printStackTrace();
37             }
38 
39             System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
40 
41             ticket--;
42         }
43     }
44 }

    注意:这里的锁对象是 this,因为在 main 方法中还是共用了同一个 Window3 对象,这里的 this 就是此对象,可以公共的锁对象。如果在 main 中创建了多个 Window3 对象并传递给 Thread来启动,这样并不可以保证同步哦!

  3、继承方式使用同步方法

    代码示例:

 1 public class WindowTest4 {
 2     public static void main(String[] args) {
 3         Window4 t1 = new Window4();
 4         Window4 t2 = new Window4();
 5         Window4 t3 = new Window4();
 6 
 7 
 8         t1.setName("窗口1");
 9         t2.setName("窗口2");
10         t3.setName("窗口3");
11 
12         t1.start();
13         t2.start();
14         t3.start();
15 
16     }
17 }
18 
19 class Window4 extends Thread {
20     private static int ticket = 100;
21 
22     @Override
23     public void run() {
24         while (true) {
25             show();
26         }
27     }
28 
29     private static synchronized void show() {  //同步监视器:Window4.class
30         if (ticket > 0) {
31 
32             try {
33                 Thread.sleep(100);
34             } catch (InterruptedException e) {
35                 e.printStackTrace();
36             }
37 
38             System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
39             ticket--;
40         }
41     }
42 }

    注意:在继承中这样使用的锁对象就是 Window4.class,当前的类对象。

    切记不能写成这样

//private synchronized void show(){ //同步监视器:t1,t2,t3。此种解决方式是错误的

     这样他们的锁对象就不再是类对象,而是以每个实例对象为锁的,并不是公共的锁,不能保证同步。

  4、小结

    (1)同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。

    (2)非静态的同步方法,同步监视器是实现类对象:this;

        静态的同步方法,同步监视器是当前类对象:当前类本身

  

五、同步的总结

  1、分析同步原理

    

  2、同步机制中的锁

    (1)同步锁机制

    (2)synchronized 的锁是什么?

      ① 任意对象都可以作为同步锁,所有对象都自动含有单一的锁(监视器)。

      ② 同步方法的锁:静态方法(类名.class)、非静态方法(this)

      ③ 同步代码块:自己指定,很多时候也是指定为 this 或 类名.class

    (3)注意

      ① 必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则无法保证共享资源的安全;

      ② 一个线程类中的所有静态方法共用一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)

  3、同步的范围

    (1)如何找问题,即代码是否存在线程安全?(重要)

      ① 明确哪些代码是多线程运行的代码;

      ② 明确多个线程是否有共享数据;

      ③ 明确多线程运行代码中是否有多条语句操作共享数据;

    (2)如果解决?(重要)

      ① 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行;

      ② 即所有操作共享数据的这些语句都要放在同步范围中;

    (3)切记

      ① 范围太小:没锁住所有有安全问题的代码

      ② 范围太大:没发挥多线程的功能;

  4、释放锁的操作

    (1)当前线程的同步方法、同步代码块执行结束;

    (2)当前线程在同步代码块、同步方法中遇到 break、return 终止了该代码块、该方法的继续执行;

    (3)当前线程在同步代码块、同步方法中出现了未处理的 Error 或 Exception,导致异常结束;

    (4)当前线程在同步代码块、同步方法中执行了线程对象的 wait() 方法,当前线程暂停,并释放锁。

  5、不会释放锁的操作

    (1)线程执行同步代码块或同步方式时,程序调用 Thread.sleep()、Thread.yield() 方法暂停当前线程的执行;

    (2)线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起,该线程不会释放锁(不同监视器);

       应尽量避免使用 suspend() 和 resume() 来控制线程;

六、Lock 锁——JDK5.0 新增锁

  1、Lock 锁

    (1)从 JDK5.0 开始,Java 提供了更强大的线程同步机制——通过显示定义同步锁对象来实现同步,同步锁使用 Lock 对象充当;

    (2)java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。

        锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象。

    (3)ReentrantLock 类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock,可以显式加锁、释放锁。

  2、语法

class A{
    private final ReentrantLock lock = new ReenTrantLock();
        public void m(){
            lock.lock();
            try{
                //保证线程安全的代码;
            }
            finally{
                lock.unlock();
            }
        }
}        

    注意:如果同步代码有异常,要将 unlock( )写入finally语句块

  3、实现方式使用 Lock 锁

    使用步骤

      ① 在成员位置创建一个ReentrantLock对象(Lock接口的一个实现类)

      ② 在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁

      ③ 在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁

    示例:

 1 public class WindowTest5 {
 2     public static void main(String[] args) {
 3         Window5 w = new Window5();
 4 
 5         Thread t1 = new Thread(w);
 6         Thread t2 = new Thread(w);
 7         Thread t3 = new Thread(w);
 8 
 9         t1.setName("窗口1");
10         t2.setName("窗口2");
11         t3.setName("窗口3");
12 
13         t1.start();
14         t2.start();
15         t3.start();
16     }
17 }
18 
19 class Window5 implements Runnable {
20 
21     private int ticket = 100;
22     //1.实例化ReentrantLock
23     private ReentrantLock lock = new ReentrantLock();
24 
25     @Override
26     public void run() {
27         while (true) {
28             try {
29                 //2.调用锁定方法 lock()
30                 lock.lock();
31 
32                 if(ticket > 0){
33 
34                     try {
35                         Thread.sleep(100);
36                     } catch (InterruptedException e) {
37                         e.printStackTrace();
38                     }
39 
40                     System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
41                     ticket--;
42                 }else{
43                     break;
44                 }
45             } finally {
46                 //3.调用解锁方法:unlock(),无论程序是否异常,都会把锁释放
47                 lock.unlock();
48             }
49         }
50     }
51 }

  4、继承方式使用 Lock 锁

    代码示例:

 1 public class WindowTest6 {
 2     public static void main(String[] args) {
 3         Window6 w1 = new Window6();
 4         Window6 w2 = new Window6();
 5         Window6 w3 = new Window6();
 6 
 7         w1.setName("窗口一");
 8         w2.setName("窗口二");
 9         w3.setName("窗口三");
10 
11         w1.start();
12         w2.start();
13         w3.start();
14     }
15 }
16 
17 class Window6 extends Thread {
18 
19     private static int ticket = 100;
20     //1.实例化ReentrantLock
21     private static ReentrantLock lock = new ReentrantLock();
22 
23     @Override
24     public void run() {
25         while (true) {
26             try {
27                 //2.调用锁定方法 lock()
28                 lock.lock();
29 
30                 if(ticket > 0){
31 
32                     try {
33                         Thread.sleep(100);
34                     } catch (InterruptedException e) {
35                         e.printStackTrace();
36                     }
37 
38                     System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
39                     ticket--;
40                 }else{
41                     break;
42                 }
43             } finally {
44                 //3.调用解锁方法:unlock()
45                 lock.unlock();
46             }
47         }
48     }
49 }

  5、synchronized 与 Lock 的对比

    (1)Lock 是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized 是隐式锁,出了作用自动释放;

    (2)Lock 只有代码块锁,synchronized 有代码块锁和方法所;

    (3)使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

    优先使用顺序:

Lock ——>同步代码块(已经进入了方法体,分配了相应资源)——>同步方法(在方法体之外)

七、线程的死锁问题

  1、死锁

    (1)不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁;

    (2)出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续;

    (3)死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

  2、死锁的必要条件

    死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件

    1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
    2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
    3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
    4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

  3、解决方法

    (1)专门的算法、原则;

    (2)尽量减少同步资源的定义;

    (3)尽量避免嵌套同步

  4、示例

    示例一:

 1 //死锁的演示
 2 class A {
 3     public synchronized void foo(B b) { //同步监视器:A类的对象:a
 4         System.out.println("当前线程名: " + Thread.currentThread().getName()
 5                 + " 进入了A实例的foo方法"); //
 6         try {
 7             Thread.sleep(200);
 8         } catch (InterruptedException ex) {
 9             ex.printStackTrace();
10         }
11         System.out.println("当前线程名: " + Thread.currentThread().getName()
12                 + " 企图调用B实例的last方法"); //
13         b.last();
14     }
15 
16     public synchronized void last() {//同步监视器:A类的对象:a
17         System.out.println("进入了A类的last方法内部");
18     }
19 }
20 
21 class B {
22     public synchronized void bar(A a) {//同步监视器:b
23         System.out.println("当前线程名: " + Thread.currentThread().getName()
24                 + " 进入了B实例的bar方法"); //
25         try {
26             Thread.sleep(200);
27         } catch (InterruptedException ex) {
28             ex.printStackTrace();
29         }
30         System.out.println("当前线程名: " + Thread.currentThread().getName()
31                 + " 企图调用A实例的last方法"); //
32         a.last();
33     }
34 
35     public synchronized void last() {//同步监视器:b
36         System.out.println("进入了B类的last方法内部");
37     }
38 }
39 
40 public class DeadLock1 implements Runnable {
41     A a = new A();
42     B b = new B();
43 
44     public void init() {
45         Thread.currentThread().setName("主线程");
46         // 调用a对象的foo方法
47         a.foo(b);
48         System.out.println("进入了主线程之后");
49     }
50 
51     public void run() {
52         Thread.currentThread().setName("副线程");
53         // 调用b对象的bar方法
54         b.bar(a);
55         System.out.println("进入了副线程之后");
56     }
57 
58     public static void main(String[] args) {
59         DeadLock1 dl = new DeadLock1();
60         new Thread(dl).start();
61 
62 
63         dl.init();
64     }
65 }

    示例二:

 1 public class DeadLock2 {
 2     public static void main(String[] args) {
 3 
 4         StringBuffer s1 = new StringBuffer();
 5         StringBuffer s2 = new StringBuffer();
 6 
 7 
 8         new Thread() {
 9             @Override
10             public void run() {
11 
12                 synchronized (s1) {
13 
14                     s1.append("a");
15                     s2.append("1");
16 
17                     try {
18                         Thread.sleep(100);
19                     } catch (InterruptedException e) {
20                         e.printStackTrace();
21                     }
22 
23 
24                     synchronized (s2) {
25                         s1.append("b");
26                         s2.append("2");
27 
28                         System.out.println(s1);
29                         System.out.println(s2);
30                     }
31                 }
32             }
33         }.start();
34 
35 
36         new Thread(new Runnable() {
37             @Override
38             public void run() {
39                 synchronized (s2) {
40 
41                     s1.append("c");
42                     s2.append("3");
43 
44                     try {
45                         Thread.sleep(100);
46                     } catch (InterruptedException e) {
47                         e.printStackTrace();
48                     }
49 
50                     synchronized (s1) {
51                         s1.append("d");
52                         s2.append("4");
53 
54                         System.out.println(s1);
55                         System.out.println(s2);
56                     }
57                 }
58             }
59         }).start();
60     }
61 
62 }
原文地址:https://www.cnblogs.com/niujifei/p/14404178.html