Java并发编程技术知识点梳理(第四篇)锁优化和无锁

Java并发编程技术知识点梳理(第四篇)

目录

  • 锁优化
    • 减少锁的持有时间
    • 减少锁粒度
    • 用读写分离锁替换独占锁
    • 锁分离
    • 锁粗化
  • JDK内部的锁优化
    • 锁偏向
    • 轻量级锁
    • 自旋锁
    • 锁消除
  • 死锁
  • 无锁
    • 比较并交换(CAS)
    • CAS案例:无锁的线程安全整数(AtomicInteger)
    • CAS案例:无锁的对象引用(AtomicReference)
    • CAS案例:带有时间戳的对象引用(AtomicStampedReference)
    • CAS案例:数组也能无锁(AtomicIntegerArray)

1 锁优化

锁是最常用的同步方法之一,在高并发的环境下,激烈的锁竞争会导致程序的性能下降。所以对锁进行优化是非常必要的。

减少锁的持有时间

在锁竞争过程中,单个线程对锁的持有时间直接影响着系统性能。如果线程对锁的持有时间越长,那么,锁的竞争也越激烈。
解决方案:只在必须要同步的代码块上加锁,减少线程对锁的持有时间。

  • 下面是处理正则表达式的Pattern类
/**
 * Creates a matcher that will match the given input against this pattern.
 *
 * @param  input
 *         The character sequence to be matched
 *
 * @return  A new matcher for this pattern
 */
public Matcher matcher(CharSequence input) {
    if (!compiled) {
        synchronized(this) {
            if (!compiled)
                compile();
        }
    }
    Matcher m = new Matcher(this, input);
    return m;
}

matcher()方法有条件的进行锁申请,只有在表达式未编译时,进行局部的加锁。这种处理方式大大提高了matcher()方法的执行效率。

减小锁粒度

减小锁粒度也是一种削弱多线程锁竞争的有效手段。
典型使用场景:ConcurrentHashMap类的实现
如果需要在ConcurrentHashMap中增加一个新数据,并不是将整个HashMap加锁,而是首先根据hashcode得到该数据应该被存放到哪个段中,然后对该段进行加锁,并完成put()方法操作。

public V put(K key, V value) {
        return putVal(key, value, false);
    }

/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

减小锁粒度会带来新的问题,即当系统需要取得全局锁时,其消耗的资源会比较多。比如size()方法。它将返回ConcurrentHashMap类的有效表项的数量,要获取这个信息需要取得所有子段的锁。
所以,减小锁粒度的应用场景:只有在类似于size()方法获取全局信息的方法调用并不频繁时,这种减小锁粒度的方法才能真正意义上提高系统的吞吐量。

用读写分离锁来替换独占锁

读写分离锁ReadWriteLock。
使用读写分离锁替代独占锁是减小锁粒度的一种特殊情况,减小锁粒度是通过分割数据结构实现的,那么读写分离锁是对系统功能点(读数据,写数据)的分割。

  • 适用场景:读多写少
  • 并发集合工具:CopyOnWriteArrayList正是采用了“读多写少”思想
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;

/**
 * Gets the array.  Non-private so as to also be accessible
 * from CopyOnWriteArraySet class.
 */
final Object[] getArray() {
    return array;
}
/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return {@code true} (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}
/**
 * {@inheritDoc}
 *
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E get(int index) {
    return get(getArray(), index);
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
    return (E) a[index];
}

锁分离

将读写锁的思想进一步延伸,就是锁分离。读写锁根据读写操作功能的不同,进行了锁分离。使用类似的分离思想,可以针对应用程序的功能特点,对独占锁进行分离。
并发集合工具LinkedBlockingQueue采用了这种思想。

/**
 * Inserts the specified element at the tail of this queue, waiting if
 * necessary for space to become available.
 *
 * @throws InterruptedException {@inheritDoc}
 * @throws NullPointerException {@inheritDoc}
 */
public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    // Note: convention in all put/take/etc is to preset local var
    // holding count negative to indicate failure unless set.
    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly();
    try {
        /*
         * Note that count is used in wait guard even though it is
         * not protected by lock. This works because count can
         * only decrease at this point (all other puts are shut
         * out by lock), and we (or some other waiting put) are
         * signalled if it ever changes from capacity. Similarly
         * for all other uses of count in other wait guards.
         */
        while (count.get() == capacity) {
            notFull.await();
        }
        enqueue(node);
        c = count.getAndIncrement();
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();
}
public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();
    try {
        while (count.get() == 0) {
            notEmpty.await();
        }
        x = dequeue();
        c = count.getAndDecrement();
        if (c > 1)
            notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
    if (c == capacity)
        signalNotFull();
    return x;
}

通过takeLock和putLock两把锁,LinkedBlockingQueue实现了取数据和写数据的分离。

锁粗化

通常情况下,为了保证多线程间的有效并发,会要求每一个线程持有锁的时间尽量短,在使用完公共资源后,尽快释放锁。只有这样,等待在这个锁上的其他线程才能尽早的获得资源执行任务。但是,如果对同一个锁不断的进行请求,同步和释放,其本身也会消耗系统资源,不利于性能的优化。
虚拟机对此,便会把所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步的次数。

package test22;

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (this){
                    for (int i = 0; i < 100; i++) {
                        //synchronized (this) {
                            System.out.println("哈哈");
                        //}
                    }
                }
            }
        });
        thread.start();
    }
}

2 JDK内部的锁优化

锁偏向

如果一个线程获得了锁,那么锁就进入偏向模式。当这个线程再次请求锁时,无须再做任何同步操作。这样就节省了锁申请的操作,从而提高了程序性能。使用Java虚拟机参数-XX:+UseBiasedLocking可以开启偏向锁。

  • 适用场景:几乎没有锁竞争的场合。

轻量级锁

如果偏向锁失败,那么虚拟机并不会立即挂起线程,他还会使用一种轻量级锁的优化手段。轻量级锁就是简单的将对象头部作为指针指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁。如果线程获得轻量级锁成功,则可以进入临界区。如果轻量级锁加锁失败,则表示其他线程抢先争夺到了锁,那么当前线程的锁请求就会膨胀为重量级锁。

自旋锁

锁膨胀后,为了避免线程真实的操作系统层面挂起,虚拟机还会启用自旋锁。当前线程暂时无法获得锁,而且什么时候获得锁是未知的,也许在几个CPU时钟周期后就可以得到锁。因此,虚拟机会让当前线程做几个空循环,在经过若干次循环后,如果可以得到锁,那么顺利进入临界区,否则,才会真正的将线程在操作系统层面挂起。

锁消除

Java虚拟机在JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。通过锁消除,可以节省毫无意义的请求锁时间。

package test23;

import java.util.Vector;

public class Main {
    public static void main(String[] args) {
        String[] result = createStrings();
        for (String str:result) {
            System.out.println(str);
        }
    }

    private static String[] createStrings() {
        Vector<String> v = new Vector<>();
        for (int i = 0; i < 100; i++) {
            v.add(Integer.toString(i));
        }
        return v.toArray(new String[]{});
    }
}

锁消除涉及的一项关键技术为逃逸分析。观察某个变量是否会逃出某一个作用域。在本例中,变量v显然没有逃出createStrings()方法之外。虚拟机会将变量v内部的加锁操作去除。如果createStrings()返回的不是String数组,而是v本身,那么就认为变量v逃逸出了当前方法,也就是说变量v有可能被其他线程访问到,如此,虚拟机不能消除v中的锁操作。
使用-XX:+EliminateLocks参数可以开启锁消除。
使用-XX:+DoEscapeAnalysis参数打开逃逸分析,必须在-server模式下进行。

3.死锁

死锁就是两个或多个线程相互占用对方需要的资源,而都不进行释放,导致彼此之间相互等待对方释放资源。产生了无限制等待的现象。

  • 哲学家问题
package test2;

import java.util.concurrent.TimeUnit;

public class DeadLock implements Runnable{

    private String firstlock;
    private String secondlock;

    public DeadLock(String firstlock, String secondlock) {
        this.firstlock = firstlock;
        this.secondlock = secondlock;
    }

    @Override
    public void run() {
        synchronized (firstlock){
            System.out.println(Thread.currentThread().getName()+"\t持有" + firstlock + "需要" + secondlock);
            try {
                TimeUnit.SECONDS.sleep(2L);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            synchronized (secondlock){
                System.out.println(Thread.currentThread().getName()+"\t持有" + secondlock + "需要" + firstlock);
            }
        }
    }
}
package test2;
public class Main {
    public static void main(String[] args){
        String forkA = "A叉子";
        String forkB = "B叉子";
        new Thread(new DeadLock(forkA,forkB),"哲学家A").start();
        new Thread(new DeadLock(forkB,forkA),"哲学家B").start();
    }
}

避免死锁的方法:

  • 1.使用无锁的函数
  • 2.通过重入锁的中断
  • 3.重入锁的限时等待

4 无锁

对于线程同步来说,加锁是一种悲观的策略,他总是假设每一次的临界区操作会产生冲突,因此,必须对每次操作都小心谨慎,如果有多个线程同时访问临界区资源,则牺牲性能让线程等待,所以锁会阻塞线程执行。
无锁是一种乐观的策略,他假设每一次对资源的访问时没有冲突的,既然没有冲突,就不需要线程等待,所以所有的线程都可以在不停顿的状态下持续执行,如果遇到冲突,无锁会使用“比较交换”的思想来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。

比较并交换(CAS,Compare And Swap)

优点:没有锁竞争带来的系统开销,没有线程间频繁调度带来的开销。
CAS算法:包含三个参数(V,E,N),其中V表示要更新的变量,E表示预期值,N表示新值。

  • 当V值==E值时,V值=N值
  • 当V值!=E值时,说明已经有其他线程做了更新,则当前线程什么也不做。最后CAS返回当前V的真实值。

无锁的线程安全整数:AtomicInteger

package test25;

import java.util.concurrent.atomic.AtomicInteger;

public class AddThread implements Runnable {
    AtomicInteger i = new AtomicInteger();
    @Override
    public void run() {
        for (int j = 0; j < 20000; j++) {
            i.incrementAndGet();
        }
    }

    public AtomicInteger getI() {
        return i;
    }
}
package test25;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread[] ts = new Thread[10];
        AddThread addThread = new AddThread();
        for (int i = 0; i < 10; i++) {
            ts[i] = new Thread(addThread);
        }
        for (int i = 0; i < 10; i++) {
            ts[i].start();
        }
        for (int i = 0; i < 10; i++) {
            ts[i].join();
        }
        System.out.println(addThread.getI());
    }
}

无锁的对象引用(AtomicReference)

package test26;


import java.util.concurrent.atomic.AtomicReference;

public class AddThread implements Runnable {
    private AtomicReference<Integer> money;

    public AddThread(AtomicReference<Integer> money) {
        this.money = money;
    }

    @Override
    public void run() {
        while (true) {
            while (true) {
                Integer m = money.get();
                if (m < 20) {
                    if (money.compareAndSet(m, m + 20)) {
                        System.out.println("余额小于20元,充值成功,余额" + money.get() + "元");
                        break;
                    } else {
                        System.out.println("余额大于20元,无须充值");
                        break;
                    }
                }
            }
        }
    }
}
package test26;

import java.util.concurrent.atomic.AtomicReference;

public class SubThread implements Runnable{
    private AtomicReference<Integer> money;

    public SubThread(AtomicReference<Integer> money) {
        this.money = money;
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            while (true){
                Integer m = money.get();
                if(m>10){
                    System.out.println("大于10元");
                    if(money.compareAndSet(m,m-10)){
                        System.out.println("成功消费10元,余额"+ money.get());
                        break;
                    }
                }else{
                    System.out.println("没有足够的金额");
                    break;
                }
            }
            try{
                Thread.sleep(100);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}
package test26;


import java.util.concurrent.atomic.AtomicReference;

public class Main {
    public static void main(String[] args) {
        AtomicReference<Integer> money = new AtomicReference<>();
        //设置账户初始值小于20,这是需要充值的账户。
        money.set(19);
        for (int i = 0; i < 3; i++) {
            new Thread(new AddThread(money)).start();
        }
        new Thread(new SubThread(money)).start();

    }
}

带有时间戳的对象引用

package test27;


import java.util.concurrent.atomic.AtomicStampedReference;

public class AddThread implements Runnable {
    private AtomicStampedReference<Integer> money;
    int stamp;

    public AddThread(AtomicStampedReference<Integer> money,int stamp) {
        this.money = money;
        this.stamp = stamp;
    }

    @Override
    public void run() {
        while (true) {
            while (true) {
                Integer m = money.getReference();
                if (m < 20) {
                    if (money.compareAndSet(m, m + 20,stamp,stamp+1)) {
                        System.out.println("余额小于20元,充值成功,余额" + money.getReference() + "元");
                        break;
                    } else {
                        System.out.println("余额大于20元,无须充值");
                        break;
                    }
                }
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
package test27;


import java.util.concurrent.atomic.AtomicStampedReference;

public class SubThread implements Runnable{
    private AtomicStampedReference<Integer> money;
    public SubThread(AtomicStampedReference<Integer> money) {
        this.money = money;
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            while (true){
                Integer m = money.getReference();
                int stamp = money.getStamp();
                if(m>10){
                    System.out.println("大于10元");
                    if(money.compareAndSet(m,m-10,stamp,stamp+1)){
                        System.out.println("成功消费10元,余额"+ money.getReference());
                        break;
                    }
                }else{
                    System.out.println("没有足够的金额");
                    break;
                }
            }
            try{
                Thread.sleep(100);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}
package test27;
import java.util.concurrent.atomic.AtomicStampedReference;

public class Main {
    public static void main(String[] args) {
        //设置账户初始值小于20,这是需要充值的账户。
        AtomicStampedReference<Integer> money = new AtomicStampedReference<>(19,0);

        for (int i = 0; i < 3; i++) {
            final int timestamp = money.getStamp();
            new Thread(new AddThread(money,timestamp)).start();
        }
        new Thread(new SubThread(money)).start();

    }
}

和对象的过程变化相关,要使用带时间戳的对象引用。例如转账,金融类的。

数组 AtomicIntegerArray

package test28;

import java.util.concurrent.atomic.AtomicIntegerArray;

public class AddThread implements Runnable {
    AtomicIntegerArray arr;

    public AddThread(AtomicIntegerArray arr) {
        this.arr = arr;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            arr.getAndIncrement(i%arr.length());
        }
    }

    public AtomicIntegerArray getArr() {
        return arr;
    }
}
package test28;

import java.util.concurrent.atomic.AtomicIntegerArray;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerArray arr = new AtomicIntegerArray(10);
        AddThread at = new AddThread(arr);
        Thread[] ts = new Thread[10];
        for (int i = 0; i < 10; i++) {
            ts[i] = new Thread(at);
        }
        for (int i = 0; i < 10; i++) {
            ts[i].start();
        }
        for (int i = 0; i < 10; i++) {
            ts[i].join();
        }
        System.out.println(at.getArr());
    }
}

发布了20 篇原创文章 · 获赞 14 · 访问量 8789

猜你喜欢

转载自blog.csdn.net/yemuxiaweiliang/article/details/104644393