当前位置: 首页 > 后端技术 > Java

【多线程】synchronized的基本原理

时间:2023-04-01 21:11:50 Java

现在的节奏是要每周更新一次,不,绝对不是。这篇文章基本上也是一篇关于同步的糟糕文章。希望自己写的比别人简单易懂,哈哈哈。其实关于多线程的知识点很多,不管是哪种语言都是一样的,所以以后会穿插其他的知识点来讲解,不然太枯燥了。Threadisnotsafe《Java并发编程实战》中有这么一句话当多个线程访问一个类时,如果不需要在运行环境中考虑这些线程的调度和交替,也不需要额外的同步和调用者代码不用做其他事情协调,这个类的行为还是正确的,那么这个类就是线程安全的。通俗地说,如果你想让代码是线程安全的,其实就是保证在访问状态的时候不会出错。对象的状态一般是指数据。但大部分数据是共享和可变的。其实在我们日常开发中,遇到最多的线程不安全问题更多的是对某个变量的修改是否能达到预期,所以下面的例子更多的侧重于简单的保证变量的修改是安全的。先看著名的i++不安全示例包concurrent.safe;importjava.util.concurrent.CountDownLatch;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;publicclassSynchronizedDemo{//常用方法,代码块,静态方法publicstaticvoidmain(String[]args)throwsInterruptedException{intthreadSize=1000;ThreadAddExample示例=newThreadAddExample();//确保主线程在每个子线程之后结束finalCountDownLatchcountDownLatch=newCountDownLatch(threadSize);//以不推荐的方式启动一个线程池ExecutorServiceexecutorService=Executors.newCachedThreadPool();for(inti=0;i{example.add();countDownLatch.countDown();});}countDownLatch.await();//关闭线程池,否则会一直阻塞executorService.shutdown();System.out.println(example.get());}}类ThreadAddExample{私人静态intcnt=0;publicvoidadd(){cnt++;}publicintget(){返回cnt;}}整个过程就是创建一个线程池,然后执行1000个任务,每个任务对cnt进行++操作,最后读取cnt但不保护它,所以必须有两个线程同时修改cnt变量时间,导致一个线程的修改无效。在这个例子中,cnt是不可能等于1000的。看一下运行结果,可以看到结果和预想的一样,有时差的多,有时差的少,主要看CPU。用法针对以上情况,需要采用一定的同步措施来保证执行结果的正确性。本文主要使用synchronized关键字代码块。在上面的类中添加一个新方法publicvoidaddWithBlockSync1(){synchronized(ThreadAddExample.class){cnt++;}}使用ThreadAddExample类作为锁,这样每个线程都必须能够获取到这个类才能修改cnt资源。最终结果如下。可以看到无论运行多少次,结果都是1000,一个或多个线程同时修改cnt。我们来看另一个同样使用synchronized包围代码块的例子。publicvoidaddWithBlockSync2(){synchronized(newThreadAddExample()){cnt++;}}注意,这里使用的锁是线程自己new的一个实例。不安全?第一种情况就像一个只有一扇门的房间,每个线程只能用同一个钥匙进入房间,所以线程是安全的。第二种情况是线程自己创建了一个新的实例,相当于为线程创建了多扇门,线程只需要打开自己的门就可以进入房间。如果锁定对象不是newThreadAddExample()而是thispublicvoidaddWithBlockSync3(){synchronized(this){cnt++;}}测试结果是可以保证线程安全,因为锁是this,整个过程和上面不同的是我们只new了一个对象。还有一种常用的方法是直接在方法体中加上synchronized关键字publicsynchronizedvoidaddWithMethodSync(){cnt++;}可以发现静态方法也可以实现线程安全。除了上述方法外,还有一个常用的方法,在静态方法中使用关键字publicsynchronizedstaticvoidaddWithStaticSync(){cnt++;}。结果如下:原理是使用javap-verbosexxx.class查看字节码文件的同步代码块,可以看到同步代码块只是一个new对象。Lock,或者说使用这种单锁,其实主要是通过monitorenter和monitorexit来保证线程的安全。方法体可以看到,方法体的flags字段中有一个ACC_SYNCHRONIZED标志。两种方法的原理大概是这样的。接下来,我们将重点关注监视器。对象头简要描述了对象头的组成,但这种组成似乎没有任何客观的外在表现。在这里,我只写了大多数书籍和博客都同意的结构。其他的我暂时不关心。后面会写虚拟机。相关文章会详细介绍,只要知道对象是由对象头、实例数据和对齐填充组成,并且对象头中有一个指向监视器的指针,这个监视器就可以看成是重量级锁。monitor的数据结构在jvm的源码中。具体是指hotspot的源码。重要的变量注释也写在后面。因为每个对象都有一个对象头,每个对象头都有一个指向监视器的指针,所以每个对象都可以作为锁;因为monitor中有一个count字段,所以反编译可以看到monitorenter和monitorexit,两次使用monitorexit找网上的博客就是为了保证异常情况下可以释放锁。