并发编程基础、同步容器、线程池

基础

关键字

synchronized

  • 锁的是对象。可能锁对象包括: this, 临界资源对象, Class 类对象。
    • synchronized(this)和synchronized方法都是锁当前对象。
    • 静态同步方法,锁的是当前类型的类对象。
  • 加锁的目的: 就是为了保证操作的原子性。
  • 同步方法只影响锁定同一个锁对象的同步方法。不影响其他线程调用非同步方法,或调用其他锁资源的同步方法。
  • 同步方法只能保证当前方法的原子性,不能保证多个业务方法之间的互相访问的原子性。注意在商业开发中,多方法要求结果访问原子操作,需要多个方法都加锁,且锁定同一个资源
  • 同一个线程,多次调用同步代码,锁定同一个锁对象,可重入。在不同线程中,不可重入。
  • 子类同步方法覆盖父类同步方法,可以指定调用父类的同步方法。相当于锁的重入。
  • 在同步方法中发生异常的时候,自动施放锁资源,不会影响其他线程执行。(在同步业务逻辑中,在catch中处理)
  • 锁,锁的是对象,不是引用。(后 重置对象,不会影响线程)
    • 同步代码一旦加锁后,那么会有一个临时的锁引用执行锁对象,和真实的引用无直接关联
    • 在锁未释放前,修改锁对象引用,不会影响同步代码的执行

volatile

  • 通知OS操作系统底层,在CPU计算过程中,都要检查内存中数据的有效性,保证最新内存数据被使用。
  • 保证可见性,但不能保证原子性。

相关类

AtomicXXX

比如AtomicInteger/AtomicBoolean/AtomicIntegerArray/AtomicLong…
原子操作类型,其中的每个方法都是原子操作,可以保证原子操作,可以保证线程安全

AtomicInteger count = new AtomicInteger(0);

CountDownLatch

可以和锁混合使用或替代锁的功能,在门闩未完全开放前等待,当门闩完全开方后执行,避免锁的效率底下问题。

  • CountDownLatch latch = new CountDownLatch(5); // 创建5个门闩
  • latch.await(); // 等待门闩开方
  • latch.countDown(); // 减门闩上的锁

将门 闩减完后执行

CountDownLatch latch = new CountDownLatch(5);


void m1(){
   try {
      latch.await();// 等待门闩开放。
   } catch (InterruptedException e) {
      e.printStackTrace();
   }
   System.out.println("m1() method");
}


void m2(){
   for(int i = 0; i < 10; i++){
      if(latch.getCount() != 0){
         System.out.println("latch count : " + latch.getCount());
         latch.countDown(); // 减门闩上的锁。
      }
      try {
         TimeUnit.MILLISECONDS.sleep(500);
      } catch (InterruptedException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
      }
      System.out.println("m2() method : " + i);
   }
}


public static void main(String[] args) {
   final Test_15 t = new Test_15();
   new Thread(new Runnable() {
      @Override
      public void run() {
         t.m1();
      }
   }).start();
   
   new Thread(new Runnable() {
      @Override
      public void run() {
         t.m2();
      }
   }).start()

ReentrantLock

重入锁,建议应用的同步方式,相对效率比synchronized高,量级较轻。

  • Lock lock = new ReentranLock();
    • 创建
  • lock.lock();
    • 加锁
  • lock.unlock();
    • **解锁 放在finally中,**手动释放所标记
  • lock.tryLock();
    • 尝试锁,如果有锁,无法获取锁标记,返回false;如果获取锁标记,返回true。
    • 尝试锁在解除锁标记( lock.unlock())的时候,一定要判断是否获取到锁标记。
    • 如果当前线程没有获取到锁标记,会抛出异常。
  • lock.lockInterruptibly();
    • 尝试打断,类似于睡眠唤醒,阻塞等待锁,可以被其他的线程打断阻塞状态。
  • Condition condition= lock.newCondition();
    • 为Lock增加条件,当条件满足时,做什么事情,如加锁或解锁。
    • condition.await();
      • 释放当前线程占用的锁,并阻塞当前线程,等待唤醒
    • condition.signalAll();
      • 唤醒

阻塞状态

  • 普通阻塞
    • sleep(100000),可以被打断。
    • 调用thread.interrupt(),可以打断阻塞状态,抛出异常。
  • 等待队列
    • wait() 被调用,也是一种阻塞状态,只能有notify唤醒。
    • 无法打断。
  • 锁池队列
    • 无法获取锁标记。
    • 不是所有的锁池队列都可被打断。
      • 使用ReenrantLock的lock方法,获取锁标记的守护,如果需要阻塞等待锁标记,无法被打断。
      • 使用ReenrantLock的lockInterruptibly方法,获取锁标记的时候,如果需要阻塞等待,可以被打断。

代替synchronized

Lock lock = new ReentrantLock();

void m1(){
   try{
      lock.lock(); // 加锁
      for(int i = 0; i < 10; i++){
         TimeUnit.SECONDS.sleep(1);
         System.out.println("m1() method " + i);
      }
   }catch(InterruptedException e){
      e.printStackTrace();
   }finally{
      lock.unlock(); // 解锁
   }
}

void m2(){
   lock.lock();
   System.out.println("m2() method");
   lock.unlock();
}

public static void main(String[] args) {
   final Test_01 t = new Test_01();
   new Thread(new Runnable() {
      @Override
      public void run() {
         t.m1();
      }
   }).start();
   try {
      TimeUnit.SECONDS.sleep(1);
   } catch (InterruptedException e) {
      e.printStackTrace();
   }
   new Thread(new Runnable() {
      @Override
      public void run() {
         t.m2();
      }
   }).start();
}

公平锁*private static *ReentrantLock *lock *= *new *ReentrantLock(true);


ThreadLocal

在多线程环境中,为每一个线程保存一份线程变量的工具
https://www.cnblogs.com/dolphin0520/p/3920407.html

  • public T get()
  • public void set(T value)
  • public void remove()
  • protected T initialValue()

使用ThreadLocal的时候,一定注意回收资源问题,每个线程结束之前,将当前线程保存的线程变量一定要删除。ThreadLoacl.remove() 避免内存泄漏

实例

1 监听容器数量

题目
自定义容器,提供新增元素(add)和获取元素数量(size)方法。
启动两个线程。线程1向容器中新增10个数据。线程2监听容器元素数量,当容器元素数量为5时,线程2输出信息并终止。

使用volatile,保证t的可见性

public static void main(String[] args) {
   final volatile List<Object> t = new ArrayList<>();
   final Object lock = new Object();
   
   new Thread(new Runnable() {
       @Override
       public void run() {
          for(int i = 0; i < 10; i++){
             System.out.println("add Object to Container " + i);
             t.add(new Object());
             try {
                TimeUnit.SECONDS.sleep(1);
             } catch (InterruptedException e) {
                e.printStackTrace();
             }
          }
       }
    }).start();

    new Thread(new Runnable(){
       @Override
       public void run() {
          while(true){
             if(t.size() == 5){
                System.out.println("size = 5");
                break;
             }
          }
       }
    }).start();
}

使用wait和notify
wait是指在一个已经进入了同步锁的线程内,让自己
暂时让出同步锁*,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了notify方法(notify并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了),调用wait方法的一个或多个线程就会解除wait状态,重新参与竞争对象锁,程序如果可以再次得到锁,就可以继续向下运行。

public static void main(String[] args) {
   final volatile List<Object> t = new ArrayList<>();
   final Object lock = new Object();
   
   new Thread(new Runnable(){
      @Override
      public void run() {
         synchronized (lock) {
            if(t.size() != 5){
               try {
                  lock.wait(); // 线程进入等待队列。
               } catch (InterruptedException e) {
                  e.printStackTrace();
               }
            }
            System.out.println("size = 5");
            lock.notifyAll(); // 唤醒其他等待线程
         }
      }
   }).start();
   
   new Thread(new Runnable() {
      @Override
      public void run() {
         synchronized (lock) {
            for(int i = 0; i < 10; i++){
               System.out.println("add Object to Container " + i);
               t.add(new Object());
               if(t.size() == 5){
                  lock.notifyAll();
                  try {
                     lock.wait();
                  } catch (InterruptedException e) {
                     e.printStackTrace();
                  }
               }
               try {
                  TimeUnit.SECONDS.sleep(1);
               } catch (InterruptedException e) {
                  e.printStackTrace();
               }
            }
         }
      }
   }).start();
}

*使用CountDownLatch 门闩

public static void main(String[] args) {
   final Container t = new Container();
   final CountDownLatch latch = new CountDownLatch(1);

   new Thread(new Runnable(){
      @Override
      public void run() {
         if(t.size() != 5){
            try {
               latch.await(); // 等待门闩的开放。 不是进入等待队列
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
         }
         System.out.println("size = 5");
      }
   }).start();
   
   new Thread(new Runnable() {
      @Override
      public void run() {
         for(int i = 0; i < 10; i++){
            System.out.println("add Object to Container " + i);
            t.add(new Object());
            if(t.size() == 5){
               latch.countDown(); // 门闩-1
            }
            try {
               TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
         }
      }
   }).start();
}

2 生产者 消费者

题目
自定义同步容器,容器容量上限为10。可以在多线程中应用,并保证数据线程安全。

wait、notify 都是和while 配合应用的。可以避免多线程并发判断逻辑失效的问题。
使用synchronized

public class TestContainer01<E> {

   private final LinkedList<E> list = new LinkedList<>();
   private final int MAX = 10;
   private int count = 0;
   
   public synchronized int getCount(){
      return count;
   }
   
   public synchronized void put(E e){
      while(list.size() == MAX){
         try {
            this.wait();
         } catch (InterruptedException e1) {
            e1.printStackTrace();
         }
      }
      
      list.add(e);
      count++;
      this.notifyAll();
   }
   
   public synchronized E get(){
      E e = null;
      while(list.size() == 0){
         try{
            this.wait();
         } catch (InterruptedException e1) {
            e1.printStackTrace();
         }
      }
      e = list.removeFirst();
      count--;
      this.notifyAll();
      return e;
   }
   
   public static void main(String[] args) {
      。。。
   }
   

}

*使用ReentrantLock

public class TestContainer02<E> {

   private final LinkedList<E> list = new LinkedList<>();
   private final int MAX = 10;
   private int count = 0;
   
   private Lock lock = new ReentrantLock();
   private Condition producer = lock.newCondition();
   private Condition consumer = lock.newCondition();
   
   public int getCount(){
      return count;
   }
   
   public void put(E e){
      lock.lock();
      try {
         while(list.size() == MAX){
            System.out.println(Thread.currentThread().getName() + " 等待。。。");
            // 进入等待队列。释放锁标记。
            // 借助条件,进入的等待队列。
            producer.await();
         }
         System.out.println(Thread.currentThread().getName() + " put 。。。");
         list.add(e);
         count++;
         // 借助条件,唤醒所有的消费者。
         consumer.signalAll();
      } catch (InterruptedException e1) {
         e1.printStackTrace();
      } finally {
         lock.unlock();
      }
   }
   
   public E get(){
      E e = null;

      lock.lock();
      try {
         while(list.size() == 0){
            System.out.println(Thread.currentThread().getName() + " 等待。。。");
            // 借助条件,消费者进入等待队列
            consumer.await();
         }
         System.out.println(Thread.currentThread().getName() + " get 。。。");
         e = list.removeFirst();
         count--;
         // 借助条件,唤醒所有的生产者
         producer.signalAll();
      } catch (InterruptedException e1) {
         e1.printStackTrace();
      } finally {
         lock.unlock();
      }
      
      return e;
   }
   
   public static void main(String[] args) {
      。。。
   }  
}

同步容器

解决并发情况下的容器线程安全问题的。给多线程准备一个线程安全的容器对象。
线程安全的容器对象,都是使用synchronized方法实现,类似native,java8使用CAS

  • Vecror
  • Hashtable

concurrent包中的同步容器,大多是使用系统底层技术实现的线程安全。

Map/Set

ConcurrentHashMap/ConcurrentHshSet

底层哈希实现的同步 Map(Set)。效率高,线程安全。使用系统底层技术实现线程安全。量级较 synchronized 低。 key 和 value 不能为 null。

ConcurrentSkipListMap/ConcurrentSkipListSet

底层跳表(SkipList) 实现的同步 Map(Set)。有序,效率比 ConcurrentHashMap 稍低。

List

CopyOnWriteArrayList

写时复制集合。写入效率低,读取效率高。每次写入数据,都会创建一个新的底层数组。
牺牲空间实现线程安全。
适用于写少读多的情况。

Queue

ConcurrentLinkedQueue

基础链表同步队列。

LinkedBlockingQueue

阻塞队列,队列容量不足自动阻塞,队列容量为 0 自动阻塞。

  • put 自动阻塞,队列容量满后,自动阻塞
  • take 自动阻塞方法,队列容量为0后,自动阻塞

ArrayBlockingQueue

底层数组实现的有界队列。 自动阻塞。根据调用 API(add/put/offer) 不同,有不同特性。
当容量不足的时候,有阻塞能力。

  • add 方法在容量不足的时候,抛出异常。
  • put 方法在容量不足的时候,阻塞等待。
  • offer 方法,
    • 单参数 offer 方法,不阻塞。容量不足的时候,返回 false。当前新增数据操作放弃。
    • 三参数 offer 方法(offer(value,times,timeunit)),容量不足的时候,阻塞 times 时长(单位为 timeunit),如果在阻塞时长内,有容量空闲,新增数据返回 true。如果阻塞时长范围内,无容量空闲,放弃新增数据,返回 false。

DelayQueue

延时队列。根据比较机制,实现自定义处理顺序的队列。 常用于定时任务。
如:定时关机。

LinkedTransferQueue

转移队列, 使用 transfer 方法,实现数据的即时处理。没有消费者,就阻塞。

  • add
    • 队列会保存数据,不做阻塞等待
  • transfer
    • 是TransferQueue特有的方法,必须有消费者(take()方法的调用者)。
    • 如果没有任意线程消费数据,transfer方法阻塞。
    • 一般用于处理即时消息。

SynchronusQueue

同步队列,是一个容量为 0 的队列。 是一个特殊的 TransferQueue。
必须现有消费线程等待,才能使用的队列。

  • add 方法,无阻塞。若没有消费线程阻塞等待数据,则抛出异常。
  • put 方法,有阻塞。若没有消费线程阻塞等待数据,则阻塞。

线程池

相关类/接口

  • Executor
    • 一种处理机制
    • 线程池顶级接口。 定义方法, void execute(Runnable)。
    • 常用方法 - void execute(Runnable)
    • 作用是: 启动线程任务的。
  • ExecutorService
    • 线程池服务类型,所有的线程池类型都实现这个接口。
    • 实现这个接口代表可以提供线程池能力。
    • void execute(Runnable)
    • Future submit(Calleable)
    • Future submit(Runnable)
    • void shutdown();
      • 优雅关闭,不是强行关闭线程池,回收线程池中的资源。而是不再处理新的任务,将已接收的任务处理完毕后再关闭。
    • boolean isTerminated();
      • 是否已经结束,相当于回收了资源
    • boolean isShudown();
      • 是否已经关闭,是否调用过shutdown方法
  • Executors
    • Executor的工具类。类似Collections。
    • 可以更简单的创建若干种线程池。
  • Future
    • 未来结果,代表线程任务执行结束后的结果。
    • T get() 阻塞等待线程执行结束并得到结果
    • T get(long,TimeUnit) 阻塞固定时长,等待线程执行结束后的结果
  • Callable
    • 可执行接口,类似Runnable接口,可以启动一个线程的接口
    • Object call()
      • 相当于Runnable接口中的run方法,区别为此方法有返回值

线程池状态

  • Running
    • 线程池正在执行中,活动状态
  • ShuttingDown
    • 线程池正在关闭过程中。优雅关闭。一旦进入这个状态,线程池不再接受新的任务,处理所有已经接受的任务,处理完毕后,关闭线程池。
  • Terminated
    • 线程池已经关闭。

FixedThreadPool

容量固定的线程池。活动状态和线程池容量是有上限的线程池。 所有的线程池中,都有一个任务队列。使用的是** BlockingQueue作为任务的载体。 当任务数量大于线程池容量的时候,没有运行的任务保存在任务队列中,当线程有空闲的,自动从队列中取出任务执行。
使用场景: 大多数情况下,使用的线程池,首选推荐 FixedThreadPool。 OS 系统和硬件是有线程支持上限。不能随意的无限制提供线程池。
线程池默认的容量
上限**是 Integer.MAX_VALUE
常见的线程池容量: PC - 200。 服务器 - 1000~10000
queued tasks - 任务队列
completed tasks - 结束任务队列

ExecutorService service = 
      Executors.newFixedThreadPool(5);
for(int i = 0; i < 6; i++){
   service.execute(new Runnable() {
      @Override
      public void run() {
         try {
            TimeUnit.MILLISECONDS.sleep(500);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
         System.out.println(Thread.currentThread().getName() + " - test executor");
      }
   });
}

CashedThreadPool

缓存的线程池。容量不限(Integer.MAX_VALUE)。自动扩容。 容量管理策略:如果线程池中的线程数量不满足任务执行,创建新的线程。每次有新任务无法即时处理的时候,都会创建新的线程。 当线程池中的线程空闲时长达到一定的临界值(默认 60 秒),自动释放线程。
默认线程空闲** 60 秒**,自动销毁。
应用场景: 内部应用或测试应用。 内部应用,有条件的内部数据瞬间处理时应用,如:
电信平台夜间执行数据整理(有把握在短时间内处理完所有工作,且对硬件和软件有足够的
信心)。 测试应用 ,在测试的时候 ,尝试得到硬件或软件的最高负载量 ,用于提供
FixedThreadPool 容量的指导。

ExecutorService service = Executors.newCachedThreadPool();
System.out.println(service);

for(int i = 0; i < 5; i++){
   service.execute(new Runnable() {
      @Override
      public void run() {
         try {
            TimeUnit.MILLISECONDS.sleep(500);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
         System.out.println(Thread.currentThread().getName() + " - test executor");
      }
   });
}

ScheduledThreadPool

计划任务线程池。可以根据计划自动执行任务的线程池。
scheduleAtFixedRate(Runnable, start_limit, limit, timeunit)
**runnable **- 要执行的任务。
**start_limit **- 第一次任务执行的间隔。
**limit **- 多次任务执行的间隔。
**timeunit **- 多次任务执行间隔的时间单位。
使用场景: 计划任务时选用(DelaydQueue),如:电信行业中的数据整理,没分钟整理,没消失整理,每天整理等。

ScheduledExecutorService service = Executors.newScheduledThreadPool(3);
System.out.println(service);

// 定时完成任务。 scheduleAtFixedRate(Runnable, start_limit, limit, timeunit)
// runnable - 要执行的任务。
service.scheduleAtFixedRate(new Runnable() {
   @Override
   public void run() {
      try {
         TimeUnit.MILLISECONDS.sleep(500);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
      System.out.println(Thread.currentThread().getName());
   }
}, 0, 300, TimeUnit.MILLISECONDS);

SingleThreadExecutor

单一容量的线程池。
使用场景: 保证任务顺序时使用。 如: 游戏大厅中的公共频道聊天。 秒杀。

ExecutorService service = Executors.newSingleThreadExecutor();
System.out.println(service);

for(int i = 0; i < 5; i++){
   service.execute(new Runnable() {
      @Override
      public void run() {
         try {
            TimeUnit.MILLISECONDS.sleep(500);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
         System.out.println(Thread.currentThread().getName() + " - test executor");
      }
   });
}

ForkJoinPool

分支合并线程池(mapduce 类似的设计思想)。适合用于处理复杂任务。
初始化线程容量与 CPU 核心数相关。
线程池中运行的内容必须是 ForkJoinTask 的子类型(RecursiveTask,RecursiveAction)。**ForkJoinPool - 分支合并线程池。 可以递归完成复杂任务。 要求可分支合并的任务必须是ForkJoinTask **类型的子类型。 其中提供了分支和合并的能力。 ForkJoinTask 类型提供了两个抽象子类型, **RecursiveTask **有返回结果的分支合并任务,**RecursiveAction **无返回结果的分支合并任务。(Callable/Runnable) compute 方法:就是任务的执行逻辑。
**ForkJoinPool **没有所谓的容量。默认都是 1 个线程。根据任务自动的分支新的子线程。
当子线程任务结束后,自动合并。 所谓自动是根据 fork 和 join 两个方法实现的。
应用: 主要是做科学计算或天文计算的。 数据分析的。

public class Test_ForkJoinPool {

    final static int[] numbers = new int[1000000];
    final static int MAX_SIZE = 50000;
    final static Random r = new Random();


    static {
        for (int i = 0; i < numbers.length; i++) {
            numbers[i] = r.nextInt(1000);
        }
    }

    static class AddTask extends RecursiveTask<Long> { // RecursiveAction
        int begin, end;

        public AddTask(int begin, int end) {
            this.begin = begin;
            this.end = end;
        }
        
        @Override
        protected Long compute() {
            if ((end - begin) < MAX_SIZE) {
                long sum = 0L;
                for (int i = begin; i < end; i++) {
                    sum += numbers[i];
                }
                // System.out.println("form " + begin + " to " + end + " sum is : " + sum);
                return sum;
            } else {
                int middle = begin + (end - begin) / 2;
                AddTask task1 = new AddTask(begin, middle);
                AddTask task2 = new AddTask(middle, end);
                task1.fork();// 就是用于开启新的任务的。 就是分支工作的。 就是开启一个新的线程任务。
                task2.fork();
                // join - 合并。将任务的结果获取。 这是一个阻塞方法。一定会得到结果数据。
                return task1.join() + task2.join();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException, IOException {
        long result = 0L;
        for (int i = 0; i < numbers.length; i++) {
            result += numbers[i];
        }
        System.out.println(result);

        ForkJoinPool pool = new ForkJoinPool();
        AddTask task = new AddTask(0, numbers.length);

        Future<Long> future = pool.submit(task);
        System.out.println(future.get());

    }

}

*ThreadPoolExecutor

线程池底层实现。 除 ForkJoinPool 外,其他常用线程池底层都是使用 ThreadPoolExecutor实现的。

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue)
  • int corePoolSize
    • 核心容量,创建线程池的时候,默认有多少线程。 也是线程池保持的最少线程数
  • int maximumPoolSize
    • 最大容量,线程池最多有多少线程
  • long keepAliveTime
    • 生命周期, 0 为永久。当线程空闲多久后,自动回收。
  • TimeUnit unit
    • 生命周期单位,为生命周期提供单位,如:秒,毫秒
  • BlockingQueue workQueue
    • 任务队列,阻塞队列。 注意,泛型必须是

使用场景: 默认提供的线程池不满足条件时使用。如:初始线程数据 4,最大线程数
200,线程空闲周期 30 秒。
推荐手动创建线程池。

// 模拟fixedThreadPool, 核心线程5个,最大容量5个,线程的生命周期无限。
ExecutorService service = 
      new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS, 
            new LinkedBlockingQueue<Runnable>());
for(int i = 0; i < 6; i++){
   service.execute(new Runnable() {
      @Override
      public void run() {
         try {
            TimeUnit.MILLISECONDS.sleep(500);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
         System.out.println(Thread.currentThread().getName() + " - test executor");
      }
   });
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值