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 条评论
登录 后参与评论

相关文章

来自专栏屈定‘s Blog

设计模式--模板方法模式的思考

模板方法同样也是一种很实用的方法,目的是提高代码复用,并且统一大体的算法流程,比如一个一台电脑主机,定义好放置CPU,硬盘,内存等空位后,就形成了一个骨架,那么...

1414
来自专栏Golang语言社区

无辜的goroutine

简介: 本文主要是针对一些对于goroutine的“指控”提出我自己的看法,特别是轩脉刃的一篇博客文章《论go语言中goroutine的使用》提出了gorout...

34911
来自专栏Jimoer

Java设计模式学习记录-装饰模式

装饰模式也是一种结构型模式,主要是目的是相对于类与类之间的继承关系来说,使用装饰模式可以降低耦合度。JDK中有不少地方都使用到了装饰模式,例如Java的各种I/...

591
来自专栏精讲JAVA

OutOfMemoryError异常系列之方法区溢出

继续上一篇文章讲解,在上一篇中给大家留下了一个小问题,就是在jdk1.6中返回的是两个false,在jdk1.7中返回的是true false,,上一次代码没有...

1798
来自专栏ml

C++ template的一些高级用法(元编码,可变参数,仿函数,using使用方法,. C++ 智能指针)

1 .  通用函数可变参数模板      对于有些时候,我们无法确切的知道,函数的参数个数时,而又不想过多的使用所谓的函数重载,那么就可以效仿下面的例子: 1...

5144
来自专栏一个会写诗的程序员的博客

给 Java 开发者的 Kotlin 快速上手教程(Kotlin for Java Developers)v0.1

1995年,当年如日中天的Sun公司发布了Java语言,引起了巨大的轰动,与当时主流的C语言和Basic语言比起来,Java语言简单、面向对象、稳定、与平台无关...

573
来自专栏芋道源码1024

我的编码习惯 —— API 接口定义

工作中,少不了要定义各种接口,系统集成要定义接口,前后台掉调用也要定义接口。接口定义一定程度上能反应程序员的编程功底。列举一下工作中我发现大家容易出现的问题:

894
来自专栏java一日一条

编写难于测试的代码的5种方式

有一次,我在一个讲座上听到主持人问听众如何故意编写难于测试的代码。在场的小伙伴都惊呆了,因为没有任何人会故意写这种糟糕的代码。我记得他们甚至给不出一个好的答案。

892
来自专栏北京马哥教育

Python break 语句

? 文 | 豌豆 来源 | 菜鸟教程 ? 豌豆贴心提醒,本文阅读时间3分钟,文末有秘密! Python break语句,就像在C语言中,打破了最小封闭...

3107
来自专栏大神带我来搬砖

锱铢必较:编写政治正确的代码——来聊聊java8的Optional

1768

扫码关注云+社区