专栏首页指点的专栏Java 多线程(4)---- 线程的同步(中)

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

前言

在前一篇文章: 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 关键字来实现对多线程之间的同步控制。相关代码:

/**
 * 使用 synchronized 关键字实现线程同步
 */
class X {
	Object obj = new Object();

	// 使用 synchronized 关键字修饰方法,某个时刻只有一个线程能进入方法中执行代码
	// 对于实例方法(非静态方法),针对的是当前类对象的锁,对于静态方法,针对的是当前类的 Class 对象的锁
	// 当前线程执行到这里的时候,synchronized 关键字会检测当前对象的锁是否已经被其他线程获取,
	// 如果是,那么当前线程会陷入阻塞,直到获取当前对象锁的线程释放当前对象锁
	// 否则当前线程就获取当前对象的锁并进入方法中执行代码
	public synchronized void solve() {
		// do something...
	}

	public void solve2() {
		// 使用 synchronized 关键字修饰代码块,某个时刻只有一个线程能进入修饰的代码块中执行代码
		// 当前线程执行到这里的时候,synchronized 关键字会检测 obj 对象的锁是否已经被其他线程获取,
		// 如果是,那么当前线程会陷入阻塞,直到获取 obj 对象锁的线程释放 obj 对象锁
		// 否则当前线程就获取 obj 对象的锁并进入代码块中执行代码
		synchronized (obj) {
			// do something...
		}
	}
}

下面我们用 synchronized 关键字来解决我们上文卖车票出现的问题:

/**
 * 使用 synchronized 关键字实现线程同步的售卖火车票的测试类
 */
public static class SellTickets3 {
	private static int tickets = 10; // 10 张火车票
	
	// 使用 synchronized 关键字修饰方法,确保某一时刻只有一个线程能够进入该方法中执行代码
	protected synchronized static void sell() {
		// 再次判断当前票数是否大于 0 ,确保正确性
		if (tickets > 0) {
			System.out.println(Thread.currentThread().getName() + "卖出了第 " + tickets-- + " 张票");
		}
	}
	
	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) {
	SellTickets3.startSell();
}

对比使用 ReentrantLock 实现的线程同步,使用 synchronized 关键字我们只需要把 sell 方法使用这个关键字进行修饰,就能使得这个方法成为一个同步方法。当然我们也可是将 sell 方法中的代码块用 synchronized 关键字包裹起来,同样也可以达到效果,这里就不贴代码了,感兴趣的小伙伴可以自己试试。现在来看看结果:

结果同样是正确的。这里想声明一点的是:synchronized 关键字锁住的是对象,而不是代码块。即其针对的是对象的锁资源。 相信仔细看了上面的方法解释和代码的小伙伴已经注意到了。

对于 Object 类,其提供了一些其他的方法用于实现更加精细的线程之间的同步控制:

我们框出了 5 个方法,需要注意的是:这 5 个方法均只能在 synchronized 修饰的方法或者代码块中调用。 其实准确的来说是只能当线程获取了某个对象的锁的时候才能调用这个对象的这 5 个方法。 我们来看一下这 5 个方法的作用:

Object.wait() // 使得调用这个方法的线程释放这个 Object 对象的锁并且陷入无限等待,
			  // 直到某个线程调用了这个 Object 对象的 notify 或者 notifyAll 方法
			  // 线程被唤醒之后进入就绪状态,等待 CPU 资源
			  // 如果当前线程的中断标志为 true,那么会抛出一个 InterruptedException 异常

Object.wait(long timeout) 
// 使得调用这个方法的线程释放这个 Object 对象的锁并且等待参数指定的时间,单位为毫秒
// 直到这个等待的时间段过去、某个线程调用了这个 Object 对象的 notify 或者 notifyAll 方法
// 线程被唤醒之后进入就绪状态,等待 CPU 资源
// 如果当前线程的中断标志为 true,那么会抛出一个 InterruptedException 异常

Object.wait(long timeout, int nanos) 
// 使得调用这个方法的线程释放这个 Object 对象的锁并且等待参数指定的时间,
// 第二个参数是纳秒,提供更加精确的控制
// 直到这个等待的时间段过去、某个线程调用了这个 Object 对象的 notify 或者 notifyAll 方法
// 线程被唤醒之后进入就绪状态,等待 CPU 资源
// 如果当前线程的中断标志为 true,那么会抛出一个 InterruptedException 异常

Object.notify() // 唤醒一个因调用这个 Object 对象的 wait() 方法而陷入等待状态的线程,具体哪个线程未知。

Object.notifyAll() // 唤醒所有因调用这个 Object 对象的 wait() 方法而陷入等待状态的线程。

那么对于这些方法,我们可以怎么使用呢?或者说哪些场景能够用到。假想一下现在有一个任务: 模拟一个银行转账程序,一个用户向另一个用户账户转账。根据前面讲的,我们知道,转账的代码同一时刻肯定是只能有一个线程进入执行的,显而易见在多个线程执行时我们应该保证多个线程之间的同步关系,我们来看看代码:

/**
 * 使用 synchronized 和对象的 wait 方法、notifyAll 方法模拟银行账户转账
 */
public static class TransferTest {
	int[] accountBalance; // 每个账户的余额,为了简便,这里直接假设为 int 类型
	
	public TransferTest(int[] accountBalance) {
		if (accountBalance == null) {
			// 此处应做特殊处理
			return ;
		}
		// 账户信息赋初值
		this.accountBalance = accountBalance;
	}
	
	// 获得账户的总额
	public long  getAccountSum() {
		long res = 0;
		for (int i = 0; i < accountBalance.length; i++) {
			System.out.println("账户" + i + "余额:" + accountBalance[i]);
			res += accountBalance[i];
		}
		return res;
	}
	
	/**
	 * 进行转账的同步方法
	 * @param fromIndex 转账方账户下标
	 * @param toIndex 接受方账户下标
	 * @param money 转账金额
	 */
	protected synchronized void transfer(int fromIndex, int toIndex, int money) {
		if (money < 0) {
			// 此处应做特殊处理
			return ;
		}
		System.out.println("账户" + fromIndex + "想向" + toIndex + "账户转账" + money + "元");
		// 如果转账方账户余额不足,那么调用当前对象的 wait 方法使得当前线程释放对象锁陷入无限等待,
		// 直到其它线程调用了 notify 或者 notifyAll 方法,
		while (accountBalance[fromIndex] < money) {
			System.out.println("账户余额不足,无法转账!");
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		// 账户余额变更
		accountBalance[fromIndex] -= money;
		accountBalance[toIndex] += money;
		System.out.println("转账成功");
		notifyAll(); // 唤醒所有因调用了当前对象的 wait 方法而陷入等待的线程
	}
	
	public void startTransfer() {
		Random random = new Random();
		for (int i = 0; i < 10; i++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					// 转账方、接收方、转账金额
					int fromAccount;
					int toAccount;
					int money;
					for (int j = 0; j < 10; j++) {
						fromAccount = random.nextInt(accountBalance.length);
						toAccount = random.nextInt(accountBalance.length);
						if (fromAccount == toAccount) {
							j--;
							continue;
						}
						money = random.nextInt(500);
						transfer(fromAccount, toAccount, money);
					}
				}
			}).start();
		}
	}
}

public static void main(String[] args) {
	TransferTest test = new TransferTest(new int[]{500, 500, 500, 500, 500, 500, 500, 500, 500, 500});
	test.startTransfer();
	
	try {
		Thread.sleep(3000);
	} catch (InterruptedException e) {
		// TODO 自动生成的 catch 块
		e.printStackTrace();
	}
	System.out.println("当前账户总余额: " + test.getAccountSum());
}

我们用了 10 个子线程,每个子线程进行 10 次转账,一共是 100 次转账,当然存在转账不成功的情况(转账方余额不足)。 来看看结果:

可以看到,转账完成之后余额还是 5000,并没有改变,证明了我们的想法是可行的。

那么,对于上面这个例子,我们能否通过 ReentrantLock 类实现呢,答案是肯定的。在 ReentrantLock 类中还提供了一个方法:

Condition	newCondition() // 返回一个 Condition 实例对象用来实现锁的功能

我们再来看一下 Condition 类的一些方法:

void await() // 同 Object.wait() 方法

boolean	await​(long time, TimeUnit unit) // 和 Object.wait(long time, TimeUnit unit) 方法一样

long awaitNanos​(long nanosTimeout) // 当前线程释放锁资源,陷入等待,时间为参数指定的纳秒数

boolean	awaitUntil​(Date deadline) // 当前线程释放锁资源,陷入等待状态,
// 直到其他线程调用该对象的 signal 、signalAl 方法、等待时间过去或者当前线程发生中断

void signal() // Wakes up one waiting thread. 相当于 Object.notify 方法

void signalAll() // Wakes up all waiting threads. 相当于 Object.notifyAll 方法

对于这些方法,如果你理解了上面 Object 类中的相关方法,那么这些方法对你一点难度都没有。这些方法同样需要在线程获取了 ReentrantLock 锁资源的情况下才能调用,即必须要在 ReentrantLock 对象的 lock() 方法和 unlock() 方法之间调用。 下面我们用 ReentrantLock 来实现上面的转账程序:

/**
 * 使用 ReentrantLock 类和 Condition 类模拟银行账户转账
 */
public static class TransferTest2 {
	int[] accountBalance; // 每个账户的余额,为了简便,这里直接假设为 int 类型
	
	ReentrantLock lock = new ReentrantLock(); // 创建锁对象
	Condition con = lock.newCondition();
	
	public TransferTest2(int[] accountBalance) {
		if (accountBalance == null) {
			// 此处应做特殊处理
			return ;
		}
		// 账户信息赋初值
		this.accountBalance = accountBalance;
	}
	
	// 获得账户的总额
	public long  getAccountSum() {
		long res = 0;
		for (int i = 0; i < accountBalance.length; i++) {
			System.out.println("账户" + i + "余额:" + accountBalance[i]);
			res += accountBalance[i];
		}
		return res;
	}
	
	/**
	 * 进行转账的同步方法
	 * @param fromIndex 转账方账户下标
	 * @param toIndex 接受方账户下标
	 * @param money 转账金额
	 */
	protected void transfer(int fromIndex, int toIndex, int money) {
		// 当前线程尝试获取锁资源
		lock.lock();
		try {
			if (money < 0) {
				// 此处应做特殊处理
				return ;
			}
			System.out.println("账户" + fromIndex + "想向" + toIndex + "账户转账" + money + "元");
			// 如果转账方账户余额不足,那么调用 con 对象的 await 方法使得当前线程释放对象锁陷入无限等待,
			// 直到其它线程调用了 con 对象的 signal 或者 signalAll 方法,
			while (accountBalance[fromIndex] < money) {
				System.out.println("账户余额不足,无法转账!");
				try {
					con.await();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			// 账户余额变更
			accountBalance[fromIndex] -= money;
			accountBalance[toIndex] += money;
			System.out.println("转账成功");
			con.signalAll(); // 唤醒所有因调用了 con 对象的 await 方法而陷入等待的线程
		} catch (Exception e) {
			
		} finally {
			lock.unlock(); // 当前线程释放锁资源
		}
	}
	
	public void startTransfer() {
		Random random = new Random();
		for (int i = 0; i < 10; i++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					// 转账方、接收方、转账金额
					int fromAccount;
					int toAccount;
					int money;
					for (int j = 0; j < 10; j++) {
						fromAccount = random.nextInt(accountBalance.length);
						toAccount = random.nextInt(accountBalance.length);
						if (fromAccount == toAccount) {
							j--;
							continue;
						}
						money = random.nextInt(500);
						transfer(fromAccount, toAccount, money);
					}
				}
			}).start();
		}
	}
}

public static void main(String[] args) {
	TransferTest2 test = new TransferTest2(new int[]{500, 500, 500, 500, 500, 500, 500, 500, 500, 500});
	test.startTransfer();
	
	try {
		Thread.sleep(3000);
	} catch (InterruptedException e) {
		// TODO 自动生成的 catch 块
		e.printStackTrace();
	}
	System.out.println("当前账户总余额: " + test.getAccountSum());
}

我们仅仅修改了 transfer 方法的一些代码,思路和使用 synchronized 几乎一样。来看看结果:

利用 ReentrantLock 类同样的达到了我们想要的效果。

ReentrantLock 和 synchronized 的区别

我们上面已经分别用了 ReentrantLock 类和 synchronized 关键字实现了一些线程同步的需求。那么它们之间有什么不同呢? 简单一句话来说:ReentrantLock 类比 synchronized 类更加灵活,但是在实现功能方面需要我们去做的也会更多,比如我们必须手动的调用 lock.lock() 方法来使得线程获取锁资源,也需要调用 lock.unlock() 方法来使得获得这个锁资源的线程释放锁资源。而使用 synchronized 关键字确并不需要,其在内部已经帮我们做了这些事。那么说 ReentrantLocksynchronized 关键字更加灵活体现在哪里呢? 我们在上文介绍 ReentrantLock 类提供的方法时候介绍过一个 tryLock 方法,这个方法也会尝试获取锁,但是当它在获取锁失败之后不会陷入阻塞状态,而是会返回获取锁的结果。这样的话我们就可以在线程获取锁资源失败的时候令这个线程去做别的事。而不是让这个线程陷入阻塞状态。 我们知道这个方法还有一个重载的版本是在一定时间段内不断获取锁资源,如果成功,返回 true,失败返回 false。我们可以将这两个方法组合起来使用,一个简单的框架是:

// 如果在参数给定的时间内成功获取锁资源,那么执行相关任务
if (lock.tryLock() || lock.tryLock(time, unit)) {
	// do something...
} else {
	// do something else...
}

我们用一个简单的例子看看:

/**
 * ReentrantLock 中 tryLock 方法的测试
 */
public static class ReentrantLockTest {
	static ReentrantLock lock = new ReentrantLock();
	static int sum = 0;
	
	// 每次允许一个线程进入方法执行累加代码
	protected static void increase() {
		try {
			// 如果 1ms 以内得到了锁资源,那么就进行累加
			if (lock.tryLock() || lock.tryLock(1, TimeUnit.MILLISECONDS)) {
				try {
					// 进行累加
					for (int j = 0; j < 100000; j++) {
						sum++;
					}
				} catch (Exception e) {
				} finally {
					lock.unlock(); // 线程释放锁资源
				}
			// 没有得到锁资源就去做别的事
			} else {
				System.out.println(Thread.currentThread().getName() + "没有获取到锁资源,去做别的事了");
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	public static void start() {
		for (int i = 0; i < 10; i++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					increase();
				}
			}, "线程" + (i+1)).start();
		}
	}
}
public static void main(String[] args) {
	ReentrantLockTest.start();
	
	while (Thread.activeCount() > 1) {
		Thread.yield();
	}
	System.out.println(ReentrantLockTest.sum);
}

我们用 10 个线程来分别执行累加代码,如果线程在 increase 方法中得到了锁资源,那么就将 sum 的值循环增加 100000 ,否则就打印出一句话并结束。我们来看看结果:

因为有 3 个线程在 increase 方法中没有的到锁资源,即没有执行对 sum 的累加代码,所以结果正好是 700000。

到这里我们就差不多把线程同步的一些东西讲完了,实现线程同步其实就是通过一些手段来保证一些代码的原子性,使得多个线程并发执行这些代码的时候不会出现一些意外的错误。 我们已经用 ReentrantLock 类和 synchronized 实现了线程的同步,但是对于 ReentrantLock 类,其还有一些其他的特性,在这里就不细讲了,有兴趣的小伙伴可以参考这篇文章: https://blog.csdn.net/yanyan19880509/article/details/52345422

在 Java 中还有其他的一些锁可以实现同步和一些其他的需求,比如 ReentrantReadWriteLock (读写锁),这个类的锁资源有两种:读锁和写锁,其中读锁不具有排他性,即允许多个线程并发读取数据,而写锁具有排他性,即同一时刻只能有一个线程进行写数据的操作,适用于经常需要读取数据而对于写入数据的次数相对较少的数据结构。 关于这个类的更多信息,可以参考官方文档: https://docs.oracle.com/javase/10/docs/api/index.html?overview-summary.html

好了,Java 中线程的同步的中篇文章就到这里了。 如果博客中有什么不正确的地方,还请多多指点。如果这篇文章对您有帮助,请不要吝啬您的赞,欢迎继续关注本专栏。

谢谢观看。。。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java 多线程(1)---- 初识线程

    多线程想必大家都不会陌生。因为在日常使用和开发中,多线程的使用实在是太常见了。我们都知道,发明多线程的目的是为了更好的利用计算机的 CPU 资源。比如在一个进程...

    指点
  • Java 多线程(7)----线程池(下)

    在上篇文章:Java 多线程—线程池(上) 中我们看了一下 Java 中的阻塞队列,我们知道阻塞队列是一种可以对线程进行阻塞控制的队列,并且在前面我们也使用了阻...

    指点
  • Java 多线程(8)---- 线程组和 ThreadLocal

    在上面文章中,我们从源码的角度上解析了一下线程池,并且从其 execute 方法开始把线程池中的相关执行流程过了一遍。那么接下来,我们来看一个新的关于线程的知识...

    指点
  • 1.11守护线程

    在Java中有两种线程,一种为用户线程,一种为守护线程。 守护线程是一种特殊的线程,它具有“陪伴”的含义,当进程中不存在非守护线程时,则守护线程自动销毁。 典型...

    用户1134788
  • Java并发编程之线程池必用知识点

    再使用线程池之前,我们应该了解为什么需要使用线程池。进行执行任务(task)的时候我们一般情况是new Thread进行执行,如果进行大量的并发任务的时候呢?

    静默加载
  • 我画了25张图展示线程池工作原理和实现原理,原创干货,建议先收藏再阅读

    有朋友留言提到文中的场景是IO密集型操作,不是CPU密集操作,不需要使用线程池,我猜这位朋友可能想表达的是IO密集且阻塞时间久的不要使用线程池方案解决。IO密集...

    JavaQ
  • Java 线程池ThreadPoolExecutor原理及源码全面解析(基于JDK8)

    1、线程在java中是一个对象,更是操作系统的资源,线程创建、销毁都需要时间。 如果创建时间+销毁时间>执行任务时间就很不合算 2、Java对象占用堆内存,...

    JavaEdge
  • Thread 源码面试

    每个线程都有一个优先级。优先级高的线程优先于优先级低的线程执行。每个线程可能被标记为守护线程,也可能不被标记为守护线程。

    JavaEdge
  • Java线程组ThreadGroup

    一个线程集合。是为了更方便地管理线程。父子结构的,一个线程组可以集成其他线程组,同时也可以拥有其他子线程组。

    JavaEdge
  • Java 线程池讲解——针对 IO 密集型任务

    针对 IO 密集型的任务,我们可以针对原本的线程池做一些改造,从而可以提高任务的处理效率。

    健程之道

扫码关注云+社区

领取腾讯云代金券