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中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

C#中读取XML文件的四种常用方法

《C#中读取XML文件的四种常用方法》Xml是Internet环境中跨平台的,依赖于内容的技术,是当前处理结构化文档信息的有力工具,下面我们就来看看C#中读取XML文件的方法都有哪些吧... 目录XML简介格式C#读取XML文件方法使用XmlDocument使用XmlTextReader/XmlTextWr

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

Spring AI集成DeepSeek的详细步骤

《SpringAI集成DeepSeek的详细步骤》DeepSeek作为一款卓越的国产AI模型,越来越多的公司考虑在自己的应用中集成,对于Java应用来说,我们可以借助SpringAI集成DeepSe... 目录DeepSeek 介绍Spring AI 是什么?1、环境准备2、构建项目2.1、pom依赖2.2

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

Spring Cloud LoadBalancer 负载均衡详解

《SpringCloudLoadBalancer负载均衡详解》本文介绍了如何在SpringCloud中使用SpringCloudLoadBalancer实现客户端负载均衡,并详细讲解了轮询策略和... 目录1. 在 idea 上运行多个服务2. 问题引入3. 负载均衡4. Spring Cloud Load

Springboot中分析SQL性能的两种方式详解

《Springboot中分析SQL性能的两种方式详解》文章介绍了SQL性能分析的两种方式:MyBatis-Plus性能分析插件和p6spy框架,MyBatis-Plus插件配置简单,适用于开发和测试环... 目录SQL性能分析的两种方式:功能介绍实现方式:实现步骤:SQL性能分析的两种方式:功能介绍记录

在 Spring Boot 中使用 @Autowired和 @Bean注解的示例详解

《在SpringBoot中使用@Autowired和@Bean注解的示例详解》本文通过一个示例演示了如何在SpringBoot中使用@Autowired和@Bean注解进行依赖注入和Bean... 目录在 Spring Boot 中使用 @Autowired 和 @Bean 注解示例背景1. 定义 Stud