java线程同步方法、同步代码段、volatile关键字

2024-08-25 04:32

本文主要是介绍java线程同步方法、同步代码段、volatile关键字,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

我们模拟实现这样一个简单的场景:有一个银行类bank,里面存有用户账户的所有的钱(account)我们会通过线程(MyThread)向里面存钱(saveMoney)和取钱(getMoney)。

我们会依照线程同步方法、同步代码段和读写安全的顺序依次讲解,先看没有做任何处理时的情况:


同步方法:

先看bank类:

class Bank{int account;
public int getAccount() {return account;}//存钱public synchronized void saveMoney(int money,MyThread thread){try {thread.sleep(1000);//每1秒存一次} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}account += money;System.out.println(String.format("%s,save:%d,account:%d,time:%s",thread.threadName,money,getAccount(),System.currentTimeMillis() - thread.startTime));}
//取钱public int getMoney(){return account;}}

线程:相当于我们业务处理类,我们每秒向bank里面存一次钱

class MyThread extends Thread{ Bank bank = null;//当前的银行int[] money = null;//要存的钱String threadName = null;//为当前线程命个名字long startTime = 0;//线程开始run的时间public MyThread(Bank bank,int[] money,String threadName){this.bank = bank;this.money = money;this.threadName = threadName;}
//获取每一份要存的钱public  void getMoneyToSave(){startTime = System.currentTimeMillis();//开始查询的时间戳for(int i:money){//遍历所有要存的钱bank.saveMoney(i, MyThread.this);//存钱}}@Overridepublic void run() {getMoneyToSave();}}


下面我们开始用两个Thread向一个Bank里面存钱:

public static void main(String args[]){
Test test = new Test();test.useThread();}private void useThread(){Bank bank = new Bank();int[] money = {1,2,3,4,5,6,7,8};MyThread thread1 = new MyThread(bank,money,"thread1");MyThread thread2 = new MyThread(bank,money,"thread2");thread1.start();thread2.start();}

我们总共向Bank里面存了72,看我们实现后的结果,有时候最终结果是正确的,需要多次尝试:


实际结果我们用了8057毫秒,总共存了72,但是实际结果只有了67。这个实际结果就是因为两个线程运行了同一代码段而导致出现的问题,我们解释一下从save:4结束到save:5开始过程中发生的情况:

我们希望的流程是:

1.thread1获取当前bank的account为20,

2.thread1中的account为20,向bank里面存5,

3.bank的account变为25,

4.thread2获取当前bank的account为25,

5.thread2中的account为25,向bank里面存5,

6.bank的account变为30,

但是当前有thread1和thread2,实际情况就有可能出现如下情况:

1.thread1获取当前bank的account为20,

2.thread2获取当前bank的account为20,

3.thread1中的account为20,向bank里面存5,

4.bank的account变为25,

5.thread2中的account为20,向bank里面存5,

6.bank的account变为25,

7.thread2打印结果thread2,save:5,account:25,time:5055

8.thread1打印结果thread1,save:5,account:25,time:5055

就是这种情况导致线程同步出现的问题。

我们只需要在saveMoney()方法加上sychronized就可以实现线程的同步

  //存钱public synchronized void saveMoney(int money,MyThread thread){account += money;System.out.println(String.format("%s,save:%d,account:%d,time:%s",thread.threadName,money,getAccount(),System.currentTimeMillis() - thread.startTime));}



这样就保证了我们多线程对同时调用同一个方法或代码段时只有一个线程在使用,这个就是我们所谓的线程同步

我们上面用的同步方法saveMoney()相当于是给这个方法上了一个”锁“,当一个线程进来的时候,自动会把“锁”锁上,完成方法之后会把“锁”给打开。这样就能保证一段时间内只有一个线程在访问。听说使用wait方法能够主动把这个“锁”给打开,我们就来试一试。

我们做如下修改:

把bank里面的saveMoney方法里面的thread.sleep(1000)改为thread.wait(1000),(假设你现在并不清楚sleep和wait方法的区别)

然后我们尝试一下,发先报错了,如下:

我们去jdk文档里面看一下解释:

证明当前线程并不是监视器监听的对象,但是同步方法里面设置的监听的对象是什么呢?我们该如何去在同步方法里面调用wait方法呢?

我们把thread.wait(1000);

改为Bank.this.wait(1000);

我们再尝试一下,发现能够正常运行了。

证明同步方法实际上是把当前类对象的实例当作监听对象来实行监听。

顺便讲一下,wait和sleep的区别,实际上并没有必要特意把这 两个方法放一起比较,

因为wait方法属于Object类,所有的Object都有wait方法,它的作用就是把某个对象

作为线程的“锁”,当进入synchronized(object)的时候把锁锁上,别人不能进来(要保证是同一个Object),出去的时候把锁打开,其他人才能进来。wait方法就可以主动控制锁什么时候打开,而并不是一定要退出synchronized代码段的时候。

而sleep方法属于Thread类,它是线程的一个方法,作用是使当前线程睡眠,并不涉及任何锁的操作。

同步代码段:

下面我们继续上面的内容,讲一下同步代码段,我们用同步代码段的方式来实现一下同步方法:

我们把方法里面的synchronized声明去掉,改成如下方式:

  //存钱  public  void saveMoney(int money,MyThread thread){

	synchronized(Bank.this){
    try { Bank.this.wait(1000);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}
  account += money;System.out.println(String.format("%s,save:%d,account:%d,time:%s",thread.threadName,money,getAccount(),System.currentTimeMillis() - thread.startTime));
}}

这样写的结果是和同步方法的结果是一样的。

这个情况下我们再考虑一个问题,例如我再使用一个一模一样的的同步方法saveMoney2(),让这个Bank同时调用saveMoney和saveMoney2方法,会不会出现什么问题呢?

结果是不会的。因为当两个方法都是同步方法时,它的同步锁是当前类对象的实例,当前类的对象的实例只有一个,也就是说在多线程的情况下,当一个类里面的多个方法被声明为同步方法时,同一时刻,该类的实例只会访问其中的一个。这里其实同步方法是同步代码段的一个特例,普通正常的情况下我们就不尝试了,只要synchronized(object)当中的object没有被占用,里面的代码就是可以被访问的。再说一个特例,synchronized(Bank.class),这个同步比较特殊一点,这个其实是加了一个类锁,也就是说一个时刻只能有这个类的一个实例去访问。具体思路如下:

我们有两个bank共享一个用户的account,两个bank都可以向用户的account里面存钱

具体代码修改如下:

添加一个Account类,用来存放用户账户信息,里面有个increaseMoney方法用来价钱向里面存钱:

class Account{int money;String userName;public Account(int money, String userName) {this.money = money;this.userName = userName;}public synchronized void increaseMoney(int money,MyThread thread){this.money += money;System.out.println(String.format("%s,save:%d,%s:%d,time:%s",thread.threadName,money,thread.bank.getBankName(),thread.bank.account.getMoney(),System.currentTimeMillis() - thread.startTime));System.out.println(thread1.bank.getBankName() + ":" + thread1.bank.account.getMoney());System.out.println(thread2.bank.getBankName() + ":" + thread2.bank.account.getMoney());System.out.println("------------------");}public int getMoney() {return money;}public String getUserName() {return userName;}public void setUserName(String userName) {this.userName = userName;}}

bank里面的存钱方法修改如下,同步锁改成Bank.class,里面的int account该成Account类,并调用increaseMoney方法向里面存钱:

		//存钱public  void saveMoney(int money,MyThread thread){synchronized(Bank.class){try {	Bank.class.wait(1000);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}account.increaseMoney(money,thread);}}

初始化的方法改成这样,定义两个银行bank和anotherBank,两个银行共享一个账户account,bank用thread1向里面存钱,anotherBank用thread2向里面存钱:

	private void useThread(){Account account = new Account(100,"myUser");Bank bank = new Bank(account,"bank");Bank anotherBank = new Bank(account,"anotherBank");int[] money = {1,2,3,4,5,6,7,8};thread1 = new MyThread(bank,money,"thread1");thread2 = new MyThread(anotherBank,money,"thread2");thread1.start();thread2.start();}


下面是部分的运行结果:


我们看到规律thread1运行之后一定会是thread2运行,thread2之后一定是thread1。2个银行是轮流存钱的。

而如果我们改成synchronized(Bank.this)之后,结果如下:

虽然大部分情况下顺序是对的,但是还是会有出错的时候:



volatile关键字

这里我们虽然实现了线程的同步,但是,在多线程的情况下,实现了线程的同步,结果一定就会是对的吗?结果肯定是不对的。我们这里没有出现任何错误的原因

是我们存钱这个操作几乎不消耗任何的操作时间,我们实现的思路如下:

我们用一个while循环来模拟一个耗时操作,用一个Boolean变量来判断余额是否大于等于200,当anotherBank里面的余额大于等于200之后才开始加钱

先在全局变量里面定义一个Boolean对象:

public  Boolean hasEnoughMoney = false;


在MyThread里面getMoneyToSave多加一个while循环:

			//获取每一份要存的钱public  void getMoneyToSave(){startTime = System.currentTimeMillis();//开始查询的时间戳while(threadName.equals("thread1")){if(hasEnoughMoney){break;}	 }for(int i:money){//遍历所有要存的钱bank.saveMoney(i, MyThread.this);//存钱
//					bank.saveMoney2(i, MyThread.this);//存钱2}}
在Acount里面多写一个当money大于等于200时,使hasEnoughMoney = true:
		public synchronized void increaseMoney(int money,MyThread thread){this.money += money;System.out.println(String.format("%s,save:%d,%s:%d,time:%s",thread.threadName,money,thread.bank.getBankName(),thread.bank.account.getMoney(),System.currentTimeMillis() - thread.startTime));
//			System.out.println(thread1.bank.getBankName() + ":" + thread1.bank.account.getMoney());
//			System.out.println(thread2.bank.getBankName() + ":" + thread2.bank.account.getMoney());System.out.println("------------------");if(this.money >=200){hasEnoughMoney = true;System.out.println(thread.threadName + ":" + hasEnoughMoney);}//}


当线程是thread1并且余额为200及以上时,才开始加钱。我们试一下效果,最后几行结果是这样的:

这边当hasEnoughMoney被设置为true之后,while循环还在继续进行,也就是说:

			 while(threadName.equals("thread1")){if(hasEnoughMoney){break;}	 }

while循环里面的hasEnoughMoney还是false的,并没有跳出循环。

但是我们将hasEnoughMoney设置为volatile之后,我们再测试结果:

public	volatile  Boolean hasEnoughMoney = false;


我们发现thread1居然开始运行了,并且bank里面也已经加到300了。

这个就是volatile的作用,让volatile定义的变量随时都可以获取到内存当中的最新值,但是定义为volatile的变量是非常消耗性能的,

定义为volatile的变量最好是遍历操作远多于修改操作,并且,在对象内部定义的volatile的变量在外部想获取时,最好是返回该变量的副本而不知将该变量直接返回。

下面提供一些在Java API中可以参考的内容:

关于wait、notify操作:java.lang.Object.class

关于sleep、yiled、join操作:java.lang.Thread.class

下面都是实现了RandomAccess的线性存储结构的:

关于非同步存储对象:java.util.ArrayList.class

关于同步存储对象(synchronized):java.util.Vector.class java.util.Stack.class

关于线程安全存储对象(synchronized&volatile):java.util.concurrent.CopyOnWriteArrayList.class

现在暂时我只看了这么多,都是自己重新写过的例子,小白入门不久,如果有不当的地方请各位大神海涵。





这篇关于java线程同步方法、同步代码段、volatile关键字的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1104549

相关文章

Java实现字节字符转bcd编码

《Java实现字节字符转bcd编码》BCD是一种将十进制数字编码为二进制的表示方式,常用于数字显示和存储,本文将介绍如何在Java中实现字节字符转BCD码的过程,需要的小伙伴可以了解下... 目录前言BCD码是什么Java实现字节转bcd编码方法补充总结前言BCD码(Binary-Coded Decima

python获取指定名字的程序的文件路径的两种方法

《python获取指定名字的程序的文件路径的两种方法》本文主要介绍了python获取指定名字的程序的文件路径的两种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要... 最近在做项目,需要用到给定一个程序名字就可以自动获取到这个程序在Windows系统下的绝对路径,以下

SpringBoot全局域名替换的实现

《SpringBoot全局域名替换的实现》本文主要介绍了SpringBoot全局域名替换的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录 项目结构⚙️ 配置文件application.yml️ 配置类AppProperties.Ja

Java使用Javassist动态生成HelloWorld类

《Java使用Javassist动态生成HelloWorld类》Javassist是一个非常强大的字节码操作和定义库,它允许开发者在运行时创建新的类或者修改现有的类,本文将简单介绍如何使用Javass... 目录1. Javassist简介2. 环境准备3. 动态生成HelloWorld类3.1 创建CtC

JavaScript中的高级调试方法全攻略指南

《JavaScript中的高级调试方法全攻略指南》什么是高级JavaScript调试技巧,它比console.log有何优势,如何使用断点调试定位问题,通过本文,我们将深入解答这些问题,带您从理论到实... 目录观点与案例结合观点1观点2观点3观点4观点5高级调试技巧详解实战案例断点调试:定位变量错误性能分

Python中 try / except / else / finally 异常处理方法详解

《Python中try/except/else/finally异常处理方法详解》:本文主要介绍Python中try/except/else/finally异常处理方法的相关资料,涵... 目录1. 基本结构2. 各部分的作用tryexceptelsefinally3. 执行流程总结4. 常见用法(1)多个e

Java实现将HTML文件与字符串转换为图片

《Java实现将HTML文件与字符串转换为图片》在Java开发中,我们经常会遇到将HTML内容转换为图片的需求,本文小编就来和大家详细讲讲如何使用FreeSpire.DocforJava库来实现这一功... 目录前言核心实现:html 转图片完整代码场景 1:转换本地 HTML 文件为图片场景 2:转换 H

Java使用jar命令配置服务器端口的完整指南

《Java使用jar命令配置服务器端口的完整指南》本文将详细介绍如何使用java-jar命令启动应用,并重点讲解如何配置服务器端口,同时提供一个实用的Web工具来简化这一过程,希望对大家有所帮助... 目录1. Java Jar文件简介1.1 什么是Jar文件1.2 创建可执行Jar文件2. 使用java

SpringBoot实现不同接口指定上传文件大小的具体步骤

《SpringBoot实现不同接口指定上传文件大小的具体步骤》:本文主要介绍在SpringBoot中通过自定义注解、AOP拦截和配置文件实现不同接口上传文件大小限制的方法,强调需设置全局阈值远大于... 目录一  springboot实现不同接口指定文件大小1.1 思路说明1.2 工程启动说明二 具体实施2

Java实现在Word文档中添加文本水印和图片水印的操作指南

《Java实现在Word文档中添加文本水印和图片水印的操作指南》在当今数字时代,文档的自动化处理与安全防护变得尤为重要,无论是为了保护版权、推广品牌,还是为了在文档中加入特定的标识,为Word文档添加... 目录引言Spire.Doc for Java:高效Word文档处理的利器代码实战:使用Java为Wo