• java锁优化


    一、锁优化的思路和方法

    锁优化是指:在多线程的并发中当用到锁时,尽可能让性能有所提高。一般并发中用到锁,就是阻塞的并发,前面讲到一般并发级别分为阻塞的和非阻塞的(非阻塞的包含:无障碍的,无等待的,无锁的等等),一旦用到锁,就是阻塞的,也就是一般最糟糕的并发,因此锁优化就是在堵塞的情况下去提高性能;所以所锁的优化就是让性能尽可能提高,不管怎么提高,堵塞的也没有无锁的并发底。让锁定的障碍降到最低,锁优化并不是说就能解决锁堵塞造成的性能问题。这是做不到的。

    方法如下:

     减少锁持有时间

     减小锁粒度

     锁分离

     锁粗化

     锁消除

    二、减少锁持有时间

    举例:

    public synchronized void syncMethod(){
      othercode1();
      mutextMethod();
      othercode2();
    }
    

    使用这个锁会造成其他线程进行等待,因此让锁的的持有时间减少和锁的范围,锁的零界点就会降低,其他线程就会很快获取锁,尽可能减少了冲突时间。

    改进优化如下:

    public void syncMethod2(){
      othercode1();
      synchronized(this){
      	mutextMethod();
      }
      othercode2();
    }
    

    三、减小锁粒度

     将大对象,拆成小对象,好处是:大大增加并行度,降低锁竞争(同时偏向锁,轻量级锁成功率提高)

     提高偏向锁,轻量级锁成功率

     HashMap的同步实现( HashMap他是非线程安全的实现)

        – Collections.synchronizedMap(Map m)(多线程下使用时:用该synchronizedMap封装方式先封装让他实现线程同步的)
    
        – 返回SynchronizedMap对象 
    

    内部实现如下:就是实现对set与get进行加锁,进行互斥上同步,不管读还是写都会拿到这个互斥对象。他变成很重的对象,不管读还是写,都会互斥阻塞,读堵塞写,写堵塞读,当多个读和写时线程会一个一个的进来。

    public V get(Object key) {
    	synchronized (mutex) {return m.get(key);}
     }
    public V put(K key, V value) {
    	synchronized (mutex) {return m.put(key, value);}
    }
    

     ConcurrentHashMap(高性能的hash表,他就是做了减少锁粒度的实现,他被拆分好像16个Segment,每个Segment就是一个个小的hashmap.。就是把大的hash表拆成若干个小的hash表。)

    – 若干个Segment :Segment[] segments
    
    – Segment中维护HashEntry
    
    – put操作时• 先定位到Segment,锁定一个Segment,执行put
    

    在减小锁粒度后, ConcurrentHashMap允许若干个线程同时进入

    五、锁分离

    就是把读堵塞写,写堵塞读,读读堵塞,写写堵塞就可以使用所分离;锁分离,就是读写锁分离,读不用改变数据,所以所有的读不会产生堵塞。当写的时候才去进行堵塞。一般读情况大于锁,所以使用读写锁会有所提高系统性能。如下图

    1、 ReadWriteLock : 维护了一对锁,读锁可允许多个读线程并发使用,写锁是独占的。

      根据功能进行锁分离
    
    所有读写锁的实现必须确保写操作对读操作的内存影响。换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。
    
    读写锁比互斥锁允许对于共享数据更大程度的并发。每次只能有一个写线程,但是同时可以有多个线程并发地读数据。ReadWriteLock适用于读多写少的并发情况。 
    
    读多写少的情况,可以提高性能(根据功能模块是进行不同锁,读锁跟读锁同时进入情况其实就属于无等待的并发,因此这种操作就是把堵塞的变成非堵塞的,性能就是有所改变)
    
    读锁 写锁
    读锁 可访问 不可访问
    写锁 不可访问 不可访问

    ReadWriteLock源码剖析:https://blog.csdn.net/qq_19431333/article/details/70568478

    2、 读写分离思想可以延伸,只要操作互不影响,锁就可以分离

      LinkedBlockingQueue     LinkedBlockingQueue 用法:https://www.cnblogs.com/edgedance/p/7082078.html
    
        – 队列
    
        – 链表
    

    思想也可以理解为:在forkjioning有所提到,就是任务work的偷窃,当线程执行自己的任务,和一个线程去盗取别人的任务,他们的任务队列中的数据他们是从两个不同的端去拿的,这就是热点分离基本思想,一个从头部拿,一个从尾部拿。如下:

    头部和尾部之间的操作是不冲突的,所以可以进行高并发操作,当然当队列中只有一个数据情况就另当别论你了。

    六、锁粗化

    (一)、通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早的获得资源执行任务。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化。

    因此可以把很多次请求的锁拿到一个锁里面,但前提是:中间不需要的同步的代码块很很快的执行完。

    1.举例如下:

    public void demoMethod(){
      synchronized(lock){
      	//do sth.
      }
      //做其他不需要的同步的工作,但能很快执行完毕
      synchronized(lock){
      	//do sth.
      }
    }
    

    改进优化如下:

    public void demoMethod(){
      //整合成一次锁请求
      synchronized(lock){
        //do sth.
        //做其他不需要的同步的工作,但能很快执行完毕
      }
    }
    

    2.举例如下:

    for(int i=0;i<CIRCLE;i++){
      synchronized(lock){
      }
    }
    

    该进入下:

    synchronized(lock){
      for(int i=0;i<CIRCLE;i++){
      }
    }
    

    七、锁消除

    在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作。

    有时候对完全不可能加锁的代码执行了锁操作,因为些锁并不是我们加的,是JDK的类引用进来的,当我们使用的时候,会自动引进来,所以我们会在不可能出现在多线程需要同步的情况就执行了锁操作。在某些条件成熟下,系统会消除这些锁。如下:

    public static void main(String args[]) throws InterruptedException {
        long start = System.currentTimeMillis();
        for (int i = 0; i < CIRCLE; i++) {
        	craeteStringBuffer("JVM", "Diagnosis");
        }
        long bufferCost = System.currentTimeMillis() - start;
        System.out.println("craeteStringBuffer: " + bufferCost + " ms");
    }
    public static String craeteStringBuffer(String s1, String s2) {
        StringBuffer sb = new StringBuffer();  //他就是实现的多线程同步功能
        sb.append(s1);  //这两个就是同步操作
        sb.append(s2);
        return sb.toString();
    }
    

    sb是线程安全的。但事实上sb他在栈空间引用的,他是局部变量,他就是在线程内部才会有的,在局部变量表中,只有一个线程可以执行他,其他线程是不可靠,能访问到他的因此对sb进行所有同步操作都是无意义的。

    因此对些情况,虚拟机提供了一些优化,就是如下操作,虚拟机开启server模式

    同时进行开启逃逸分析DoEscapeAnalysis,如果没有逃逸的就把锁去掉(EliminateLocks)。逃逸分析是指:看sb是否有可能逃出StringBuffer的作用域。变成sb公有的,全局的变量,变成其他线程可访问的了。

    进行逃逸分析的执行时间,(同时加上去掉锁操作),

    进行逃逸分析的执行时间,(没有加上去掉锁操作)。

    server模式用法简单讲解:

    与client模式相比,server模式的启动比较慢,因为server模式会尝试收集更多的系统性能信息,使用更复杂的优化算法对程序进行优化。因此当系统完全启动并进入运行稳定期后,server模式的执行速度会远远快于client模式,所以在对于后台长期运行的系统,使用-server参数启动对系统的整体性能可以有不小的帮助,但对于用户界面程序,运行时间不长,又追求启动速度建议使用-client模式启动。

    未来发展64位系统必然取代32位系统,而64位系统中的虚拟机更倾向于server模式。

    八、虚拟机内的锁优化

     偏向锁

     轻量级锁

     自旋锁

    1.首先看下:对象头Mark 详细讲解:https://blog.csdn.net/zhoufanyang_china/article/details/54601311

     Mark Word,对象头的标记,32位 (对象头部保存一些对象的一些信息,32位是指系统的位数)
    
     描述对象的hash、锁信息,垃圾回收标记,年龄
    
        – 指向锁记录的指针
    
        – 指向monitor的指针
    
        – GC标记
    
        – 偏向锁线程ID
    

    2、偏向锁(偏心,就是偏向当前占有锁的线程,他的思想是悲观的思想,一般我们都是杞人忧天的,大多情况是没有竞争的,就可以使用偏向锁,可以对一个线程操作提高性能)

    思想:那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步,退出同步也,无需每次加锁解锁都去CAS更新对象头,如果不一致意味着发生了竞争,锁已经不是总是偏向于同一个线程了,这时候需要锁膨胀为轻量级锁,才能保证线程间公平竞争锁。

    在没有实际竞争的情况下,还能够针对部分场景继续优化。如果不仅仅没有实际竞争,自始至终,使用锁的线程都只有一个,那么,维护轻量级锁都是浪费的。偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。

    “偏向”的意思是,偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),因此,只需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀为轻量级锁。

    偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定。

    (1.)大部分情况是没有竞争的,所以可以通过偏向来提高性能

    (2.)所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程

    (3.)将对象头Mark的标记设置为偏向,并将线程ID写入对象头Mark

    (4.) 只要没有竞争,获得偏向锁的线程,在将来进入同步块,不需要做同步

    (5.)当其他线程请求相同的锁时,偏向模式结束

    (6.) -XX:+UseBiasedLocking

    ​ –默认启用

    (6.) 在竞争激烈的场合,偏向锁会增加系统负担(每次偏向模式都会失败,因为线程竞争,就会是偏向锁结束;所以每一次都很容易结束偏向锁,就加大了偏向锁的每一次判断,偏向锁就没有任何效果)

    public static List<Integer> numberList =new Vector<Integer>();   //Vector带有锁
    public static void main(String[] args) throws InterruptedException {
      long begin=System.currentTimeMillis();
      int count=0;
      int startnum=0;
      while(count<10000000){
      numberList.add(startnum);
      startnum+=2;
      count++;
      }
      long end=System.currentTimeMillis();
      System.out.println(end-begin);
    }
    
    

    在系统起来时虚拟机默认启用偏向时间是4,因为开始的竞争是很激烈的。

    3.轻量级锁(就是如果在偏向锁失败时,系统就会有可能去进行轻量级锁,目的是尽可能不要动用操作系统中层面的互斥,性能差,因为对于操作系统来说,虚拟机本身就是应用,所以我们在应用层面去解决线程同步问题。)

    自旋锁的目标是降低线程切换的成本。如果锁竞争激烈,我们不得不依赖于重量级锁,让竞争失败的线程阻塞;如果完全没有实际的锁竞争,那么申请重量级锁都是浪费的。轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。

    顾名思义,轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。

    Mark Word是对象头的一部分;每个线程都拥有自己的线程栈(虚拟机栈),记录线程和函数调用的基本信息。二者属于JVM的基础内容,此处不做介绍。

    当然,由于轻量级锁天然瞄准不存在锁竞争的场景,如果存在锁竞争但不激烈,仍然可以用自旋锁优化,自旋失败后再膨胀为重量级锁。

    思想就是:判断线程是否持有某个对象锁,去看他的头部是否设置了这个对象的mark值,如果有,就说明线程拥有了锁。

     BasicObjectLock

        – 嵌入在线程栈中的对象
    

     普通的锁处理性能不够理想,轻量级锁是一种快速的锁定方法。

     如果对象没有被锁定(判断步骤)

    – 将对象头的Mark指针保存到锁对象中
    
    – 将对象头设置为指向锁的指针(在线程栈空间中)
    

    如下操作:在虚拟机层面去进行快速持有锁与非持有锁判断操作,其实就是CAS操作。cas成功,说明你持有锁,费则则没有。

    lock->set_displaced_header(mark);
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark))
    {
     TEVENT (slow_enter: release stacklock) ;
     return ;
    }
    

    lock位于线程栈中

    产生问题:

    1. 如果轻量级锁获取失败(CAS失败),表示存在竞争,升级为重量级锁(常规锁)
    2. 在没有锁竞争的前提下,减少传统锁使用OS(操作系统)互斥量产生的性能损耗

    3.在竞争激烈时,轻量级锁会多做很多额外操作,导致性能下降

    扩展CAS:

    CAS:Compare and Swap, 翻译成比较并交换。

    java.util.concurrent包中借助CAS实现了区别于synchronouse同步锁的一种乐观锁。

    CAS操作包含三个操作数——内存位置(V),预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器将会自动将该位置值更新为新值,否则,不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。

    通过以上定义我们知道CAS其实是有三个步骤的

    1.读取内存中的值

    2.将读取的值和预期的值比较

    3.如果比较的结果符合预期,则写入新值

    https://blog.csdn.net/liu88010988/article/details/50799978
    https://blog.csdn.net/qq_35357656/article/details/78657373

    4.自旋锁(可以防止在操作系统层面线程被挂起)当轻量级锁没有拿到失败时,他就有可能动用操作系统方面的互斥,有可能动用是指,他还可能进行自旋锁操作。

    当竞争存在时,如果线程可以很快获得锁,那么可以不在OS层挂起线程,让线程做几个空操作(自旋);当拿不到锁时,不立即去挂掉线程,而是做空循环,尝试再去拿到锁,当别人释放锁时,你就可以拿到锁。避免线程在操作系统层面挂起。避免8万个时间周期的浪费。

     JDK1.6中-XX:+UseSpinning开启 1.6可关闭和开启操作,

     JDK1.7中,去掉此参数,改为内置实现 1.7则把他改为内置开启

     如果同步块很长,自旋失败,会降低系统性能

     如果同步块很短,自旋成功,节省线程挂起切换时间,提升系统性能

    因此减少锁的持有时间也会增加自旋成功率。ConcurrentHashMap就可以使用这个自旋锁,hashmap的操作是非常快的,所以自旋等待的可能性就会提高。

    5.偏向锁,轻量级锁,自旋锁总结(这些都是在虚拟机层面的优化,不是java层面的方式)

     他们不是Java语言层面的锁优化方法,是虚拟机层面的方法

     内置于JVM中的获取锁的优化方法和获取锁的步骤

    – 偏向锁可用会先尝试偏向锁
    
    – 轻量级锁可用会先尝试轻量级锁
    
    – 以上都失败,尝试自旋锁
    
    – 再失败,尝试普通锁,使用OS互斥量在操作系统层挂起  OS互斥量:
    
    

    (1)、偏向锁、轻量级锁、重量级锁适用于不同的并发场景:

    偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。
    轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。
    重量级锁:有实际竞争,且锁竞争时间长。
    另外,如果锁竞争时间短,可以使用自旋锁进一步优化轻量级锁、重量级锁的性能,减少线程切换。

    如果锁竞争程度逐渐提高(缓慢),那么从偏向锁逐步膨胀到重量锁,能够提高系统的整体性能。

    三种锁的详细解析:https://blog.csdn.net/zqz_zqz/article/details/70233767

    https://blog.csdn.net/noble510520/article/details/78834224

    6.一个错误使用锁的案例-对不变模式的数据类型进行加锁操作

    public class IntegerLock {
    	static Integer i=0;  
    	public static class AddThread extends Thread{
        public void run(){
          for(int k=0;k<100000;k++){
            synchronized(i){
          		i++;
          	}
       	 }
    		}
    	}
      public static void main(String[] args) throws InterruptedException {
        AddThread t1=new AddThread();
        AddThread t2=new AddThread();
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
      }
    }
    
    

    interge 是不变模式的,也就是i值不会发生变化,变化的是i的引用。static Integer i=0; 是不变的,Interge是不可变的,对他i++是不会改变的,因此这里i++实际的动作是对原始的int做操作,对Interge做++其内部是对他自动拆箱成int进行i++的,这时候改变的不是interge对象的值,而是改变i本身的引用,当i++时,会生成新的Interge,并复到i上,而不是把原来i进行操作,如果每一次都对i做同步,但不同的线程操作的i对象可能不是同一个i,第一个可能执行原来的i,下一个线程可能执行新的i对象。(可以用上面代码测试)

    7.ThreadLocal用法案例

    ThreadLocal跟锁是没有关系,ThreadLocal是最彻底的,可以把锁完全给替代的东西。

    基本思想是:多线程中对有数据冲突的对象进行加锁操作,那么去掉锁的简单方法是,为每一个线程都提供一个对象的实例,不同的线程访问自己的对象。

    他是线程局部的变量

    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    public static class ParseDate implements Runnable{
    int i=0;
    public ParseDate(int i){this.i=i;}
    public void run() {
    try {
    Date t=sdf.parse("2015-03-29 19:29:"+i%60);  //sdf对象他不是线程安全的
    System.out.println(i+":"+t);
    } catch (ParseException e) {
    e.printStackTrace();
    }
    }
    }
    public static void main(String[] args) {
    ExecutorService es=Executors.newFixedThreadPool(10);
    for(int i=0;i<1000;i++){
    es.execute(new ParseDate(i));
    }
    }
    
    

    SimpleDateFormat被多线程访问

    优化:线程安全的

    static ThreadLocal<SimpleDateFormat> tl=new ThreadLocal<SimpleDateFormat>();
    public static class ParseDate implements Runnable{
    int i=0;
    public ParseDate(int i){this.i=i;}
    public void run() {
    try {
    if(tl.get()==null){
    tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));//每一次要new一个对象
    }
    Date t=tl.get().parse("2015-03-29 19:29:"+i%60);
    System.out.println(i+":"+t);
    } catch (ParseException e) {
    e.printStackTrace();
    }
    }
    }
    public static void main(String[] args) {
    ExecutorService es=Executors.newFixedThreadPool(10);
    for(int i=0;i<1000;i++){
    es.execute(new ParseDate(i));
    }
    }
    
    

    为每一个线程分配一个实例

    另外一个错误案例:他不会去维护每一个对象的拷贝,实际上tl.get()是把ThreadLocal对象指向同一个对象实例,对所有线程来说他还是同一个对象。

    static ThreadLocal<SimpleDateFormat> tl=new ThreadLocal<SimpleDateFormat>();
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    public static class ParseDate implements Runnable{
    int i=0;
    public ParseDate(int i){this.i=i;}
    public void run() {
    try {
    if(tl.get()==null){
    tl.set(sdf);
    }
    Date t=tl.get().parse("2015-03-29 19:29:"+i%60);//这个还不是线程安全的,操作还是同一个线程,ThreadLocal指定的还是同一个对象,
    System.out.println(i+":"+t);
    } catch (ParseException e) {
    e.printStackTrace();
    }
    }
    }
    public static void main(String[] args) {
    ExecutorService es=Executors.newFixedThreadPool(10);
    for(int i=0;i<1000;i++){
    es.execute(new ParseDate(i));
    }
    }
    
    

    如果使用共享实例,起不到效果

    总结:对于工具等api对象类,数据库连接实例等希望对每个线程持单独有一个对象,就会减少线程的开销,比如SimpleDateFormat

    不需要线程之间相互影响,不会产生冲突,就可以使用他。

    ThreadLocal源码分析:https://www.cnblogs.com/eternityz/p/12238824.html

    参考

    原文:https://blog.csdn.net/gududedabai/article/details/80911855

  • 相关阅读:
    使用beautifulsoup4解析内容
    爬虫的概述以及使用(request发送请求)
    爬虫的概述以及使用(urllib发送请求)
    NoteBook的安装和使用
    云端服务器
    SQLServer数据库访问链接
    python爬虫调度器
    Python的应用领域
    Python小记2
    Python小记1
  • 原文地址:https://www.cnblogs.com/eternityz/p/12238815.html
Copyright © 2020-2023  润新知