并发学习(二)

并发学习(二)

并发相关的容器

1、ConcurrentHashMap:JDK1.7底层采用Segment数组和HashEntry数组结构组成。Segment实现ReentrantLock,是一种可重入锁、扮演锁的角色。HashEntry则是存储键值对数据。每一个Segment守护着一个HashEntry数组里的每一个元素。要对HashEntry数组进行修改,首先需要获取到Segment的锁.JDK1.8时取消了Segment,采用CAS和synchronized来保证并发安全。

2、CopyOnWriteArrayList:所有可变操作,底层都是基于创建数组的新副本来实现的,当对list修改的时候,不修改原来的内容,而是对原有数据进行一次复制,后修改复制的这个副本,再将副本替换原来的数据(将原来的内存指针指向新的内存)。这样保证了写操作不影响读操作。

读操作的实现

没有任何同步控制,理由是array不会发生修改,只会被另一个array替换,保证数据的安全

  /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;
    public E get(int index) {
        return get(getArray(), index);
    }
    @SuppressWarnings("unchecked")
    private E get(Object[] a, int index) {
        return (E) a[index];
    }
    final Object[] getArray() {
        return array;
    }

写操作

写入add方法在添加集合的时候加了锁,保证了同步,防止因为并发而copy出多个副本

  /**
     * 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();//释放锁
        }
    }

3、ConcurrentLinkedQueue

java提供的queue分为阻塞队列跟非阻塞队列,其中阻塞队列为BLockingQueue,非阻塞队列的典型即为ConcurrentLinkedQueue。阻塞队列可以通过加锁来实现,非阻塞队列可以通过CAS来实现。

ConcurrentLinkedQueue内部使用链表的数据结构,使用CAS非阻塞算法实现线程安全的

4、BlockingQueue

阻塞队列,当队列容器满时,线程会被阻塞,直到队列未满。继承自Queue。相关实现类:

BlockingQueue 的实现类

实现类

  • ArrayBlockingQueue

    有界队列实现类,底层采用数组来实现。一旦创建,容量不能改变,不管是插入操作还是读取操作,都需要获取到锁来控制

    默认情况,ArrayBlockingQueue不能保证线程访问队列的公平性(等待时间长的先访问,等待时间久的后访问)。如果要保证公平性,在创建的时候需要

    private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10,true);
    
  • LinkedBlockingQueue

    底层基于单向链表的数据结构实现的阻塞队列,可以作为无界队列,也可以作为有界队列,与ArrayBlockingQueue相比具有更高的吞吐率。为了防止其容量的迅速增长,一般在创建时指定其大小,如果未指定,则以Integer.MAX_VALUE作为链表长度

       /**
         *某种意义上的无界队列
         * Creates a {@code LinkedBlockingQueue} with a capacity of
         * {@link Integer#MAX_VALUE}.
         */
        public LinkedBlockingQueue() {
            this(Integer.MAX_VALUE);
        }
    
        /**
         *有界队列
         * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity.
         *
         * @param capacity the capacity of this queue
         * @throws IllegalArgumentException if {@code capacity} is not greater
         *         than zero
         */
        public LinkedBlockingQueue(int capacity) {
            if (capacity <= 0) throw new IllegalArgumentException();
            this.capacity = capacity;
            last = head = new Node<E>(null);
        }
    
  • PriorityBlockingQueue

    是一个支持优先级的无界阻塞队列。默认情况下采用自然顺序进行排序,或者通过自定义类实现compareTo方法来指定元素的排序规则。或者通过初始化构造参数 Comparator 来指定排序规则。

    并发采用ReentrantLock,队列为无界队列,构造函数只能指定初始化队列的长度,后面空间不够的时候会自动扩容

  • ConcurrentSkipListMap

    采用跳表的数据结构,跳表的数据结构如下:

    2级索引跳表

    本质即为维护多个链表,并且链表是分层的。最顶层的链表维护了跳表内所有的元素,每上面一层链表都是下面一层链表的子集。跳表内所有链表的元素都是排序的

    举例:查找元素18

    在跳表中查找元素18

    查找时,可以从顶级链表开始找。一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表继续找。这也就是说在查找过程中,搜索是跳跃式的。如上图所示,在跳表中查找元素18。

    使用跳表实现Map 和使用哈希算法实现Map的另外一个不同之处是:哈希并不会保存元素的顺序,而跳表内所有的元素都是排序的。因此在对跳表进行遍历时,你会得到一个有序的结果。所以,如果你的应用需要有序性,那么跳表就是你不二的选择。JDK 中实现这一数据结构的类是ConcurrentSkipListMap。

乐观锁与悲观锁

1、乐观锁:做最好的打算,每次拿取数据的时候都认为别人不会修改,所以不会上锁。但是在跟新的时候会判断一下在此期间别人有没有跟新这个数据,可以使用版本号机制和CAS算法实现。适用于多读的应用类型,这样可以提高吞吐率

2、悲观锁:每次去读取数据的时候都认为别人会修改,所以每次在拿数据的时候都会上把锁。这样别的线程想拿到数据就会阻塞直至拿取到锁。适用于多写少读的场景。传统的数据库里的表锁、行锁、写锁、读锁都是悲观锁的机制,都是在操作之前上锁。java中的synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

3、乐观锁的两种实现机制

  • 版本号机制

    在数据表中加上版本号version字段,表示数据被修改的次数。当数据被修改的时候,版本号+1.当线程1要跟新数据的时候,会同时读取version的值,在提交跟新的时候,若刚才读取到的version值与当前数据库的version值相同时才跟新,否则重试跟新操作,直到跟新成功

  • CAS算法

    compare and swap(比较与交换),是一种又名的无锁算法。所涉及到的三个操作数

    • 需要读写内存值V
    • 进行比较的值A
    • 拟写入的新值B

    当且仅当V的值等于A时,CAS通过原子操作用新值B来跟新V的值,否则不会执行任何操作(比较和替换是原子操作).如果不相等,则进行自旋,不断地进行尝试

4、乐观锁地缺点:

  • ABA问题

    如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。

    JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

    import java.util.concurrent.atomic.AtomicInteger;
    
    public class AtomicIntegerDefectDemo {
        public static void main(String[] args) {
            defectOfABA();
        }
    
        static void defectOfABA() {
            final AtomicInteger atomicInteger = new AtomicInteger(1);
    
            Thread coreThread = new Thread(
                    () -> {
                        final int currentValue = atomicInteger.get();
                        System.out.println(Thread.currentThread().getName() + " ------ currentValue=" + currentValue);
    
                        // 这段目的:模拟处理其他业务花费的时间
                        try {
                            Thread.sleep(300);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                        boolean casResult = atomicInteger.compareAndSet(1, 2);
                        System.out.println(Thread.currentThread().getName()
                                + " ------ currentValue=" + currentValue
                                + ", finalValue=" + atomicInteger.get()
                                + ", compareAndSet Result=" + casResult);
                    }
            );
            coreThread.start();
    
            // 这段目的:为了让 coreThread 线程先跑起来
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            Thread amateurThread = new Thread(
                    () -> {
                        int currentValue = atomicInteger.get();
                        boolean casResult = atomicInteger.compareAndSet(1, 2);
                        System.out.println(Thread.currentThread().getName()
                                + " ------ currentValue=" + currentValue
                                + ", finalValue=" + atomicInteger.get()
                                + ", compareAndSet Result=" + casResult);
    
                        currentValue = atomicInteger.get();
                        casResult = atomicInteger.compareAndSet(2, 1);
                        System.out.println(Thread.currentThread().getName()
                                + " ------ currentValue=" + currentValue
                                + ", finalValue=" + atomicInteger.get()
                                + ", compareAndSet Result=" + casResult);
                    }
            );
            amateurThread.start();
        }
    }
    
    Thread-0 ------ currentValue=1
    Thread-1 ------ currentValue=1, finalValue=2, compareAndSet Result=true
    Thread-1 ------ currentValue=2, finalValue=1, compareAndSet Result=true
    Thread-0 ------ currentValue=1, finalValue=2, compareAndSet Result=true
    
  • 循环开销太大

    自旋CAS如果跟新不成功,会长时间进行循环等待,会给CPU带来非常大地执行开销

  • 只能保证一个共享变量的原子性

    CAS只对单个变量有效,当操作涉及多个共享变量时CAS无效。JDK1.5开始,提拱了AtomicReference类,可以把多个共享变量放在对象里,合并成一个共享变量来进行CAS操作

5、CAS和synchronized的使用场景

简单来说,CAS适用于读操作较多,写操作较少的场景。synchronized适用于写操作较多,读操作较少的场景

  • 对于资源竞争较少的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态之间的切换操作会浪费消耗cpu资源。而CAS基于硬件实现,不需要进入内核态,不需要切换线程,且操作自旋几率小,因而可以获取更高的性能
  • 对于竞争资源严重的情况,CAS自旋的概率大,从而浪费CPU资源,效率低于synchronized

javaSE1.6之后synchronized引入了偏向锁和轻量级锁,减少获得锁和释放锁带来的性能消耗问题。底层实现主要依靠Lock_Free的队列,基本思路为自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲公平性,但获得了更高吞吐率。在线程冲突较少的情况,可以获得和CAS类型的性能。冲突多,性能远高于CAS。

原子类

1、Atomic原子类:指一个操作不可中断,即使是在多个线程一起执行,一个操作一旦开始,就不会被其它线程所干扰。存在于java.util.concurrent.atomic包下

JUC原子类概览

基本类型的原子类

  • AtomicInteger
  • AtomicLong:长整型原子类
  • AtimicBoolean:布尔型原子类

三个类提供的方法几乎一样,以AtomicInteger为例:

public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

相关使用

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerTest {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		int temvalue = 0;
		AtomicInteger i = new AtomicInteger(0);
		temvalue = i.getAndSet(3);
		System.out.println("temvalue:" + temvalue + ";  i:" + i);//temvalue:0;  i:3
		temvalue = i.getAndIncrement();
		System.out.println("temvalue:" + temvalue + ";  i:" + i);//temvalue:3;  i:4
		temvalue = i.getAndAdd(5);
		System.out.println("temvalue:" + temvalue + ";  i:" + i);//temvalue:4;  i:9
	}

}

使用原子类得基本数据类型,在多线程得环境下保证了数据的安全

//不使用原子类需要加锁才能保证数据的安全性
class Test {
        private volatile int count = 0;
        //若要线程安全执行执行count++,需要加锁
        public synchronized void increment() {
                  count++; 
        }

        public int getCount() {
                  return count;
        }
}
//使用原子类不需要加锁就能保证数据的安全
class Test2 {
        private AtomicInteger count = new AtomicInteger();

        public void increment() {
                  count.incrementAndGet();
        }
      //使用AtomicInteger之后,不需要加锁,也可以实现线程安全。
       public int getCount() {
                return count.get();
        }
}

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的高开销。

数组类型的原子类

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray:引用类型数组原子类

上面的数组原子类方法几乎相同,这里以AtomicIntegerArray类为例:

//常用方法
public final int get(int i) //获取 index=i 位置元素的值
public final int getAndSet(int i, int newValue)//返回 index=i 位置的当前的值,并将其设置为新值:newValue
public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增
public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减
public final int getAndAdd(int delta) //获取 index=i 位置元素的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update)
public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
import java.util.concurrent.atomic.AtomicIntegerArray;

public class AtomicIntegerArrayTest {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		int temvalue = 0;
		int[] nums = { 1, 2, 3, 4, 5, 6 };
		AtomicIntegerArray i = new AtomicIntegerArray(nums);
		for (int j = 0; j < nums.length; j++) {
			System.out.println(i.get(j));
		}
		temvalue = i.getAndSet(0, 2);
		System.out.println("temvalue:" + temvalue + ";  i:" + i);
		temvalue = i.getAndIncrement(0);
		System.out.println("temvalue:" + temvalue + ";  i:" + i);
		temvalue = i.getAndAdd(0, 5);
		System.out.println("temvalue:" + temvalue + ";  i:" + i);
	}

}java

引用类型的原子类

基本类型的原子类只能跟新一个变量,如果原子需要跟新多个变量,需要应用引用类型的原子类

  • AtomicReference
  • AtomicStampedReference:原子跟新引用类型里的字段原子类
  • AtomicMarkableReference:原子跟新带有标记位的引用类型

以AtomicReference为例

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceTest {

	public static void main(String[] args) {
		AtomicReference<Person> ar = new AtomicReference<Person>();
		Person person = new Person("SnailClimb", 22);
		ar.set(person);
		Person updatePerson = new Person("Daisy", 20);
		ar.compareAndSet(person, updatePerson);

		System.out.println(ar.get().getName());    //Daisy
		System.out.println(ar.get().getAge());     //20
	}
}

class Person {
	private String name;
	private int age;

	public Person(String name, int age) {
		super();
		this.name = name;
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}

}

**AtomicStampedReference **

import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicStampedReferenceDemo {
    public static void main(String[] args) {
        // 实例化、取当前值和 stamp 值
        final Integer initialRef = 0, initialStamp = 0;
        final AtomicStampedReference<Integer> asr = new AtomicStampedReference<>(initialRef, initialStamp);
        System.out.println("currentValue=" + asr.getReference() + ", currentStamp=" + asr.getStamp());

        // compare and set
        final Integer newReference = 666, newStamp = 999;
        final boolean casResult = asr.compareAndSet(initialRef, newReference, initialStamp, newStamp);
        System.out.println("currentValue=" + asr.getReference()
                + ", currentStamp=" + asr.getStamp()
                + ", casResult=" + casResult);

        // 获取当前的值和当前的 stamp 值
        int[] arr = new int[1];
        final Integer currentValue = asr.get(arr);
        final int currentStamp = arr[0];
        System.out.println("currentValue=" + currentValue + ", currentStamp=" + currentStamp);

        // 单独设置 stamp 值
        final boolean attemptStampResult = asr.attemptStamp(newReference, 88);
        System.out.println("currentValue=" + asr.getReference()
                + ", currentStamp=" + asr.getStamp()
                + ", attemptStampResult=" + attemptStampResult);

        // 重新设置当前值和 stamp 值
        asr.set(initialRef, initialStamp);
        System.out.println("currentValue=" + asr.getReference() + ", currentStamp=" + asr.getStamp());

        // [不推荐使用,除非搞清楚注释的意思了] weak compare and set
        // 困惑!weakCompareAndSet 这个方法最终还是调用 compareAndSet 方法。[版本: jdk-8u191]
        // 但是注释上写着 "May fail spuriously and does not provide ordering guarantees,
        // so is only rarely an appropriate alternative to compareAndSet."
        // todo 感觉有可能是 jvm 通过方法名在 native 方法里面做了转发
        final boolean wCasResult = asr.weakCompareAndSet(initialRef, newReference, initialStamp, newStamp);
        System.out.println("currentValue=" + asr.getReference()
                + ", currentStamp=" + asr.getStamp()
                + ", wCasResult=" + wCasResult);
    }
}
currentValue=0, currentStamp=0
currentValue=666, currentStamp=999, casResult=true
currentValue=666, currentStamp=999
currentValue=666, currentStamp=88, attemptStampResult=true
currentValue=0, currentStamp=0
currentValue=666, currentStamp=999, wCasResult=true

**AtomicMarkableReference **

import java.util.concurrent.atomic.AtomicMarkableReference;

public class AtomicMarkableReferenceDemo {
    public static void main(String[] args) {
        // 实例化、取当前值和 mark 值
        final Boolean initialRef = null, initialMark = false;
        final AtomicMarkableReference<Boolean> amr = new AtomicMarkableReference<>(initialRef, initialMark);
        System.out.println("currentValue=" + amr.getReference() + ", currentMark=" + amr.isMarked());

        // compare and set
        final Boolean newReference1 = true, newMark1 = true;
        final boolean casResult = amr.compareAndSet(initialRef, newReference1, initialMark, newMark1);
        System.out.println("currentValue=" + amr.getReference()
                + ", currentMark=" + amr.isMarked()
                + ", casResult=" + casResult);

        // 获取当前的值和当前的 mark 值
        boolean[] arr = new boolean[1];
        final Boolean currentValue = amr.get(arr);
        final boolean currentMark = arr[0];
        System.out.println("currentValue=" + currentValue + ", currentMark=" + currentMark);

        // 单独设置 mark 值
        final boolean attemptMarkResult = amr.attemptMark(newReference1, false);
        System.out.println("currentValue=" + amr.getReference()
                + ", currentMark=" + amr.isMarked()
                + ", attemptMarkResult=" + attemptMarkResult);

        // 重新设置当前值和 mark 值
        amr.set(initialRef, initialMark);
        System.out.println("currentValue=" + amr.getReference() + ", currentMark=" + amr.isMarked());

        // [不推荐使用,除非搞清楚注释的意思了] weak compare and set
        // 困惑!weakCompareAndSet 这个方法最终还是调用 compareAndSet 方法。[版本: jdk-8u191]
        // 但是注释上写着 "May fail spuriously and does not provide ordering guarantees,
        // so is only rarely an appropriate alternative to compareAndSet."
        // todo 感觉有可能是 jvm 通过方法名在 native 方法里面做了转发
        final boolean wCasResult = amr.weakCompareAndSet(initialRef, newReference1, initialMark, newMark1);
        System.out.println("currentValue=" + amr.getReference()
                + ", currentMark=" + amr.isMarked()
                + ", wCasResult=" + wCasResult);
    }
}
currentValue=null, currentMark=false
currentValue=true, currentMark=true, casResult=true
currentValue=true, currentMark=true
currentValue=true, currentMark=false, attemptMarkResult=true
currentValue=null, currentMark=false
currentValue=true, currentMark=true, wCasResult=true

对象属性修改类型原子类

当要修改某个类里的某个字段时,需要用到对象属性修改类型原子类

  • AtomicIntegerFielUpdater:原子跟新整形字段的跟新器
  • AtomicLongFieldUpdater:原子跟新长整形字段的跟新器
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

步骤:1、对象属性修改原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个跟新器,并且需要设置想要跟新的类和属性。2、跟新对象属性必须使用public volatile修饰符

import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class AtomicIntegerFieldUpdaterTest {
	public static void main(String[] args) {
		AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");

		User user = new User("Java", 22);
		System.out.println(a.getAndIncrement(user));// 22  获取到的是对象的那个属性值
		System.out.println(a.get(user));// 23
	}
}

class User {
	private String name;
	public volatile int age;   //这个属性值必须使用volatile修饰

	public User(String name, int age) {
		super();
		this.name = name;
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}

}

AQS的原理

1、abstractQueuedSynchronizer(AQS),这个类在java.util.concurrent.locks下面。AQS是用来构建锁和同步器的框架。java提供两种加锁机制,一种是关键字synchronized,另一种就是concurrent包下的lock锁。其中ReentrankLock就是AQS机制

AQS加锁例子

import java.util.concurrent.locks.ReentrantLock;

public class App {

    public static void main(String[] args) throws Exception {
        final int[] counter = {0};

        ReentrantLock lock = new ReentrantLock();

        for (int i= 0; i < 50; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    lock.lock();
                    try {
                        int a = counter[0];
                        counter[0] = a + 1;
                    }finally {
                        lock.unlock();
                    }
                }
            }).start();
        }

        // 主线程休眠,等待结果
        Thread.sleep(5000);
        System.out.println(counter[0]);
    }
}

开50个线程同时跟新counter

2、AQS原理

如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效线程,并且将共享资源设置为锁定状态。如果被请求的资源被占用,AQS通过CLH队列锁(双向链表,将线程封装成一个CLH锁队列的一个节点)实现线程阻塞等待以及被唤醒时锁的分配机制。

enter image description here

3、AQS的底层

img

img

从AQS的源码中可以看出,AQS底层就是一个双向链表+int类型状态,且他们的变量都是被transientvolatile修饰

img

AQS是如何获取锁的:

来看看ReentrantLock的lock方法:

final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

可以看出,其获取锁是通过CAS去修改state值,即通过CAS来拿取锁

因此AQS的实现总体可以表示为:使用int成员变量来表示同步状态,通过内置的FIFO队列来完成线程的阻塞排队。AQS通过CAS对该同步状态进行原子操作实现对其值得修改

3、AQS对资源的共享方式

  • Exclusive(独占):只有一个线程能执行,如ReentrantLock。又分为公平锁和非公平锁

    • 公平锁:按照线程进入队列的顺序执行,先到者先拿到锁
    • 非公平锁:当线程要获取锁时,无序队列顺序直接拿锁,先抢到先获取锁
  • Share(共享):多个线程同时执行,如Semaphore/CountDownLatch

自定义同步器在实现时只需要实现共享资源state的获取与释放即可,自定义同步器是基于模板方法的,一般方式如下:

  • 继承ABstractQueuedSynchronizer并重写指定的方法
  • 将AQS组合进同步实现组件中,并调用其模板方法,而这些模板方法会调用使用者重写的方法

AQS使用了模板方法,自定义同步器时需要重写AQS提供的模板方法

isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。

AQS类中的其它方法都是final,所以无法被其它类使用,只有这几个方法被其他类使用

以ReentrantLock为例,state初始化为0,表示未锁定状态,当有A线程lock时,调用tryAcquire()独占该锁并将state+1.此后其它线程再调用tryACquired方法时会失败,直到A线程unlock()释放锁,将state的值变为0,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryReleasetryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock

4、Semaphore(信号量)-允许多个线程同时访问某个资源

public class SemaphoreExample1 {
  // 请求的数量
  private static final int threadCount = 550;

  public static void main(String[] args) throws InterruptedException {
    // 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢)
    ExecutorService threadPool = Executors.newFixedThreadPool(300);
    // 一次只能允许执行的线程数量。
    final Semaphore semaphore = new Semaphore(20);

    for (int i = 0; i < threadCount; i++) {
      final int threadnum = i;
      threadPool.execute(() -> {// Lambda 表达式的运用
        try {
          semaphore.acquire();// 获取一个许可,所以可运行线程数量为20/1=20
          test(threadnum);
          semaphore.release();// 释放一个许可
        } catch (InterruptedException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
        }

      });
    }
    threadPool.shutdown();
    System.out.println("finish");
  }

  public static void test(int threadnum) throws InterruptedException {
    Thread.sleep(1000);// 模拟请求的耗时操作
    System.out.println("threadnum:" + threadnum);
    Thread.sleep(1000);// 模拟请求的耗时操作
  }
}

执行acquire方法阻塞,直到一个许可证可以获得并拿走许可证。Semaphore只是维持了一个可获得许可证数量

Semaphore的两种模式:

  • 公平模式:调用acquire的顺序就是获取许可证的顺序,遵顼FIFO
  • 非公平模式:抢占式的

两个构造方法:

 public Semaphore(int permits) {
        sync = new NonfairSync(permits);          //提供许可证的数量
    }

    public Semaphore(int permits, boolean fair) {          //提供许可证的数量以及决定是否是公平模式还是非公平。默认是非公平模式
          sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }

5、CountDownLatch(倒计时器)

同步工具类,它允许一个或多个线程一直等待,直到其它线程都操作执行完后再执行

用法:

  • 某一线程执行之前等待n个线程执行完毕。CountDownLatch的计数器初始化为n,每当一个任务线程执行完毕之后就将计数器减1(countdownlatch.countDown()),当计数器的值变为0时,在CountDownLatch上的await()的线程就会被唤醒。
  • 实现多个线程开始执行任务的最大并行性。做法是初始化一个共享的CountDownLatch对象,将计数器初始化为1,多个线程在开始执行任务前首先coundownlatch.await(),当主线程调用countDown()时,计数器变为0,多个线程同时被唤醒
  • 死锁检测:使用n个线程访问共享资源,在每次测试阶段的线程数目是不同的,并尝试产生死锁
public class CountDownLatchExample1 {
  // 请求的数量
  private static final int threadCount = 550;

  public static void main(String[] args) throws InterruptedException {
    // 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢)
    ExecutorService threadPool = Executors.newFixedThreadPool(300);
    final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
    for (int i = 0; i < threadCount; i++) {
      final int threadnum = i;
      threadPool.execute(() -> {// Lambda 表达式的运用
        try {
          test(threadnum);
        } catch (InterruptedException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
        } finally {
          countDownLatch.countDown();// 表示一个请求已经被完成
        }

      });
    }
    countDownLatch.await();
    threadPool.shutdown();
    System.out.println("finish");
  }

  public static void test(int threadnum) throws InterruptedException {
    Thread.sleep(1000);// 模拟请求的耗时操作
    System.out.println("threadnum:" + threadnum);
    Thread.sleep(1000);// 模拟请求的耗时操作
  }
}

从上可以看出,我们定义550个线程数,只有等这550的请求都处理了,主线程才会执行System.out.println(“finish”);

其他N个线程必须引用闭锁对象,因为他们需要通知CountDownLatch对象,他们已经完成了各自的任务。这种通知机制是通过 CountDownLatch.countDown()方法来完成的;每调用一次这个方法,在构造函数中初始化的count值就减1。所以当N个线程都调 用了这个方法,count的值等于0,然后主线程就能通过await()方法,恢复执行自己的任务。

不足:是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制可以再次对其设置值,当CountDownLatch使用完毕之后,它不能再次被使用了

6、CyclicBarrier(循环栅栏)

让一组线程到达一个同步点时被阻塞,直到最后一个线程达到同步点,屏障才开门,所有的线程才会继续工作。

构造方法为CyclicBarrier(int parties),其参数表示被屏障拦截的线程数,每个线程调用await方法告诉CyclicBarrier我已经达到屏障,然后当前线程被阻塞。

应用场景:多线程计算数据,最后合并计算结果的应用场景。

public class CyclicBarrierExample2 {
  // 请求的数量
  private static final int threadCount = 550;
  // 需要同步的线程数量
  private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);

  public static void main(String[] args) throws InterruptedException {
    // 创建线程池
    ExecutorService threadPool = Executors.newFixedThreadPool(10);

    for (int i = 0; i < threadCount; i++) {
      final int threadNum = i;
      Thread.sleep(1000);
      threadPool.execute(() -> {
        try {
          test(threadNum);
        } catch (InterruptedException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
        } catch (BrokenBarrierException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
        }
      });
    }
    threadPool.shutdown();
  }

  public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
    System.out.println("threadnum:" + threadnum + "is ready");
    try {
      /**等待60秒,保证子线程完全执行结束*/  
      cyclicBarrier.await(60, TimeUnit.SECONDS);
    } catch (Exception e) {
      System.out.println("-----CyclicBarrierException------");
    }
    System.out.println("threadnum:" + threadnum + "is finish");
  }

}
运行结果,如下:

threadnum:0is ready
threadnum:1is ready
threadnum:2is ready
threadnum:3is ready
threadnum:4is ready
threadnum:4is finish
threadnum:0is finish
threadnum:1is finish
threadnum:2is finish
threadnum:3is finish
threadnum:5is ready
threadnum:6is ready
threadnum:7is ready
threadnum:8is ready
threadnum:9is ready
threadnum:9is finish
threadnum:5is finish
threadnum:8is finish
threadnum:7is finish
threadnum:6is finish
......

7、CyclicBarrier和CountDownLatch的区别

CountDownLatch是计算器,只是用一次,而CyclicBarrier的计数器提供reset功能,可以多次使用。

CountDownLatch: A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.(CountDownLatch: 一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;)

CyclicBarrier : A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point.(CyclicBarrier : 多个线程互相等待,直到到达同一个同步点,再继续一起执行。)

CyclicBarrier和CountDownLatch的区别

8、ReentrantReadWriteLock 可以保证多个线程可以同时读,所以在读操作远大于写操作的时候,读写锁就非常有用了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值