Java并发程序设计

99 阅读15分钟

1、ArrayList、HashMap线程不安全

2、Collections.synchronizedList() 生成线程安全的同步容器,底层使用synchorized

3、Java提供的同步容器还有Vector、Stack和Hashtable,基于synchronized实现

同步容器、并发容器

并发容器:List、Map、Set和Queue

1、JDK并发容器

1、List

ArrayList和vector内部都是使用数组实现的,区别:ArrayList非线程安全;Vector线程安全

LinkedList内部使用链表实现,非线程安全

1.1、CopyOnWriteArrayList【高效读取】

原理:修改数组元素的时候会复制出一个新数组,对新数组进行操作,将指针指向新数组

写入不会阻塞读取操作、写入和写入之间需要同步等待

迭代器是只读的

内部有一个ReentrantLock lock

    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);  // 将数组指向新数组,数组使用volatile修饰,修改完保证读取可见
            return true;
        } finally {
            lock.unlock();
        }
    }
    

hashMap是非线程安全的,如果想使用线程安全的HashMap

2.1、Collections.synchronizedMap()

 Map<Integer, String> map = Collections.synchronizedMap(new HashMap<Integer, String>()) //生成一个SynchronizedMap 的Map,构造函数包含一个HashMap

实现原理:所有的方法上面都有synchronized (mutex) {...} 每一次操作都需要获取mutex锁

  private static class SynchronizedMap<K,V>
         implements Map<K,V>, Serializable {
         private static final long serialVersionUID = 1978198479659022715L;
         private final Map<K,V> m;     // Backing Map
         final Object      mutex;        // Object on which to synchronize
         .....
 }

2.2、ConcurrentHashMap

key、value不能为null

实现原理:todo

32.3、ConcurrentSkipListMap【重点】

key、value不能为null

实现原理:底层是跳表的数据结构,类似于平衡树,但是与平衡树还是存在区别

跳表的数据结构:多层链表,最底层的链表维护了跳表内所有元素,每上面一层都是全部元素的子集,所有链表元素key都是排序的

image.png

详细描述跳表的插入删除:juejin.cn/post/684490…

3、Set

3.1、CopyOnWriteArraySet

3.2、ConcurrentSkipListSet

4、Queue

阻塞、非阻塞(Blocking):当队列满时入队操阻塞;当队列空时出队操作阻塞

单端(Queue)、双端(Deque):单端-队尾入队,队首出队;双端-对尾队首都可以出队入队

4.1、单端阻塞

包含:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、LinkedTransferQueue、PriorityBlockingQueue和DelayQueue

BlockingQueue,是一个接口

image.png

a、ArrayBlockingQueue:内部持有队列是数据,支持有界

存放对象的是对象数组final Object[] items

 void put(E e) throws InterruptedException;  // 压入元素并且通知notEmpty
 boolean offer(E e);  // 压入元素
 E poll();  // 从队列头弹出元素,队列为空返回null
 E take() throws InterruptedException;  // 从队列头弹出元素,队列为空一直等待

取值:使用take()取元素时,若队列为空,线程在notEmpty的Condition上进入阻塞等待状态;取出元素后会唤醒在notFull

添加元素:put()添加元素时完成添加之后会执行signal()唤醒在notEmpty上的线程;如果队列已满线程则会在notFull的Condition上进入阻塞等待状态

b、LinkedBlockingQueue:内部持有队列是链表,支持有界

SynchronousQueue:不持有队列

LinkedTransferQueue:融合了LinkedBlockingQueue、SynchronousQueue的功能

PriorityBlockingQueue:支持按照优先级出队

DelayQueue:支持延时出队

4.2、双端阻塞

LinkedBlockingDeque

4.3、单端非阻塞【重点】

ConcurrentLinkedQueue:队列使用链表结构存储,在高并发环境中性能最好的队列。

实现原理:todo 难点

   public boolean offer(E e) {
         checkNotNull(e);
         final Node<E> newNode = new Node<E>(e);
         for (Node<E> t = tail, p = t;;) {
             Node<E> q = p.next;
             if (q == null) {
                 //p是最后的节点
                 if (p.casNext(null, newNode)) {
                     // p,casNext() 成功将newNode添加到链表尾部
                     if (p != t) // p!=t 代表着此时t代表的还不是尾部节点,意味着别的线程可能已经t的指向变更了
                         casTail(t, newNode);  // 将t指到最新的尾部节点
                     return true; 
                 }
                 // CAS竞争失败,再次尝试
             }
             else if (p == q)
                 // We have fallen off list.  If tail is unchanged, it
                 // will also be off-list, in which case we need to
                 // jump to head, from which all live nodes are always
                 // reachable.  Else the new tail is a better bet.
                 p = (t != (t = tail)) ? t : head;
             else
                 // Check for tail updates after two hops.
                 p = (p != t && t != (t = tail)) ? t : q;
         }
     }

4.4、双端非阻塞

ConcurrentLinkedDeque

2、无锁工具类

互斥锁与无锁的对比

无锁的实现原理:CAS(Compare And Swap)指令。只有当目前count的值和期望值expect相等时,才会将count更新为newValue

只有count=expect的时候才会操作count=newValue

自旋:循环尝试。重新读取最新的值并进行newValue的计算

Java原子类:AtomicLong、getAndIncrement()、addAndGet(n)方法内部就是基于CAS实现的

 do {
   // 获取当前值
   oldV = xxxx;
   // 根据当前值计算新值
   newV = ...oldV...
 }while(!compareAndSet(oldV,newV);

2.1、原子化基本数据类型

AtomicBoolean、AtomicInteger和AtomicLong

2.2、原子化的对象引用类型

AtomicReference、AtomicStampedReference和AtomicMarkableReference

AtomicStampedReference:解决ABA问题的思路--增加一个版本号维度

2.3、原子化数组类型

AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray

3、线程池

3.1、如何创建线程池

优势:避免了重复创建、销毁线程的动作

创建一个线程系统都做了哪些事情?todo

JDK提供了一套Executor框架

image.png

Executors是线程池工厂,支持快速创建线程池。ps:大厂现在不支持使用,因为Executors里面大部分使用的都是无界队列

 ExecutorService newFixedThreadPool(int nThreads); // 生成线程数个数固定的线程池
 ExecutorService newSingleThreadExecutor();     // 只有一个线程的线程池
 ExecutorService newCachedThreadPool();    // 根据实际情况调整线程数的线程池
 ScheduledExecutorService newSingleThreadScheduledExecutor();   // 单个线程
 ScheduledExecutorService newScheduledThreadPool(int corePoolSize);  //创建一个线程池,可以安排命令在给定延迟后运行,或定期执行[计划任务]。

ScheduledExecutorService:计划执行。

 ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);  // 在给定的delay[unit]后对任务进行调度
 ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit); // 以上一个任务的开始时间为起点,间隔period时间后,调用下一次任务
 ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit); // 上一个任务结束后,经过delay时间进行任务调度

3.2、线程池核心参数

线程池在创建的时候内部都是使用:ThreadPoolExecutor 进行创建的

 ThreadPoolExecutor(
   int corePoolSize,    // 线程池保有的最小线程数
   int maximumPoolSize,   // 线程池创建的最大线程数
   long keepAliveTime,    // 空闲线程空闲的时间
   TimeUnit unit,        // 空闲线程空闲时间单位
   BlockingQueue workQueue,  // 工作队列
   ThreadFactory threadFactory,   // 自定义如何创建线程,创建线程的工厂类
   RejectedExecutionHandler handler   // 任务拒绝策略
 )   

ThreadFactory:线程池中创建线程的工厂

ThreadPoolExecutor提供的四种拒绝策略:

  • CallerRunsPolicy:提交任务的线程自己去执行该任务。
  • AbortPolicy:默认的拒绝策略,会throws RejectedExecutionException。
  • DiscardPolicy:直接丢弃任务,没有任何异常抛出。
  • DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。

这几个拒绝策略都实现了RejectedExecutionHandler接口,该接口有一个rejectedExecution方法,可以自定义拒绝策略

线程池执行过程中最重要的是接收异常并进行处理,因为某个任务出现异常会导致后面的任务都无法执行

 try {
   //业务逻辑
 } catch (RuntimeException x) {
   //按需处理
 } catch (Throwable x) {
   //按需处理
 } 

线程池的工作队列:

1、ArrayBlockingQueue 基于数组的有界阻塞队列

2、LinkedBlockingQueue 基于链表阻塞队列,可设置队列长度

3、SynchronousQueue 不存储元素的阻塞队列,先出后进

4、PriorityBlockingQueue 一个有优先级的无限阻塞队列

5、DelayQueue 一个任务定时周期的延迟执行的队列

创建线程池的时候,返回值是ExecutorService,ExecutorService继承Executor,Executor的方法包含以下几个:

 void execute(Runnable command);    // Executor接口的方法
 // 提交Runnable任务
 Future<?> submit(Runnable task);   // ExecutorService方法,Runnable接口run()方法没有返回值
 // 提交Callable任务
 <T> Future<T> submit(Callable<T> task);    // ExecutorService方法,Callable接口的call()方法带有返回值
 // 提交Runnable任务及结果引用  
 <T> Future<T> submit(Runnable task, T result);   // ExecutorService方法

image.png

Q:为什么是先添加队列而不是先创建最大线程?

  • 队列作为一个有限的缓冲区,并且阻塞队列自带阻塞和唤醒功能,无任务执行时有take方法挂起,不占用cpu资源
  • 创建新线程要获取全局锁,其他线程就要阻塞,影响效率

Q:线程池.execute()、submit() 有什么区别

execute:有返回值。只支持提交Runnable的任务

submit:无返回值。支持Runnable、Callable的任务,但是Runnable的返回值始终都是void;通过get()获取结果的时候catch异常

Q:如何评估合适的线程数量?

3.3、Fork/Join框架

JDK中提供了一个ForkJoinPool线程池

 <T> ForkJoinTask<T> submit(ForkJoinTask<T> task);  // 往forkjoin线程池提交一个forkjoin任务

image.png

1、forkjoin有四个submit方法,区别于线程池,多了一个ForkJoinTask的submit方法

2、forkjoin使用一个无锁的栈来管理空闲线程

3、空闲的线程会从繁忙的线程等待任务队列的尾部取出任务进行执行

forkjoin任务:支持分解和join等待的任务

image.png

 abstract class RecursiveAction extends ForkJoinTask<Void>  // 无返回值
 abstract class RecursiveTask<V> extends ForkJoinTask<V>    // 返回V类型

使用方式:可以自定义任务实现以上两个抽象类任务,重写compute()方法进行任务的计算执行逻辑。在compute()方法中可以通过任务.fork()方法分解子任务,最终循环所有任务,通过.join()方法获取所有任务的结果

 public class ForkJoinTest extends RecursiveTask<Long> {
     @Override
     protected Long compute() {
        
         List<ForkJoinTest> taskList = Lists.newArrayList();
         for (int i = 0; i < 100; i++) {
             // ... 一系列判断,创建个子任务
             ForkJoinTest task = new ForkJoinTest();
             task.fork();  // 子任务发起调用
         }
         for (ForkJoinTest task : taskList) {
             task.join(); // 获取子任务结果
         }
 ​
         return null;
     }
 }

4、Java并发基础

进程:

线程:程序执行的最小单位,线程之间切换调度成本小

线程生命周期:Thread.State枚举

wait操作:runnable->waiting状态

线程调用sleep之后会进入什么状态

4.1、创建线程

1、继承Thread

 public class MyThread extends Thread {
     public static void main(String[] args) {
         MyThread myThread = new MyThread();
         myThread.start();
     }
 
     @Override
     public void run() {
         System.out.println("【Thread】Hello:" + Thread.currentThread().getName());
     }
 }

2、实现Runnable接口

 public class MyRunnable implements Runnable {
     @Override
     public void run() {
         System.out.println("【Runnable】Hello:" + Thread.currentThread().getName());
     }
 
     public static void main(String[] args) {
         MyRunnable runnable = new MyRunnable();
         Thread thread = new Thread(runnable);
         thread.start();
     }
 }

调用start的含义:告知Java虚拟机,县城规划期空闲时应立即启动调用了start()方法的线程

4.2、线程中断

B线程调用A线程的interrupt()方法对A进行中断。A线程有一个标识位属性,标记为已中断

A可以调用isInterrupted()判断是否被中断

Thread.interrupted()对当前线程中断标志位进行复位

许多抛出InterruptedException异常的方法,在抛出该异常前会进行中断标识位的清除,例如线程A执行操作紧接着调用了Thread.sleep(n),此时B中调用了A线程的interrupt()方法,然后调用A的Thread.interrupted()方法,此时会返回false

4.3、suspend、resume、stop

suspend():线程的暂停

resume():线程的回复

stop():线程的终止

接口已经过期了,不建议使用

4.4、正确的中断线程

1、线程显式设置标识位flag,通过设置一个cancel方法将flag设置为false,实现run()方法的中断

2、通过interrupt()方法

4.5、等待通知机制

等待通知是任意的Java对象都具备的,所以wait、notify是定义在Object上

image.png

使用wait()、notify()、notifyAll()的时候需要先调用对象A加锁,在线程中调用对象A的wait()、notify()、notifyAll()方法

4.6、ThreadLocal【重点】

1、是什么

ThreadLocal是一个线程的局部变量,只有当前线程可以访问,因为只有当前线程可以访问,所以是线程安全的。

2、什么情况下使用

在多线程操作同一个对象的时候,如果是只有一个实例,会造成调用实例时的阻塞等待,如果为每个线程都创建一个实例,线程内可用,一是不会出现线程不安全的问题;二是会提高多线程下的执行效率

  • 事务操作情况下可以使用ThreadLocal存储线程事务信息
  • 数据库连接的Session会话管理

3、使用方式

 public class ThreadLocalTest {
     private static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<>();
 ​
     static class ThreadMy implements Runnable {
         private int i = 0;
 ​
         public ThreadMy(int i) {
             this.i = i;
         }
 ​
         @Override
         public void run() {
             // 取出当前线程的ThreadLocal的实例
             SimpleDateFormat curThreadTl = tl.get();
             if (Objects.isNull(curThreadTl)) {
                 tl.set(new SimpleDateFormat("yyyyMMdd:HH:mm:ss"));
             }
             // 使用该实例执行业务逻辑
             System.out.println("Thread i:" + i + tl.get().format(System.currentTimeMillis()));
         }
     }
 ​
     public static void main(String[] args) {
         // 创建线程池,解析日期
         ExecutorService pool = Executors.newFixedThreadPool(10);
         for (int i = 0; i < 1000; i++) {
             pool.execute(new ThreadMy(i));
         }
     }
 }

4、实现原理

其中比较重要的几个方法分别是get()、set()

 public T get() {
     // 获取当前线程引用标识
     Thread t = Thread.currentThread();
     // 从Thread中取出ThreadLocalMap(存储的是线程所有的“局部变量”),ThreadLocalMap是一个Entry<k,v>结构
     ThreadLocalMap map = getMap(t);
     if (map != null) {
         ThreadLocalMap.Entry e = map.getEntry(this);
         if (e != null) {
             @SuppressWarnings("unchecked")
             T result = (T)e.value;
             return result;
         }
     }
     return setInitialValue();
 }
 ​
 public void set(T value) {
     // 取出当前线程的引用标识
     Thread t = Thread.currentThread();
     // 获取线程的ThreadLocalMap
     ThreadLocalMap map = getMap(t);
     if (map != null)
         // set 当前线程对应的value值
         map.set(this, value);
     else
         createMap(t, value);
 }

5、会导致内存泄露

原因:

正确做法:

4.7、守护线程

1、怎么设置

 thread.setDaemon(true);  // 在start()调用前设置守护为true
 thread.start();

2、特征

  • 用户线程执行完成之后,当前只有守护线程时程序会随之结束,即便守护线程还在while(true) {...}
  • 垃圾回收线程就是一个守护线程

4.8、线程安全和synchorized

1、什么是线程安全

多个线程操作同一个共享变量的时候操作的结果与预期一致

哪个区域的空间存在线程安全的问题:堆

堆是进程、线程共有的空间。所有线程都可访问到进程中的堆空间,会造成线程安全问题

栈是线程独有的,保存运行状态和局部变量的,是线程安全的

2、volatile修饰的一定安全吗

不一定。volatile只能保证一个线程修改之后,其他线程可见,在两个线程同时对变量进行赋值的时候会产生冲突,thread1和thread2同时对变量a(此时是1)++,执行完之后a=2,实际预期应该3

3、synchorized的使用

  • 给指定对象加锁。
  • 作用于实例方法。相当于对当前实例加锁
  • 作用于静态方法。相当于对当前类加锁

4、可重入锁【ReentrantLock】与synchorized的区别,可重入锁的优势:

  • 可重入锁对逻辑控制灵活性远好于synchorized,显式的加锁释放锁,lock()、unlock()
  • 中断响应。避免死锁,reentrantLock.lockInterruptibly(),使用线程thread.interrupt()执行线程的中断
  • 锁申请等待限时。避免线程长时间拿不到锁陷入无限时等待。reentrantLock.tryLock(等待时长,计时单位),不带单位情况下如果获取不到锁立即返回false
  • 公平锁。synchorized锁是非公平的。可重入锁可设置公平锁,构造函数中设置fair属性为true, new ReentrantLock(true)

公平锁:按照时间顺序,保证先到者先得,后到者后得

4.9、JDK并发包

4.9.1、Condition条件

1、什么场景下使用?

Condition条件是配合可重入锁使用的

2、如何使用?

  • 可重入锁 ReentrantLock的newCondition()方法可以生成一个Condition
  • await()方法:是当前线程进入等待,释放当前锁
  • awaitUninterruptbily()方法:在await基础上,不会在等待过程中响应中断
  • singal()方法:唤醒一个Condition等待队列中的一个线程。condition.singal() 唤醒线程的时候需要先持有锁才能执行singal方法,执行完成之后释放锁

3、有什么使用案例?

ArrayBlockingQueue的put和take方法

4.9.2、信号量Semaphore

1、作用?

线程之间协作的一种方式,信号量可以允许多个线程同时访问某一个资源。

synchorized、重入锁ReentrantLock都是一次只允许一个线程访问一个资源,线程之间串行。

2、如何使用?

 // 构造函数
 public Semaphore(int permits) {  // permits:准入许可数,代表可进入的线程数
     sync = new NonfairSync(permits);
 }
 
 public Semaphore(int permits, boolean fair) {
     sync = fair ? new FairSync(permits) : new NonfairSync(permits);
 }
 
 // 主要方法
 public void acquire(){}  // 尝试获取一个准入许可
 public void acquireUninterruptibly(){} // 尝试获取一个准入许可,不响应中断
 public boolean tryAcquire(){} // 尝试获取一个准入许可,成功返回true,失败返回false,不等待
 public boolean tryAcquire(long timeout, TimeUnit unit){} // 同上一个,但是会有等待
 public void release(){} // 释放一个许可,信号量上许可数-1

4.9.3、读写锁ReadWriteLock

1、概念

读写分离的锁,支持多个读线程同时访问共享资源,写与读,写与写还是互斥串行

image.png 有点:

  • 降低了锁竞争
  • 提高了读效率

2、使用方式

 public class ReadWriteLockTest {
     public static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
     // 由可重入锁对象生成读写锁
     static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
     static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
 ​
     private static int sharedVariable = 0;
 ​
     public int handleRead(Lock lock) {
         try {
             lock.lock();
             Thread.sleep(1000);
             System.out.println(Thread.currentThread() + " read value: "+ sharedVariable);
             return sharedVariable;
         } catch (InterruptedException e) {
             e.printStackTrace();
         } finally {
             lock.unlock();
         }
         return 0;
     }
 ​
     public void handleWrite(Lock lock) {
         try {
             lock.lock();
             Thread.sleep(2000);
             sharedVariable++;
             System.out.println(Thread.currentThread() + "write value: " + sharedVariable);
         } catch (InterruptedException e) {
             e.printStackTrace();
         } finally {
             lock.unlock();
         }
     }
 
     public static void main(String[] args) {
         final ReadWriteLockTest readWriteLockTest = new ReadWriteLockTest();
         // 读线程
         Runnable readThread = new Runnable() {
             @Override
             public void run() {
                 // 使用读写锁的读锁,多个线程操作hanleRead的时候是并行的
                 readWriteLockTest.handleRead(readLock);
             }
         };
         // 写线程
         Runnable writeThread = new Runnable() {
             @Override
             public void run() {
                 // 使用读写锁的读锁,多个线程操作hanleWrite的时候是串行的
                 readWriteLockTest.handleWrite(writeLock);
             }
         };
         long startCurTime = System.currentTimeMillis();
         List<Thread> threadList = Lists.newArrayList();
         // 开18个线程读取
         for (int i = 0; i < 18; i++) {
             Thread thread = new Thread(readThread);
             thread.start();
             threadList.add(thread);
         }
         for (int i = 18; i < 20; i++) {
             Thread thread = new Thread(writeThread);
             thread.start();
             threadList.add(thread);
         }
 ​
         // 主线程等待所有子线程执行完成
         for (Thread thread : threadList) {
             try {
                 thread.join();
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
         System.out.println("Progress end: " + (System.currentTimeMillis() - startCurTime));
     }
 }

4.9.3、倒计时器 CountDownLatch

1、含义

倒计数器:控制某个线程等待,让线程等待到倒计数结束再开始执行,一般是正常执行等待前置准备操作执行完成

2、使用实例

一个线程A在CountDownLatch等待,启动n个线程进行前置准备操作,每个执行完成之后调用CountDownLatch的countDown()方法,倒计数-1,线程A此时便可以继续执行

 public class CountDownLatchTest implements Runnable {
     final static CountDownLatch latch = new CountDownLatch(10);
 ​
     @Override
     public void run() {
         // 线程执行操作
         try {
             Thread.sleep(1000);
             System.out.println("【" + Thread.currentThread().getName() +"】" + " prepare ok!");
             latch.countDown();
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
     }

     public static void main(String[] args) {
         CountDownLatchTest latchTest = new CountDownLatchTest();
         ExecutorService pool = Executors.newFixedThreadPool(10);
         for (int i = 0; i < 10; i++) {
             // 开启10个线程
             pool.submit(latchTest);
         }
         try {
             latch.await();  // 等待在latch上的线程【此时是主线程】进入等待
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println( "wait in CountDownLatch Thread 【" + Thread.currentThread().getName() +"】 is running...");
         pool.shutdown();   // 关闭线程池
     }
 }

4.9.4、循环栅栏 CyclicBarrier

1、作用

循环栅栏。多线程并发控制工具。可以实现线程间的计数等待,与CountDownLatch相似,比CountDownLatch强大

栅栏的作用:等待指定个数的线程都到达时触发所有线程的任务执行,否则等待

可循环利用的栅栏:允许N波线程的等待和统一执行