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内存泄漏问题的排查、优化与最佳实践

《Java内存泄漏问题的排查、优化与最佳实践》在Java开发中,内存泄漏是一个常见且令人头疼的问题,内存泄漏指的是程序在运行过程中,已经不再使用的对象没有被及时释放,从而导致内存占用不断增加,最终... 目录引言1. 什么是内存泄漏?常见的内存泄漏情况2. 如何排查 Java 中的内存泄漏?2.1 使用 J

JAVA系统中Spring Boot应用程序的配置文件application.yml使用详解

《JAVA系统中SpringBoot应用程序的配置文件application.yml使用详解》:本文主要介绍JAVA系统中SpringBoot应用程序的配置文件application.yml的... 目录文件路径文件内容解释1. Server 配置2. Spring 配置3. Logging 配置4. Ma

Java 字符数组转字符串的常用方法

《Java字符数组转字符串的常用方法》文章总结了在Java中将字符数组转换为字符串的几种常用方法,包括使用String构造函数、String.valueOf()方法、StringBuilder以及A... 目录1. 使用String构造函数1.1 基本转换方法1.2 注意事项2. 使用String.valu

C#使用yield关键字实现提升迭代性能与效率

《C#使用yield关键字实现提升迭代性能与效率》yield关键字在C#中简化了数据迭代的方式,实现了按需生成数据,自动维护迭代状态,本文主要来聊聊如何使用yield关键字实现提升迭代性能与效率,感兴... 目录前言传统迭代和yield迭代方式对比yield延迟加载按需获取数据yield break显式示迭

java脚本使用不同版本jdk的说明介绍

《java脚本使用不同版本jdk的说明介绍》本文介绍了在Java中执行JavaScript脚本的几种方式,包括使用ScriptEngine、Nashorn和GraalVM,ScriptEngine适用... 目录Java脚本使用不同版本jdk的说明1.使用ScriptEngine执行javascript2.

c# checked和unchecked关键字的使用

《c#checked和unchecked关键字的使用》C#中的checked关键字用于启用整数运算的溢出检查,可以捕获并抛出System.OverflowException异常,而unchecked... 目录在 C# 中,checked 关键字用于启用整数运算的溢出检查。默认情况下,C# 的整数运算不会自

Spring MVC如何设置响应

《SpringMVC如何设置响应》本文介绍了如何在Spring框架中设置响应,并通过不同的注解返回静态页面、HTML片段和JSON数据,此外,还讲解了如何设置响应的状态码和Header... 目录1. 返回静态页面1.1 Spring 默认扫描路径1.2 @RestController2. 返回 html2

Python中使用defaultdict和Counter的方法

《Python中使用defaultdict和Counter的方法》本文深入探讨了Python中的两个强大工具——defaultdict和Counter,并详细介绍了它们的工作原理、应用场景以及在实际编... 目录引言defaultdict的深入应用什么是defaultdictdefaultdict的工作原理

使用Python进行文件读写操作的基本方法

《使用Python进行文件读写操作的基本方法》今天的内容来介绍Python中进行文件读写操作的方法,这在学习Python时是必不可少的技术点,希望可以帮助到正在学习python的小伙伴,以下是Pyth... 目录一、文件读取:二、文件写入:三、文件追加:四、文件读写的二进制模式:五、使用 json 模块读写

Spring常见错误之Web嵌套对象校验失效解决办法

《Spring常见错误之Web嵌套对象校验失效解决办法》:本文主要介绍Spring常见错误之Web嵌套对象校验失效解决的相关资料,通过在Phone对象上添加@Valid注解,问题得以解决,需要的朋... 目录问题复现案例解析问题修正总结  问题复现当开发一个学籍管理系统时,我们会提供了一个 API 接口去