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

相关文章

Linux换行符的使用方法详解

《Linux换行符的使用方法详解》本文介绍了Linux中常用的换行符LF及其在文件中的表示,展示了如何使用sed命令替换换行符,并列举了与换行符处理相关的Linux命令,通过代码讲解的非常详细,需要的... 目录简介检测文件中的换行符使用 cat -A 查看换行符使用 od -c 检查字符换行符格式转换将

Java编译生成多个.class文件的原理和作用

《Java编译生成多个.class文件的原理和作用》作为一名经验丰富的开发者,在Java项目中执行编译后,可能会发现一个.java源文件有时会产生多个.class文件,从技术实现层面详细剖析这一现象... 目录一、内部类机制与.class文件生成成员内部类(常规内部类)局部内部类(方法内部类)匿名内部类二、

SpringBoot实现数据库读写分离的3种方法小结

《SpringBoot实现数据库读写分离的3种方法小结》为了提高系统的读写性能和可用性,读写分离是一种经典的数据库架构模式,在SpringBoot应用中,有多种方式可以实现数据库读写分离,本文将介绍三... 目录一、数据库读写分离概述二、方案一:基于AbstractRoutingDataSource实现动态

Springboot @Autowired和@Resource的区别解析

《Springboot@Autowired和@Resource的区别解析》@Resource是JDK提供的注解,只是Spring在实现上提供了这个注解的功能支持,本文给大家介绍Springboot@... 目录【一】定义【1】@Autowired【2】@Resource【二】区别【1】包含的属性不同【2】@

springboot循环依赖问题案例代码及解决办法

《springboot循环依赖问题案例代码及解决办法》在SpringBoot中,如果两个或多个Bean之间存在循环依赖(即BeanA依赖BeanB,而BeanB又依赖BeanA),会导致Spring的... 目录1. 什么是循环依赖?2. 循环依赖的场景案例3. 解决循环依赖的常见方法方法 1:使用 @La

Java枚举类实现Key-Value映射的多种实现方式

《Java枚举类实现Key-Value映射的多种实现方式》在Java开发中,枚举(Enum)是一种特殊的类,本文将详细介绍Java枚举类实现key-value映射的多种方式,有需要的小伙伴可以根据需要... 目录前言一、基础实现方式1.1 为枚举添加属性和构造方法二、http://www.cppcns.co

Elasticsearch 在 Java 中的使用教程

《Elasticsearch在Java中的使用教程》Elasticsearch是一个分布式搜索和分析引擎,基于ApacheLucene构建,能够实现实时数据的存储、搜索、和分析,它广泛应用于全文... 目录1. Elasticsearch 简介2. 环境准备2.1 安装 Elasticsearch2.2 J

Java中的String.valueOf()和toString()方法区别小结

《Java中的String.valueOf()和toString()方法区别小结》字符串操作是开发者日常编程任务中不可或缺的一部分,转换为字符串是一种常见需求,其中最常见的就是String.value... 目录String.valueOf()方法方法定义方法实现使用示例使用场景toString()方法方法

Java中List的contains()方法的使用小结

《Java中List的contains()方法的使用小结》List的contains()方法用于检查列表中是否包含指定的元素,借助equals()方法进行判断,下面就来介绍Java中List的c... 目录详细展开1. 方法签名2. 工作原理3. 使用示例4. 注意事项总结结论:List 的 contain

Java实现文件图片的预览和下载功能

《Java实现文件图片的预览和下载功能》这篇文章主要为大家详细介绍了如何使用Java实现文件图片的预览和下载功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... Java实现文件(图片)的预览和下载 @ApiOperation("访问文件") @GetMapping("