Java 多线程详解(三)------线程的同步

Java 多线程详解(一)------概念的引入:https://cloud.tencent.com/developer/article/1012542

Java 多线程详解(二)------如何创建进程和线程:https://cloud.tencent.com/developer/article/1012550

介绍完如何创建进程以及线程了,那么我们接着来看一个实例:

利用多线程模拟 3 个窗口卖票

第一种方法:继承 Thread 类

创建窗口类 TicketSell 

package com.ys.thread;

public class TicketSell extends Thread{
	//定义一共有 50 张票,注意声明为 static,表示几个窗口共享
	private static int num = 50;
	
	//调用父类构造方法,给线程命名
	public TicketSell(String string) {
		super(string);
	}
	@Override
	public void run() {
		//票分 50 次卖完
		for(int i = 0 ; i < 50 ;i ++){
			if(num > 0){
				try {
					sleep(10);//模拟卖票需要一定的时间
				} catch (InterruptedException e) {
					// 由于父类的 run()方法没有抛出任何异常,根据继承的原则,子类抛出的异常不能大于父类, 故我们这里也不能抛出异常
					e.printStackTrace();
				}
				System.out.println(this.currentThread().getName()+"卖出一张票,剩余"+(--num)+"张");
			}
		}
	}
	

}

  创建主线程测试:

package com.ys.thread;

public class TestTicket {

	public static void main(String[] args) {
		//创建 3 个窗口
		TicketSell t1 = new TicketSell("A窗口");
		TicketSell t2 = new TicketSell("B窗口");
		TicketSell t3 = new TicketSell("C窗口");
		
		//启动 3 个窗口进行买票
		t1.start();
		t2.start();
		t3.start();
	}
}

  结果:这里我们省略了一些,根据电脑配置,结果会随机出现不同

B窗口卖出一张票,剩余48张
A窗口卖出一张票,剩余47张
C窗口卖出一张票,剩余49张
C窗口卖出一张票,剩余46张
B窗口卖出一张票,剩余44张
A窗口卖出一张票,剩余45张
A窗口卖出一张票,剩余43张
...
C窗口卖出一张票,剩余5张
A窗口卖出一张票,剩余4张
B窗口卖出一张票,剩余3张
A窗口卖出一张票,剩余2张
C窗口卖出一张票,剩余3张
B窗口卖出一张票,剩余1张
C窗口卖出一张票,剩余0张
A窗口卖出一张票,剩余-1张

第二种方法:实现 Runnable 接口

  创建窗口类 TicketSellRunnable

package com.ys.thread;

public class TicketSellRunnable implements Runnable{

	//定义一共有 50 张票,继承机制开启线程,资源是共享的,所以不用加 static
	private int num = 50;
	
	@Override
	public void run() {
		//票分 50 次卖完
		for(int i = 0 ; i < 50 ;i ++){
			if(num > 0){
				try {
					//模拟卖一次票所需时间
					Thread.sleep(10);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName()+"卖出一张票,剩余"+(--num)+"张");
			}
		}
	}

}

  创建主线程测试:

package com.ys.thread;

public class TicketSellRunnableTest {
	public static void main(String[] args) {
		TicketSellRunnable t = new TicketSellRunnable();
		
		Thread t1 = new Thread(t,"A窗口");
		Thread t2 = new Thread(t,"B窗口");
		Thread t3 = new Thread(t,"C窗口");
		
		t1.start();
		t2.start();
		t3.start();
	}

}

  结果:同理为了篇幅我们也省略了中间的一些结果

B窗口卖出一张票,剩余49张
C窗口卖出一张票,剩余48张
A窗口卖出一张票,剩余49张
B窗口卖出一张票,剩余47张
A窗口卖出一张票,剩余45张
......
A窗口卖出一张票,剩余4张
C窗口卖出一张票,剩余5张
A窗口卖出一张票,剩余3张
B窗口卖出一张票,剩余2张
C窗口卖出一张票,剩余1张
B窗口卖出一张票,剩余0张
A窗口卖出一张票,剩余-2张
C窗口卖出一张票,剩余-1张

结果分析:这里出现了票数为 负数的情况,这在现实生活中肯定是不存在的,那么为什么会出现这样的情况呢?

解决办法分析:即我们不能同时让超过两个以上的线程进入到 if(num>0)的代码块中,不然就会出现上述的错误。我们可以通过以下三个办法来解决:

1、使用 同步代码块

2、使用 同步方法

3、使用 锁机制

①、使用同步代码块

语法:
synchronized (同步锁) {
    //需要同步操作的代码          
}

同步锁:为了保证每个线程都能正常的执行原子操作,Java 线程引进了同步机制;同步锁也叫同步监听对象、同步监听器、互斥锁;
Java程序运行使用的任何对象都可以作为同步监听对象,但是一般我们把当前并发访问的共同资源作为同步监听对象

注意:同步锁一定要保证是确定的,不能相对于线程是变化的对象;任何时候,最多允许一个线程拿到同步锁,谁拿到锁谁进入代码块,而其他的线程只能在外面等着

  实例:

public void run() {
		//票分 50 次卖完
		for(int i = 0 ; i < 50 ;i ++){
			//这里我们使用当前对象的字节码对象作为同步锁
			synchronized (this.getClass()) {
				if(num > 0){
					try {
						//模拟卖一次票所需时间
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName()+"卖出一张票,剩余"+(--num)+"张");
				}
			}
			
		}
	}

2、使用 同步方法

语法:即用  synchronized  关键字修饰方法

@Override
	public void run() {
		//票分 50 次卖完
		for(int i = 0 ; i < 50 ;i ++){
			sell();
			
		}
	}
	private synchronized void sell(){
		if(num > 0){
			try {
				//模拟卖一次票所需时间
				Thread.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"卖出一张票,剩余"+(--num)+"张");
		}
	}

注意:不能直接用 synchronized 来修饰 run() 方法,因为如果这样做,那么就会总是第一个线程进入其中,而这个线程执行完所有操作,即卖完所有票了才会出来。

3、使用 锁机制

public interface Lock

  主要方法:

  常用实现类:

public class ReentrantLock
extends Object
implements Lock, Serializable//一个可重入互斥Lock具有与使用synchronized方法和语句访问的隐式监视锁相同的基本行为和语义,但具有扩展功能。

  例子:

package com.ys.thread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TicketSellRunnable implements Runnable{

	//定义一共有 50 张票,继承机制开启线程,资源是共享的,所以不用加 static
	private int num = 50;
	//创建一个锁对象
	Lock l = new ReentrantLock();
	
	@Override
	public void run() {
		//票分 50 次卖完
		for(int i = 0 ; i < 50 ;i ++){
			//获取锁
			l.lock();
			try {
				if(num > 0){
				//模拟卖一次票所需时间
				Thread.sleep(10);
				System.out.println(Thread.currentThread().getName()+"卖出一张票,剩余"+(--num)+"张");
				}
			} catch (Exception e) {
				e.printStackTrace();
			}finally{
				//释放锁
				l.unlock();
			}
			
			
		}
	}
	private void sell(){
		
	}

}

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

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏用户3030674的专栏

Android性能优化——之防止内存泄露

又是好久没有写博客了,一直都比较忙,最近终于有时间沉淀和整理一下最近学到和解决的一些问题。

521
来自专栏Java3y

监听器第一篇【基本概念、Servlet各个监听器】

什么是监听器 监听器就是一个实现特定接口的普通java程序,这个程序专门用于监听另一个java对象的方法调用或属性改变,当被监听对象发生上述事件后,监听器某个方...

3436
来自专栏xingoo, 一个梦想做发明家的程序员

Java多线程之Runable与Thread

Java多线程是Java开发中的基础内容,但是涉及到高并发就有很深的研究可做了。 最近看了下《Java并发实战》,发先有些地方,虽然可以理解,但是自己在应用...

1949
来自专栏个人随笔

面向对象的七大设计原则

1. 单一职责原则(Single Responsibility Principle) 每一个类应该专注于做一件事情。 每一个职责都是变化的一个轴线,如果一个类有...

3238
来自专栏Golang语言社区

Go Channel 源码剖析

0. 引言 这篇文章介绍一下 Golang channel 的内部实现,包括 channel 的数据结构以及相关操作的代码实现。代码版本 go1.9rc1,部分...

4886
来自专栏陈纪庚

js事件循环

之前有看过一些事件循环的博客,不过一阵子没看就发现自己忘光了,所以决定来自己写一个博客总结下!

782
来自专栏Java Edge

UML 类图1 类

在UML 2.0的13种图形中,类图是使用频率最高的UML图之一。Martin Fowler在其著作《UML Distilled: A Brief Guide ...

501
来自专栏逍遥剑客的游戏开发

J2ME 的优化措施

1787
来自专栏H2Cloud

Boost ASIO proactor 浅析

Boost ASIO proactor 浅析 前情提要: Boost asio 的socket的异步非阻塞模式才有的是proactor模式,当IO操作介绍后回调...

3866
来自专栏二进制文集

思维导图学 《Java编程思想》

没想到会有很多人留言要这个思维导图,一一回复有点累,放在 Github 上,方便大家下载。欢迎交流求`Star`

563

扫码关注云+社区