Java线程同步机制详解(非常全面,附带实例)
在单线程程序中,每次只能做一件事情,后面的事情需要等待前面的事情完成后才可以进行,但是如果使用多线程程序,就会发生两个线程抢占资源的问题,如两个人同时说话、两个人同时过同一座独木桥等,因此在多线程编程中需要防止这些资源访问的冲突。
Java 提供了线程同步的机制来防止资源访问的冲突。
以火车站售票系统为例,在代码中判断当前票数是否大于 0。如果大于 0,则执行将该票出售给乘客的功能,但当两个线程同时访问这段代码时(假如这时只剩下一张票),第一个线程将票售出,与此同时第二个线程也已经执行完成判断是否有票的操作,并得出票数大于 0 的结论,于是它也执行售出操作,这样就会产生负数。因此,在编写多线程程序时,应该考虑线程安全问题。实质上线程安全问题来源于两个线程同时存取单一对象的数据。
例如,在项目中创建 ThreadSafeTest 类,该类实现 Runnable 接口,在未考虑到线程安全问题的基础上,模拟火车站售票系统的功能的代码如下:
这是由于同时创建了 4 个线程,这 4 个线程都执行 run() 方法,在 num 变量为 1 时,线程一、线程二、线程三、线程四都对 num 变量有存储功能,当线程一执行 run() 方法时,还没有来得及做递减操作,就指定它调用 sleep() 方法进入就绪状态,这时线程二、线程三和线程四也都进入了 run() 方法,发现 num 变量依然大于0,但此时线程一休眠时间已到,将 num 变量值递减,同时线程二、线程三、线程四也都对 num 变量进行递减操作,从而产生了负值。
那么,该如何解决资源共享的问题呢?所有解决多线程资源冲突问题的方法基本上都是采用给定时间只允许一个线程访问共享资源的方法,这时就需要给共享资源上一道锁。这就好比一个人上洗手间时,他进入洗手间后会将门锁上,出来时再将锁打开,然后其他人才可以进入。
Object 为任意一个对象,每个对象都存在一个标志位,并具有两个值,分别为 0 和 1。一个线程运行到同步块时首先检查该对象的标志位,如果为 0 状态,则表明此同步块内存在其他线程,这时当期线程处于就绪状态,直到处于同步块中的线程执行完同步块中的代码后,这时该对象的标识位被设置为 1,当期线程才能开始执行同步块中的代码,并将 Object 对象的标识位设置为 0,以防止其他线程执行同步块中的代码。
【实例 1】开发线程安全的火车售票系统。创建 SynchronizedTest 类,修改之前线程不安全的火车售票系统,把对 num 操作的代码设置在同步块中。
修改例 1 的代码,将共享资源操作放置在一个同步方法中,代码如下:
Java 提供了线程同步的机制来防止资源访问的冲突。
Java线程安全
实际开发中,使用多线程程序的情况很多,如银行排号系统、火车站售票系统等。这种多线程的程序通常会发生问题。以火车站售票系统为例,在代码中判断当前票数是否大于 0。如果大于 0,则执行将该票出售给乘客的功能,但当两个线程同时访问这段代码时(假如这时只剩下一张票),第一个线程将票售出,与此同时第二个线程也已经执行完成判断是否有票的操作,并得出票数大于 0 的结论,于是它也执行售出操作,这样就会产生负数。因此,在编写多线程程序时,应该考虑线程安全问题。实质上线程安全问题来源于两个线程同时存取单一对象的数据。
例如,在项目中创建 ThreadSafeTest 类,该类实现 Runnable 接口,在未考虑到线程安全问题的基础上,模拟火车站售票系统的功能的代码如下:
public class ThreadSafeTest implements Runnable { int num = 10; // 设置当前总票数 public void run() { while (true) { // 设置无限循环 if (num > 0) { // 判断当前票数是否大于 0 try { Thread.sleep(100); // 使当前线程休眠 100 毫秒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "----票数" + num--); // 票数减 1 } } } public static void main(String[] args) { // 实例化类对象 ThreadSafeTest t = new ThreadSafeTest(); // 以该类对象分别实例化 4 个线程 Thread tA = new Thread(t, "线程一"); Thread tB = new Thread(t, "线程二"); Thread tC = new Thread(t, "线程三"); Thread tD = new Thread(t, "线程四"); tA.start(); // 分别启动线程 tB.start(); tC.start(); tD.start(); } }运行本实例,最后几行结果为:
线程三 ---- 票数9
线程二 ---- 票数10
线程四 ---- 票数8
线程一 ---- 票数7
线程二 ---- 票数6
线程三 ---- 票数6
线程一 ---- 票数5
线程四 ---- 票数4
线程三 ---- 票数3
线程二 ---- 票数2
线程一 ---- 票数1
线程四 ---- 票数0
线程二 ---- 票数-1
线程三 ---- 票数-2
这是由于同时创建了 4 个线程,这 4 个线程都执行 run() 方法,在 num 变量为 1 时,线程一、线程二、线程三、线程四都对 num 变量有存储功能,当线程一执行 run() 方法时,还没有来得及做递减操作,就指定它调用 sleep() 方法进入就绪状态,这时线程二、线程三和线程四也都进入了 run() 方法,发现 num 变量依然大于0,但此时线程一休眠时间已到,将 num 变量值递减,同时线程二、线程三、线程四也都对 num 变量进行递减操作,从而产生了负值。
那么,该如何解决资源共享的问题呢?所有解决多线程资源冲突问题的方法基本上都是采用给定时间只允许一个线程访问共享资源的方法,这时就需要给共享资源上一道锁。这就好比一个人上洗手间时,他进入洗手间后会将门锁上,出来时再将锁打开,然后其他人才可以进入。
Java同步块
Java 中提供了同步机制,可以有效地防止资源冲突。同步机制使用 synchronized 关键字,使用该关键字包含的代码块被称为同步块,也将其称为临界区,语法如下:synchronized(Object){ }通常将共享资源的操作放置在 synchronized 定义的区域内,这样当其他线程获取到这个锁时,就必须等待锁被释放后才可以进入该区域。
Object 为任意一个对象,每个对象都存在一个标志位,并具有两个值,分别为 0 和 1。一个线程运行到同步块时首先检查该对象的标志位,如果为 0 状态,则表明此同步块内存在其他线程,这时当期线程处于就绪状态,直到处于同步块中的线程执行完同步块中的代码后,这时该对象的标识位被设置为 1,当期线程才能开始执行同步块中的代码,并将 Object 对象的标识位设置为 0,以防止其他线程执行同步块中的代码。
【实例 1】开发线程安全的火车售票系统。创建 SynchronizedTest 类,修改之前线程不安全的火车售票系统,把对 num 操作的代码设置在同步块中。
public class SynchronizedTest implements Runnable { int num = 10; // 设置当前总票数 public void run() { while (true) { // 设置无限循环 synchronized (this) { // 设置同步代码块 if (num > 0) { // 判断当前票数是否大于 0 try { Thread.sleep(100); // 使当前线程休眠 100 毫秒 } catch (InterruptedException e) { e.printStackTrace(); } // 票数减 1 System.out.println(Thread.currentThread().getName() + "----票数" + num--); } } } } public static void main(String[] args) { // 实例化类对象 SynchronizedTest t = new SynchronizedTest(); // 以该类对象分别实例化 4 个线程 Thread tA = new Thread(t, "线程一"); Thread tB = new Thread(t, "线程二"); Thread tC = new Thread(t, "线程三"); Thread tD = new Thread(t, "线程四"); tA.start(); // 分别启动线程 tB.start(); tC.start(); tD.start(); } }运行结果如下:
线程一——票数10
线程一——票数9
线程一——票数8
线程一——票数7
线程一——票数6
线程一——票数5
线程一——票数4
线程一——票数3
线程一——票数2
线程一——票数1
Java同步方法
同步方法就是在方法前面用 synchronized 关键字修饰的方法,其语法如下:synchronized void f() {}当某个对象调用了同步方法时,该对象上的其他同步方法必须等待该同步方法执行完毕后才能被执行。必须将每个能访问共享资源的方法修饰为 synchronized,否则就会出错。
修改例 1 的代码,将共享资源操作放置在一个同步方法中,代码如下:
int num = 10; public synchronized void doit() { // 定义同步方法 if(num > 0){ try { Thread.sleep(10); } catch(InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "----票数" + num--); } } public void run() { while(true) { doit(); // 在 run() 方法中调用该同步方法 } }将共享资源的操作放置在同步方法中,运行结果与使用同步块的结果一致。