java并发笔记

进程与线程关系

1、进程与线程均是CPU执行时间段的描述。

2、进程是资源分配的基本单位,线程是CPU调度的基本单位。

3、一个进程里至少有一个线程。

4、同一进程里的各个线程可以共享变量,它们之间的通信称之为线程间通信。

5、线程可以看作粒度更小的进程。

线程的状态

1、NEW:构造了thread实例,但是还没有start

2、RUNNABLE:线程正在运行或者正等待被cpu执行

3、BLOCKED:线程调用synchronized关键字等待获取monitor锁

4、WAITING:线程调用了无超时的wait、join、park方法

5、TIMED_WAITING:线程调用了有超时的wait、sleep、join、parkNanos、parkUntil方法

6、TERMINATED:线程终止/完成了运行

join和yield

Join是Thread类成员方法,该方法作用是当前线程等待某个线程结束后再继续执行后续代码。

Yield是Thread类静态方法,该方法作用是让当前线程放弃cpu,并等待cpu重新调度,只能让给优先级比自己高的。

中断

线程的thread.interrupt()方法是中断线程,将会设置该线程的中断状态位,即设置为true,中断的结果线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身。线程进入终止状态有两种情况:

1.run方法运行结束

2.run方法中抛出异常。

如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、thread.wait、1.5中的condition.await、以及可中断的通道上的 I/O 操作方法后可进入阻塞状态),则在线程在检查中断标示时如果发现中断标示为true,则会在这些阻塞方法(sleep、join、wait、1.5中的condition.await及可中断的通道上的 I/O 操作方法)调用处抛出InterruptedException异常,并且在抛出异常后立即将线程的中断标示位清除,即重新设置为false。抛出异常是为了线程从阻塞状态醒过来,并在结束线程前让程序员有足够的时间来处理中断请求。

synchronized在获锁的过程中是不能被中断的,意思是说如果产生了死锁,则不可能被中断(请参考后面的测试例子)。与synchronized功能相似的reentrantLock.lock()方法也是一样,它也不可中断的,即如果发生死锁,那么reentrantLock.lock()方法无法终止,如果调用时被阻塞,则它一直阻塞到它获取到锁为止。但是如果调用带超时的tryLock方法reentrantLock.tryLock(long timeout, TimeUnit unit),那么如果线程在等待时被中断,将抛出一个InterruptedException异常,这是一个非常有用的特性,因为它允许程序打破死锁

thread.isInterrupted() 和 Thread.interrupted()区别

Thread.interrupted()调用后会重置中断状态为false,相当于清空了中断标志位。而thread.isInterrupted()却不会清空中断标志位。

中断的作用

1、将中断标记为设置为true

2、将挂起的线程唤醒

如何停止线程

1、外部通过设置isCancelThread 来标记是否让线程停止运行。这种写法有个缺陷,若是线程在调用doSomething1() 或doSomething2()时阻塞,那么将不会立即检测isCancelThread的值,也即是不能立即停止线程。

public class TestThread {
    static volatile boolean isCancelThread = false;
    public static void main(String args[]) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while(!isCancelThread || !Thread.currentThread().isInterrupted()) {
                    try {
                        doSomething1();
                        doSomething2();
                    } catch (Exception e) {
                        if (e instanceof InterruptedException) {
                            isCancelThread = true;
                        }
                    }
                }
            }
        });

        t1.start();

        //另一个线程两种方式停止线程
        //1、设置isCancelThread = true
        //2、调用t1.interrupt()
    }

    private static void doSomething1() {
        //具体逻辑
    }

2、针对doSomething1() 或doSomething2()可能阻塞的问题,外部通过使用
Thread.interrupt()中断线程,此时需要捕获异常,捕获到了中断异常意味着可以停止线程运行了。

while(!Thread.currentThread().isInterrupted()) {
        try {
            doSomething1();
            doSomething2();
        } catch (Exception e) {
            if (e instanceof InterruptedException) {
                Thread.currentThread().interrupt();
            }
        }
    }
Volatile

线程并发三要素:原子性,可见性,有序性

new Thread(() -> {
            int oldValue = sharedVariable;
            while (sharedVariable < MAX) {
                if (sharedVariable != oldValue) {
                    System.out.println(Thread.currentThread().getName() + " watched the change : " + oldValue + "->" + sharedVariable);
                    oldValue = sharedVariable;
                }
            }
            System.out.println(Thread.currentThread().getName() + " stop run");
        }, "t1").start();

        new Thread(() -> {
            int oldValue = sharedVariable;
            while (sharedVariable < MAX) {
                System.out.println(Thread.currentThread().getName() + " do the change : " + sharedVariable + "->" + (++oldValue));
                sharedVariable = oldValue;
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + " stop run");
        }, "t2").start();

结果发现:

t2将sharedVariable改变为10,t1只有一次打印

t2线程停止,t1未停止,程序没有停止运行

但是在t1里增加打印或者sleep,能够让t1监测到sharedVariable的变化

造成这种原因是:JIT发现这里是重复判断“sharedVariable != oldValue”,因此优化了代码:只在第一次取sharedVariable的值,后续不再取最新值,然后一直死循环,最终导致程序没退出。而增加了println或者sleep后,JIT取消了优化。

cpu实现了缓存一致性协议,尽可能保证cache之间数据一致性。但是为了充分利用cpu性能,增加了Store Buffer和Invalidate Queue缓存,导致cpu可能产生指令顺序不一致问题,看起来像是指令重排了,通过增加内存读写屏障来规避此问题。

MESI通过加入内存屏障指令来确保数据顺序一致性,那么这个内存屏障是什么时候插入呢?实际上在编译为汇编语句的时候,当JVM发现有变量被volatile修饰时,会加入LOCK前缀指令,这个指令的作用就是增加内存屏障。这就是为什么有了MESI协议,我们还需要volatile的原因之一。因为volatile变量读写前后插入内存屏障指令,所以cache之间的数据能够及时感知到,共享变量就达到了线程间可见性,也就是说volatile修饰的变量具有线程间可见。

Unsafe

常用的功能:

  • 对象操作
  • CAS
  • 线程挂起/唤醒
对象操作

Java 虽然屏蔽了指针,但是底层还是通过指针访问的。因此,只要获取了对象在内存中的地址,找到其中字段在对象里的偏移,就可以访问相应的字段。

class Student {
        int age;
        char name;

        private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
        private static final long AGE;

        static {
            try {
                //AGE 为age变量在Student对象里的偏移量
                AGE = U.objectFieldOffset
                        (Student.class.getDeclaredField("age"));
            } catch (ReflectiveOperationException e) {
                throw new Error(e);
            }
        }

        //改变age的值
        private void setAge(int age) {
            U.putInt(this, AGE, age);
        }
    }

1、通过Unsafe获取age字段在Student对象里的偏移量

2、通过对象基准地址+偏移量就可以定位到age字段,进而可以访问(读/写)

3、此处是拿到偏移量后通过Unsafe修改

CAS

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作

  1. ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

  1. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

  2. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

锁的本质

需要理解线程互斥和线程同步的区别

线程互斥只需要一个阻塞队列,利用CAS去获取锁,获取锁成功的进入临界区,而没有成功的线程则插入阻塞队列的队尾,当释放锁的时候,则从队列的头部取出唤醒,继续竞争锁。

而线程同步则多了一个同步队列。

当只有一个变量的时候,我们可以采用CAS可以实现互斥的访问共享变量。

static AtomicInteger a = new AtomicInteger(0);
    private static void inc() {
        a.incrementAndGet();
    }

但是当多个变量的时候,而且每个变量都需要互斥怎么处理,这个时候我们把多个变量看做一个临界区,当多个线程操作的时候,只有获取到锁的才能进入临界区操作。其它没拿到"锁"的线程一直尝试拿"锁",当拥有锁的线程退出临界区后释放"锁",其它就可以拿到"锁"了。

但是这又有问题,不只是两个线程竞争锁,而是很多线程同时竞争锁。临界区执行时间很长,锁很难被释放出来。那么没获取到锁的线程一直无限循环去尝试获取,如此一来很浪费CPU。在这种情况下,我们采取线程挂起/唤醒策略。将挂起的线程放入队列里,当另一个线程释放锁后从队列里取出当初被挂起的线程并唤醒它,被唤醒的线程继续去竞争锁。

前面讲的可以理解为多个线程互斥访问临界区,这些线程对临界区的操作仅仅是互斥,并没有其它依赖关系。

1、当需要等待的时候,走红色线条部分。将线程放入到等待队列里、释放锁,并唤醒阻塞队列里的线程。

2、当需要通知的时候,走绿色线条部分,将线程从等待队列里移除,并将之加入到阻塞队列里。

3、线程同步的过程比线程互斥过程新增了等待队列,该队列存储着因某种条件而挂起等待的线程。当另一个线程发现条件满足后,通知等待队列里的线程,让它继续做事。

所以可以简单的理解为,锁是CAS + 阻塞队列+等待队列一起合作实现线程同步。

Synchronized
synchronized 各种使用方式

1、无论是修饰方法还是代码块,最终都是获取对象锁(类锁是Class对象的锁)

2、实例方法与对象锁获取的是同一把锁(普通对象锁)

3、静态方法与类锁获取的是同一把锁(类锁-Class对象锁)

synchronized

monitorenter 表示获取锁

monitorexit 表示释放锁

两者之间的操作就是被锁住的临界区

其中monitorexit 有两个,后面一个是发生异常时会执行

修饰方法和代码块的区别:

1、修饰代码块时编译后会在临界区前后加入monitorenter、monitorexit 指

2、修饰方法时进入/退出方法时会判断ACC_SYNCHRONIZED 标记是否存在

3、不管是用monitorenter/monitorexit 还是ACC_SYNCHRONIZED,最终都是在对象头上做文章,都需要获取锁。

对象头

new 出来的对象放在堆里,而对象在堆里的结构为:对象头、实例数据(age/name)、填充字节。

对象头可以分为:MarK Word,Klass Word,和数组长度,其中数组长度只有数组对象才会有数组长度部分。

Mark Word有五种状态,biased_lock占1bit,来控制偏向锁与非偏向锁,lock 占2bits,来控制四种状态,

代表状态 lock
无锁 01
偏向锁 01
轻量级锁 00
重量级锁 10
GC标记 11

轻量级锁:线程间交替执行临界区的情形下使用的锁

偏向锁:当锁偏向某个线程时,该线程再次获取锁无需CAS

重量级锁:一个线程没有获取到锁然后挂起自己,等其他的线程释放锁后唤醒自己,而线程的挂起、唤醒需要CPU切换上下文,此过程代价比较大,称为重量级锁。

轻量级锁与重量级锁的区别:

1、每次加锁只需要一次CAS

2、不需要分配ObjectMonitor对象

3、线程无需挂起与唤醒

偏向锁相比轻量级锁的优势:

同一个线程多次获取锁时,无需再次进行CAS,只需要简单比较。

public class TestDemo {
    public static void main(String args[]) {
        Object object = new Object();
        //打印虚拟机的信息
        System.out.println(VM.current().details());
        //打印对象大小
        System.out.println(ClassLayout.parseInstance(object).instanceSize());
        //打印对象头大小
        System.out.println(ClassLayout.parseInstance(object).headerSize());
        //打印对象信息
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
    }
}

这种情况下为无锁状态。

public class TestDemo {
    public static void main(String args[]) {
        Object object = new Object();
        //打印对象信息
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
        synchronized (object) {
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
        }
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
    }
}

上锁前后都是无锁状态,上了锁后是轻量级锁。

JVM 启动的时候没有立即开启偏向锁,而是延迟开启。延迟时间是4s

public class TestDemo {
    public static void main(String args[]) {
        try {
            Thread.sleep(4500);
        } catch (Exception e) {

        }
        Object object = new Object();
        //打印对象信息
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
        synchronized (object) {
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
        }
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
    }
}

偏向锁一旦开启了,默认就是偏向锁。退出临界区后,还是偏向锁。

public class TestDemo {
    static Object object = new Object();
    public static void main(String args[]) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("before get lock in Thread1");
                System.out.println(ClassLayout.parseInstance(object).toPrintable());
                synchronized (object) {
                    System.out.println("after get lock in Thread1");
                    System.out.println(ClassLayout.parseInstance(object).toPrintable());
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            System.out.println("before get lock in Thread2");
                            System.out.println(ClassLayout.parseInstance(object).toPrintable());
                            synchronized (object) {
                                System.out.println("after get lock in Thread2");
                                System.out.println(ClassLayout.parseInstance(object).toPrintable());
                            }

                        }
                    }, "t2").start();

                    sleep(5000);
                }
            }
        }, "t1").start();
    }
}

t1未获取锁之前为无锁,t1获取锁之后为轻量级锁,t2获取锁之前为轻量级锁,t2获取锁之后则为重量级锁。

偏向锁的加锁过程

偏向锁里存储了偏向线程的id,epoch,偏向锁标记(biased_lock),锁标记(lock)等信息。这些信息统称为Mark Word。

1、先判断Mark Word里的线程id是否有值。

1.1、如果没有,说明还没有线程占用锁,则直接将t1的线程id记录到Mark Word里。可能会存在多个线程同时修改Mark Word,因此需要进行CAS修改Mark Word。

1.2、如果已有id值,那么判断分两种情况:

1.2.1、该id是t1的id,则此次获取锁是个重入的过程,直接就获取了。

1.2.2、如果该id不是t1的id,说明已经有其它线程获取了锁,t1想要获取锁就需要走撤销流程。

重量级锁的原理

当锁处在轻量级锁的状态时,Mark Word 存放着指向Lock Record指针,Lock Record是线程私有的。Mark Word 存放着指向ObjectMonitor的指针,ObjectMonitor是线程间共享的并且拥有比Lock Record更多的信息。

锁的膨胀过程

创建、获取ObjectMonitor对象的过程既是锁膨胀过程。

  1. 判断是不是重量锁,是的话直接返回,不是继续判断是否正在膨胀。
  2. 正在膨胀则等待膨胀完,continue,如果不是正在膨胀,则判断是不是轻量级锁。
  3. 如果是轻量级锁,通过CAS将Mark word指向ObjectMonitor,并将_owner指向Lock Record,然后返回,如果不是轻量级锁,即无所状态,通过CAS将Mark word指向ObjectMonitor,并将_owner置空。然后返回重量级锁对象。
加锁过程

先CAS尝试修改ObjectMonitor的_owner字段,会有几种结果:
1、锁没被其它线程占用,当前线程成功获取锁。

2、锁被当前线程占用,当前线程重入该锁,获取锁成功。

3、锁被LockRecord占用,而LockRecord又属于当前线程,属于重入,重入次数为1。

4、以上条件都不满足,调用EnterI()函数。

5、多次尝试加锁。(再次加锁失败后,尝试10次自旋加锁)

6、自旋加锁还是失败,将线程包装后加入到阻塞队列里。

7、再尝试获取锁

8、失败后将自己挂起。

9、被唤醒后继续尝试获取锁

10、成功则退出流程,失败继续走上面的流程

释放锁的过程
  1. 先判断是否有重入,有重入,直接释放锁。
  2. 没有重入,先释放锁,然后判断——cxq与EntryList是否有线程在等待呗唤醒。没有的话则直接释放锁完成
  3. 有则再次获取锁,失败则吃方所完成,
  4. 成功则从EntryList的头节点取,取出成功则释放锁,并且唤醒线程。
  5. 失败则从cxq取头节点,并将EntryList指向_cxq的头。

1、加锁过程是不断地尝试加锁,实在不行了才放入队列里,而且还是插入队列头的位置,最后才挂起自己。

2、想象一种场景:现在A线程持有锁,B线程在队列里等待,在A释放锁的时候,C线程刚好插进来获取锁,还未等B被A唤醒,C就获取了锁,B苦苦等待那么久还是没有获取锁。B线程不排队的行为造成了不公平竞争锁。

3、再想象另一种场景:还是A线程持有锁,B线程在队列里等待,此时C线程也要获取锁,因此要进入队列里排队,此处进入的是队列头,也就是在B的前面排着。当A释放锁后,唤醒队列里的头节点,也就是C线程。C线程插队的行为造成了不公平竞争锁。

4、综合1、2、3点可知,因为有走后门(不排队)\、插队(插到队头)、重量级锁是不公平锁。

总结图
微信图片_20210917174119.jpg
AQS

第一、得需要共享变量作为"锁芯"。由于这个共享变量是多线程共享,为保证线程间的可见性,因此需要用volatile关键字修饰。

第二、当线程竞争锁成功时则进入临界区执行代码,当失败时需要加入到队列里进行等待,因此需要一个同步队列,用以存放因获取锁失败而挂起的线程。

第三、线程之间需要同步,A线程等待B线程生产数据,B线程生产了数据通知A线程,因此需要一个等待(条件)队列。

数据结构准备好之后,需要操作以上数据结构来实现锁功能。

线程竞争锁:通过CAS操作"锁芯",操作成功则执行临界区代码,失败则加入到同步队列。

线程被唤醒:拿到锁的线程执行完临界区代码后释放锁,并唤醒同步队列里等待的线程,被唤醒的线程继续竞争锁。

线程等待某个条件:线程因为某种条件不满足于是加入到等待队列里,释放锁,并挂起等待。 (使用condition才会初始化这个队列)

条件满足线程被唤醒:条件满足,线程被唤醒后继续竞争锁。

独占模式(EXCLUSIVE)

如图所示,有1,2,3三个线程,假设1先拿到锁,此时将state设置为1,并将当前线程的名字设置进去。然后2进来争用锁,相当于acquire操作,调用getState,发现state的值为1,表示锁已经被别人拿了,所以则会初始化一个同步队列,初始化一个头结点,然后将线程2封装成一个Node节点,放在头结点的后面,并将头结点的waitStatus变成SIGNAL,将线程2放入队列的时候有两步操作,1,入队列,2申请入队。而在申请入队的时候会有一次争用锁的机会,如果在这个时候拿到锁,就不需要入队列。而将waitStatus变成SIGNAL表示头结点的下一个节点是需要唤醒的Node。

release操作:

释放锁的时候,则将2唤醒,需要下面三个步骤:

1、检查当前线程是否和持有锁的线程是同一个线程

2、修改当前锁的状态,置为0

3、唤醒队列中头结点的下一个节点的线程

唤醒动作:

1、将自己的waitStatus设置成0

2、真正的唤醒操作,调用LockSupport.unpark(s.thread)
共享模式

acquire操作

1、共享模式可以被多个线程同时拿到,具体同步器的实现类,是否允许,看每个同步器自己的实现。

2、节点的waitStatus设置成 SHARED

3、只修改state字段,不设置持有锁的线程(区别)

release操作:

1、修改当前锁的状态,置为0

2、唤醒队列中头结点的下一个节点的线程

唤醒动作:

1、将自己的waitStatus设置成0

2、真正的唤醒操作,调用LockSupport.unpark(s.thread)

3、取当前被唤醒的节点的一个节点,如果下一个节点也是SHARED模式,则直接唤醒。


微信图片_20220215103500.jpg
Condition

在前面的基础上多加了一个条件队列。调用await的时候,创建条件节点,必要的时候初始化条件队列或者吧自己加入条件队列中,释放当前线程所占有的锁,执行AQS的release操作,然后挂起当前线程。调用single操作的时候,将条件队列中的节点(firstWaiter)转移到同步队列中。将加入同步队列中的条件节点的waitStatus变成SIGNAL,因为在同步队列的waitStatus为CONDITION。

其他的跟前面的操作一模一样。

微信图片_20220215103512.jpg
v2-02b2de58d34b5f021f3981eabe3b8638_r.jpg
v2-3c14a01499d9af34eeda4ec392d36065_r.jpg

关于ReentrantLock的源码分析请参考:

AQS源码分析

原子类
 /**
     * LongAdder
     * LongAdder克服了高并发下使用AtomicLong的缺点。既然AtomicLong的性能瓶颈是由于过多线程同时去竞争一个变量的更新而产生的,LongAdder则是把一个变量分解为多个变量,让同样多的线程去竞争多个资源,解决了性能问题。
     * LongAdder 是把一个变量拆成多份,变为多个变量,有点像 ConcurrentHashMap 中 的分段锁把一个Long型拆成一个base变量外加多个Cell,每个Cell包装了一个Long型变量。
     * 这样,在同等并发量的情况下,争夺单个变量更新操作的线程量会减少,这变相地减少了争夺共享资源的并发量。
另外,多个线程在争夺同一个Cell原子变量时如果失败了, * 它并不是在当前Cell变量上一直自旋CAS重试,而是尝试在其他Cell的变量上进行CAS尝试,这个改变增加了当前线程重试CAS成功的可能性。 * 最后,在获取LongAdder当前值时,是把所有Cell变量的value值累加后再加上base返回的。LongAdder维护了一个延迟初始化的原子性更新数组(默认情况下Cell数组是null)和一个基值变量base。 * 由于Cells占用的内存是相对比较大的,所以一开始并不创建它,而是在需要时创建,也就是惰性加载。 */ @RequiresApi(api = Build.VERSION_CODES.N) private void AtomicExample9() { AtomicLongTest(); LongAdderTest(); } @RequiresApi(api = Build.VERSION_CODES.N) private void LongAdderTest() { int requestTotal = 100; final LongAdder count = new LongAdder(); final CountDownLatch countDownLatch = new CountDownLatch(requestTotal); long start = System.currentTimeMillis(); for (int i = 0; i < requestTotal; i++) { new Thread(new Runnable() { @Override public void run() { try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } add(count); countDownLatch.countDown(); } }).start(); Log.e("zzf","count=" + count); Log.e("zzf","耗时:" + (System.currentTimeMillis() - start)); } try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } } @RequiresApi(api = Build.VERSION_CODES.N) private void add(LongAdder count) { count.add(1); } private void AtomicLongTest() { // 并发线程数 int requestTotal = 500; // 求和总数 final int sumTotal = 1000000; final AtomicLong count = new AtomicLong(0); ExecutorService executorService = Executors.newFixedThreadPool(requestTotal); final CountDownLatch countDownLatch = new CountDownLatch(requestTotal); long start = System.currentTimeMillis(); for (int i = 0; i < requestTotal; i++) { executorService.execute(new Runnable() { @Override public void run() { add2(sumTotal,count); countDownLatch.countDown(); } }); } Log.e("zzf","count=" + count); Log.e("zzf","耗时:" + (System.currentTimeMillis() - start)); try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } executorService.shutdown(); } private void add2(int sumTotal,AtomicLong count) { for (int j = 0; j < sumTotal; j++) { count.getAndIncrement(); } } /** * AtomicStampedReference * 原子更新带有版本号的引用类型,该类将整数值与引用关联起来,可用于原子的更新数据和数据版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题。 */ private void AtomicExample8() { /** * 1表示版本号 */ final AtomicStampedReference atomicStampedReference = new AtomicStampedReference<>(99, 1); Thread t1 = new Thread(new Runnable() { @Override public void run() { int stamp = atomicStampedReference.getStamp(); Log.e("zzf",Thread.currentThread().getName() + "---首次 stamp: " + stamp); atomicStampedReference.compareAndSet(99, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); Log.e("zzf",Thread.currentThread().getName() + "---第二次 stamp: " + atomicStampedReference.getStamp()); atomicStampedReference.compareAndSet(100, 99, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); Log.e("zzf",Thread.currentThread().getName() + "---第三次 stamp: " + atomicStampedReference.getStamp()); } },"t1"); Thread t2 = new Thread(new Runnable() { @Override public void run() { int stamp = atomicStampedReference.getStamp(); Log.e("zzf",Thread.currentThread().getName() + "---首次 stamp: " + stamp); try { Log.e("zzf",Thread.currentThread().getName() + "---你的校园网正在尝试重新连接......"); TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } boolean result = atomicStampedReference.compareAndSet(99, 100, stamp, stamp + 1); Log.e("zzf",Thread.currentThread().getName() + "---修改成功与否:" + result + " 当前 stamp:" + atomicStampedReference.getStamp()); Log.e("zzf",Thread.currentThread().getName() + "---当前课程已选人数:" + atomicStampedReference.getReference()); } },"t2"); t1.start(); t2.start(); } /** * AtomicMarkableReference 通过 boolean 值作为是否更改的标记,所以他的版本号只有 true 和false。在并发中如果两个版本号不断地切换, * 任然不能很好地解决 ABA 问题,只是从某种程度降低了ABA事件发生。 * AtomicMarkableReference只有true和false两种状态,如果来回切换,也不能很好的解决ABA问题。所以引入AtomicStampedReference。 */ private void AtomicExample7() { Teacher teacher = new Teacher( "小春哥",200); AtomicMarkableReference markableReference = new AtomicMarkableReference<>(teacher, true); Teacher newTeacher = new Teacher("懵懂少年",210); Log.e("zzf","当前返回状态: "+markableReference.compareAndSet(teacher, newTeacher, true, false)); Log.e("zzf","AtomicMarkableReference 状态: "+markableReference.isMarked()); Teacher twoTeacher = new Teacher("懵懂少年",201); Log.e("zzf","当前返回状态: "+markableReference.compareAndSet(teacher, newTeacher, true, false)); Log.e("zzf","AtomicMarkableReference 状态: "+markableReference.isMarked()); } /** * 允许原子更新指定类的指定voltile字段 * 更新原子对象的对象有两个条件 * 1:因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。 * newUpdater(Teacher.class, String.class, "name"):表示想要更新Teacher类中name字段,name字段是String类型,所以传入String.class */ private void AtomicExample6() { AtomicReferenceFieldUpdater referenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(Teacher.class, String.class, "name"); Teacher teacher = new Teacher("小春哥", 200); referenceFieldUpdater.compareAndSet(teacher, "小春哥", "公众号:山间木匠"); Log.e("zzf",teacher.getName() + "----------" + teacher.getTicketNum()); } /** * 原子更新引用类型,更新一个对象 */ @RequiresApi(api = Build.VERSION_CODES.N) private void AtomicExample5() { AtomicReference atomicReference = new AtomicReference<>(); BinaryOperator binaryOperator = new BinaryOperator() { @Override public Teacher apply(Teacher teacher, Teacher teacher2) { return teacher2; } }; Teacher teacher = new Teacher("小春哥", 200); // 将当前对象设置到引用对象 AtomicReference 中 atomicReference.set(teacher); Teacher updateTeacher = new Teacher("懵懂少年", 180); // teacher 和 引用类型AtomicReference 保存的对象一致 则能修改成功 atomicReference.compareAndSet(teacher, updateTeacher); Log.e("zzf",atomicReference.get().getName() + "----------" + atomicReference.get().getTicketNum()); Teacher accumulateTeacher = new Teacher("懵懂少年", 210); // 原子性地更新指定对象,并且返回AtomicReference更新后的值 atomicReference.accumulateAndGet(accumulateTeacher, binaryOperator); Log.e("zzf",atomicReference.get().getName() + "----------" + atomicReference.get().getTicketNum()); } /** * 数组 */ @RequiresApi(api = Build.VERSION_CODES.N) private void AtomicExample4() { AtomicLongArray arr = new AtomicLongArray(5); LongUnaryOperator longUnaryOperator = new LongUnaryOperator() { @Override public long applyAsLong(long operand) { return operand + 10; } }; LongBinaryOperator longBinaryOperator = new LongBinaryOperator() { @Override public long applyAsLong(long left, long right) { return left + right; } }; /** * 自增 */ Log.e("zzf","索引 0 incrementAndGet=" + arr.getAndIncrement(0)); Log.e("zzf","索引 0 incrementAndGet=" + arr.getAndIncrement(0)); /** * 自减 */ Log.e("zzf","索引 0 incrementAndGet=" + arr.decrementAndGet(0)); Log.e("zzf","索引 0 incrementAndGet=" + arr.decrementAndGet(0)); /** * 以原子方式将输入的数值与实例中的值(AtomicLongArray(0)里的value)相加 */ Log.e("zzf","索引 0 addAndGet=" + arr.addAndGet(0, 100)); Log.e("zzf","*********** JDK 1.8 ***********"); /** * 返回新值 */ Log.e("zzf","索引 0 getAndUpdate=" + arr.updateAndGet(0, longUnaryOperator)); /** * 返回旧值 */ Log.e("zzf","索引 0 getAndUpdate=" + arr.getAndUpdate(0, longUnaryOperator)); /** * 使用给定函数应用给指定下标和给定值的结果原子更新当前值,并返回当前值 */ Log.e("zzf","索引 1 accumulateAndGet=" + arr.accumulateAndGet(1, 10, longBinaryOperator)); } /** * 原子 */ private void AtomicExample2() { int requestTotal = 10; final CountDownLatch countDownLatch = new CountDownLatch(requestTotal); long start = System.currentTimeMillis(); for (int i = 0; i < requestTotal; i++) { new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } //add(); add1(); countDownLatch.countDown(); } }).start(); } try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } Log.e("zzf","count=" + count); Log.e("zzf","count=" + count1); Log.e("zzf","耗时:" + (System.currentTimeMillis() - start)); } private void add() { ++count; } private void add1() { count1.getAndIncrement(); } /** * AtomicLong通过CAS提供了非阻塞的原子性操作,相比使用阻塞算法的同步器来说它的性能已经很好了,但是JDK开发组并不满足于此。 * 使用AtomicLong时,在高并发下大量线程会同时去竞争更新同一个原子变量,但是由于同时只有一个线程的CAS操作会成功, * 这就造成了大量线程竞争失败后,会通过无限循环不断进行自旋尝试CAS的操作,而这会白白浪费CPU资源。 */ @RequiresApi(api = Build.VERSION_CODES.N) private void AtomicExample1() { AtomicLong count = new AtomicLong(0); LongUnaryOperator longUnaryOperator = new LongUnaryOperator() { @Override public long applyAsLong(long operand) { return 1; } }; LongBinaryOperator longBinaryOperator = new LongBinaryOperator() { @Override public long applyAsLong(long left, long right) { return left + right; } }; /** * getAndIncrement:以原子方式将当前值加1,返回旧值 (i++) */ Log.e("zzf","getAndIncrement=" + count.getAndIncrement()); /** * 以原子方式将当前值加1,返回新值(++i) */ Log.e("zzf","incrementAndGet=" + count.incrementAndGet()); /** * 以原子方式将当前值减少 1,返回旧值(i--) */ Log.e("zzf","incrementAndGet=" + count.getAndDecrement()); /** * /以原子方式将当前值减少 1,返回旧值 (--i) */ Log.e("zzf","incrementAndGet=" + count.decrementAndGet()); /** * 以原子方式将输入的数值与实例中的值(AtomicLong里的value)相加,并返回结果 */ Log.e("zzf","addAndGet=" + count.addAndGet(10)); /** * 以原子方式设置为`newValue`的值,并返回旧值 */ Log.e("zzf","getAndSet=" + count.getAndSet(100)); Log.e("zzf","get=" + count.get()); Log.e("zzf","*********** JDK 1.8 ***********"); /** * 使用将给定函数定函数的结果原子更新当前值,返回上一个值 */ Log.e("zzf","getAndUpdate=" + count.getAndUpdate(longUnaryOperator)); Log.e("zzf","getAndUpdate=" + count.getAndUpdate(longUnaryOperator)); Log.e("zzf","get=" + count.get()); /** *使用给定函数应用给当前值和给定值的结果原子更新当前值,返回上一个值 */ Log.e("zzf","getAndAccumulate=" + count.getAndAccumulate(2, longBinaryOperator)); Log.e("zzf","getAndAccumulate=" + count.getAndAccumulate(2, longBinaryOperator)); }
Semaphore/CountDownLatch/CyclicBarrier
/**
     * semaphore
     *
     * 可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。
     * 适用于那些资源有明确访问数量限制的场景,常用于限流 。
     */
    private void AtomicExample12() {
        final Semaphore semaphore=new Semaphore(10);
        for(int i=0;i<20;i++){
            Thread thread=new Thread(new Runnable() {
                public void run() {
                    try {
                        System.out.println("===="+Thread.currentThread().getName()+"来到停车场");
                        if(semaphore.availablePermits()==0){
                            Log.e("zzf","车位不足,请耐心等待");
                        }
                        semaphore.acquire();//获取令牌尝试进入停车场
                        Log.e("zzf",Thread.currentThread().getName()+"成功进入停车场");
                        Thread.sleep(new Random().nextInt(1000));//模拟车辆在停车场停留的时间
                        Log.e("zzf",Thread.currentThread().getName()+"驶出停车场");
                        semaphore.release();//释放令牌,腾出停车场车位
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },i+"号车");

            thread.start();
        }
    }

    /**
     * CyclicBarrier与CountDownLatch的区别
     *
     * CyclicBarrier的计数器由自己控制,而CountDownLatch的计数器则由使用者来控制,在CyclicBarrier中线程调用await方法不仅会将自己阻塞还会将计数器减1,
     * 而在CountDownLatch中线程调用await方法只是将自己阻塞而不会减少计数器的值。
     *
     * CountDownLatch只能拦截一轮,而CyclicBarrier可以实现循环拦截。
     *
     * CountDownLatch是线程组之间的等待,即一个(或多个)线程等待N个线程完成某件事情之后再执行;而CyclicBarrier则是线程组内的等待,
     * 即每个线程相互等待,即N个线程都被拦截之后,然后依次执行。
     */

    /**
     * CyclicBarrier,CyclicBarrier 基于 Condition 来实现的。
     *
     * 在CyclicBarrier类的内部有一个计数器,每个线程在到达屏障点的时候都会调用await方法将自己阻塞,此时计数器会减1,
     * 当计数器减为0的时候所有因调用await方法而被阻塞的线程将被唤醒。
     */
    private void AtomicExample11() {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5, new Runnable() {
            @Override
            public void run() {
                Log.e("zzf","都到了");
            }
        });

        Thread_01[] threads = new Thread_01[5];
        for (int i = 0; i < threads.length; i++){
            threads[i] = new Thread_01(cyclicBarrier);
            threads[i].start();
        }
    }

    /**
     * CountDownLatch
     * 类似于join,可以使一个获多个线程等待其他线程各自执行完毕后再执行。CountDownLatch 定义了一个计数器,和一个阻塞队列, 当计数器的值递减为0之前,阻塞队列里面的线程处于挂起状态,
     * 当计数器递减到0时会唤醒阻塞队列所有线程,这里的计数器是一个标志,可以表示一个任务一个线程,也可以表示一个倒计时器,
     * CountDownLatch可以解决那些一个或者多个线程在执行之前必须依赖于某些必要的前提业务先执行的场景。
     *
     * 在下面的例子中,主线程会等待四个子线程执行完再执行。
     *
     * CountDownLatch 基于 AQS 的共享模式的使用。
     */
    private void AtomicExample10() {
        //用于聚合所有的统计指标
        final Map map=new HashMap();
        //创建计数器,这里需要统计4个指标
        final CountDownLatch countDownLatch=new CountDownLatch(4);
        //记录开始时间
        long startTime=System.currentTimeMillis();
        Thread countUserThread=new Thread(new Runnable() {
            public void run() {
                try {
                    System.out.println("正在统计新增用户数量");
                    Thread.sleep(3000);//任务执行需要3秒
                    map.put("userNumber",1);//保存结果值
                    countDownLatch.countDown();//标记已经完成一个任务
                    Log.e("zzf","统计新增用户数量完毕");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread countOrderThread=new Thread(new Runnable() {
            public void run() {
                try {
                    System.out.println("正在统计订单数量");
                    Thread.sleep(3000);//任务执行需要3秒
                    map.put("countOrder",2);//保存结果值
                    countDownLatch.countDown();//标记已经完成一个任务
                    Log.e("zzf","统计订单数量完毕");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread countGoodsThread=new Thread(new Runnable() {
            public void run() {
                try {
                    System.out.println("正在商品销量");
                    Thread.sleep(3000);//任务执行需要3秒
                    map.put("countGoods",3);//保存结果值
                    countDownLatch.countDown();//标记已经完成一个任务
                    Log.e("zzf","统计商品销量完毕");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread countmoneyThread=new Thread(new Runnable() {
            public void run() {
                try {
                    System.out.println("正在总销售额");
                    Thread.sleep(3000);//任务执行需要3秒
                    map.put("countmoney",4);//保存结果值
                    countDownLatch.countDown();//标记已经完成一个任务
                    Log.e("zzf","统计销售额完毕");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        //启动子线程执行任务
        countUserThread.start();
        countGoodsThread.start();
        countOrderThread.start();
        countmoneyThread.start();
        try {
            //主线程等待所有统计指标执行完毕
            countDownLatch.await();//这个在哪个线程,就阻塞哪个线程,等待其他线程执行完再执行
            long endTime=System.currentTimeMillis();//记录结束时间
            Log.e("zzf","------统计指标全部完成--------");
            Log.e("zzf","统计结果为:"+map.toString());
            Log.e("zzf","任务总执行时间为"+(endTime-startTime)/1000+"秒");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
参考文章

https://www.jianshu.com/p/5334a131151e

你可能感兴趣的:(java并发笔记)