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锁机制来解决共享数据问题。

相关文章
|
5天前
|
Java 数据库
【Java多线程】对线程池的理解并模拟实现线程池
【Java多线程】对线程池的理解并模拟实现线程池
13 1
|
1天前
|
Java
Java中的多线程编程:基础知识与实践
【5月更文挑战第13天】在计算机科学中,多线程是一种使得程序可以同时执行多个任务的技术。在Java语言中,多线程的实现主要依赖于java.lang.Thread类和java.lang.Runnable接口。本文将深入探讨Java中的多线程编程,包括其基本概念、实现方法以及一些常见的问题和解决方案。
|
1天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第13天】 在Java开发中,并发编程是一个复杂且重要的领域。它不仅关系到程序的线程安全性,也直接影响到系统的性能表现。本文将探讨Java并发编程的核心概念,包括线程同步机制、锁优化技术以及如何平衡线程安全和性能。通过分析具体案例,我们将提供实用的编程技巧和最佳实践,帮助开发者在确保线程安全的同时,提升应用性能。
10 1
|
2天前
|
Java 调度
Java一分钟之线程池:ExecutorService与Future
【5月更文挑战第12天】Java并发编程中,`ExecutorService`和`Future`是关键组件,简化多线程并提供异步执行能力。`ExecutorService`是线程池接口,用于提交任务到线程池,如`ThreadPoolExecutor`和`ScheduledThreadPoolExecutor`。通过`submit()`提交任务并返回`Future`对象,可检查任务状态、获取结果或取消任务。注意处理`ExecutionException`和避免无限等待。实战示例展示了如何异步执行任务并获取结果。理解这些概念对提升并发性能至关重要。
17 5
|
2天前
|
安全 Java 调度
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第12天】 在现代软件开发中,多线程编程是提升应用程序性能和响应能力的关键手段之一。特别是在Java语言中,由于其内置的跨平台线程支持,开发者可以轻松地创建和管理线程。然而,随之而来的并发问题也不容小觑。本文将探讨Java并发编程的核心概念,包括线程安全策略、锁机制以及性能优化技巧。通过实例分析与性能比较,我们旨在为读者提供一套既确保线程安全又兼顾性能的编程指导。
|
3天前
|
Java
Java一分钟:线程协作:wait(), notify(), notifyAll()
【5月更文挑战第11天】本文介绍了Java多线程编程中的`wait()`, `notify()`, `notifyAll()`方法,它们用于线程间通信和同步。这些方法在`synchronized`代码块中使用,控制线程执行和资源访问。文章讨论了常见问题,如死锁、未捕获异常、同步使用错误及通知错误,并提供了生产者-消费者模型的示例代码,强调理解并正确使用这些方法对实现线程协作的重要性。
13 3
|
3天前
|
安全 算法 Java
Java一分钟:线程同步:synchronized关键字
【5月更文挑战第11天】Java中的`synchronized`关键字用于线程同步,防止竞态条件,确保数据一致性。本文介绍了其工作原理、常见问题及避免策略。同步方法和同步代码块是两种使用形式,需注意避免死锁、过度使用导致的性能影响以及理解锁的可重入性和升级降级机制。示例展示了同步方法和代码块的运用,以及如何避免死锁。正确使用`synchronized`是编写多线程安全代码的核心。
55 2
|
3天前
|
安全 Java 调度
Java一分钟:多线程编程初步:Thread类与Runnable接口
【5月更文挑战第11天】本文介绍了Java中创建线程的两种方式:继承Thread类和实现Runnable接口,并讨论了多线程编程中的常见问题,如资源浪费、线程安全、死锁和优先级问题,提出了解决策略。示例展示了线程通信的生产者-消费者模型,强调理解和掌握线程操作对编写高效并发程序的重要性。
43 3
|
3天前
|
安全 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第11天】在Java并发编程中,线程安全和性能优化是两个重要的主题。本文将深入探讨这两个方面,包括线程安全的基本概念,如何实现线程安全,以及如何在保证线程安全的同时进行性能优化。我们将通过实例和代码片段来说明这些概念和技术。
4 0
|
3天前
|
Java 调度
Java并发编程:深入理解线程池
【5月更文挑战第11天】本文将深入探讨Java中的线程池,包括其基本概念、工作原理以及如何使用。我们将通过实例来解释线程池的优点,如提高性能和资源利用率,以及如何避免常见的并发问题。我们还将讨论Java中线程池的实现,包括Executor框架和ThreadPoolExecutor类,并展示如何创建和管理线程池。最后,我们将讨论线程池的一些高级特性,如任务调度、线程优先级和异常处理。