Java——多线程编程(二):多线程的同步、安全问题(概念理解+应用举例)

简介: Java——多线程编程(二):多线程的同步、安全问题(概念理解+应用举例)

文章目录:


1.为什么要实现多线程同步?

2.线程安全 

2.1 什么是线程安全?(卖电影票实例) 

3.多线程同步的三种实现方式 

3.1 同步代码块 

3.2 同步方法 

3.3 同步锁

1.为什么要实现多线程同步?


多线程的并发执行可以提高程序的效率,但是,当多个线程去访问同一个资源时,也会引发一些安全问题。例如,当统计一个班级的学生数目时,如果有同学进进出出,则很难统计正确。为了解决这样的问题,需要实现多线程的同步,即限制某个资源在同一时刻只能被一个线程访问

2.线程安全


线程安全问题其实就是由多个线程同时处理共享资源所导致的。要想解决线程安全问题,必须得保证处理共享资源的代码在任意时刻只能有一个线程访问。为此,Java中提供了线程同步机制。

假如Java程序中有多个线程在同时运行,而这些线程可能会同时运行一部分的代码。如果说该Java程序每次运行的结果和单线程的运行结果是一样的,并且其他的变量值也都是和预期的结果是一样的,那么就可以说线程是安全的。


2.1 什么是线程安全?(卖电影票实例) 


情况1:该电影院开设一个售票窗口,一个窗口卖一百张票,没有问题。就如同单线程程序不会出现安全问题一样。

情况2:该电影院开设nn>1)个售票窗口,每个售票窗口售出指定号码的票,也不会出现问题。就如同多线程程序,没有访问共享数据,不会产生问题。

情况3:该电影院开设nn>1)个售票窗口,每个售票窗口出售的票都是没有规定的(如:所有的窗口都可以出售1号票),这就会出现问题了,假如三个窗口同时在卖同一张票,或有的票已经售出,还有窗口还在出售。就如同多线程程序,访问了共享数据,会产生线程安全问题。

下面就是情况3对应的程序代码:👇👇👇 


class MovieTicket01 implements Runnable {
  private static int ticketNumber=10;//电影票数量
  @Override
  public void run() {
    while(ticketNumber>0) {
      try {
        //提高程序安全的概率,让线程先睡眠10ms
        Thread.sleep(10);
      }catch(InterruptedException e) {
        e.printStackTrace();
      }
      //电影票出售
      System.out.println("售票窗口(" + Thread.currentThread().getName() + 
          ")正在出售:" + MovieTicket01.ticketNumber + "号电影票");
      ticketNumber--;//出售一张就自减1
    }
  }
}
public class Thread04 {
  public static void main(String[] args) {
    //创建Runnable接口子类的实现对象
    MovieTicket01 movieTicket=new MovieTicket01();
    //创建Thread线程类对象
    Thread window1=new Thread(movieTicket);
    Thread window2=new Thread(movieTicket);
    Thread window3=new Thread(movieTicket);
    //为这三个线程命名
    window1.setName("window1");
    window2.setName("window2");
    window3.setName("window3");
    //调用start()方法启动线程
    window1.start();
    window2.start();
    window3.start();
  }
}

大家一看这个输出结果肯定就会发现问题,窗口1卖出了10号电影票,然后窗口3和窗口2竟然还在卖10号电影票,这显然是不符合逻辑的吧!!!三个窗口(线程)同时出售不指定号数的票(访问共享数据),出现了卖票重复的情况。

出现这种情况的原因就是因为JVM默认的是抢占调度方式,三个线程谁先抢到CPU谁执行,所以自然就乱套了。要解决这个问题,我们就需要通过多线程同步、安全来实现。

3.多线程同步的三种实现方式


3.1 同步代码块 

注意lock锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是相同的。锁对象的创建代码不能放到run()方法中,否则每个线程运行到run()方法都会创建一个新对象,这样每个线程都会有一个不同的锁。

原理:当线程执行同步代码块时,首先会检查lock锁对象的标志位;

          默认情况下标志位为1,此时线程会执行Synchronized同步代码块,同时将锁对象的标志位置为0

          当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为0,新线程会发生阻塞,等待当前线程执行完同步代码块后;

          锁对象的标志位被置为1,新线程才能进入同步代码块执行其中的代码,这样循环往复,直到共享资源被处理完为止。

class MovieTicket02 implements Runnable {
  private static int ticketNumber=10;//电影票数量
  /*不能放在run()方法中!!!
  否则每个线程运行到run()方法都会创建一个新对象,
  这样每个线程都会有一个不同的锁。*/
  Object object=new Object();//创建锁对象
  @Override
  public void run() {
    //同步代码块
    synchronized(object) {//设置此线程要执行的任务
      while(ticketNumber>0) {
        try {
          //提高程序安全的概率,让线程先睡眠10ms
          Thread.sleep(10);
        }catch(InterruptedException e) {
          e.printStackTrace();
        }
        //电影票出售
        System.out.println("售票窗口(" + Thread.currentThread().getName() + 
            ")正在出售:" + MovieTicket02.ticketNumber + "号电影票");
        ticketNumber--;//出售一张就自减1
      }
    }
  }
}
public class Thread05 {
  public static void main(String[] args) {
    //创建Runnable接口子类的实现对象
    MovieTicket02 movieTicket=new MovieTicket02();
    //创建Thread线程类对象
    Thread window1=new Thread(movieTicket);
    Thread window2=new Thread(movieTicket);
    Thread window3=new Thread(movieTicket);
    //为这三个线程命名
    window1.setName("window1");
    window2.setName("window2");
    window3.setName("window3");
    //调用start()方法启动线程
    window1.start();
    window2.start();
    window3.start();
  }
}

这个时候,控制台不再出售不存在的电影号数以及重复的电影号数了。

通过代码块中的锁对象,可以使用任意的对象。但是必须保证多个线程使用的锁对象是同一。锁对象作用:把同步代码块锁住,只让一个线程在同步代码块中执行。

总结:同步中的线程,没有执行完毕,不会释放锁,同步外的线程,没有锁,进不去同步。


3.2 同步方法 

说明:在方法前面也可以使用 synchronized 关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能。被 synchronized 修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行。

注意:同步方法也有锁,它的锁就是当前调用该方法的对象,就是this指向的对象。

          Java中静态方法的锁是该方法所在类的class对象,该对象可以直接类名.class的方式获取。

          同步代码块和同步方法解决多线程问题有好处也有弊端。同步解决了多个线程同时访问共享数据时的线程安全问题,只要加上同一个锁,在同一时间内只能有一条线程执行,但是线程在执行同步代码时每次都会判断锁的状态,非常消耗资源,效率较低


class MovieTicket03 implements Runnable {
  private static int ticketNumber=10;//电影票数量
  /*不能放在run()方法中!!!
  否则每个线程运行到run()方法都会创建一个新对象,
  这样每个线程都会有一个不同的锁。*/
  Object object=new Object();//创建锁对象
  @Override
  public void run() {
    ticket();//设置此线程要执行的任务
  }
  //同步方法
  public synchronized void ticket() {
    while(ticketNumber>0) {
      try {
        //提高程序安全的概率,让线程先睡眠10ms
        Thread.sleep(10);
      }catch(InterruptedException e) {
        e.printStackTrace();
      }
      //电影票出售
      System.out.println("售票窗口(" + Thread.currentThread().getName() + 
          ")正在出售:" + MovieTicket03.ticketNumber + "号电影票");
      ticketNumber--;//出售一张就自减1
    }
  }
}
public class Thread06 {
  public static void main(String[] args) {
    //创建Runnable接口子类的实现对象
    MovieTicket03 movieTicket=new MovieTicket03();
    //创建Thread线程类对象
    Thread window1=new Thread(movieTicket);
    Thread window2=new Thread(movieTicket);
    Thread window3=new Thread(movieTicket);
    //为这三个线程命名
    window1.setName("window1");
    window2.setName("window2");
    window3.setName("window3");
    //调用start()方法启动线程
    window1.start();
    window2.start();
    window3.start();
  }
}

这里的输出结果和同步代码块是一样的!!! 


3.3 同步锁


问题synchronized同步代码块和同步方法使用一种封闭式的锁机制,使用起来非常简单,也能够解决线程同步过程中出现的线程安全问题,但也有一些限制,例如它无法中断一个正在等候获得锁的线程,也无法通过轮询得到锁,如果不想等下去,也就没法得到锁。

解决:从JDK 5开始,Java增加了一个功能更强大的Lock锁。Lock锁与synchronized隐式锁在功能上基本相同,其最大的优势在于Lock锁可以让某个线程在持续获取同步锁失败后返回,不再继续等待,另外Lock锁在使用时也更加灵活

注意

ReentrantLock类是Lock锁接口的实现类,也是常用的同步锁,在该同步锁中除了 lock() 方法和 unlock() 方法外,还提供了一些其他同步锁操作的方法,例如 tryLock() 方法可以判断某个线程锁是否可用。

在使用Lock同步锁时,可以根据需要在不同代码位置灵活的上锁和解锁,为了保证所有情况下都能正常解锁以确保其他线程可以执行,通常情况下会在finally{}代码块中调用 unlock() 方法来解锁


import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class MovieTicket04 implements Runnable {
  private static int ticketNumber=10;//电影票数量
  Lock reentrantLock=new ReentrantLock();//锁对象
  @Override
  public void run() {
    while(ticketNumber>0) {
      reentrantLock.lock();//加锁
      try {   
        Thread.sleep(20);
        //电影票出售
        System.out.println("售票窗口(" + Thread.currentThread().getName() + 
            ")正在出售:" + MovieTicket04.ticketNumber + "号电影票");
        ticketNumber--;//出售一张就自减1
      }catch(InterruptedException e) {
        e.printStackTrace();
      }finally {
        reentrantLock.unlock();//解锁
      }
    }
  }
}
public class Thread07 {
  public static void main(String[] args) {
    //创建Runnable接口子类的实现对象
    MovieTicket04 movieTicket=new MovieTicket04();
    //创建Thread线程类对象
    Thread window1=new Thread(movieTicket);
    Thread window2=new Thread(movieTicket);
    Thread window3=new Thread(movieTicket);
    //为这三个线程命名
    window1.setName("window1");
    window2.setName("window2");
    window3.setName("window3");
    //调用start()方法启动线程
    window1.start();
    window2.start();
    window3.start();
  }
}

同步锁的方式与前两种方式有所不同。前两种方式,只有线程1能够进入同步机制执行代码,在Lock锁机制中,三个线程都可以进行执行,通过Lock锁机制来解决共享数据问题。

相关文章
|
7月前
|
Java
如何在Java中进行多线程编程
Java多线程编程常用方式包括:继承Thread类、实现Runnable接口、Callable接口(可返回结果)及使用线程池。推荐线程池以提升性能,避免频繁创建线程。结合同步与通信机制,可有效管理并发任务。
299 6
|
7月前
|
IDE Java 编译器
java编程最基础学习
Java入门需掌握:环境搭建、基础语法、面向对象、数组集合与异常处理。通过实践编写简单程序,逐步深入学习,打牢编程基础。
411 1
|
7月前
|
安全 前端开发 Java
从反射到方法句柄:深入探索Java动态编程的终极解决方案
从反射到方法句柄,Java 动态编程不断演进。方法句柄以强类型、低开销、易优化的特性,解决反射性能差、类型弱、安全性低等问题,结合 `invokedynamic` 成为支撑 Lambda 与动态语言的终极方案。
316 0
|
10月前
|
Java API 微服务
为什么虚拟线程将改变Java并发编程?
为什么虚拟线程将改变Java并发编程?
455 83
|
机器学习/深度学习 消息中间件 存储
【高薪程序员必看】万字长文拆解Java并发编程!(9-2):并发工具-线程池
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的强力并发工具-线程池,废话不多说让我们直接开始。
430 0
|
8月前
|
算法 Java
Java多线程编程:实现线程间数据共享机制
以上就是Java中几种主要处理多线程序列化资源以及协调各自独立运行但需相互配合以完成任务threads 的技术手段与策略。正确应用上述技术将大大增强你程序稳定性与效率同时也降低bug出现率因此深刻理解每项技术背后理论至关重要.
549 16
|
7月前
|
Java 调度 数据库
Python threading模块:多线程编程的实战指南
本文深入讲解Python多线程编程,涵盖threading模块的核心用法:线程创建、生命周期、同步机制(锁、信号量、条件变量)、线程通信(队列)、守护线程与线程池应用。结合实战案例,如多线程下载器,帮助开发者提升程序并发性能,适用于I/O密集型任务处理。
728 0
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
通过本文,您可以了解如何在业务线程中注册和处理Linux信号。正确处理信号可以提高程序的健壮性和稳定性。希望这些内容能帮助您更好地理解和应用Linux信号处理机制。
295 26
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
321 17
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####