百木园-与人分享,
就是让自己快乐。

各种锁、volatile、synchronized、单例模式

1、

(1)各种锁

==可重入锁(递归锁):广义上的可重入锁,并非单指ReentrantLock。指的是同一线程外层函数获得锁后,内层递归函数仍然有获得该锁的代码,但不锁影响。

==公平锁:加锁前检查是否有排队等候的线程,优先排队等待的线程,按排队顺序来。

==不公平锁:加锁时,不考虑排队等候的线程,直接获取锁,获取不到自动到队尾等候,效率比公平锁高,因为公平锁维护了一个队列。可插队。

==读写锁:ReadWriteLock\\ReentrantReadWriteLock,在读的地方使用读锁,在写的地方使用写锁,多个读锁不互斥,读锁和写锁互斥。

==共享锁和独占锁

         独占锁,每次只能有一个线程持有锁,避免了读/读冲突。某个只读线程获取锁,其它读线程只能等待。如ReentrantLock独占方式实现的互斥锁。

         共享锁,允许多个线程同时获取锁,并发访问资源。如ReadWriteLock允许读读,AQS队列等待线程锁的获取也是共享锁。

(2)锁的四个状态:无锁,重量级锁,轻量级锁,偏量锁

重量级锁:同一时间,有多个线程同时竞争锁

      依赖操作系统的Mutex Lock来实现,会进行用户态和内核态的转换,和进程的上下文切换,使用系统的互斥量来实现锁,效率低(synchronized效率低的原因)

轻量级锁:存在两个线程一前一后执行同步代码块时,不存在同时竞争锁

      适应的场合是线程交替执行同步块的情况和没有多线程竞争的情况,如果存在同一时间访问同一锁的情况,会到导致锁膨胀为重量级锁。多次依赖CAS原子指令来获取轻量级锁和释放锁。

偏量锁:总是由同一线程多次访问同一同步代码块时,此时是偏量锁

     线程访问加锁代码时,会在对象头中存储当前线程的ID,后续这个线程进入和退出这段加锁代码时,就不需要再次加锁和释放锁。只需要在置换ThreadID时进行一次CAS原子指令,会节省性能消耗。只有一个线程执行同步块时偏量锁能提高性能。

 

2、volatile

(1)CPU缓存模型:解决CPU处理速度和内存处理速度不对等的问题

(2)内存:可以看作外存的高速缓存,是硬盘数据用于解决硬盘访问速度过慢的问题

(3)JMM内存模型:解决缓存不一致性问题

抽象  线程和主内存之间的关系

==主存:共享内存,所有线程创建的实例对象都存放在主内存中

==本地内存:每个线程都有一个私有的本地内存来存储共享变量的副本。 每个线程只能访问自己的本地内存,无法访问其它线程的本地内存。

==造成的问题,一个线程在主存中修改一个变量的值,另一个线程还在继续使用副本中的变量的值,会造成数据不一致性问题。解决方法:volatile

(4)并发编程的三个特性:

==1、原子性:一次或多次操作,要么都执行,要么都不执行。

        synchronized可以保证代码片段的原子性。volatile不能保证

==2、可见性:一个线程对共享变量进行修改,另外的线程能立即可见修改后的最新值。

       synchronized和volatile可以保证共享变量的可见性。

==3、有序性:代码执行过程中的代码执行顺序,未必是编写代码时的顺序,可能造成指令重排。

       volatile可以禁止指令重排。

(5)synchronized和volatile的区别

==1、synchronized和volatile是互补的存在,不是对立的存在

==2、volatile轻量级实现,性能比synchronized好

==3、volatile只能作用于变量,synchronized能作用于变量、方法、类

==4、volatile主要用于解决多线程间内存可见性问题,使对volatile变量的读写直接写入主存,保证变量的可见性。

          synchronized主要用于解决执行控制问题,能够对修饰的代码进行加锁,阻止其它线程的访问,保证线程间访问资源的同步性。

==5、volatile不会造成线程阻塞,没有锁操作,synchronized会造成线程阻塞,会进行锁操作。

==6、volatile、synchronized可以保证可见性,synchronized还可以保证原子性。

         synchronized保证可见性的原理:创建一个内存屏障,内存屏障指令保证所有CPU操作结果都会直接刷到主存中,从而保证操作的内存可见性,同时,内存屏障也可以禁止指令重排,从而保证有序性。

         volatile保证可见性原理:会创建一个内存屏障,使得内存屏障的指令不能重新排序,保证有序性。同时,本地线程中的volatile变量的值会立即写入到主存中,并使其它线程共享的volatile变量无效化,其它线程必须重新从主存中读取其变量值。

 

3、synchronized

(1)synchronized :解决多线程间访问的资源的同步性,可以保证被修饰的方法或代码块在任意时刻只有一个线程执行。

(2)synchronized:重量级的锁

(3)使用方式:(3种)

==1、修饰普通方法:作用于当前对象实例,加锁,锁的是当前实例对象

synchronized void method() {
   //业务代码
}

==2、修饰静态方法:作用于当前类,加锁,锁的是当前的class类

      进入同步代码前要获得当前class类的锁,synchronized加到static静态方法和synchronized(.class)代码块上,都是给class类上锁。

synchronized static void method() {
   //业务代码
}

==3、修饰代码块:指定加锁对象,对指定的对象|类加锁

     进入同步代码前要获得给定对象|类的锁

     synchronized(this|object)表示进入同步代码块前要获得给定对象的锁

     synchronized(类.class)表示进入同步代码块前要获得当前class的锁

synchronized(this) {
   //业务代码
}

====尽量不要使用synchronized(String a),因为字符串常量池有缓存功能

====一个线程A调用一个实例对象的非静态synchronized方法,而线程B需要调用这个实例对象所属类的静态synchronized方法,是允许的,不会发生互斥现象,因为访问静态synchronized方法占用的锁,锁的是当前类,而非静态synchronized方法占用的锁,锁的是当前的实例对象。

(4)synchronized的底层实现原理

=1、synchronized关键字底层原理属于JVM层面

       javap 可以查看类的相关字节码信息

==【1】、synchronized同步代码块的实现

使用 monitorenter 和 monitorexit 指令

      monitor是基于C++实现的,有ObjectMonitor实现,每个对象都有内置一个ObjectMonitor对象

      wait/notify 也依赖于monitor对象,因此只有在同步代码块和方法种才能调用wait/notify方法,否则会抛出java.lang.IllegalMonitorStateException的异常

===== monitorenter指令: 指向同步代码块的开始位置

===== monitorexit指令:指向同步代码块的结束位置

1、当执行monitorenter指令时,线程会获取对象监视器monitor的持有权

2、同时,会尝试获取对象的锁,锁计数器为0说明锁可以被获取,获取后锁计数器设为1

3、线程拥有对象锁才能执行monitorexit指令释放锁,执行monitorexit指令后,锁计数器设为0,表明锁被释放,其它线程可以尝试获取锁

4、获取对象锁失败,当前线程会一直阻塞等待,直到锁被运行中的线程释放

==【2】、synchronized修饰方法的实现

没有使用monitorenter 和 monitorexit 指令

使用 ACC_SYNCHRONIZED标识

=====ACC_SYNCHRONIZED标识,指明了该方法是一个同步方法

JVM通过ACC_SYNCHRONIZED标识辨别一个方法是否为同步方法,从而执行相应的同步调用

=====如果是实例方法,JVM会通过该ACC_SYNCHRONIZED标识,尝试获取实例对象的锁

=====如果是静态方法,JVM会通过该ACC_SYNCHRONIZED标识,尝试获取当前类class的锁

==【3】synchronized修饰同步代码块和方法本质:获取对象监视器monitor

(5)构造方法可以使用synchronized关键字修饰吗?

不能,因为构造方法本身就属于线程安全的,不存在同步的构造方法一说,因为实例对象还没实例,怎么会有竞争呢?

(6)synchronized为什么是非公平锁?体现在哪里呢?

==[1] 当持有锁的线程释放锁时,该线程会执行以下重要操作:

        先将锁的持有者owner属性赋值为null;

        唤醒等待链表中的一个线程。

        在唤醒等待的线程期间(还没唤醒),若有其它线程尝试获取锁,可以马上获取到锁。(不公平,可插队)

==[2]当线程尝试获取锁失败,会进入阻塞,会将线程放入链表里边,但唤醒的顺序不确定,先进入链表不代表会先被唤醒。(不公平,没顺序)

(7)synchronized锁升级的原理

==[1]过程: 偏量锁——>轻量级锁——>重量级锁

       一个线程第一次访问同步代码块时,锁对象的对象头中有一个ThreadId字段,第一次访问时,ThreadId为空,jvm会让其获得偏量锁,并将ThreadId设置为其线程id(依赖一次CAS原子指令);

       当线程再次进入这个同步代码块时,会先判断ThreadId是否与线程id一致,一致则可以直接使用此对象(不用再获取锁、释放锁);不一致则进行锁升级,偏量锁升级为轻量级锁,通过锁自旋(依赖多次CAS原子指令),循环一定次数后来获取锁,获取要使用的对象;

       轻量级锁自旋一定次数后还没有获取到使用的对象,就会进行锁升级,轻量级锁升级为重量级锁(依赖操作系统的Metux Lock),来获取锁和要获取的对象;

       这些过程构成了synchronized锁的升级。

==[2]目的:锁升级是为了减低锁带来的性能消耗。

      Java6后优化synchronized的实现方式,使用了这种锁升级的方式,减低了锁带来的性能消耗。

(8)JVM对 synchronized 的优化

==[1]新增了锁的状态:锁消除、锁粗化、自旋锁

==[2]锁膨胀(升级):实际中到一定情况下会进行升级

=====膨胀(升级)方向:无锁--->偏量锁--->轻量级锁--->重量级锁

=====膨胀不可逆 膨胀的方向不能逆向

=====膨胀原理:有一个线程多次执行同一同步代码块时,此时,便是偏量锁;当存在两个线程一前一后执行同一同步代码块时,不存在同时竞争锁的情况下,此时便是轻量级锁;(期间,ThreadId存在不同,便会进行锁升级,偏量级锁升级为轻量级锁);当存在多个线程同时竞争锁对象,同时要执行同一同步代码块时,便是重量级锁。(期间,存在多线程竞争,轻量级锁升级为重量级锁)

==[3]锁膨胀:优化更彻底,去除不可能存在竞争的锁

     优化前:synchronized(object){}

     优化后:直接去掉该synchronized(){}

     原因:因为object锁是私有变量,不存在竞争关系

==[4]锁粗化:更极端的优化处理,通过扩大锁的范围,避免反复加锁和释放锁。

    优化前:for(){synchronized(xxx.class){}}

    优化后:synchronized(xxx.class){for(){}}

    原因:避免反复加锁解锁,扩大锁的范围

==[5]自旋锁:轻量级锁切换线程时,线程执行循环一定次数等待锁的释放,不让出CPU,缺点:一直没获取锁,一直不释放CPU,会带来性能开销

==[6]自适应自旋锁:自旋锁的优化,自旋次数不再固定,自旋次数由前一次在同一锁上的自旋时间及锁的拥有者的状态来决定。

(9)synchronized锁能降级吗?(降为无锁状态)

可以!!!但要具体的触发时机

==[1]触发时机:在全局安全点(sagepoint),执行清理任务时会触发尝试降级锁。

==[2]降级锁的操作:1、恢复锁对象的markword对象头

                                  2、重置ObjectMonitor,将ObjectMonitor放入全局空闲列表,等待后续使用。

 

4、单例模式与volatile、synchronized

(1)饿汉式单例(使用前创建对象,反射可以破坏单例)

//饿汉式 单例
public class aHungry {
   // 在使用时就已经存在 可能会浪费空间
   private byte[] data1 = new byte[1024*1024];
   private byte[] data2 = new byte[1024*1024];
   private byte[] data3 = new byte[1024*1024];
   private byte[] data4 = new byte[1024*1024];
   //构造器私有
   private aHungry(){}
   private final static aHungry hungry = new aHungry();// 直接加载对象
   public static aHungry getInstance(){
      return hungry;
  }
}

(2)懒汉式单例(使用时才进行对象创建)

//懒汉式单例
public class bLazyMan {
   private bLazyMan() {          System.out.println(Thread.currentThread().getName()+\"==> ok\");
  }
   private  static bLazyMan bLazyMan;
   public static bLazyMan getInstance(){
      //没加锁 当要使用时才创建对象
          if (bLazyMan==null){
             bLazyMan = new bLazyMan();
        }
         return bLazyMan;
  }
public static void main(String[] args) {
//       bLazyMan.getInstance(); // 单线程下 单例
//       多线程并发下 懒汉式单例 会有问题 可能有一个或多个线程 因此可以加锁 -->双重检测锁模式懒汉式 DCL懒汉式
       for (int i = 0; i < 10; i++) {
           new Thread(() -> {
               bLazyMan.getInstance();
          }).start();
      }
  }
}

(3)双重检验锁懒汉式单例 DCL懒汉式

//双重检验锁
public class CLockLazyMan {
   private CLockLazyMan() {          System.out.println(Thread.currentThread().getName()+\"==> ok\");
  }
   private volatile static CLockLazyMan cLockLazyMan;
   public static CLockLazyMan getInstance(){
 //先判断对象是否已经实例过,没有实例过才进入加锁代码      
     if (cLockLazyMan==null){
         //对类对象进行加锁
           synchronized (cLockLazyMan.class){
               if (cLockLazyMan==null){
                   cLockLazyMan = new CLockLazyMan();  //不是原子性操作
 /* 正确的指令顺序
    1 分配内存空间
    2 执行构造方法 初始化对象
    3 把这个对象指向内存空间
    但 可能会出现 指令重排 即指令重排 返回cLockLazyMan还没有初始化对象 因此还要加上volatile
 */
              }
          }
      }
       return cLockLazyMan;
  }
public static void main(String[] args) {
//       cLockLazyMan.getInstance(); // 单线程下 单例
//   双重检测锁模式懒汉式 DCL懒汉式
       for (int i = 0; i < 10; i++) {
           new Thread(() -> {
               cLockLazyMan.getInstance();
          }).start();
      }
  }
}

  正确的指令顺序

      1 为cLockLazyMan分配内存空间

      2 执行构造方法 初始化cLockLazyMan对象

      3 把这个对象cLockLazyMan指向分配好的内存空间

  但由于JVM具有指令重排的特性 可能会出现 指令重排 1->3->2

       指令重排在单线程环境下不会出现问题,但多线程环境下可能会发生指令重排,会返回cLockLazyMan还没有初始化对象 ,线程1执行1、3后,线程2执行调用getInstance()时,会发现cLockLazyMan不为空,并返回,但此时的cLockLazyMan还没被初始化呢

   因此要对变量cLockLazyMan加上volatile,禁止JVM的指令重排,保证多线程下的正常允许。

 

静态内部类单例

//静态内部类
public class cHolder {
   private cHolder(){}
   private static cHolder getInstance(){
       return InnerClass.cHolder;
  }
   public static class InnerClass{
       private static final cHolder cHolder = new cHolder();
  }
}

枚举(枚举本身是单例) 枚举没有无参构造,是有参构造(String,int)

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
   throw new IllegalArgumentException(\"Cannot reflectively create enum objects\");
ConstructorAccessor ca = constructorAccessor;   // read volatile

 

5、CAS(Compare And Swap) 比较并交换 操作CPU原语(不可分割,连续不中断 即保证原子性)

(1)CAS:可以解决多线程并行下使用锁造成性能损耗的一种机制。

(2)CAS操作数(3个):内存位置(V)预期原值(A)以及新值(B)

(3)原理:

==简意:比较当前工作内存中的值和主内存中的值,如果这个值是期望的,就执行操作,如果不是就一直循环。

==定义:

           如果内存位置的值(V)与预期的值(A)相匹配,处理器会自动将该位置的值设置为新值(B);

           如果处理器不做任何操作,一个线程会从主内存中得到num值,并对num值进行操作,在写入值时,线程会把第一次取到的num值和主存中的num值进行比较,如果相等,就会将改变后的num值写入主内存中,如果不相等,就会一直循环对比,直到成功为止。

(4)理解CAS

例:使用原子类AtomicInteger进行理解

public static void main(String[] args) {
   //定义原子类型 定义为 2022
   AtomicInteger atomicInteger = new AtomicInteger(2022);
   // public final boolean compareAndSet(int expect, int update)
   // 比较并更换     期望 更新   如果期望达到了就更新 否则不更新
   System.out.println(atomicInteger.compareAndSet(2022, 2023)); //true
   System.out.println(atomicInteger.get());  //2023 达到期望的2022   进行更换
   //再次 比较更换
   System.out.println(atomicInteger.compareAndSet(2022, 2023));  //false
   System.out.println(atomicInteger.get()); //2023 但没有更换成功
}

==AtomicInteger的源码,使用Unsafe类 native类,使用c++操作内存

 

 

 

 

 

 

==通过 原子类自增1的方法 理解CAS

public final int getAndIncrement() {
   return unsafe.getAndAddInt(this, valueOffset, 1);
}

 

 

 

(4)CAS的产生:修饰共享变量时会使用volatile关键字,volatile保证可见性和有序性,但无法保证原子性(多线程下会造成不安全的现象),利用CAS可以利用CPU原语(不可分割,连续不中断)保证现场操作的原子性。

(5)优缺点:

==优点:乐观锁的思想,非阻塞的轻量级锁,即一个线程的失败或挂起不影响其它线程的失败或挂起的算法。

==缺点:

====底层是自旋锁,自旋循环会产生消耗,循环时间开销大,占用CPU资源

==== 一次性只能保证一个共享变量的原子性操作,对于多个共享变量操作时,循环CAS无法保证操作的原子性。

====可能会产生ABA问题(狸猫换太子)

例如:主存原A=1 线程1 CAS(1,2),线程2 CAS(1,3)、CAS(3,1)

若线程2比线程1先执行,主存中A=1已经发生了改变了,但 线程1不知情(A=1已经不是原来的A=1了)

 

 

(6)解决问题

==1、解决自旋时间消耗开销大的问题

====若JVM支持处理器的pause指令,就可以解决。

====pause指令的作用:[1]延迟流水线执行指令de-pipeline,使CPU不过多消耗执行资源,延迟时间取决于具体的实现版本。

[2]避免在退出循环时因内存顺序冲突而引起CPU流水线被清空,从而提高CPU的执行效率

==2、解决一次性只能保证一个共享变量的原子操作的问题

====方法一:可以使用锁,不使用CAS了

====方法二:可以把多个共享变量合并成一个共享变量用CAS操作

AtomicReference类保证引用对象间的原子性,可以把多个变量放在一个对象里来进行CAS操作。

==3、解决ABA问题

===方法一:添加版本号(乐观锁原理)

===方法二:AtomicStampedReference类 引用原子类(方法一的实际操作)

使用AtomicStampedReference的compareAndSet()方法:首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

public AtomicStampedReference(V initialRef (期望), int initialStamp(时间戳、版本)) 

public boolean compareAndSet(V expectedReference,//预期值
                            V newReference,//新值
                            int expectedStmap,//预期戳记
                            int newStmap //新戳记
                          )  

例:解决ABA问题

//解决ABA问题   原子引用 类似 乐观锁原理   有时间戳(版本号)
public class cCAStoABATest {
   public static void main(String[] args) {
       //如果泛型是包装类 要注意对象的引用问题 范围问题
       //正常工作中 一般引用的是一个对象
       AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(4,1);
       new Thread(()->{
           //获取当前时间戳 即版本号
           int stamp = atomicStampedReference.getStamp();
           System.out.println(\"a 1 ==> \"+stamp);

           try {
               TimeUnit.SECONDS.sleep(2);
          } catch (InterruptedException e) {
               e.printStackTrace();
          }
           //CAS操作
           System.out.println(\"a 2\"+atomicStampedReference.compareAndSet(4, 28, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
           System.out.println(\"a 2 ==> \"+atomicStampedReference.getStamp());
           System.out.println(\"a 2\"+atomicStampedReference.compareAndSet(28, 4, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
           System.out.println(\"a 3 ==> \"+atomicStampedReference.getStamp());
      },\"a\").start();
       new Thread(()->{
           //获取当前时间戳 即版本号
           int stamp = atomicStampedReference.getStamp();
           System.out.println(\"b 1 ==> \"+stamp);
           try {
               TimeUnit.SECONDS.sleep(2);
          } catch (InterruptedException e) {
               e.printStackTrace();
          }
           //stamp 不是原来的版本了 就会失败
           System.out.println(\"b 2\"+atomicStampedReference.compareAndSet(4, 28, stamp, stamp + 1));
           System.out.println(\"b 2 ==> \"+atomicStampedReference.getStamp());
      },\"b\").start();
  }
}

 

(7)CAS使用时机:

==1、线程数较少、等待时间短,可以采用自旋锁进行CAS

==2、线程数较大、等待时间长,不建议使用,占用CPU资源消耗高

 

6、Atomic原子类 (java.util.concurrent.atomic)juc包下

(1)原子:指一个操作时不可中断的,即使是多线程的情况下,一个操作一旦开始,便不能被其它线程干扰。

(2)原子类:具有原子性\\原子操作特征的类

(3)类型(4类)

基本类型:

===整型原子类:AtomicInteger

===长整型原子类:AtomicLong

===布尔型原子类:AtomicBoolean

数组类型:

===引用类型原子类:AtomicReference

===原子更新带版本号的引用类型:AtomicStampedReference

用于解决原子的更新数据和数据的版本号,解决CAS的ABA问题

===原子更新带有标记位的引用类型:AtomicMarkableReference

对象的属性修改类型:

===原子更新整型字段的更新器:AtomicIntegerFieldUpdater

===原子更新长整型字段的更新器:AtomicLongFieldUpdater

===原子更新引用类型字段的更新器:AtomicReferenceFieldUpdater

(4)Atomic类的线程安全原理——AtomicInteger类为例

==1、原理分析

AtomicInteger源码:

// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
   try {
       valueOffset = unsafe.objectFieldOffset
          (AtomicInteger.class.getDeclaredField(\"value\"));
  } catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;

 

AtomicInteger类主要利用CAS+volatile+native方法保证原子操作,避免了synchronized的高开销,执行效率提升。

==CAS原理:期望值和原值进行比较,相同则更新,不同则自旋

==UnSafe.objectFieldOffset()方法是一个本地native方法:用于拿到原值的内存地址,返回的是valueOffser

==value是一个volatile变量,使内存可见。


来源:https://www.cnblogs.com/hexiayuliang666/p/16156232.html
本站部分图文来源于网络,如有侵权请联系删除。

未经允许不得转载:百木园 » 各种锁、volatile、synchronized、单例模式

相关推荐

  • 暂无文章