Java中的多线程与锁(一)

版权声明:本文为博主原创文章,转载请注明出处! https://blog.csdn.net/SLN2432713617/article/details/89608095
1. 简介
  1. 先来引入多线程编程中存在的问题。下面是一个例子(多个线程同时更新计数器):
/*
 * 多个线程同时更新计数器(模拟多线程中存在的问题)
 */
public class Temp_1 {
	public static void main(String[] args) {
		// 连续模拟操作 10 次
		for(int i = 0;i < 10;i++) {
			update_counter_demo();
		}
	}
	// 构造工作者线程,模拟多线程同时更新计数器
	public static void update_counter_demo(){
		Counter counter = new Counter(0);
		int worker_thread_num = 10; // 10 个工作者线程
		Thread[] workers = new Thread[worker_thread_num];
		for(int j = 0;j < worker_thread_num;j++) {			
			workers[j] = new Thread(new Runnable() {
				@Override
				public void run() {
					try {
						Thread.sleep(100); // 先等待 100 毫秒
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					for(int i = 0;i < 100;i++) { // 执行累加 100 次
						counter.plusOne();
					}
				}
			});
		}
		// 启动所有工作者线程
		for(int i = 0;i < worker_thread_num;i++) {
			workers[i].start();
		}
		// 等待线程结束
		for(int i = 0;i < worker_thread_num;i++) {
			try {
				workers[i].join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		// 打印计数器数值
		System.out.println(counter);
	}
	/* 简单计数器 */
	private static class Counter {
		private int count;
		public Counter(int count) {
			this.count = count;
		}
		/* 计数器加 1 */
		public void plusOne() {
			count++;
		}
		@Override
		public String toString() {
			return "[ count is " + count + " ]";
		}
	}
}

执行结果如下:

[ count is 838 ]
[ count is 932 ]
[ count is 995 ]
[ count is 969 ]
[ count is 827 ]
[ count is 979 ]
[ count is 1000 ]           // 正确结果
[ count is 1000 ]           // 正确结果
[ count is 820 ]
[ count is 998 ]

可以发现,连续模拟操作 10 次,其中 碰巧 有 2 次的运作结果是正确的!我们知道,错误结果是 未同步多个线程对共享变量的操作 导致的。

  • 多个线程操作共享变量时存在 可见性 问题,线程 A 对计数器的累加结果对线程 B 不可见。而对于计数器的累加操作,当前操作是基于前一次的操作结果的,各个操作结果前后彼此关联,如果每一次的累加操作都是基于前一个累加操作的结果进行,就不会出现错误结果,这便是将多个相同操作串行执行。
  • 可以将累加操作简化为 3 条指令(当然实际情况可能复杂的多):
    1. 读取操作(读取计数器的数值到线程私有内存)
    2. 执行加 1 运算
    3. 写入操作(将私有内存的数值写回到主内存)
  • 当多个线程同时执行相同的累加操作(同一个方法即 plusOne() )时,由于线程调度的原因,多个线程的累加操作互相交织在一起,多个线程轮流使用 CPU 时间片(即 线程调度)其本身并不会造成问题,问题在于这多个线程的写入操作相互覆盖以及写入操作之前的读取操作 即步骤 1 的结果不可信(这种 先读取后写入的方式中,写入操作基于一个不可靠的读取操作),如下图所示(3 个线程各执行一次累加操作后,线程 D 读取到的计数为 1 !),多个线程的累加操作步骤相互穿插在一起,它们彼此之间没有协作,没有交流/通信,各自做各自的事情,却彼此相互影响!
    多线程操作共享变量
  • 解决问题的方式:引入 ‘互斥操作’,同一时刻只允许一个线程执行累加操作(即 互斥性),这样使得多个线程中的累加操作串行执行,如下图
    1. 这里可能会想到 ‘原子操作’。原子操作 是指一组不可分割的操作,它们要么都执行成功,要么都不执行。这是事务的概念。而这里要面对的是多个线程操作共享数据的问题,即怎样 使得分散在多个线程中的会相互影响的操作能够不再彼此干扰对方,而造成彼此干扰的根本原因则在于由多组操作指令组合成的不可预知的指令序列试图更改一个共享变量(如上图中,由线程 A / B / C 中的一共 3 * 3 = 9 个指令组成的指令序列)。
      多线程串行执行
  1. Java 语言提供了最基本的互斥操作保障,即 synchronized 关键字,相当于排它锁的效果,修改上述程序中的计数器 Counter 类,代码如下(仅仅使用 synchronized 关键字修饰执行累加操作的方法,使得分散在多个线程中的累加操作被串行执行):
    1. 经常与 synchronized 一起提到的还有 volatile 关键字。但是,在这个多个线程执行写入操作的场景中,volatile 关键字显得力不从心,它仅仅只能保证线程能读到计数器的最新的数值,但并不能阻止写入覆盖,也不能保证读取操作的可靠性。
/* 简单计数器 */
	private static class Counter {
		private int count;
		public Counter(int count) {
			this.count = count;
		}
		/* 计数器加 1,使用 synchronized 关键字修饰*/
		synchronized public void plusOne() {
			count++;
		}
		@Override
		public String toString() {
			return "[ count is " + count + " ]";
		}
	}

再次运行程序,执行结果如下:

[ count is 1000 ]
[ count is 1000 ]
[ count is 1000 ]
[ count is 1000 ]
[ count is 1000 ]
[ count is 1000 ]
[ count is 1000 ]
[ count is 1000 ]
[ count is 1000 ]
[ count is 1000 ]

可见运行结果正确,synchronized 关键字为我们提供保障。

  1. 说完 synchronized 关键字,再来看看 Java 中的 ‘等待 / 通知’机制,即 Object 类中提供的 wait() / notify() 方法。
    • 我们知道 wait() / notify() 必须结合 synchronized 一起使用, 使得不同线程中运行的 synchronized 方法或代码块可以相互通信/交流(即 线程主动等待 与 被动唤醒)(wait() 和 notify() 方法必须配合使用) 。wait() 方法提供的等待机制将等待的线程排队,并且处理线程的中断请求以及 响应 notify() 函数的通知来唤醒某个等待的线程。利用 synchronized 提供的 排它性 结合 wait() 和 notify() 函数提供的等待/通知机制,可以实现一个简单的锁,使用自定义锁代替计数器中累加操作的 synchronized 关键字,代码如下(仅仅修改计数器 Counter 类以及新增自定义锁类 MyLock):
/*
 * 多个线程同时更新计数器(使用自定义锁同步多线程操作)
 */
public class Temp_1 {
	public static void main(String[] args) {
		// 连续模拟操作 10 次
		for(int i = 0;i < 10;i++) {
			update_counter_demo();
		}
	}
	// 构造工作者线程,模拟多线程同时更新计数器
	public static void update_counter_demo(){
		Counter counter = new Counter(0);
		int worker_thread_num = 10;
		Thread[] workers = new Thread[worker_thread_num];
		for(int j = 0;j < worker_thread_num;j++) {			
			workers[j] = new Thread(new Runnable() {
				@Override
				public void run() {
					try {
						Thread.sleep(100); // 先等待 100 毫秒
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					for(int i = 0;i < 100;i++) {
						counter.plusOne();
					}
				}
			});
		}
		// 启动所有工作者线程
		for(int i = 0;i < worker_thread_num;i++) {
			workers[i].start();
		}
		// 等待线程结束
		for(int i = 0;i < worker_thread_num;i++) {
			try {
				workers[i].join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		// 打印计数器数值
		System.out.println(counter);
	}
	/* 简单计数器 */
	private static class Counter {
		private int count;
		private MyLock lock;
		public Counter(int count) {
			this.count = count;
			this.lock = new MyLock();
		}
		/* 计数器加 1 */
		public void plusOne() {
			lock.lock(); // 获取锁:保护累加操作
			count++;
			lock.unLock(); // 释放锁
		}
		@Override
		public String toString() {
			return "[ count is " + count + " ]";
		}
	}
}
/* 简单锁实现 */
class MyLock{
	private int count = 0;
	/* 获取锁 */
	public void lock() {
		synchronized(this) {			
			while(count != 0) {
				try {
					wait(); // 当前线程主动等待(期望被 notify() 或 interrupted())
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			count++;
		}
	}
	/* 释放锁 */
	public void unLock() {
		synchronized (this) {
			if(count > 0) {
				count--;
				notify(); // 通知(唤醒)在同一个对象监视器上等待的线程
			}
		}
	}
}

执行程序,运行结果正确。自定义锁 MyLock 中,lock() 方法执行加锁操作,它在一个 synchronized 代码块中检查锁的状态,如果锁的状态不为 0 则表示‘已加锁’,执行等待操作,否则通过更改锁的状态,从而获取锁。释放锁的操作也类似, 在 synchronized 提供的排它性保障下,通过更改锁的状态来表示‘加锁’与‘解锁’ 。当然,这个自定义锁的明显缺陷有:1 没有记录是那个线程拥有锁,从而不能阻止未执行加锁操作的线程释放锁。

  1. 同步的概念。与 ‘同步’ 相对的是 ‘异步’,它们都是在多线程编程中会使用到的概念。先说 ‘异步’,线程 A 委托线程 B 执行某个操作,但它不必等待线程 B 执行完毕,而是继续做其它的事情。而 ‘同步’ 并不意味着两个或多个线程同时执行,‘同步’ 是指多个线程 互相配合,相互协作 以完成工作,线程之间有沟通,有交流,并不是一个个‘孤岛’。上述使用 synchronized 关键字和使用 ‘等待 / 通知’机制同步多线程以完成计数器累加都是同步的例子。

猜你喜欢

转载自blog.csdn.net/SLN2432713617/article/details/89608095
今日推荐