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

相关文章
|
19天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
4天前
|
缓存 安全 算法
Java 多线程 面试题
Java 多线程 相关基础面试题
|
21天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
21天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
21天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
116 2
|
21天前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
51 1
|
1月前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
8天前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
48 17
|
21天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
45 3
|
29天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
48 6