当前位置: 首页 > 科技观察

Java多线程同步的五种方法

时间:2023-03-12 12:01:55 科技观察

1.简介前几天面试的是大佬,要扒一扒很多基础知识。闲话不多说,进入正题。2、为什么需要线程同步?因为当我们有多个线程同时访问一个变量或对象时,如果这些线程中既有读写操作,变量值或对象的状态就会混乱,从而导致程序异常。比如一个银行账户同时被两个线程操作,一个取100元,一个存100元。假设账户原本是0元,如果取款线程和存款线程同时发生,会发生什么情况?提现不成功,账户余额为100,提现成功,账户余额为0,请问是哪个?很难说。所以多线程同步就是为了解决这个问题。3.代码Bank.javapackagethread不同步时测试;/***@authorww**/publicclassBank{privateintcount=0;//账户余额//存钱publicvoidaddMoney(intmoney){count+=money;System.out.println(System.currentTimeMillis()+"存入:"+money);}//取钱publicvoidsubMoney(intmoney){if(count-money<0){System.out.println("余额不足");return;}count-=money;System.out.println(+System.currentTimeMillis()+"取出:"+money);}//查询publicvoidlookMoney(){System.out.println("账户余额:"+count);}}SyncThreadTest.javapackagethreadTest;publicclassSyncThreadTest{publicstaticvoidmain(Stringargs[]){finalBankbank=newBank();Threadtadd=newThread(newRunnable(){@Overridepublicvoidrun(){//TODOAuto-generatedmethodstubwhile(true){try{Thread.sleep(1000);}catch(InterruptedExceptione){//TODOAuto-generatedcatchblocke.printStackTrace();}bank.addMoney(100);bank.lookMoney();System.out.println("\n");}}});Threadtsub=newThread(newRunnable(){@Overridepublicvoidrun(){//TODOAuto-generatedmethodstubwhile(true){bank.subMoney(100);bank.lookMoney();System.out.println("\n");try{Thread.sleep(1000);}catch(InterruptedExceptione){//TODOAuto-generatedcatchblocke.printStackTrace();}}}});tsub.start();tadd.start();}}代码很简单,就不解释了,大家看看是怎么实现的?截了一部分,是不是很乱,看不懂账户余额:0余额不足账户余额:1001441790503354入金:100账户余额:1001441790504354入金:100账户余额:1001441790504354出金:100账户balance:1001441790505355存入:100账户余额:1001441790505355取款:100账户余额:100四、使用同步时的代码(1)同步方法:synchronized关键字修饰的方法。由于java的每个对象都有一个内置锁,当方法被this关键字修饰时,内置锁将保护整个方法。调用该方法前需要获取内置锁,否则会处于阻塞状态。修改Bank.java然后看运行结果:InsufficientbalanceAccountbalance:0InsufficientbalanceAccountbalance:01441790837380Depositin:100Accountbalance:1001441790838380Withdrawal:100Accountbalance:01441790838380Depositin:100Accountbalance:1001443178108withdrawal39:100账户余额:0感觉瞬间就明白了。注意:synchronized关键字也可以修饰静态方法。如果此时调用静态方法,整个类就会被锁定。(2)同步代码块是被synchronized关键字修饰的语句块。该关键字修饰的语句块会自动添加内置锁实现同步Bank.java代码如下:packagethreadTest;/***@authorww**/publicclassBank{privateintcount=0;//账户余额//savemoneypublicvoidaddMoney(intmoney){synchronized(this){count+=money;}System.out.println(System.currentTimeMillis()+"deposit:"+money);}//取钱publicvoidsubMoney(intmoney){synchronized(this){if(count-money<0){System.out.println("余额不足");return;}count-=money;}System.out.println(+System.currentTimeMillis()+"takeout:"+money);}//查询publicvoidlookMoney(){System.out.println("账户余额:"+count);}}结果如下:余额不足账户余额:01441791806699入金:100账户balance:1001441791806700Withdrawal:100Accountbalance:01441791807699Deposit:100Accountbalance:100效果和方法1类似注意:同步是一个开销很大的操作,所以synchronizat的内容离子应最小化。通常不需要同步整个方法,只需要使用synchronized代码块同步关键代码即可。(3)使用特殊的域变量(Volatile)实现线程同步a.volatile关键字为域变量访问提供了一种无锁机制b。使用volatile修改域,相当于告诉虚拟机域可能被其他线程c更新。所以每次使用这个域,都需要重新计算,而不是使用寄存器中的值。d.volatile不会提供任何原子操作,也不能用来修改final变量Bank.java代码如下:packagethreadTest;/***@authorww**/publicclassBank{privatevolatileintcount=0;//accountbalance//depositmoneypublicvoidaddMoney(intmoney){count+=money;System.out.println(System.currentTimeMillis()+"存入:"+money);}//取钱publicvoidsubMoney(intmoney){if(count-money<0){System.out.println("余额不足");return;}count-=money;System.out.println(+System.currentTimeMillis()+"Takeout:"+money);}//查询publicvoidlookMoney(){System.out.println("Accountbalance:"+count);}}它是如何工作的?余额不足账户余额:0余额不足账户余额:1001441792010959入金:100账户余额:1001441792011960出金:100账户余额:01441792011961入金:100账户余额:100这是为什么?就是因为volatile不能保证原子操作,所以volatile不能替代synchronized。另外,volatile会阻止编译器对代码进行优化,所以不使用就不要应用。它的原理是每次线程要访问一个被volatile修饰的变量时,都是从内存中读取而不是从缓存中读取,所以每个线程访问的变量的值都是一样的。这确保了同步。(4)使用可重入锁实现线程同步在JavaSE5.0中,增加了java.util.concurrent包来支持同步。ReentrantLock类是实现Lock接口的可重入互斥锁。它具有与使用同步方法和块相同的基本行为和语义,并扩展了它的功能。ReenreantLock类的常用方法有:ReentrantLock():创建一个ReentrantLock实例lock():获取锁unlock():释放锁注意:ReentrantLock()也有一个可以创建公平锁的构造函数,但是由于它可以大大降低程序运行效率,不建议使用Bank.java代码修改如下:packagethreadTest;importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;/***@authorww**/publicclassBank{privateintcount=0;//账户余额//需要声明这个锁privateLocklock=newReentrantLock();//存钱publicvoidaddMoney(intmoney){lock.lock();//锁try{count+=money;System.out.println(System.currentTimeMillis()+"Deposit:"+money);}finally{lock.unlock();//unlock}}//取钱publicvoidsubMoney(intmoney){lock.lock();try{if(count-money<0){System.out.println("余额不足");return;}count-=money;System.out.println(+System.currentTimeMillis()+"取出:"+money);}finally{lock.unlock();}}//查询publicvoidlookMoney(){System.out.println("账户余额:"+count);}}它是如何工作的?Insufficientbalance账户余额:0Insufficientbalance账户余额:01441792891934存款:100账户余额:1001441792892935存款:100账户余额:2001441792892954取款:100账户余额:100效果与前两种方法类似。如果synchronized关键字能够满足用户的需求,就使用synchronized,因为它可以简化代码。如果您需要更高级的功能,请使用ReentrantLock类。这时候要注意及时释放锁,否则会出现死锁,一般在finally代码中释放锁(5)使用局部变量实现线程同步Bank.java代码如下:packagethreadTest;/***@authorww**/publicclassBank{privatestaticThreadLocalcount=newThreadLocal(){@OverrideprotectedIntegerinitialValue(){//TODOAuto-generatedmethodstubereturn0;}};//省钱publicvoidaddMoney(intmoney){count.set(count.get()+money);System.out.println(System.currentTimeMillis()+"Deposit:"+money);}//取钱publicvoidsubMoney(intmoney){if(count.get()-money<0){System.out.println("余额不足");return;}count.set(count.get()-money);System.out.println(+System.currentTimeMillis()+"取出:"+money);}//查询publicvoidlookMoney(){System.out.println("账户余额:"+count.get());}}运行效果:余额不足账户余额:0余额不足账户余额:01441794247939存款:100元ntbalance:100lessthan1441794248940入金:100账户余额:0账户余额:200less放弃?再看ThreadLocal的原理:如果用ThreadLocal来管理变量,每个使用该变量的线程都会得到一份该变量的副本,副本之间相互独立,这样每个线程就可以随意修改自己的变量。现在你明白了,每个线程运行一个副本,也就是说,存钱和取钱是两个账户,知识名是一样的。所以就会出现上面的效果。ThreadLocal和同步机制a.ThreadLocal和同步机制都是为了解决同一个变量在多个线程中的访问冲突问题b.前者采用“以空间换时间”的方式,后者采用“以时间换空间”的方式。好吧。各有优缺点,各有应用场景。手动,去吃饭。