java多线程编程核心技术——第二章

时间:2023-03-08 16:48:54

第一节synchronized同步方法目录


1.1方法内的变量为线程安全的

说到线程安全,就要提到非线程安全问题了:

  “非线程安全问题”:在多个线程同时对同一个对象中的实例变量进行访问时发生。产生的结果就是脏读(读到被修改过的数据)。

  “线程安全”获得的实例变量是经过同步处理的,不会出现脏读的情况。

  “非线程安全”的问题存在于实例变量中,如果将实例变量私有,则不存在“非线程安全”问题,所得就是线程安全了。

  注:在方法内部声明变量,是不存在“非线程安全”问题的。原因是:方法内部的变量是私有的特性。

  


1.2实例变量非线程安全

  如果多个线程同时访问一个对象的实例变量,则可能会出现“非线程安全问题”。

  有多个实例变量可能会出现交叉的情况,如果仅有一个实例变量时可能会出现覆盖的情况。

  若想解决非线程安全问题,需要在对实例变量操作的方法上加synchronized关键字做修饰。


1.3多个对象多个锁

  若多个线程,调用多个实例中的同步方法,会发现结果是不同步的。

  原因:关键字synchronized取得的锁时对象锁,而不是一段代码或者一个方法(函数)的锁。

    所以要想实现同步效果,必须是多个线程访问同一个实例中的变量才能够实现。若出现多个线程访问多个实例中的同步方法,自然会发现结果是异步的。


1.4synchronized与锁对象

  因为synchronized是给对象上锁的,所以当一个线程获得对象锁的时候(此时该线程必定进入对应的同步方法或者同步代码块),其他线程若想获得同步方法的执行权需要排队等待。

  但是若其他线程调用非synchronized修饰的方法时,则无需排队,可以直接调用。

  简述:

  1. A线程先持有object对象的Lock锁,B线程可以以异步的方式调用object对象中的非synchronized类型的方法。
  2. A线程先持有object对象的Lock锁,B线程如果在这时调用object对象中的synchronized类型的方法时,需要等待,也就是同步。

1.5脏读

  脏读:在读取实例变量时,此值已经被其他线程修改过了。

  通常解决方法就是,在实例变量的赋值和获取方法上都使用synchronized修饰,这样就可以实现同步,避免脏读的情况出现。


1.6synchronized锁重入

  关键字synchronized具有锁重入的能力,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。这也证明了在一个synchronized方法/块内部调用本地其他synchronized方法/块时,是永远可以得到锁的。

  可重入锁概念:自己可以再次获得自己的内部锁,比如有1条线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获得这个对象的锁的时候还是可以获得的,如果不可锁重入,会导致进入死循环。

  可重入锁也可以出现在继承中:当存在父子类继承时,子类完全可以使用锁重入调用父类的同步方法。


1.7出现异常锁自动释放

  当一个线程执行的代码出现异常时,其所持有的锁会自动释放,锁被释放后,其他排队等候的线程就会获得当前的对象锁。


1.8同步不具有继承性

  同步是不可以被继承的。

  子类继承了父类,并且重写了父类中的同步方法,若有多线程同时执行子类中的重写方法时,会发现是非同步的。这时如果想要实现同步,必须在子类的方法前加上synchronized。


第二节synchronized同步语句块目录


2.1synchronized方法的弊端

  synchronized方法的弊端:代码执行时间过长,占用锁致使其他线程必须排队等待。

  可以通过使用同步代码块来解决这个问题。

  注:就是通过一个例子表现了同步方法会花费很长的时间,不过项目里面具体使用什么应该根据实际需要来考虑。


2.2synchronized同步代码块的使用

  两个线程同时访问同一个对象的synchronized(this)同步代码块时,在代码运行期间只能有一个线程执行该段代码块,另一个线程必须等待当前线程完成执行才能够执行该段代码。

  使用上面这句话完成的例子带来的效率也没有提高,原因在于同步代码块中还是包含了所有的代码,包括线程安全和非线程安全的代码。


2.3用同步代码块解决同步方法的弊端

  当一个线程访问object中的synchronized同步代码块时,其他线程可以访问该object对象中非synchronized(this)同步代码块的内容。

  根据上面那句话对同步代码块的优化方案就是,将线程安全且耗时的代码放在同步代码块外面,将非线程安全的代码放在同步代码快中,利用多线程的“同时性”(并发性)实现缩短时间提高效率,且保持当前对象持有锁,避免非线程安全的问题出现。


2.4一半异步,一半同步  

  注:不在synchronized代码块中的代码是异步的,在synchronized中的代码是同步的。

  书上通过例子为我们表现了什么叫一半异步一半同步(例子中被执行代码中有一个for循环输出是异步的,还有一个for循环输出是在synchronized中是同步)。


2.5synchronized代码块间的同步性

  若一个线程访问了object的一个synchronized(this)同步代码块时,其他线程对同一个object中所有的其他synchronized(this)同步代码块的访问将被阻塞。

  这个现象表明了:synchronized使用的是一个对象监视器(此处用的是this)。

  随即产生了疑惑:synchronized(this)/synchronized(非this对象)与同步方法是否同步,于是做了测试。

  synchronized(this)与当前对象的同步方法是同步的,两者公用一把对象锁。

  synchronized(非this对象)与当前对象的同步方法是异步的,两者不共用一把对象锁。


2.6验证synchronized(this)代码块是锁定当前对象的

  synchronized修饰代码块时与synchronized修饰方法时是一样的都是锁定当前对象(对当前对象加锁)。

  同我们上一小节的结论一样,当前对象的同步方法与synchronized(this)公用一把对象锁(当前对象),执行的过程是同步的。


2.7将任意对象作为对象监视器

  多个线程调用同一个对象的synchronized同步方法或者synchronized(this)同步代码块时,调用的方法时按顺序执行的,是同步的是阻塞的。

  说明同一个对象的synchronized同步方法与synchronized(this)代码块的对象锁(当前对象锁)有两个用途。

  • 若已经有线程获得对象锁,对其他synchronized同步方法或synchronized(this)同步代码块调用起阻塞作用(即该线程的代码执行是按照同步代码的调用顺序完成的)。
  • 同一时间只有一个线程可以执行synchronized修饰的隔离区内的代码。

  注:第一条是针对一个线程来说的,第二条是针对多条线程来说的。

  synchronized的对象监视器除了可以使用this外,还可以使用任意对象作为“对象监视器”来实现同步功能,这个“任意对象”大多数是实例变量或方法参数(实际工作中个人认为单例应用的应该比较多)。使用格式:synchronized(非this对象)。

  synchronized(非this对象X)格式的作用只有1种:

    在多个线程持有“对象监视器”为同一个对象的前提下,一时间只有一个线程可以执行synchronized(非this对象X)同步代码块中的代码。

  锁非this对象的优点:如果一个类中有很多synchronized方法,虽然能够保证同步,但是一定会收到阻塞,会影响程序的效率,如果使用synchronized(非this对象),则synchronized(非this对象)与synchronized(this)与synchronized方法是异步的,不必争抢this锁,可以根据项目的构建实现运行效率的优化。

  注:同步代码块放在非同步synchronized方法中进行声明,并不能保证调用方法的线程的执行权同步/顺序性。线程的调用是无序的,但是同步代码块执行的顺序是同步的,这样容易出现脏读(i++与println()出现脏读就是这个原因)。


2.8细化论证3个结论  

三个结论:

  •   当多个线程同时执行synchronized(x){}同步代码块时呈同步效果
  •   当其他线程执行x对象中的synchronized同步方法时呈同步效果
  •   当其他线程执行x对象中的synchronized(this)方法时也呈现同步效果

  注:可以这样理解,因为synchronized(x)中,对象监视器是x,即是将锁加载了x上,那么对于x对象来说,他本身已经被上锁,则他的synchronized方法与synchronized(this)自然与synchronized(x){}呈同步效果。


2.9静态同步synchronized方法与synchronized(class)代码块

  关键字synchronized还可以应用在static静态方法上,如果这样写,那是对当前的*.java文件对应的Class类进行加锁。

  注:synchronized关键字修饰static静态方法是给Class类加锁,而synchronized关键字加到非static静态方法上是给对象上锁。

  Class锁可以对类的所以对象实例起作用,使其代码的执行是同步的(但是注意如果有代码块锁的是Class的实例对象,则是不同的锁,是对象锁,此时的代码与Class锁的代码执行是异步的)。

  synchronized(class)代码块的作用与synchronized static方法的作用是一样的。

  这一节其实比较难理解,但是如果你能够理解锁的对象之间的区别或许就能更好的理解为什么同步为什么异步。


2.10数据类型String的常量池特性

  由于String具有常量池缓存的功能,将synchronized(String)同步代码块与String联合起来使用时,要注意常量池带来的影响。

  若有String A = String B = “相同的字符串”; 则synchronized(A)与synchronized(B)就具有一把相同的锁,就有可能出现阻塞的现象。


2.11同步synchronized方法无限等待与解决

  同步方法很容易造成死循环。

  比如在synchronized方法中出现无限循环时,这是就陷入了阻塞,其他想要获取该对象同步锁的方法都无法获取该对象的同步锁了。其实是为下一节死锁做个铺垫。


2.12多线程的死锁

  死锁是一个经典的多线程问题,因为不同的线程都在等待根本不可能释放的锁(),从而导致所有的任务都无法进行。在多线程技术中死锁是必须避免的,因为这会造成线程的“假死”。

  死锁是程序设计的bug,在设计程序的过程中要避免出现互相持有对方锁的情况。

  死锁出现的核心:存在互相等待对方释放锁就有可能出现死锁。


2.13内置类与静态内置类

  就是写了内部类跟静态内部类怎么创建。


2.14内部类与同步:实验1

当内部类中有两个不同方法,且使用的是不同的锁时,这两个方法的运行结果是异步的。


2.15内部类与同步:实验2

实际上内部类中的同步效果也符合2.8的三个结论。


2.16锁对象的改变

  在将任何数据类型作为同步锁时,需要注意的是,是否有多个线程同时持有锁对象,如果同时持有相同的锁对象,则这些线程间就是同步的;如果持有不同的锁对象,这些线程之间就是异步的。

  第二小节小总结:实际上第二章后半部分一直在针对不用的使用情况去印证前面已经证实过的结论(也就是说没有太多卵用,核心的东西都在前八个知识点)。


第三节volatile关键字

  注:关键字volatile的主要作用是使变量在多个线程间可见


3.1关键字volatile与死循环

  什么都没说。写了个死循环的案例,可能是这本书是一本很基础的书吧。。

  有时候看得我挺无语的


3.2解决同步死循环


3.3解决异步死循环

  在线程中若有变量private Boolean isRunning = true;该变量存在于公共堆栈及线程的私有堆栈中。此时将JVM设置为-server时,为了线程的运行的效率,线程一直在私有堆栈中取得isRunning的值,若修改isRunning,更新的会是公共堆栈中的isRunning的值,所以会导致出现异常。

  问题的实质是:私有堆栈的值和公共堆栈中不同步造成的,使用volatile关键字可以解决,使线程访问isRunning时强制从公共堆栈中取值。

  java多线程编程核心技术——第二章java多线程编程核心技术——第二章

  volatile关键字增加了实例变量在多个线程之间的可见性,但volatile关键字很致命的一点在于其不支持原子性。

  synchronized与volatile的比较:

  •   volatile是线程同步的轻量级实现,性能比synchronized要好,并且volatile只能修饰变量,而synchronized可以修饰方法与代码块。
  •   多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。
  •   volatile能保证数据的可见性,但不能保证数据的原子性;而synchronized可以保证原子性,也能间接保证可见性,其可以将私有内存与公共内存的东西进行同步。
  •   volatile解决的是多个线程之间的可见性,synchronized解决的是多个线程之间访问资源的同步性。

  线程安全包括原子性与可见性两个方面,java的同步机制都是围绕着这两个方面确保线程安全的。


3.4volatile的非原子特性  

  关键字volatile虽然增加了实例变量在多个线程之间的可见性,但它不具备同步性,那么就不具备原子性。

  关键字volatile主要使用的场合是在多个线程中可以感知实例变量被更改了,并且可以获得最新的值使用,也就是说多线程读取共享变量时可以获得最新值使用。

java多线程编程核心技术——第二章

  对于JVM而言,volatile修饰的变量只能保证其从主内存read到线程内存是最新的,但是use与assign是多次出现的,且是非原子性的。可能出现主内存修改后,由于线程内存已经加载导致值不变进而出现脏读。

  所以对于多个线程访问同一个实例变量还是需要加锁同步。


3.5使用原子类进行i++操作  

  除了synchronized可以实现在i++操作时的同步外,AtomicInteger原子类也可以实现。

  原子类型是不可分割的整体没有其他线程可以中断或检查正在原子操作的变量。

java多线程编程核心技术——第二章java多线程编程核心技术——第二章

  使用原子类的incrementAndGet()方法即可实现同步


3.6原子类也并不完全安全

  原因在于:虽然每一次原子操作是不可打断的,但是如果有多次原子操作,且这些原子操作之间的调用顺序不是同步的,那么出来的结果也可能不是同步的。

  实质:虽然最小操作是原子的,但是现在异步出现在上一级中,也就是说原子操作的执行顺序出现了异步的情况,最终也会导致结果出现异步,也就是说线程不安全。


3.7synchronized代码块有volatile同步的功能

  synchronized不仅有使多个线程访问统一资源时顺序同步的功能,他也可以将线程的私有内存变量与公共内存变量同步的功能。

  注:synchronized可以保证在同一时刻,只有一个线程可以执行某一个方法或某一个代码块,它包含两个特征:互斥性与可见性。

  synchronized不仅仅可以解决一个线程看到对象处于不一致的状态(这里说的是可见性,指的是说可以保证对象在线程内存与公共内存保持一致。)

  还可以保证进入同步方法或同步代码块的每一个线程,都能看到由同一个锁保护之前所有的修改效果(这里说的就是互斥性了,可以看到在自己进行加锁前(前提是当前线程抢到执行权),上一个线程锁在其锁期间完成的全部操作)。

  学习多线程八个大字:“外练互斥、内修可见”。

  看到这里是不是有点傻眼。。。好几小节的东西最好突然告诉你,其实你已经在使用的就是相对最好的。

  基础的学习就是这样,一步一步才能踏实,然后才能够有自己的东西。只有看到不好的地方,才会想要做到更好的地方。

  枯燥吗?还好,只是长时间高强度的话,脑袋却是有点懵,然后呢?还行吧,越大越发现学习的魅力,不得不说有点悲哀。

  以自己的失败为鉴吧。