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();
      }
      }
      }
      }
  • 注意:

    • 等待和唤醒必须是由同一个对象调用,这里用Resource的对象

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类中

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

Java多线程02(线程安全、线程同步、等待唤醒机制)的更多相关文章

  1. “全栈2019”Java多线程第二十四章:等待唤醒机制详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  2. Java多线程间通信-解决安全问题、等待唤醒机制

    /*1.增加一个知识点一个类怎么在所有的类中,让其它类来共同修改它的数据呢?可以用单例设计模式可以用静态可以在其它类中做一个构造函数,接受同一个对象,这样就可以实现对象 2.状态选择可以用数字0 1 ...

  3. 《java多线程编程核心技术》不使用等待通知机制 实现线程间通信的 疑问分析

    不使用等待通知机制 实现线程间通信的 疑问分析 2018年04月03日 17:15:08       ayf 阅读数:33 编辑 <java多线程编程核心技术>一书第三章开头,有如下案例: ...

  4. Android-Lock-多线程通讯(生产者 消费者)&等待唤醒机制

    此篇博客以 生产面包

  5. JAVA之旅(十四)——静态同步函数的锁是class对象,多线程的单例设计模式,死锁,线程中的通讯以及通讯所带来的安全隐患,等待唤醒机制

    JAVA之旅(十四)--静态同步函数的锁是class对象,多线程的单例设计模式,死锁,线程中的通讯以及通讯所带来的安全隐患,等待唤醒机制 JAVA之旅,一路有你,加油! 一.静态同步函数的锁是clas ...

  6. java ->多线程_线程同步、死锁、等待唤醒机制

    线程安全 如果有多个线程在同时运行,而这些线程可能会同时运行这段代码.程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的. l  我们通过一个案例,演示线 ...

  7. Java多线程(二) —— 线程安全、线程同步、线程间通信(含面试题集)

    一.线程安全 多个线程在执行同一段代码的时候,每次的执行结果和单线程执行的结果都是一样的,不存在执行结果的二义性,就可以称作是线程安全的. 讲到线程安全问题,其实是指多线程环境下对共享资源的访问可能会 ...

  8. java基础(27):线程安全、线程同步、等待唤醒机制

    1. 多线程 如果有多个线程在同时运行,而这些线程可能会同时运行这段代码.程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的. 我们通过一个案例,演示线程 ...

  9. Java多线程(一) —— 线程的状态详解

    一.多线程概述  1. 进程 是一个正在执行的程序.是程序在计算机上的一次运行活动. 每一个进程执行都有一个执行顺序.该顺序是一个执行路径,或者叫一个控制单元. 系统以进程为基本单位进行系统资源的调度 ...

随机推荐

  1. C6.cpp

    可以将 一个array对象赋给另一个对象 对于下标值出现负数的情况下可以解释为在头指针的位置处向前移动对应的字节 可以使用vector.at(n_elem)来获取元素等价于vector[n_elem] ...

  2. Cache-control使用Cache-control:private学习笔记【转载】

    网页缓存由 HTTP消息头中的Cache-control控制,常见取值有private.no-cache.max-age.must- revalidate等,默认为private 其作用根据不同的重新 ...

  3. Navicat操作SQL server 2008R2文件.bak文件还原

    项目操作过程中,利用Navicat操作SQL Server2008R2数据备份,结果发现数据丢失了很多,不得不先对数据丢失部分进行差异对比,然后再重新输入. 1.利用Navicat导出的数据格式为sq ...

  4. ios监听静音键和音量键事件

    http://blog.csdn.net/slinloss/article/details/7870559

  5. 宝塔面板配置阿里云SSL证书流程

    阿里云SSL证书申请过程就不在这里说了 1 先下载阿里云成功申请的SSL证书 解压后 有3个文件 2  找到宝塔面板的 站点设置  找到SSL设置 3 找到其他证书 用文本打开.key文件  复制里面 ...

  6. Anatomy of a Database System学习笔记 - 公共模块、结语

    公共模块 1. 使用基于上下文的内存分配器进行内存分配 除了教材里常提到的buffer pool,数据库还会为其他任务分配大量内存,例如,Selinger-style查询优化需要动态的规划查询:has ...

  7. 关于前段JS代码报错问题的解决方法

    最近接手别人的一个项目,项目导入到Myeclipse中,JS代码一直报错,说missing semicolon.该错误是Myeclipse在检查JS代码的过程出现的. 后来经过检查,发现JS代码本身没 ...

  8. Slf4j与log4j及log4j2的关系及使用方法

    Slf4j与log4j及log4j2的关系及使用方法 slf4j slf4j仅仅是一个为Java程序提供日志输出的统一接口,并不是一个具体的日志实现方案,就比如JDBC一样,只是一种规则而已,所以单独 ...

  9. Win10 安装SqlServer2012 无法访问数据库

    win10系统,安装好vs2017,sqlserver2012, 在abp     执行 Update-Database 会出错 具体解决办法是SqlServer2012的1433端口没打开. 打开办 ...

  10. 性能学习随笔(1)--负载均衡之f5负载均衡

    负载均衡设计涉及软件负载和硬件负载,下文转自CSDN中一篇文章涉及f5硬负载知识 ----转载:https://blog.csdn.net/tvk872/article/details/8063489 ...