Java 多线程(4)---- 线程的同步(中)

前言在前一篇文章: Java 多线程(3)— 线程的同步(上) 中,我们看了一下 Java 中的内存模型、Java 中的代码对应的字节码(包括如何生成 Java 代码的字节码和某些字节码的含义)并且分析了 Java 代码的原子性的问题。最后我们看了一下一些常见的多线程并发导致的问题。这篇文章我们主要来看一下如何运用 Java 相关 API 来实现线程的同步,即解决我们在上篇中留下的问题。在...
摘要由CSDN通过智能技术生成

本文标题大纲

前言

在前一篇文章: Java 多线程(3)— 线程的同步(上) 中,我们看了一下 Java 中的内存模型、Java 中的代码对应的字节码(包括如何生成 Java 代码的字节码和某些字节码的含义)并且分析了 Java 代码的原子性的问题。最后我们看了一下一些常见的多线程并发导致的问题。这篇文章我们主要来看一下如何运用 Java 相关 API 来实现线程的同步,即解决我们在上篇中留下的问题。

在此之前,我们先看一下关于线程同步的定义:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作。
也就是在多个线程并发执行的时候,通过相关手段调整不同线程之间的执行顺序,来使得线程之间的执行顺序根据我们的需求来进行。

同步的实现:锁机制

我们先看一下上篇中留下的第一个问题:
卖车票问题:假设有 10 张火车票,现在有 5 个线程模拟 5 个窗口卖票。用 Java 代码模拟这一过程。

我们在上篇已经写了这个代码,这里就不贴代码了,并且我们得到了一个不正确的结果:
这里写图片描述
我们通过上篇的解释已经知道了导致这个结果的原因主要是代码中的 sell 方法不具有原子性,导致可能出现前一个线程卖出车票之后还没有对主内存之中的车票数量进行更改就让出了 CPU 资源并进入等待,进而导致虽然卖出了一张车票(打印出车票的信息)但是主内存的车票数量并没有减少,而此时下一个线程得到 CPU 资源并从主内存中读取的车票数量仍是原来的值,因此会出现两个线程(窗口)卖出同一张车票和卖出第 0 张车票(不存在的车票)的情况。

怎么去解决这个问题呢?或者说怎么实现这 5 个卖票线程之间的同步呢?这里面的主要问题在于 sell 方法在同一时刻可能有多个线程进入和执行代码,我们要实现在同一个时刻只有一个线程进入 sell 方法。

一个容易想到的办法就是当一个线程想要卖车票(执行 sell 方法)时检测一下当前是否有其他线程正在执行 sell 方法,如果当前没有其他线程在执行 sell 方法,那么当前线程就开始执行 sell 方法。那么现在的问题就是如何检测在某个时刻是否有某个线程正在执行 sell 方法,但是 Java 并没有提供相关的 API。
我们换个想法,每当有线程进入 sell 方法内执行代码的时候,我们给某个对象添加一个 锁标记。当这个线程执行完成 sell 方法代码的时候,其将这个 锁标记 清除。当其他线程想要进入 sell 方法执行代码的时候,先检测一下这个 锁标记,如果这个标记存在,证明有其他线程正在执行 sell 方法的代码,那么这个线程就陷入等待。否则这个线程就进入 sell 方法中并执行相关代码,并且重新激活这个对象的 锁标记。这样一来的话在同一时刻就只有一个线程能进入 sell 方法中了。于是对于这个问题我们的线程同步关系就设计好了。而对于这个 锁标记 的相关操作实现,Java 正好提供了一些 锁类 来完成这个功能:

ReentrantLock (重入锁)##

ReentrantLock 类是 Java 提供的最常用的锁机制的类之一。我们来看看官方文档中对这个类的部分描述:

A reentrant mutual exclusion Lock with the same basic behavior and semantics as the implicit monitor lock accessed using synchronized methods and statements, but with extended capabilities.
A ReentrantLock is owned by the thread last successfully locking, but not yet unlocking it.
A thread invoking lock will return, successfully acquiring the lock, when the lock is not owned by another thread.
The method will return immediately if the current thread already owns the lock.
This can be checked using methods isHeldByCurrentThread(), and getHoldCount().

大概意思是:
可重入的互斥锁(ReentrantLock)具有与使用 synchronized 关键字修饰的同步方法的隐式监视器锁相同的基本行为和语义,但具有扩展功能。
ReentrantLock 锁由上次成功锁定并且尚未解锁的线程拥有。
当其他线程没有获得这个锁时,执行获得锁代码的线程将返回并成功获取锁。
如果当前线程已经拥有该锁,该方法将立即返回。 这可以使用 isHeldByCurrentThread() 方法和 getHoldCount() 方法进行检查当前执行代码的线程是否拥有这个锁和某个锁对象被线程所占有的次数。

翻译中涉及到了 synchronized 关键字,对于这个关键字和几种实现同步的方法之间的区别,后文将会介绍。
我们来看一下 ReentrantLock 类的常用 API :

ReentrantLock​()	// 构造一个冲入锁的对象

boolean	isLocked​() // 查询当前锁对象是否被任意一个线程所持有

boolean	isHeldByCurrentThread​() // 查询当前锁对象是否被当前执行代码的线程所拥有

void lock​() // 当前执行代码的线程尝试获取锁,如果获取失败(当前锁已经被其他线程所拥有),
			// 那么当前执行代码的线程会陷入阻塞,直到这个锁对象被其所拥有的线程释放才会从阻塞状态唤醒

boolean tryLock​() // 当前线程尝试获取当前锁,如果获取成功,那么返回 true,否则返回 false。
				   // 和 lock 方法的区别在于当前线程获取锁失败时不会陷入阻塞状态

boolean tryLock​(long timeout, TimeUnit unit) throws InterruptedException 
// 当前执行代码的线程在参数给定的时间内不断获取这个锁对象,如果在参数给定时间内成功获取这个锁对象,
// 那么该方法返回 true 并且当前线程继续往下执行,
// 如果在参数给定时间内没有获取这个锁对象, 该方法返回 false 并且当前线程继续往下执行。
// 如果当前执行代码的线程已经被中断,那么方法会抛出一个 InterruptedException 异常,
// 关于线程中断,详见本栏第二篇文章

void unlock​() // 释放当前执行代码的线程拥有的锁对象

通过对上述方法的解释,我们大概可以构造出一个常用的使用 ReentrantLock 类实现线程并发同步处理的框架:

public class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...

   public void solve() {
     lock.lock();  // 当前执行代码的线程尝试获取锁对象,如果当前锁对象被其他线程获取,则陷入阻塞状态
				   // 保证了在同一时刻只能有一个线程进入 try 代码块中执行代码,即实现线程同步
     try {
       // do something...
     } catch (Exception e) {
     } finally {
       lock.unlock() // 最后一定记得释放锁对象,不然可能导致死锁
     }
   }
 }

在上面的框架代码中我们使用了一个 trycatchfinally 异常捕获框架,**我们知道无论 try 中的代码是否发生异常,finally 中的代码是一定会执行的。**这样的话在某个方面就保证了无论执行 try 中代码块的线程是否发生异常,其在进入 try 代码块之前获取的锁是一定会被释放的,这样就防止了死锁的发生。这个也是官方推荐的使用方法。下面我们用 ReentrantLock 类来对我们上面的卖票程序进行改进,使其产生正确的结果:

/**
 * 使用 ReentrantLock 锁实现线程同步的售卖火车票的测试类
 */
public static class SellTickets2 {
    
	private static ReentrantLock lock = new ReentrantLock();
	private static int tickets = 10; // 10 张火车票
	
	protected static void sell() {
    
		lock.lock(); // 当前执行代码的线程尝试获取锁
		try {
    
			// 再次判断当前票数是否大于 0 ,确保正确性
			if (tickets > 0) {
    
				System.out.println(Thread.currentThread().getName() + "卖出了第 " + tickets-- + " 张票");
			}
		} finally {
    
			lock.unlock(); // 车票卖完之后一定释放锁
		}
	}
	
	public static void startSell() {
    
        // 售票线程所在线程组,这个线程组中的线程专门用于售票
        ThreadGroup sellTicketThreadGroup = new ThreadGroup("sell ticket thread group");
        // 开启 5 个线程售票
        for (int i = 0; i < 5; i++) {
    
            // 新建售票线程,并将其加入售票线程组中
            new Thread(sellTicketThreadGroup, "窗口" + (i+1)) {
    
                @Override
                public void run() {
    
                    while (tickets > 0) {
    
                        sell();
                        try {
    
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
    
                            // TODO 自动生成的 catch 块
                            e.printStackTrace();
                        }
                    }
                }
            }.start();
        }
        // 如果当前售票线程组中的活动线程数大于 0 ,那么证明售票还未结束,此时主线程应该让出 CPU
        while (sellTicketThreadGroup.activeCount() > 0) {
    
            Thread.yield();
        }
        System.out.println("当前所剩车票数:" + tickets);
    }
}


public static void main(String[] args) {
    
	SellTickets2.startSell();
}

我们在类中加了一个 ReentrantLock 对象,并且对 sell 方法中的代码加入了锁控制,这样的话就保证了在某个时刻只能有一个线程执行卖票的代码,即实现了线程同步控制。这里涉及到了线程组的概念,不熟悉的小伙伴可以参考一下这篇文章:Java 多线程(8)---- 线程组和 ThreadLocal
运行结果:
这里写图片描述
可以看到,这个结果就是正确的,当然我们不能确定每张票每一次运行是具体由哪个线程卖出的,因为多线程并发调度的结果是不定的,这取决于线程调度器的调度结果。但是可以确定的是卖出票的顺序一定是从 10 递减到 1 。
OK,现在我们已经用 ReentrantLock 类来解决了我们前文留下的问题,下面我们来看一下 synchronized 关键字的相关用法。

synchronized 同步机制

我们实现线程之间同步的另一个方法是通过 synchronized 关键字。这个关键字默认帮我们实现了锁机制(线程获取锁资源和线程释放锁资源)。我们一般会用其去修饰方法或者修饰某个需要进行同步控制的代码块。在看这个关键的相关代码操作之前,我们需要对 Java 中的 Object 对象进行了解:
我们知道,Java 中 Object 类是最基础的类,所有的 Java 类都是直接或者间接继承 Object 类。其实这个类中带有一个 锁标记 用于和 synchronized 配合实现线程同步,只不过我们无法直接感受到这个 。但是我们可以通过 synchronized 关键字来实现对多线程之间的同步控制。相关代码:


                
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 17
    评论
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值