Java多线程02(线程安全、线程同步、等待唤醒机制)

Java多线程2(线程安全、线程同步、等待唤醒机制、单例设计模式)

1、线程安全

  • 如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
    • 通过案例演示线程的安全问题:电影院要卖票。
    • 我们模拟电影院的卖票过程。假设本场电影的座位共100个(本场电影只能卖100张票)。
    • 我们来模拟电影院的售票窗口,实现多个窗口同时卖这场电影的票(多个窗口一起卖这100张票)
    • 需要窗口,采用线程对象来模拟;
    • 需要票,Runnable接口子类来模拟;
  • 代码:
public class Tickets implements Runnable {
    private int num = 100;  //(1)
    @Override
    public void run() {  // (2)
        // 死循环,一直处于可以售票状态
        while(true) {  // (3)
            if(num>0) {  // (4)
                System.out.println(Thread.currentThread().getName()+" 第  "+ num-- + "  张票售出");  //(5)
            }
        }
    }
}

public class TicketsDemo {
    public static void main(String[] args) {  //(6)
        Tickets t = new Tickets();  // (7)
        new Thread(t).start();  // (8)
        new Thread(t).start();  // (9)
        new Thread(t).start();  // (10)
    }
}
  • 分析:
    • 三个窗口每个窗口都在买票,假设此时只剩一张票,可能会发生以下情况:
    • 线程t1执行run方法到(4)时,产生阻塞,线程t2执行run方法到(4)时叶阻塞,线程t3执行完了run方法,释放CPU,此时num=0;t1再次得到CPU时,不会再次判断,而是直接执行下一步(5),这时就会发生0--,出现出售第0张票,并且票数变成负数,这样就出现了安全隐患。
  • 运行结果发现:上面程序出现了问题
    • 票出现了重复的票
    • 错误的票 0、-1
  • 线程安全问题都是由全局变量及静态变量引起的。
  • 若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

  • 解决办法:
    • 当一个线程进入数据操作的时候,无论是否休眠,其他线程智能等待。

2、线程同步(线程安全处理Synchronized)

  • java中提供了线程同步机制,它能够解决上述的线程安全问题。
  • 线程同步的方式有两种:
    • 方式1:同步代码块
    • 方式2:同步方法

2.1 同步代码块

  • 同步代码块: 在代码块声明上 加上synchronized
synchronized (锁对象) {
    可能会产生线程安全问题的代码
}
  • 同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。

  • 使用同步代码块,对电影院卖票案例中Ticket类进行如下代码修改:

/*
    通过线程休眠,出现安全问题
    解决安全问题,Java程序,提供同步技术
    公式:
        syncronized (任意对象){
            线程要操作的共享数据
        }
*/
public class Tickets implements Runnable {
    // 定义出售的票数
    private int num = 100;
    Object obj = new Object();  // 创建对象,用于同步
    @Override
    public void run() {
        // 死循环,一直处于可以售票状态
        while(true) {
            // 线程共享数据,保证安全,加入同步代码块
            synchronized (obj) {
                if(num>0) {
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+" 第  "+ num-- + "  张票售出");
                }
            }
        }
    }
}
  • 当使用了同步代码块后,上述的线程的安全问题,解决了。
  • 分析:
    • 同步对象:可以是任意对象,可以称之为同步锁,对象监视器,注意不能用匿名内部类,因为这样在会导致每次获得锁对象都是新的对象,无法实现加锁的效果。
    • 同步是如何保证安全性的:没有锁的线程不能执行,只能等待。
    • 具体执行过程:
      • 线程遇到同步代码块后,线程判断同步锁还有没有
      • 如果同步锁有:获取锁,进入同步中,去执行,执行完毕后,离开同步代码块,线程将锁对象还回去。
      • 在同步中的线程休眠,此时另一个线程会执行;
      • 遇到同步代码块,判断对象锁是否还有,如果没有锁,该线程不能进入同步代码块中执行,被阻挡在同步代码块的外面,处于阻塞状态。
    • 加了同步之后,执行步骤增加:线程首先进同步判断锁,获取锁,出同步释放锁,导致程序运行速度的下降。
    • 没有锁的线程,不能进入同步,在同步中的线程,不出同步,不会释放锁。

2.2 同步方法(推荐使用)

  • 同步方法:在方法声明上加上synchronized
public synchronized void method(){
    可能会产生线程安全问题的代码
}
  • 同步方法中的锁对象是 this
  • 使用同步方法,对电影院卖票案例中Ticket类进行如下代码修改:
/*
    采用同步方法的形式解决线程安全问题
    好处:代码量少,简洁
    做法:将线程共享数据和同步抽取到方法中
 */
public class Tickets implements Runnable {
    private int num = 100;
    @Override
    public void run() {
        // 死循环,一直处于可以售票状态
        while(true) {
            payTicket();
        }
    }
    
    public synchronized void payTicket() {
        if(num>0) { 
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" 第  "+ num-- + "  张票售出");
        }
    }
}
  • 问题:同步方法中有锁吗?
    • 有,同步方法中的对象锁是本类方法的引用
  • 静态同步方法: 在方法声明上加上static synchronized
public static synchronized void method(){
    // 可能会产生线程安全问题的代码
}
  • 静态同步方法中的锁对象是本类自己:类名.class

1.4 Lock接口

  • 查阅API,查阅Lock接口描述,Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
    • 实现类:ReentrantLock
    • Lock接口中的常用方法
      • void lock():获得锁。
      • void unlock():释放锁。
    • Lock提供了一个更加面对对象的锁,在该锁中提供了更多的操作锁的功能。
  • 我们使用Lock接口,以及其中的lock()方法和unlock()方法替代同步,对电影院卖票案例中Ticket类进行如下代码修改:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/*
    使用JDK1.5+的接口Locl,替换同步代码块,实现线程安全
    具体使用:
        Lock接口中的方法:
            lock(); // 获取锁
            unlock(); // 释放锁
        实现类:ReentrantLock
 */
public class Tickets implements Runnable {
    // 存储票数
    private static int num = 100;   
    //在类的成员位置,创建Lock接口的实现类对象
    private Lock lock = new ReentrantLock();    
    @Override
    public void run() {
        // 死循环,一直处于可以售票状态
        while(true) {
            // 调用Lock接口中的方法,获取锁
            lock.lock();
            try {
                if(num>0) {                 
                    Thread.sleep(200);
                    System.out.println(Thread.currentThread().getName()+" 第  "+ num-- + "  张票售出");
                } 
            }catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                // 释放锁,调用unlock方法
                lock.unlock();              
            }
        }
    }   
}

1.4 死锁

  • 同步锁使用的弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:
    • 程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。
  • 死锁程序:
    • 前提:必须是多线程
    • 出现同步嵌套
    • 线程进入同步,获取锁,不出去同步,不会释放锁
  • 锁的嵌套情况如下:

    synchronzied(A锁){
        synchronized(B锁){
    
        }
    }
    synchronzied(B锁){
        synchronized(A锁){
    
        }
    }
  • 注意A锁和B锁都是唯一的
  • 两个线程每个获得一个锁,且都需要对方的锁才能继续执行,因此都会一直除以阻塞状态,无法恢复,出现死锁。

  • 我们进行下死锁情况的代码演示:

    // 定义锁对象类
    /*
        不允许任何类创建该对象
        只能通过类名调用静态成员调用,不允许new
        保证了锁的唯一性
    */
    public class LockA {
        private LockA() {}
        public final static LockA locka = new LockA();
    }
    
    public class LockB {    
        private LockB() {}
        public final static LockB lockb = new LockB();
    }
    
    // 线程任务类
    public class DeadLock implements Runnable{
        private int i = 0;
        @Override
        public void run() {
            while(true) {
                if(i%2==0) {
                    // 先进入A同步,再进入B同步
                    synchronized (LockA.locka) {
                        System.out.println(i+" --> if...locka");
                        synchronized (LockB.lockb) {
                            System.out.println(i+" --> if...lockb");
                        }
                    }
                }else {
                    // 先进入B同步,再进入B同步
                    synchronized (LockB.lockb) {
                        System.out.println(i+" --> else...lockb");
                        synchronized (LockA.locka) {
                            System.out.println(i+" --> else...locka");
                        }
                    }
                }
                i++;
            }
        }
    }
    
    // 测试类
    public class DeadLockDemo {
        public static void main(String[] args) {
            DeadLock deadLock = new DeadLock();
            new Thread(deadLock).start();
            new Thread(deadLock).start();
        }
    }
    
    // 运行结果:
    0 --> if...locka
    0 --> if...lockb
    1 --> else...lockb
    1 --> if...locka
    

1.5 等待唤醒机制

  • 在开始讲解等待唤醒机制之前,有必要搞清一个概念—— 线程之间的通信。
  • 线程之间的通信:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。
  • 等待唤醒机制所涉及到的方法:
    • wait() :等待,无限等待。将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。
    • notify() :唤醒。唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。
    • notifyAll() :唤醒全部:可以将线程池中的所有wait()线程都唤醒。
  • 所谓唤醒:就是让线程池中的线程具备执行资格。
    • 必须注意的是:这些方法都是在同步中才有效。同时这些方法在使用时必须标明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程。
  • 仔细查看Java API之后,发现这些方法 并不定义在 Thread中,也没定义在Runnable接口中,却被定义在了Object类中,为什么这些操作线程的方法定义在Object类中?
    • 因为这些方法在使用时,必须要标明所属的锁,而锁又可以是任意对象。能被任意对象调用的方法一定定义在Object类中。
  • 线程通讯案例:输入线程向Resource中输入name ,sex , 输出线程从资源中输出,先要完成的任务是:
    1. 当input发现Resource中没有数据时,开始输入,输入完成后,叫output来输出。如果发现有数据,就wait();
    2. 当output发现Resource中没有数据时,就wait() ;当发现有数据时,就输出,然后,叫醒input来输入数据。
  • 下面代码,模拟等待唤醒机制的实现:
    • Resource.java

      /*
          定义资源类,有2个成员变量:
              name,sex
          同时有两个线程,对资源中的变量操作
          1个对name,sex赋值
          1个对name,sex做变量的输出打印
       */
      public class Resource {
          public String name;
          public String sex;
      }
    • Input.java

      /*
          输入线程:
              对资源对象Resource中的成员变量赋值
          要求:
              一次赋值:张三,男
              另一次:李四,女
      
       */
      public class Input implements Runnable {    
          private Resource r;
          public Input(Resource r) {
              this.r = r;
          }
          @Override
          public void run() {
              int i = 0;
              while(true) {
                  if(i%2==0) {
                      r.name = "张三";
                      r.sex = "男";                
                  }else {
                      r.name = "lisi";
                      r.sex = "nv";
                  }
                  i++;
              }
          }
      }
    • Output.java

      /*
          输出线程:对资源对象Resource中的成员变量输出值
       */
      public class Output implements Runnable {
          private Resource r;
          public Output(Resource r) {
              this.r = r;
          }
          @Override
          public void run() {
              while(true) {
                  System.out.println("姓名:"+r.name + ", 性别:"+r.sex);
              }
          }
      
      }
    • ThreadDemo.java

      /*
          开启输入线程和输出线程,实现赋值和打印
       */
      public class ThreadDemo {
          public static void main(String[] args) {
              Resource r = new Resource();  //共享数据
              Input in = new Input(r);
              Output out = new Output(r);
              new Thread(in).start();
              new Thread(out).start();
          }
      }
  • 此时会出现问题:打印出的结果并不是想要的结果

    姓名:lisi, 性别:nv
    姓名:张三, 性别:nv
    姓名:lisi, 性别:男
    姓名:lisi, 性别:nv
    姓名:lisi, 性别:nv
    姓名:张三, 性别:男
  • 分析原因,两个线程没有实现同步。
  • 实现同步的方法:给线程加同步锁。
    • 注意:给输入和输出加的同步锁应为同一个对象锁,而输入和输出线程是两个不同的线程,因此不能使用this作为对象锁,这里使用他们公用的资源类Resource对象。
  • 代码修改如下:
    • Input.java修改

      /*
          输入线程:
              对资源对象Resource中的成员变量赋值
          要求:
              一次赋值:张三,男
              另一次:李四,女
      
       */
      public class Input implements Runnable {    
          private Resource r;
          public Input(Resource r) {
              this.r = r;
          }
          @Override
          public void run() {
              int i = 0;
              while(true) {
                  synchronized (r) {
                      if(i%2==0) {
                          r.name = "张三";
                          r.sex = "男";                
                      }else {
                          r.name = "lisi";
                          r.sex = "nv";
                      }
                  }
                  i++;
              }
          }
      }
    • Input.java修改

      /*
          输入线程:
              对资源对象Resource中的成员变量赋值
          要求:
              一次赋值:张三,男
              另一次:李四,女
      
       */
      public class Input implements Runnable {    
          private Resource r;
          public Input(Resource r) {
              this.r = r;
          }
          @Override
          public void run() {
              int i = 0;
              while(true) {
                  synchronized (r) {
                      if(i%2==0) {
                          r.name = "张三";
                          r.sex = "男";                
                      }else {
                          r.name = "lisi";
                          r.sex = "nv";
                      }
                  }
                  i++;
              }
          }
      }
    • Output.java修改:

      /*
          输出线程:对资源对象Resource中的成员变量输出值
       */
      public class Output implements Runnable {
          private Resource r;
          public Output(Resource r) {
              this.r = r;
          }
          @Override
          public void run() {
              while(true) {
                  synchronized (r) {
                      System.out.println("姓名:"+r.name + ", 性别:"+r.sex);               
                  }
              }
          }
      }
  • 此时还有问题:输出没有交替进行

    姓名:张三, 性别:男
    姓名:张三, 性别:男
    姓名:张三, 性别:男
    姓名:lisi, 性别:nv
    姓名:lisi, 性别:nv
    姓名:lisi, 性别:nv
    姓名:lisi, 性别:nv
  • 分析原因:
    • 输入:输入完成以后,必须等待,等待输出打印结束后,才能进行下一次赋值。
    • 输出:输出完变量值后,必须等待,等待输入的重新赋值后,才能进行下一次打印。
  • 解决方法:
    • 输入:赋值后,执行方法wait(),永远等待,
    • 输出:变量打印输出,在输出等待之前,唤醒输入的nitify(),自己再wait等待。
    • 输入:被唤醒后,重新对变量赋值,然后唤醒输出的线程notify,自己再wait()等待。
    • 如何判断输入输出结束:设置一个标记flag,以标记为准;
      • flag = false; 说明赋值完成
      • flag = true; 获取值完成
    • 输入操作:
      • 需要不需要赋值,看标记
      • 如果标记为true,等待
      • 如果标记为false,不需要等待,赋值
      • 赋值后,将标记改为true
    • 输出操作:
      • 需要不需要获取,看标记
      • 如过标记为false,等待
      • 如果标记为true,打印
      • 打印后,将标记改为false
  • 代码修改如下:
    • Resource.java修改

      /*
          定义资源类,有2个成员变量:
              name,sex
          同时有两个线程,对资源中的变量操作
          1个对name,sex赋值
          1个对name,sex做变量的输出打印
       */
      public class Resource {
          public String name;
          public String sex;
          public boolean flag = false;
      }
    • Input.java修改

      /*
          输入线程:
              对资源对象Resource中的成员变量赋值
          要求:
              一次赋值:张三,男
              另一次:李四,女
      
       */
      public class Input implements Runnable {    
          private Resource r;
          public Input(Resource r) {
              this.r = r;
          }
          @Override
          public void run() {
              int i = 0;
              while(true) {
                  synchronized (r) {              
                      if(r.flag) { // 标记是true,等待
                          try {
                              r.wait();
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                      }
                      if(i%2==0) {
                          r.name = "张三";
                          r.sex = "男";                
                      }else {
                          r.name = "lisi";
                          r.sex = "nv";
                      }
                      // 标记改为true,将对方线程唤醒
                      r.flag = true;
                      r.notify();
                  }
                  i++;
              }
          }
      }
    • Output.java修改

      /*
          输出线程:对资源对象Resource中的成员变量输出值
       */
      public class Output implements Runnable {
          private Resource r;
          public Output(Resource r) {
              this.r = r;
          }
          @Override
          public void run() {
              while(true) {
                  synchronized (r) {              
                      if(!r.flag) {  // 判断标记,false,等待
                          try {
                              r.wait();
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                      }
                      System.out.println("姓名:"+r.name + ", 性别:"+r.sex);
                      r.flag = false;
                      r.notify();
                  }
              }
          }
      }

2 总结

同步锁

  • 多个线程想保证线程安全,必须要使用同一个锁对象
    • 同步代码块

      synchronized (锁对象){
          可能产生线程安全问题的代码
      }
  • 同步代码块的锁对象可以是任意的对象
    • 同步方法

      public synchronized void method()
          可能产生线程安全问题的代码
      }
      // 同步方法中的锁对象是 this
    • 静态同步方法

      public synchronized void method()
          可能产生线程安全问题的代码
      }
      // 静态同步方法中的锁对象是 类名.class

多线程有几种实现方案,分别是哪几种?

  • 继承Thread类
  • 实现Runnable接口
  • 通过线程池,实现Callable接口

同步有几种方式,分别是什么?

  • 同步代码块
  • 同步方法
  • 静态同步方法

启动一个线程是run()还是start()?它们的区别?

  • 启动一个线程是start()
  • 区别:
    • start: 启动线程,并调用线程中的run()方法
    • run : 执行该线程对象要执行的任务

sleep()和wait()方法的区别

  • sleep: 不释放锁对象, 释放CPU使用权;在休眠的时间内,不能唤醒
  • wait(): 释放锁对象, 释放CPU使用权;在等待的时间内,能唤醒

为什么wait(),notify(),notifyAll()等方法都定义在Object类中

  • 锁对象可以是任意类型的对象

猜你喜欢

转载自www.cnblogs.com/luoyu113/p/10239666.html