java并发编程学习笔记

目录

 

一、说明:

二、java内存模型JMM

三、volatile关键字 

3.1、可见性验证demo

3.2、不保证原子性demo

3.3、有序性demo

四、锁

4.1、锁的常见种类介绍

4.2、synchronized关键字

4.3、juc.locks

        4.3.1、ReentrantLock

        4.3.2、ReentrantReadWriteLock

4.4、synchronized/ReentrantLock的比较

4.5、死锁

五、线程

5.1、线程创建方式

5.2、实现Callable接口示例

5.3、线程池的使用说明

5.4、线程池的优势

5.5、手动创建线程池

六、JUC

6.1、atomic包下的原子类

        6.1.1、AtomicInteger、AtomicReference、AtomicReferenceArray

        6.1.2、CAS思想及ABA问题

        6.1.3、ABA问题的解决 AtomicStampedReference

6.2、线程安全的集合

6.3、CountDownLatch、CyclicBarrier、Semaphore

6.4、阻塞队列(BlockingQueue)

七、生产/消费Demo

7.1、铁三角一   synchronized/wait/notify

7.2、铁三角二   lock/await/signal

7.3、阻塞队列


一、说明:

最近学习java并发编程,按照自己理解对并发编程进行了一下整理,仅供参考,如有问题,请留言指正。另外附上自己画的脑图,仅供参考(https://naotu.baidu.com/file/a5aacb2711f5a8177f64aa5954d44051?token=9e38cbba3c6c033b)。

这些年,我们的CPU、内存、硬盘(IO设备),都在不断的更新,多核cpu已是飞入寻常百姓家,但CPU的速度加快并不代表着整个程序的加快,CPU的运算速度远远大于内存的读取速度,而内存的读取速度又远远大于硬盘的IO速度,我们的程序运行不仅仅要计算,更需要更新内存中的数据,还需要进行IO操作进行数据读取和持久化,所以程序运行速度,取决于最慢的IO操作。

为了合理利用CPU的高性能,平衡三者之间的速度差异,各个方面都做出了贡献,CPU增加了缓存,依附于CPU上的,它比内存操作更快(可以采用CPUZ工具查看自己硬件的缓存,在java中工作内存基本上是使用该内存空间)以平衡CPU与内存直接的速度差异。操作系统增加了进程、线程,以分时复用CPU。我们的编译器会进行指令重排序,使得缓存能够更加合理的得到利用。这也恰恰导致了并发程序的问题,总的来说:1、可见性(因为每个线程操作时会将变量拷贝到自己的工作内存中进行操作,而工作内存是私有的,所以对别的线程不可见)2、原子性(cpu在执行某个操作时,会被可能其他线程打断)3、有序性(编译器重排序问题)。为了更好的学习多线程编程,我们需要先了解java的内存模型JMM。


二、java内存模型JMM

JMM(java memory model) 本身是一种抽象的概念并不真实存在,它描述的是一组规则或者规范,通过这组规范定义了程序中各个变量的访问方式。

JMM关于同步的规定:1、可见性  2、原子性  3、有序性

如下图所示:当线程A和线程B分别操作共享变量x时,A、B两个线程会分别将x的值从主内存(可以理解为我们的内存条)拷贝至自己的工作内存(可理解为CPU高级缓存)中,修改完成后再写回,因为A、B的工作内存是私有的,不可见,两个线程通讯必须依靠主内存完成,所以在写回主内存时,就可能出现覆盖的现象,即线程问题。

java并发编程学习笔记_第1张图片


三、volatile关键字 

volatile关键字是个轻量级同步机制,符合jmm规定的:1、可见性 3、有序性 ,不保证原子性。所以为轻量级。在juc包下被大量使用。使用该关键字不用整体锁定,能够提高并发性,但要注意其不保证原子性。

3.1、可见性验证demo

import java.util.concurrent.TimeUnit;

class VolatileVisibleData {
    
    public volatile int a = 0;
    
    public int b = 0;
    
    public void update() {
        a = 10;
        b = 10;
    }
    
}

/**
 * @author zhengyue
 * @date 2019-09-29 21:55
 */
public class VolatileVisibleTest {

    public static void main(String[] args) {
        VolatileVisibleData data = new VolatileVisibleData();
        //t1线程用于改变a、b的值
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t" + "线程启动");
            try {
                TimeUnit.SECONDS.sleep(3);//休眠3秒确保其他线程已经读取到a、b的值
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            data.update();
            System.out.println(Thread.currentThread().getName() + "\t" + "线程已结束"
                    + ";a:" + data.a + ";b:" + data.b);
        }, "t1").start();
        //t2线程用于验证被volatile修饰的变量a 具有可见性,所以当a被修改为10的时候t2线程不在循环直接结束
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t" + "线程启动");
            while (data.a == 0) {
            }
            System.out.println(Thread.currentThread().getName() + "\t" + "线程已结束");
        }, "t2").start();
        //t3线程用于验证没有被volatile修饰的变量b不具有可见性,所以当b被修改为10的时候t3线程仍在循环
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t" + "线程启动");
            while (data.b == 0) {
            }
            System.out.println(Thread.currentThread().getName() + "\t" + "线程已结束");
        }, "t3").start();
        
    }
    
}

运行结果:

t1	线程启动
t2	线程启动
t3	线程启动
t1	线程已结束;a:10;b:10
t2	线程已结束

3.2、不保证原子性demo

import java.util.concurrent.atomic.AtomicInteger;

class VolatileNoAtomicData {
    //a使用volatile修饰 不具有原子性
    public volatile int a = 0;
    //b是juc下的原子整形类,具有原子性
    public AtomicInteger b = new AtomicInteger(0);

    /**
     * 进行a++的操作,a++是个非原子性操作,根据jmm讲解的可知
     * 分为以下3步操作
     * 1、从主内存中读取数据到工作内存中
     * 2、在工作内存中进行自增长
     * 3、将值写回主内存
     */
    public void aAdd() {
        a++;
    }

    /**
     * b++的原子操作
     */
    public void bAdd() {
        b.getAndIncrement();
    }

}
/**
 * @author zhengyue
 * @date 2019-09-29 22:13
 */
public class VolatileNoAtomicTest {

    public static void main(String[] args) {
        VolatileNoAtomicData data = new VolatileNoAtomicData();
        for (int i = 0; i < 30; i++) {//循环创建30个线程
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {//每个线程a和b都自增1000次
                    data.aAdd();
                    data.bAdd();
                }
            }, ("t" + i)).start();
        }
        //主线程等待上面30个线程全部运行完成
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("a:" + data.a);//打印a的值,a使用volatile修饰由于不保证原子性,所以应该<=3w
        System.out.println("b:" + data.b);//打印b的值,b是原子引用,所以不论怎么加最后值都应该=3w

    }

}

运行结果:

a:29734//每次运行结果都不一样<=30000
b:30000//每次运行结果都=30000

3.3、有序性demo

因为有序性运行结果不明显,所以仅给出代码及说明,使用经典的单例模式的懒汉式进行说明。

/**
 * @author zhengyue
 * @date 2019-09-23 14:48
 */
public class SingletonDemo {

    /**
     * 此处的volatile 并不是保证可见性,而是保证有序性
     */
    private static volatile SingletonDemo singletonDemo = null;

    /**
     * 无惨构造方法,若单例模式起到作用则只会打印一次
     */
    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "正在创建对象");
    }

    /**
     * 不对整个方法加锁
     * 采用双端检索机制Double Check Lock (DCL)在锁两端进行检查
     * singletonDemo = new SingletonDemo();是个非原子操作
     * 被分为以下3步:
     * 1、划分内存空间  2、初始化对象  3、设置对象引用
     * 若不加volatile修饰,则当一个线程A,创建SingletonDemo时,先执行的3再执行2,
     * 此时对象已有引用,所以在另一个线程B进行第一层判断不为空,直接返回对象,在对象使用时就会
     * 报空指针异常。加上volatile关键字后会禁止指令重排,从而避免该情况发生
     * @return
     */
    public static SingletonDemo getSingletonDemo() {
        if (singletonDemo == null) {
            synchronized (SingletonDemo.class) {
                if (singletonDemo == null) {
                    singletonDemo = new SingletonDemo();
                }
            }
        }
        return singletonDemo;
    }

    /**
     * 普通get方法
     */
    public void test() {
        System.out.println("我是test方法!");
    }



}
class SingletonTest {

    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            new Thread(() ->{
                SingletonDemo singletonDemo = SingletonDemo.getSingletonDemo();
                singletonDemo.test();
            }, String.valueOf(i)).start();
        }
    }

}

四、锁

4.1、锁的常见种类介绍

1、公平锁:采用排队机制,当锁被占用时,线程自动排队,当锁使用完成后,由排在首部的线程使用锁。

2、非公平锁:可以插队,谁抢到谁使用。提高了效率,但容易造成线程饥饿

3、乐观锁:什么都是乐观的,在修改某个值时认为别的线程没有修改过,所以在取值时并不会被加锁,在修改完成后,更新回主内存时,再对比主内存中的值与取到的值。采用版本号和CAS算法实现。java.util.concurrent.atomic包下的原子类大都采用乐观锁的方式。适用于多读类型,可以提高吞吐量

4、悲观锁:认为什么都是悲观的,在修改某个值的时候一定会有别的线程来修改。所以在取值时就进行加锁,一直到写回主内存。sync/ReentrantLock就是典型的悲观锁

5、独占锁:独自占有,只能被占有锁的线程使用。sync/ReentrantLock都是独占锁

6、共享锁:共享锁锁定的资源可以被其它用户读取,但其它用户不能修改它。ReentrantReadWriteLock是共享锁

7、可重入锁(递归锁):当持有锁的线程进入同一锁方法时不需要重新获取锁。sync/Reentrantlock是典型的可重入锁最大作用避免死锁

8、自旋锁:自旋锁在执行单元在获取锁之前,如果发现有其他执行单元正在占用锁,则会不停的循环判断锁状态,直到锁被释放,期间并不会阻塞自己。由于在等待时不断的"自旋",这也是它为什么叫做自旋锁。所以自旋锁使用时,是非常消耗CPU资源的。

手写自旋锁demo:

import java.util.concurrent.atomic.AtomicReference;


public class SpinLockDemo {

    AtomicReference atomicReference = new AtomicReference<>();

    public void lock() {
        Thread thread = Thread.currentThread();
        while (!atomicReference.compareAndSet(null, thread)) {
        }
    }

    public void unlock() {
        atomicReference.compareAndSet(Thread.currentThread(), null);
    }

    private static int num = 0;

    public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                spinLockDemo.lock();
                change();
                spinLockDemo.unlock();
            }, ("t" + i)).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(num);
    }

    public static void change() {
        num++;
    }


}

9、互斥锁:执行单元等待锁释放时,会把自己阻塞并放入到队列中。当锁被释放时,会唤醒队列上执行单元把其放入就绪队列中,并由调度算法进行调度并执行。所以互斥锁使用时会有线程的上下文切换,这可能是非常耗时的一个操作,但是等待锁期间不会浪费CPU资源。

4.2、synchronized关键字

1、底层原理:使用Monitor 通过进入MONITORENTER,和MONITOREXIT退出来保证同步,而且为保证锁会被释放在正常和异常情况下均会被MONITOREXIT退出

2、使用方式:直接写在方法上或者使用代码块的方式,synchronized 在代码块开始和结束会自动加锁释放锁无需人工操作

        synchronized (object) {
        }

3、适用场景:适用于较少的代码同步,现很少使用,因为其可控性较差,同步后代码运行效率较差

4.3、juc.locks

        4.3.1、ReentrantLock

1、使用方式:

        Lock lock = new ReentrantLock();
        lock.lock();
        try {
            
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

2、 适用场景:适用于大量代码同步,可以通过tryLock()/lockInterruptibly()来定时终止线程等待,tryLock()可以设置等待时间,获取到锁返回true获取不到则返回false,lockInterruptibly()可以通过运行线程的Thread.interrupt方法来中断等待。同时还可以精确唤醒线程。

3、线程的精确唤醒使用方法demo


public class SyncAndReentrantLoackDemo {

    public static void main(String[] args) {
        ShareResource shareResource = new ShareResource();
        PrintEnum[] values = PrintEnum.values();
        for (PrintEnum pr: values) {
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    shareResource.print(pr);
                }
            }, pr.getName()).start();
        }

    }

}

class ShareResource {

    private int number = PrintEnum.THREAD_A.getNumber();//A 1,B 2,C 3
    private Lock lock = new ReentrantLock();
    private Condition conditionA = lock.newCondition();
    private Condition conditionB = lock.newCondition();
    private Condition conditionC = lock.newCondition();

    public void print(PrintEnum printEnum) {
        lock.lock();
        try {
            while (number != printEnum.getNumber()) {
                getCondition(printEnum).await();
            }
            System.out.println(Thread.currentThread().getName() + "\t" + "我爱你" + printEnum.getCount() + "遍");
            PrintEnum next = printEnum.next();
            number = next.getNumber();
            getCondition(next).signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    public Condition getCondition(PrintEnum printEnum) {
        if (printEnum.equals(PrintEnum.THREAD_A)) {
            return conditionA;
        } else if (printEnum.equals(PrintEnum.THREAD_B)) {
            return conditionB;
        } else if (printEnum.equals(PrintEnum.THREAD_C)) {
            return conditionC;
        }
        throw new IllegalArgumentException("没有合适的printEnum");
    }

}



@Getter
enum PrintEnum {

    THREAD_A(1, "A", 5), THREAD_B(2, "B", 10), THREAD_C(3, "C", 15);

    private int number;

    private String name;

    private int count;

    PrintEnum(int number, String name, int count) {
        this.number = number;
        this.name = name;
        this.count = count;
    }

    public PrintEnum next() {
        PrintEnum[] values = PrintEnum.values();
        for (int i = 0; i < values.length; i++) {
            if (this.equals(values[i])) {
                if (i < (values.length -1)) {
                    return values[i + 1];
                } else {
                    return values[0];
                }
            }
        }
        throw new IllegalArgumentException("没有合适的printEnum");
    }


}

        4.3.2、ReentrantReadWriteLock

ReentrantReadWriteLock读写锁,是一种共享锁,允许被锁定资源被别人读取,这样就大大提高了读的并发量。

demo

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;


public class ReadWriteLockDemo {

    public static void main(String[] args) {
        MyCache cache = new MyCache();
        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(() -> {
                cache.put(temp, temp);
            }, ("t" + i)).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("==================================");
        for (int i = 5; i < 10; i++) {
            final int temp = i;
            new Thread(() -> {
                cache.get(temp - 5);
            }, ("t" + i)).start();
        }

    }

}

class MyCache {

    private volatile Map map = new HashMap<>();

    private ReadWriteLock lock = new ReentrantReadWriteLock();

    public void put(Integer key, Integer val) {
        lock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t 线程正在写入:" + key);
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            map.put(key, val);
            System.out.println(Thread.currentThread().getName() + "\t 线程写入完成:" + key);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.writeLock().unlock();
        }
    }

    public void get(Integer key) {
        lock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t 线程正在读取:" + key);
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Integer val = map.get(key);
            System.out.println(Thread.currentThread().getName() + "\t 线程读取完成:" + val);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.readLock().unlock();
        }
    }


}

4.4、synchronized/ReentrantLock的比较

1、层面不一样,synchronized是系统关键字,ReentrantLock是juc下的类

2、使用方式不一样:synchronized会自动在代码块前后加锁及释放锁,ReentrantLock需要手动加锁及释放锁,所以忘记释放锁就容易导致死锁

3、性质不一样:synchronized是非公平的可重入锁,ReentrantLock是可公平(默认是非公平的,但可以通过构造参数fair=true改为公平锁)、可重入锁

4、synchronized是等待不可中断的,ReentrantLock是可中断的,可以通过tryLock()/lockInterruptibly()进行中断等待

5、ReentrantLock可以使用条件锁精确唤醒某些线程,而synchronized没有(代码演示见4.3.1、ReentrantLock)

4.5、死锁

1、什么是死锁:死锁就是两个及两个以上的线程,当A线程持有A锁的同时尝试获取B锁,B线程持有B锁的同时尝试获取A锁。

2、死锁的代码演示

class HoldThread implements Runnable {
    private String lockA;
    private String lockB;

    public HoldThread(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }


    @Override
    public void run() {
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName() + "正在持有锁" + lockA + "尝试持有锁" + lockB);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + "持有锁" + lockB);
            }
        }
    }
}



public class HoldThreadDemo {

    private volatile String aa;

    public static void main(String[] args) {
        String lockA = "A";
        String lockB = "B";

        new Thread(new HoldThread(lockA, lockB), "t1").start();
        new Thread(new HoldThread(lockB, lockA), "t2").start();
    }

}

3、 问题排查

首先使用jps -l命令查找出正在运行的java程序,使用jstack 进程号 打印堆栈信息查找问题,问题截图

java并发编程学习笔记_第2张图片


 

五、线程

5.1、线程创建方式

1、继承Thread类 (基本上不用,因为java类只能单继承)

2、实现Runable接口(不带返回值及错误信息)

3、实现Callable接口(带有返回值和报错信息。)

4、使用线程池(现在企业级都使用该方式获取线程,其优势见:5.4、线程池的优势)

5.2、实现Callable接口示例

class Test implements Callable {

    @Override
    public Integer call() throws Exception {
        System.out.println("进来了");
        return 10;
    }
}



public class MyTest001 {
    public static void main(String[] args) {
        FutureTask futureTask = new FutureTask(new Test());
       Thread test = new Thread(futureTask);
       test.start();
        try {
            System.out.println(futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

5.3、线程池的使用说明

线程池的创建有以下四种方式,其中1/2/3 是Executor的对应工具类Executors提供,附上源码,大家可以对比手动创建线程池对各个参数的解释,来查看源码,这样你就会明白,为什么公司级不会采用这三种方式了。

1、创建固定线程数的线程池 Executors.newFixedThreadPool(num);附上源码

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue());
    }

2、创建单个线程的线程池     Executors.newSingleThreadExecutor();附上源码

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue()));
    }

3、创建可扩容的线程池         Executors.newCachedThreadPool();附上源码

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue());
    }

4、手动创建线程池  demo

ExecutorService threadPool = new ThreadPoolExecutor(2, 5,
                1l, TimeUnit.SECONDS,new LinkedBlockingDeque<>(5),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy());

5.4、线程池的优势

1、降低资源消耗,重复利用线程,避免了线程被创建和销毁时的消耗
2、提高相应速度,当任务到达时,不需要创建线程就可立即执行
3、提高线程的可管理性,方便调优和监控

附上:阿里巴巴2019 java开发手册多线程开发的部分

java并发编程学习笔记_第3张图片

5.5、手动创建线程池

1、创建demo

ExecutorService threadPool = new ThreadPoolExecutor(2, 5,
                1l, TimeUnit.SECONDS,new LinkedBlockingDeque<>(5),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy());

2、ThreadPoolExecutor 构造器7参源码

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

 3、七大参数

a、corePoolSize:核心线程数,可以理解为线程池初始化的线程数,这些数量的线程不会被销毁。

b、maximumPoolSize:最大线程数,结合线程池工作的流程,当阻塞队列中等待任务也满时,线程池就会逐渐增加线程数,来分摊任务,增加线程到最大数量maximumPoolSize。

c、keepAliveTime:最大空闲时间,当线程数空闲达到规定的这个时间时,就会被销毁,直至线程数达到核心线程数

d、unit:keepAliveTime的单位,需要传入TimeUnit的枚举类型

e、workQueue:等待队列,阻塞队列,当任务占满核心线程后,后续任务将会在此等待

f、threadFactory:线程创建工厂,一般都使用默认Executors.defaultThreadFactory()

g、handler:拒绝策略,有四大拒绝策略,拒绝策略的意思是,当阻塞队列已满,线程数也增加到最大线程数,任务仍然处理不完,则会被执行拒绝策略,来拒绝后面的任务。

4、拒绝策略

a、new ThreadPoolExecutor.AbortPolicy()  默认的拒绝策略,直接抛异常

b、new ThreadPoolExecutor.CallerRunsPolicy() 将任务返回给调用线程,假如main调用线程池达到拒绝线程数后,会将任务返回给main线程执行

c、new ThreadPoolExecutor.DiscardPolicy()  随机丢弃掉线程

d、new ThreadPoolExecutor.DiscardOldestPolicy()   丢弃掉等待时间最久的线程

5、线程池工作流程(原理)

我们可以把线程池想象为银行,我们来到银行办理业务,假如银行当值窗口有3个(核心线程数),当人们来办理业务时,会挨个去这个三个窗口办理,当人越来越多时,就需要排队,排队人员领取号码牌后去等候区等候(阻塞队列),当等候区人员也满时,工作人员向领导请示,增加工作人员,人可以增加单窗口数量是固定的,假如窗口数为6(最大线程数),随着工作人员的增加,可能会出现两种情况:1、办理的很快,来办理业务的人也越来越少了,这时来加班的工作人员就会被空闲下来,当空闲到一定时间时(最大空闲时间),就会下班走人(线程销毁至核心数)2、增加人员后仍然办理不过来,来办理业务的人仍然是越来越多,这时工作人员就会把后来的人拒绝掉(拒绝策略)

线程池初始化核心线程数,当任务过来时就丢给核心线程去执行,随着任务增加,核心线程被全部占用时,后面任务就进入阻塞队列中进行等待,当阻塞队列中的任务也满时,就会增加线程来分摊任务,当增加到核心线程数后(此时阻塞队列已满),仍有处理不了的任务,就会启动拒绝策略。若随着线程增加任务被处理完成,当非核心线程空闲下来后,当空闲时间达到固定的最大空闲时间时就会被销毁,直至线程数达到核心线程数。


六、JUC

6.1、atomic包下的原子类

        6.1.1、AtomicInteger、AtomicReference、AtomicReferenceArray

AtomicReference是带有泛型的原子类,可以将任何类型包成原子的。AtomicReferenceArray是数组类型的。

以AtomicInteger的getAndIncrement()方法为例,说下atomic是如何保证原子性的,先贴上atomicInteger.getAndIncrement() (自身加一的操作)的源代码,结合源代码我们来看下其原理:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;
    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;
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
    public AtomicInteger() {
    }
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
}
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

 首先AtomicInteger有两个构造方法,无参的和传入int的,最终是控制的成员变量value,无参情况下value是int的默认值0。另外value使用了volatile修饰,使之具有可见性和有序性。getAndIncrement()方法,调用的是unsafe类的方法,(UnSafe类:java不能直接访问操作系统底层,而是通过本地方法来访问。Unsafe类提供了硬件级别的原子操作,其中使用了大量的native方法。)使用了getAndAddInt方法,该方法是做完操作后调用compareAndSwapInt方法(也即cas:比较并交换,如果内存中的值和预期值相同则修改并返回true,若不同则返回false),若返回false则会while判断后会重新获取内存中的值进行计算,直至比较成功(自旋的概念),compareAndSwapInt方法是原子的,是unsafe类cas的实现。这样就保证了原子性。

        6.1.2、CAS思想及ABA问题

cas:比较并交换(compare and swap),乐观锁就是根据这个来的,拷贝回自己内存时,不需要进行加锁,在写回内存时会比较期望值和主内存中的真实值,若比较成功就会更新主内存中的值,否则重新获取内存中的值,然后重新操作。

cas的优缺点:优点就是在保证原子性的时候,不需要加锁,能够提高并发量。

缺点:1、循环时间长时,系统资源消耗大 2、只能保证一个共享变量的原子操作 3、ABA问题

何为ABA问题,假设现有两个线程t1和t2,主内存中变量x=A,t1、t2线程都要对x进行操作,分别将x拷贝回自己的工作内存中,这时t1操作比较复杂,运行比较慢,t2运行较快,t2将x修改为B并将其写回主内存,t1仍在运行时,t2又将修改回A,这时t1运行完成要写回主内存时,比较认为期间并没有被修改。

        6.1.3、ABA问题的解决 AtomicStampedReference

ABA问题的解决思路是,添加版本号,对版本号进行比较。atomic类下的AtomicStampedReference就给我们提供了这样的方法。上源码:

构造方法

    public AtomicStampedReference(V initialRef, int initialStamp) {
        pair = Pair.of(initialRef, initialStamp);
    }

构造方法需要传入两个参数:initialRef:初始化值;initialStamp:版本号

    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

    private boolean casPair(Pair cmp, Pair val) {
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }

 compareAndSet(比较并交换)方法:四个参数分别是:expectedReference期望值、newReference更新值、expectedStamp期望版本号、newStamp更新后的版本号,除了会比较期望值还会比较版本号,两者都通过后

6.2、线程安全的集合

1、CopyOnWriteArrayList

线程安全的ArrayList,底层原理是写时复制,读写分离,写时复制加锁,写完后更新回源文件。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();
        }
    }

 add方法:先加锁获取到原有数组后,进行复制,并扩容1,添加完成后更新原有数组,这样不会影响其他线程读取集合。

2、CopyOnWriteArraySet

底层是使用CopyOnWriteArrayList实现的,调用CopyOnWriteArrayList 的 addIfAbsent(e)方法添加元素,来保证不重复。

注意set底层是用的HashMap 用其键做存储来保证不重复的。

    //CopyOnWriteArraySet的构造方法,可以看出就是使用的CopyOnWriteArrayList
    public CopyOnWriteArraySet() {
        al = new CopyOnWriteArrayList();
    }
//CopyOnWriteArraySet的add方法,调用CopyOnWriteArrayList的addIfAbsent
    public boolean add(E e) {
        return al.addIfAbsent(e);
    }
//CopyOnWriteArrayList的addIfAbsent 判断如果存在就返回false 否则添加
    public boolean addIfAbsent(E e) {
        Object[] snapshot = getArray();
        return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
            addIfAbsent(e, snapshot);
    }

3、ConcurrentHashMap:1.8 中的 ConcurrentHashMap 数据结构和实现与 1.7 还是有着明显的差异。

其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。具体请看这篇博客,或者网上有很多关于ConcurrentHashMap底层原理的解释(https://blog.csdn.net/weixin_40413816/article/details/82979744)

6.3、CountDownLatch、CyclicBarrier、Semaphore

CountDownLatch向下计数阻塞,当计数走到0时才能够往下继续,就好比打游戏:一共有10个小怪,你全部打死之后才会出大怪。

使用demo:

public class CountDownDemo {

    public static void main(String[] args) {
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                System.out.println("小怪" + Thread.currentThread().getName() + "号,被打死");
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("大怪出现。。。");
    }
}

CyclicBarrier 向上统计阻塞,向上统计到规定数值时才会被执行,比如打游戏,集宝图碎片,集齐10张宝图碎片后你会获得一张藏宝图。

public class CyclicBarrierDemo {

    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(10, () -> {
            System.out.println("恭喜获得藏宝图一张");
        });
        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                System.out.println("收集到" + Thread.currentThread().getName() + "号宝图碎片");
                try {
                    barrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }
}

 Semaphore 信号灯,用于多个线程抢多个资源的情况,比如:小米手机秒杀的时候,多个电脑一同抢99部手机。或者10辆车抢3个车位,同一时间只能有三个车抢到车位,当一辆车开走后另一辆车才能停进去。

使用demo

public class SemaphoreDemo {

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "\t" + "抢占到车位");
                    int random = NumberUtils.random(1, 5);
                    try {
                        TimeUnit.SECONDS.sleep(random);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "\t" + "停车" + random + "秒后离开车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }

            }, ("t" + i)).start();
        }
    }
}

6.4、阻塞队列(BlockingQueue

阻塞队列的使用方法: 

类型 插入 移除 检查
抛异常 add(e) remove() element()
阻塞 put(e) take()
特殊值 offer(e) poll() peek()
超时  offer(e,t,u) poll(t,u)

阻塞队列有七种,介绍一下常用的三种

ArrayBlockingQueue(num) 由数组组成的有界阻塞队列

LinkedBlockingDeque<>()  由链表组成的有界阻塞队列,但默认接线是Integer.MAX_VALUE

SynchronousQueue<>()   不存储元素的阻塞队列,也即单个元素队列

具体使用方法可以看生产者消费者的demo 7.3

-----------------------------------------剩余四种补充说明----------------------------------------------------------

PriorityBlockingQueue<>()支持优先级排序的无界阻塞队列

DelayQueue<>()  使用优先级队列实现的延迟无界阻塞队列

LinkedTransferQueue<>()  由链表组成的无界阻塞队列

LinkedBlockingDeque<>()  由链表组成的双向阻塞队列


七、生产/消费Demo

7.1、铁三角一   synchronized/wait/notify


class ShareData01 {

    private int number = 0;

    private static int max = 10;

    public synchronized void producer() {
        while (number >= max) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        number++;
        System.out.println(Thread.currentThread().getName() + "\t 生产一个,当前总数:" + number);
        this.notifyAll();
    }

    public synchronized void consumer() {
        while (number <= 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        number--;
        System.out.println(Thread.currentThread().getName() + "\t 消费一个,当前总数:" + number);
        this.notifyAll();
    }


}


public class TraditionDemo01 {

    public static void main(String[] args) {
        ShareData01 shareData = new ShareData01();
        new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                shareData.producer();
            }
        }, "t1").start();

        new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                shareData.consumer();
            }
        }, "t2").start();



    }

}

7.2、铁三角二   lock/await/signal

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

class ShareData02 {

    private int number = 0;

    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void producer() {
        lock.lock();
        try {
            lock.lockInterruptibly();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            while (number != 0) {
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "\t 生产一个,当前总数:" + number);
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    public void consumer() {
        lock.lock();
        try {
            while (number == 0) {
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + "\t 消费一个,当前总数:" + number);
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }


}

/**
 * 1、线程                    操作(调用方法)  资源类(类)
 * 2、判断(不符合条件则休眠)  干活(业务逻辑)   通知(唤醒其他线程工作)
 * 3、防止虚假唤醒机制(用while 代替 if 判断)
 */
public class TraditionDemo02 {

    public static void main(String[] args) {
        ShareData02 shareData = new ShareData02();
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                shareData.producer();
            }, ("p" + i)).start();
        }
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                shareData.consumer();
            }, ("c" + i)).start();
        }



    }

}

7.3、阻塞队列

import org.apache.commons.lang3.StringUtils;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

class ShareData03 {

    private volatile boolean flag = true;

    private volatile AtomicInteger atomicInteger = new AtomicInteger();

    private BlockingQueue queue = null;

    public ShareData03(BlockingQueue queue) {
        this.queue = queue;
        System.out.println(queue.getClass().getName() + "阻塞队列");
    }

    public void producer() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "\t" + "生产线程启动");
        boolean offer = false;
        while (flag) {
            offer = queue.offer("蛋糕" + (atomicInteger.get() + 1) + "号", 4l, TimeUnit.SECONDS);
            if (offer) {
                atomicInteger.incrementAndGet();
                System.out.println("生产成功,当前库存:" + atomicInteger.get());
            } else {
                System.out.println("生产失败!当前库存:" + atomicInteger.get());
            }
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("生产线程结束!");
    }

    public void consumer() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "消费线程开启");
        String poll = null;
        while (flag) {
            poll = queue.poll(2l, TimeUnit.SECONDS);
            if (StringUtils.isNotBlank(poll)) {
                atomicInteger.decrementAndGet();
                System.out.println("消费" + poll + "成功!当前库存:" +  atomicInteger.get());
            } else {
                System.out.println("消费失败,当前阻塞队列中没有任何东西,当前库存" + atomicInteger.get());
                flag = false;
                return;
            }
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
        System.out.println("消费者队列已经结束!");
    }

    public void stop() {
        flag = false;
    }

}

/**
 * 1、线程                    操作(调用方法)  资源类(类)
 * 2、判断(不符合条件则休眠)  干活(业务逻辑)   通知(唤醒其他线程工作)
 * 3、防止虚假唤醒机制(用while 代替 if 判断)
 */
public class TraditionDemo03 {

    public static void main(String[] args) {
        ShareData03 shareData03 = new ShareData03(new ArrayBlockingQueue(3));

        test01(shareData03);

        try {
            TimeUnit.SECONDS.sleep(8);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println();
        System.out.println();
        System.out.println();
        shareData03.stop();
        System.out.println("程序停止!");



    }

    public static void test02(ShareData03 shareData03) {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                try {
                    shareData03.producer();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, ("p" + i)).start();
        }
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                try {
                    shareData03.consumer();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, ("c" + i)).start();
        }
    }

    public static void test01(ShareData03 shareData03) {
        new Thread(() -> {
            try {
                shareData03.producer();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "Prod").start();

        new Thread(() -> {
            try {
                shareData03.consumer();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "Consumer").start();
    }

}

 

你可能感兴趣的:(java,java并发编程)