深入研究Synchronized的各种使用方法在Java中,synchronized通常用于标记一个方法或代码块。Java中synchronized标记的代码或方法只能有一个线程同时执行synchronized修饰的方法或代码块。所以synchronized修饰的方法或者代码块不会有数据竞争,也就是说synchronized修饰的代码块是并发安全的。synchronized关键字synchronized关键字通常用在以下四个地方:synchronized修饰的实例方法。同步修饰的静态方法。修改后的实例方法的同步代码块。用于装饰静态方法的同步代码块。在实际情况中,我们需要仔细分析自己的需求,选择合适的synchronized方式,在保证程序正确性的同时,提高程序执行效率。Synchronized修饰的实例方法下面是Synchronized修饰实例方法的代码示例:publicclassSyncDemo{privateintcount;publicsynchronizedvoidadd(){count++;}publicstaticvoidmain(String[]args)throwsInterruptedException{SyncDemosyncDemo=newSyncDemo();Threadt1=newThread(()->{for(inti=0;i<10000;i++){syncDemo.add();}});Threadt2=newThread(()->{for(inti=0;i<10000;i++){syncDemo.add();}});t1.开始();t2.开始();t1.join();//阻塞线程等待线程t1执行完成t2.join();//阻塞线程,等待线程t2执行完毕System.out.println(syncDemo.count);//输出结果为20000}}上面代码中的add方法只有一个简单的count++操作,因为这个方法用synchronized修饰,所以一次只能有一个线程执行add方法,所以上面打印的结果是20000。如果add方法没有用synchronized修饰,那么线程t1和线程t2可以同时执行add方法time,这可能会导致最终的count结果小??于20000,因为count++操作不是原子的。上面分析的比较清楚了,但是我们还要知道synchronized的add方法一次只能被一个线程执行,也就是说对于SyncDemo类的一个对象来说,一次只能有一个线程进入。比如现在有两个SyncDemo对象s1和s2,一次只能有一个线程执行s1的add方法,一次只能有一个线程进入s2的add方法,但是两个不同的线程可以同时执行同时s1和s2的add方法,也就是说s1的add方法和s2的add方法没有任何关系。一个线程进入s1的add方法不会阻止另一个线程进入s2的add方法,也就是说,synchronized是在修改一个非静态方法时,只“锁”一个实例对象,不“锁”其他对象。其实这也很好理解。一个实例对象是一个独立的个体。其他物体不会影响他,他也不会影响其他物体。同步修饰静态方法同步修饰静态方法:publicclassSyncDemo{privatestaticintcount;publicstaticsynchronizedvoidadd(){count++;//注意count也要修改成static否则编译不通过}publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1=newThread(()->{for(inti=0;i<10000;i++){SyncDemo.add();}});Threadt2=newThread(()->{for(inti=0;i<10000;i++){SyncDemo.add();}});t1.开始();t2.开始();t1.join();t2.join();.out.println(SyncDemo.count);//输出结果为20000}}上面代码最终输出结果也是20000,但是和前面的程序不同。这里的add方法是用static修饰的。这种情况下,只有一个线程才能真正进入add代码块,因为如果用static修饰的话,对所有对象是通用的,所以和前面的情况不同。两个不同的线程同时执行add方法。仔细想想,如果两个不同的线程都可以执行add代码块,那么count++的执行就不是原子的了。那为什么代码没有用static修饰呢?因为在没有用static修饰的时候,每个对象的count是不一样的,内存地址也不一样,所以这种情况下count++操作还是原子的!多个方法同步修改多个方法同步修改示例:publicclassAddMinus{publicstaticintans;publicstaticsynchronizedvoidadd(){ans++;}publicstaticsynchronizedvoidminus(){ans--;}publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1=newThread(()->{for(inti=0;i<10000;i++){AddMinus.add();}});Threadt2=newThread(()->{for(inti=0;i<10000;i++){AddMinus.minus();}});t1.开始();t2.开始();t1.join();t2.join();System.out.println(AddMinus.ans);//输出结果为0}}在上面的代码中,我们用synchronized修改了add和minus两个方法。这意味着这两个函数中只有一个可以被一个线程同时执行,也正是因为add和minus函数中只有一个函数可以同时被一个线程执行,这就会导致ans的最终输出结果等于0。对于一个实例对象:publicclassAddMinus{publicintans;publicsynchronizedvoidadd(){ans++;}publicsynchronizedvoidminus(){ans--;}publicstaticvoidmain(String[]args)throwsInterruptedException{AddMinusaddMinus=newAddMinus();Threadt1=newThread(()->{for(inti=0;i<10000;i++){addMinus.add();}});Threadt2=newThread(()->{for(inti=0;i<10000;i++){addMinus.minus();}});t1.开始();t2.开始();t1.join();t2.join();System.out.println(addMinus.ans);}}上面的代码没有使用static关键字,所以我们需要创建一个实例对象才能调用add和minus方法,不过对于AddMinus的实例对象也是一样的,只有一个线程可以执行addorminus方法,所以上面代码的输出也是0。同步修改实例方法代码块同步修改实例方法代码块publicclassCodeBlock{privateintcount;publicvoidadd(){System.out.println("进入add方法");同步(这个){计数++;}}publicvoidminus(){System.out.println("输入减法");同步(这个){计数--;}}publicstaticvoidmain(String[]args)throwsInterruptedException{CodeBlockcodeBlock=newCodeBlock();Threadt1=newThread(()->{for(inti=0;i<10000;i++){codeBlock.add();}});Threadt2=newThread(()->{for(inti=0;i<10000;i++){codeBlock.minus();}});t1.开始();t2.开始();t1.join();t2.join();System.out.println(codeBlock.count);//输出结果为0}}有时候我们不需要用synchronized来修饰代码块,因为并发比较低,一个方法一次只能被一个线程执行。因此,我们可以选择使用synchronized来修饰代码块,这样一次只能有一个线程执行某个代码块,而这个代码块以外的代码仍然可以并行。比如上面代码中的add和minus方法没有用synchronized修饰,所以多个线程可以同时执行这两个方法。在上面的同步代码块中,我们使用this对象作为锁对象。只有获得锁对象的线程才能进入代码块执行,同时只能有一个线程获得锁对象。也就是说add函数和minus函数用synchronized修饰的两个代码块在同一时刻只能有一个代码块的代码被一个线程执行,所以上面的结果也是0。上面提到的锁对象这里是这个,它是CodeBlock类的一个实例对象,因为它锁定了一个实例对象,所以当实例对象不同的时候,它们之间是没有关系的,也就是说,不同的实例是用synchronized修饰的代码块没有关系,它们可以并发。同步修改静态代码块publicclassCodeBlock{privatestaticintcount;publicstaticvoidadd(){System.out.println("进入add方法");同步(CodeBlock.class){count++;}}publicstaticvoidminus(){System.out.println("输入减法");同步(CodeBlock.class){count--;}}publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1=newThread(()->{for(inti=0;i<10000;i++){CodeBlock.add();}});Threadt2=newThread(()->{for(inti=0;i<10000;i++){CodeBlock.minus();}});t1.开始();t2.开始();t1.join();t2.join();System.out.println(CodeBlock.count);}}上面的代码使用synchronized修改了静态代码块。上面代码的锁对象是CodeBlock.class。这时,它锁定的不再是一个对象,而是一个类。这时候并发变小了。当锁对象是CodeBlock的实例对象时,一段代码并发性更高,因为当锁对象是实例对象时,只有实例对象内部不能并发,实例可以并发。但是当锁对象是CodeBlock.class时,实例对象是不能并发的,因为此时的锁对象是一个类。使用什么对象作为锁对象在前面的代码中,我们分别使用了类的实例对象和类对象作为锁对象。其实你可以使用任何对象作为锁对象,但是不推荐使用字符串和基本类型包装类作为锁对象,这是因为字符串对象和基本类型的包装对象存在缓存问题。字符串有一个字符串常量池,整数有一个小整数池。所以,在使用这些对象的时候,最后可能都指向同一个对象,因为它们都指向同一个对象,线程获取锁对象的难度会增加,程序的并发度会降低。例如,在下面的示例代码中,并发性下降,因为锁定对象是同一个对象:System.out.println(Thread.currentThread().getName()+"\t我在同步代码块中");TimeUnit.SECONDS.sleep(5);}}publicstaticvoidmain(String[]args){Testt1=newTest();测试t2=新测试();Threadthread1=newThread(()->{try{t1.testFunction();}catch(InterruptedExceptione){e.printStackTrace();}});线程thread2=newThread(()->{try{t2.testFunction();}catch(InterruptedExceptione){e.printStackTrace();}});thread1.start();thread2.start();在上面的代码中,我们使用两个不同的线程来执行两个不同对象内部的testFunction函数。按理说这两个线程是可以同时执行的,因为执行的是两个不同实例对象的同步。代码块。但是,当上面的代码执行时,一个线程会先进入同步代码块,然后打印输出。等待5秒后,这个线程会退出同步代码块,另一个线程会再次进入同步代码块,也就是说这两个线程不是同时执行的,其中一个线程需要等待另一个线程执行完成后才能执行。这正是因为两个Test对象中使用的“HELLOWORLD”字符串在内存中是同一个对象,而且是存储在字符串常量池中的对象,导致了对锁对象的竞争。下面代码执行的结果是一样的。一个线程需要等待另一个线程完成后才能继续执行。这是因为在Java中,如果整数数据在[-128,127]之间,则使用小整数池。对象也是同一个对象,可以减少频繁的内存申请和回收,对内存更友好。导入java.util.concurrent.TimeUnit;publicclassTest{publicvoidtestFunction()throwsInterruptedException{synchronized(Integer.valueOf(1)){System.out.println(Thread.currentThread().getName()+"\t我在同步代码块中");时间单位。秒。睡觉(5);}}publicstaticvoidmain(String[]args){Testt1=newTest();测试t2=新测试();线程thread1=newThread(()->{try{t1.testFunction();}catch(InterruptedExceptione){e.printStackTrace();}});Threadthread2=newThread(()->{try{t2.testFunction();}catch(InterruptedExceptione){e.printStackTrace();}});thread1.start();thread2.start();}}Synchronizedwithvisibilityandreordervisibility当一个线程进入一个synchronized块时,会刷新该线程可见的所有变量,也就是说,如果其他线程修改了一个变量,而该线程需要在Synchronized代码中使用它块,它会重新刷新变量到内存,以确保这个变量对执行同步代码块的线程可见。当一个线程退出同步代码块时,该线程的工作内存也会被同步到内存中,以保证同步代码块中修改的变量对其他线程可见。重新排序Java编译器和JVM在发现可以使程序执行得更快时,可能会对程序的指令进行重新排序,即通过调换程序指令的执行顺序,可以使程序执行得更快。但是重新排序很可能会给并发程序带来问题。例如,当一个synchronized代码块中的写操作在synchronized同步代码块之外重新排序时,这显然是有问题的。在JVM的实现中,不允许synchronized代码块内部的指令和前后的指令重新排序,但是synchronized内部的指令可以和synchronized内部的指令重新排序。比较出名的是DCL单例模式,他在synchronized代码块中重新排序。如果你对DCL单例模式不是很熟悉,可以阅读本文的DCL单例模式部分。小结本文主要介绍synchronized的各种使用方法。总结如下:Synchronized修改实例方法。在这种情况下,不同的对象可以并发。同步修改实例方法。在这种情况下,不同的对象不能并发,但不同的类可以并发。sychronized修饰了多个方法,这多个方法只能同时执行一个方法,也只有一个线程可以执行。synchronized修改了实例方法代码块,同一时刻只能有一个线程执行该代码块。同步修改静态代码块。一次只能有一个线程执行这个代码块,不同的对象不能并发。应该使用什么对象作为锁对象?建议不要使用字符串和基本类型包装类作为锁对象,因为Java对这些进行了优化,很可能多个对象使用同一个锁对象,会大大降低程序的并发性Spend。当程序进入和离开Synchronized代码块时,会把线程的工作内存刷新到内存中,保证数据的可见性。这与volatile关键字非常相似。同时,Synchronizedcodeblock中的指令会和Synchronizedcodeblock不一致。之间和之后的指令被重新排序,但重新排序可能发生在同步代码块内。更多精彩内容合集可以访问项目:https://github.com/Chang-LeHu...关注公众号:一个没用的研究僧,学习更多计算机知识(Java,Python,计算机系统基础,算法和数据结构)知识。
