一文搞懂四种同步工具类

CountDownLatch解释:CountDownLatch相当于一个门闩,门闩上挂了N把锁。只有N把锁都解开的话,门才会打开。怎么理解呢?我举一个赛跑比赛的例子,赛跑比赛中必须等待所有选手都准备好了,裁判才能开发令枪。选手才可以开始跑。CountDownLatch当中主要有两个方法,一个是await()会挂上锁阻塞当前线程,相当于裁判站在起始点等待,等待各位选手准备就绪,一个是countDown方法用于解锁,相当于选手准备好了之后调用countDown方法告诉裁判自己准备就绪,当所有人都准备好了之后裁判开发令枪。 代码:public class TestCountDownLatch { public static void main(String[] args) { // 需要等待两个线程,所以传入参数为2 CountDownLatch latch = new CountDownLatch(2); // 该线程运行1秒 new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("1号选手准备就绪!用时1秒!"); latch.countDown(); } }).start(); // 该线程运行3秒 new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("2号选手准备就绪!用时3秒!"); latch.countDown(); } }).start(); try { System.out.println("请1号选手和2号选手各就各位!"); // 主线程在此等待两个线程执行完毕之后继续执行 latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } // 两个线程执行完毕后,主线程恢复运行 System.out.println("裁判发枪,1号选手和2号选手开跑!"); }}运行结果:请1号选手和2号选手各就各位!1号选手准备就绪!用时1秒!2号选手准备就绪!用时3秒!裁判发枪,1号选手和2号选手开跑!如果去掉CountDownLatch的效果呢?运行结果就会变成如下: ...

October 8, 2019 · 3 min · jiezi

线程安全类的设计合理使用ThreadLocal

一、ThreadLocal类定义ThreadLocal,意指线程局部变量,它可以为每一个线程提供一个实例变量的副本,每个线程独立访问和更改自己的副本,从而保证线程之间不会发生变量冲突,是一种通过将共享变量进行线程隔离而实现线程安全的方式。主要方法有以下三个: T get():返回此线程局部变量中当前线程副本中的值。void remove():删除此线程局部变量中当前线程的值。void set(T value):设置此线程局部变量中当前线程副本中的值。 我们可以通过一个简单的实验,来验证ThreadLocal如何保证线程安全: public class ThreadLocalTest implements Runnable { //重写ThreadLocal类的初始化方法 private ThreadLocal<Integer> i = new ThreadLocal<Integer>(){ public Integer initialValue(){ return 0; } }; public static void main(String[] args){ ThreadLocalTest threadLocalTest = new ThreadLocalTest(); new Thread(threadLocalTest,"st-0").start(); new Thread(threadLocalTest,"st-1").start(); new Thread(threadLocalTest,"st-2").start(); } @Override public void run() { for (;i.get()<100;){ i.set(i.get() + 1); System.out.println(Thread.currentThread().getName() + ": " + i.get()); } }}运行以上代码,截取部分实验结果,可以看到每个线程都是从1开始计数,每个线程更改后的变量副本并不会影响到其他线程。 st-0: 1st-1: 1st-0: 2st-1: 2st-2: 1st-0: 3st-2: 2st-1: 3二、ThreadLocal类的实现原理我们可以从java.lang.ThreadLocal类的源码出发,来了解ThreadLocal类的实现原理。 ...

October 3, 2019 · 2 min · jiezi

1114按序打印

前言LeetCode题库多线程部分的 按序打印: 我们提供了一个类: public class Foo {  public void one() { print("one"); }  public void two() { print("two"); }  public void three() { print("three"); }}三个不同的线程将会共用一个 Foo 实例。 线程 A 将会调用 one() 方法线程 B 将会调用 two() 方法线程 C 将会调用 three() 方法请设计修改程序,以确保 two() 方法在 one() 方法之后被执行,three() 方法在 two() 方法之后被执行。 示例1: 输入: [1,2,3]输出: "onetwothree"解释: 有三个线程会被异步启动。输入 [1,2,3] 表示线程 A 将会调用 one() 方法,线程 B 将会调用 two() 方法,线程 C 将会调用 three() 方法。正确的输出是 "onetwothree"。示例2: 输入: [1,3,2]输出: "onetwothree"解释: 输入 [1,3,2] 表示线程 A 将会调用 one() 方法,线程 B 将会调用 three() 方法,线程 C 将会调用 two() 方法。正确的输出是 "onetwothree"。提示:尽管输入中的数字似乎暗示了顺序,但是我们并不保证线程在操作系统中的调度顺序。 ...

September 11, 2019 · 1 min · jiezi

学习下synchronized锁的实现原理

synchronized锁是Java中的一种重量级锁。他有三种实现方法: 对代码块加锁,这时候锁住的是括号里配置的对象。对普通方法加锁,这时候锁住的是当前实例对象(this)对静态方法加锁,锁住的是当前class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8则是metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁住所有调用该方法的线程。下面逐个分析下各种实现方式。 对代码块加锁 先来看一段示例: public class Test implements Runnable{ private int index = 1 ; private final static int MAX = 100;//总票数是100张 @Override public void run(){ while (index<MAX ){ //如果已卖出的数量小于总票数,继续卖 System.out.println(Thread.currentThread() + "的电影票编号是:"+ (index));//出票 index++; //卖出的票数加1 } } public static void main(String[] args) { final Test task = new Test(); //5个线程卖票 new Thread(task,"一号窗口").start(); new Thread(task,"二号窗口").start(); new Thread(task,"三号窗口").start(); new Thread(task,"四号窗口").start(); new Thread(task,"五号窗口").start(); }}这是一段经典的卖票示例代码。运行之后就会发现出了问题(至于为什么会出问题,后面会专门出一篇文章来分析,这篇文章只是侧重学习下synchronized锁的原理)。这时候我们只要对会出现并发问题的代码加上synchronized锁就能正常运行了。(这里使用的是对代码块加锁的方式) public void run(){ synchronized (MUTEX) { while (index<=MAX ) { //如果还有余票,继续卖 System.out.println(Thread.currentThread() + "的电影票编号是:" +(index));//出票 index++; //售出的票数加1 } }}synchronized锁为什么能解决多线程问题呢,在idea中使用插件查看JVM指令可以看到加上synchronized锁的run方法的JVM指令中多了下面两个指令点开monitorenter指令,这个插件会自动帮我们跳转到官方文档的页面。可以看到对monitorenter有如下描述: ...

August 20, 2019 · 2 min · jiezi

Java并发22并发设计模式-ThreadPerMessage-与-Worker-Thread-模式

我们曾经把并发编程领域的问题总结为三个核心问题:分工、同步和互斥。其中,同步和互斥相关问题更多地源自微观,而分工问题则是源自宏观。我们解决问题,往往都是从宏观入手,同样,解决并发编程问题,首要问题也是解决宏观的分工问题。 并发编程领域里,解决分工问题也有一系列的设计模式,比较常用的主要有 Thread-Per-Message 模式、Worker Thread 模式、生产者 - 消费者模式等等。今天我们重点介绍 Thread-Per-Message 模式。 如何理解 Thread-Per-Message 模式比如写一个 HTTP Server,很显然只能在主线程中接收请求,而不能处理 HTTP 请求,因为如果在主线程中处理 HTTP 请求的话,那同一时间只能处理一个请求,太慢了!怎么办呢?可以利用代办的思路,创建一个子线程,委托子线程去处理 HTTP 请求。 这种委托他人办理的方式,在并发编程领域被总结为一种设计模式,叫做Thread-Per-Message 模式,简言之就是为每个任务分配一个独立的线程。这是一种最简单的分工方法,实现起来也非常简单。 用 Thread 实现 Thread-Per-Message 模式Thread-Per-Message 模式的一个最经典的应用场景是网络编程里服务端的实现,服务端为每个客户端请求创建一个独立的线程,当线程处理完请求后,自动销毁,这是一种最简单的并发处理网络请求的方法。 下面我们就以 echo 程序的服务端为例,介绍如何实现 Thread-Per-Message 模式。 final ServerSocketChannel ssc = ServerSocketChannel.open().bind( new InetSocketAddress(8080));// 处理请求 try { while (true) { // 接收请求 SocketChannel sc = ssc.accept(); // 每个请求都创建一个线程 new Thread(()->{ try { // 读 Socket ByteBuffer rb = ByteBuffer .allocateDirect(1024); sc.read(rb); // 模拟处理请求 Thread.sleep(2000); // 写 Socket ByteBuffer wb = (ByteBuffer)rb.flip(); sc.write(wb); // 关闭 Socket sc.close(); }catch(Exception e){ throw new UncheckedIOException(e); } }).start(); }} finally { ssc.close();} 如果你熟悉网络编程,相信你一定会提出一个很尖锐的问题:上面这个 echo 服务的实现方案是不具备可行性的。原因在于 Java 中的线程是一个重量级的对象,创建成本很高,一方面创建线程比较耗时,另一方面线程占用的内存也比较大。所以,为每个请求创建一个新的线程并不适合高并发场景。 ...

July 12, 2019 · 2 min · jiezi

多线程学习笔记3线程池

一、线程池的概念“池”,就是一个工厂,会提前生产出一些东西供使用。所以线程池就是处理多线程的一种方式。其作用就在于:复用已有资源,控制资源总量 二、为什么使用线程池如果不使用线程池,那么:(1)使用单线程,但是这种方式吞吐量非常低,且请求量一大效率就会显得非常低。(2)那如果对于每个请求都开一个线程去处理,这样一旦请求量过大的时候,线程的创建和销毁都要花费时间,并且线程本身也要占用一定的内存。 使用线程池后,既可以解决单线程低吞吐量和响应慢的问题,又解决了为每一个请求创建线程所耗费的资源问题。 线程池通过限制线程的数量,可以使线程数维持在一个合理的数量,充分发挥了CPU的作用。 而且,线程池遵循了生产者消费者模式,将任务的创建和执行解耦。 三、ThreadPoolExecutorpublic class ThreadPoolExecutor extends AbstractExecutorService { public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);}这里是ThreadPoolExecutor最重要的一个构造方法(1)corePoolSize : 线程池中维护线程的最少数量当线程数少于corePoolSize时,就创建一条新的任务,不管是否有空闲的线程。 (2)maximumPoolSize: 线程池中维护线程的最大数量当线程数到达corePoolSize,并且都不空闲,那么新任务都放到任务队列中去。当任务队列放满之后,如果线程数小于maximumPoolSize,就继续创建新线程。 (3)keepAliveTime:线程池维护线程所允许的空闲时间如果线程空闲的时间超过keepAliveTime,那么就撤销它。 (4)unit: 线程池维护线程所允许的空闲时间的单位 (5)workQueue: 线程池所使用的缓冲队列一般采用阻塞队列,有很多种:无界阻塞队列、有界阻塞队列、同步移交队列 (6)handler: 线程池对拒绝任务的处理策略AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。DiscardPolicy:也是丢弃任务,但是不抛出异常。DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)CallerRunsPolicy:由调用线程处理该任务 四、四种线程池1、CachedThreadPool先查看有没有可用的线程,没有再创建新线程 2、FixedThreadPool与上一种差不多,但是不允许随时创建新线程任意时间点,最多只能有固定数目的活动线程存在,此时如果有新的线程要建立,只能放在另外的队列中等待,直到当前的线程中某个线程终止直接被移出池子。 3、ScheduledThreadPool这个池子里的线程可以按 schedule 依次 delay 执行,或周期执行 4、SingleThreadExecutor任意时间内池子里只能有一个线程。

July 8, 2019 · 1 min · jiezi

Week-1-Java-多线程-Java-内存模型

前言学习情况记录 时间:week 1SMART子目标 :Java 多线程学习Java多线程,要了解多线程可能出现的并发现象,了解Java内存模型的知识是必不可少的。 对学习到的重要知识点进行的记录。 注:这里提到的是Java内存模型,是和并发编程相关的,不是JVM内存结构(堆、方法栈这些概念),这两个不是一回事,别弄混了。 Java 内存模型Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能得到一致效果的机制及规范。目的是解决由于多线程通过共享内存进行通信时,存在的原子性、可见性(缓存一致性)以及有序性问题。主内存与工作内存先看计算机硬件的缓存访问操作: 处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。 加入高速缓存带来了一个新的问题:缓存一致性。如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,需要一些协议来解决这个问题。 Java的内存访问操作与上述的硬件缓存具有很高的可比性: Java内存模型中,规定了所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。 内存间交互操作Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作 read:把一个变量的值从主内存传输到线程的工作内存中load:在 read 之后执行,把 read 得到的值放入线程的工作内存的变量副本中use:把线程的工作内存中一个变量的值传递给执行引擎assign:把一个从执行引擎接收到的值赋给工作内存的变量store:把工作内存的一个变量的值传送到主内存中write:在 store 之后执行,把 store 得到的值放入主内存的变量中lock:作用于主内存的变量,把一个变量标识成一条线程独占的状态unlock: 作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。内存模型三大特性原子性Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。但是 Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操作划分为两次 32 位的操作来进行,也就是说基本数据类型的访问读写是原子性的,除了long和double是非原子性的,即 load、store、read 和 write 操作可以不具备原子性。书上提醒我们只需要知道有这么一回事,因为这个是几乎不可能存在的例外情况。 虽然上面说对基本数据类型的访问读写是原子性的,但是不代表在多线程环境中,如int类型的变量不会出现线程安全问题。详细的例子可以参考范例一。 想要保证原子性,可以尝试以下几种方式: 如果是基础类型的变量的话,使用Atomic类(例如AtomicInteger)其他情况下,可以使用synchronized互斥锁来保证 限定临界区 内操作的原子性。它对应的内存间交互操作为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。可见性可见性指的是,当一个线程修改了共享变量中的值,其他线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。 ...

July 7, 2019 · 2 min · jiezi

Java并发13-ThreadPoolExecutor-如何创建正确的线程池

虽然在 Java 语言中创建线程看上去就像创建一个对象一样简单,只需要 new Thread() 就可以了,但实际上创建线程远不是创建一个对象那么简单。创建对象,仅仅是在 JVM 的堆里分配一块内存而已;而创建一个线程,却需要调用操作系统内核的 API,然后操作系统要为线程分配一系列的资源,这个成本就很高了,所以线程是一个重量级的对象,应该避免频繁创建和销毁那如何避免呢?应对方案估计你已经知道了,那就是线程池。 线程池的需求是如此普遍,所以 Java SDK 并发包自然也少不了它。这里我们需要区分 线程池与一般意义上的池化资源是不同的。一般意义上的池化资源,都是下面这样,当你需要资源的时候就调用 acquire() 方法来申请资源,用完之后就调用 release() 释放资源。 class XXXPool{ // 获取池化资源 XXX acquire() { } // 释放池化资源 void release(XXX x){ }} 但是Java 提供的线程池里面压根就没有申请线程和释放线程的方法。所以,线程池的设计,没有办法直接采用一般意义上池化资源的设计方法。那线程池该如何设计呢?目前业界线程池的设计,普遍采用的都是生产者 - 消费者模式. 线程池是一种生产者 - 消费者模式线程池的使用方是生产者,线程池本身是消费者。在下面的示例代码中,我们创建了一个非常简单的线程池 MyThreadPool,你可以通过它来理解线程池的工作原理。 // 简化的线程池,仅用来说明工作原理class MyThreadPool{ // 利用阻塞队列实现生产者 - 消费者模式 BlockingQueue<Runnable> workQueue; // 保存内部工作线程 List<WorkerThread> threads = new ArrayList<>(); // 构造方法 MyThreadPool(int poolSize, BlockingQueue<Runnable> workQueue){ this.workQueue = workQueue; // 创建工作线程 for(int idx=0; idx<poolSize; idx++){ WorkerThread work = new WorkerThread(); work.start(); threads.add(work); } } // 提交任务 void execute(Runnable command){ workQueue.put(command); } // 工作线程负责消费任务,并执行任务 class WorkerThread extends Thread{ public void run() { // 循环取任务并执行 while(true){ ① Runnable task = workQueue.take(); task.run(); } } } }/** 下面是使用示例 **/// 创建有界阻塞队列BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(2);// 创建线程池 MyThreadPool pool = new MyThreadPool( 10, workQueue);// 提交任务 pool.execute(()->{ System.out.println("hello");});在 MyThreadPool 的内部,我们维护了一个阻塞队列 workQueue 和一组工作线程,工作线程的个数由构造函数中的 poolSize 来指定。用户通过调用 execute() 方法来提交 Runnable 任务,execute() 方法的内部实现仅仅是将任务加入到 workQueue 中。MyThreadPool 内部维护的工作线程会消费 workQueue 中的任务并执行任务,相关的代码就是代码①处的 while 循环。线程池主要的工作原理就这些,是不是还挺简单的? ...

June 23, 2019 · 2 min · jiezi

全栈之路JAVA基础课程四哲学家就餐问题20190614v12

欢迎进入JAVA基础课程 本系列文章将主要针对JAVA一些基础知识点进行讲解,为平时归纳所总结,不管是刚接触JAVA开发菜鸟还是业界资深人士,都希望对广大同行带来一些帮助。若有问题请及时留言或加QQ:243042162。 谨记:“天眼”之父南仁东,心无旁骛,为崇山峻岭间的中国“天眼”燃尽生命,看似一口“大锅”,天眼是世界上最大、最灵敏的单口径射电望远镜,可以接受百亿光年外的电磁信号。南仁东总工程师执着追求科学梦想的精神,将激励一代又一代科技工作者继续奋斗,勇攀世界科技高峰。作为IT界的从业者,我们需要紧跟时代的步伐,踏过平庸,一生为科技筑梦。生产者消费者问题1. 背景 有五个哲学家,他们的生活方式是交替地进行思考和进餐,哲学家们共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五支筷子,平时哲学家进行思考,饥饿时便试图取其左、右最靠近他的筷子,只有在他拿到两支筷子时才能进餐,进餐完毕,放下筷子又继续思考。 2. 代码实现 //定义哲学家类,每个哲学家相当于一个线程class Philosopher extends Thread{ private String name; private Fork fork; public Philosopher(String name,Fork fork){ super(name); this.name=name; this.fork=fork; } public void run(){//每个哲学家的动作,思考-》拿起跨骑 while (true){ thinking(); fork.takeFork(); eating(); fork.putFork(); } } //模拟思考 public void thinking(){ System.out.println("我在思考:"+name); try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } //模拟吃放 public void eating(){ System.out.println("我在吃:"+name); try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }}class Fork{ //5双筷子,初始未未使用 private boolean[] used={false,false,false,false,false}; //拿起筷子 public synchronized void takeFork(){ String name=Thread.currentThread().getName(); int i=Integer.parseInt(name); while (used[i]||used[(i+1)%5]){//左右手有一只被使用则等待 try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } used[i]=true; used[(i+1)%5]=true; } //释放筷子 public synchronized void putFork(){ String name=Thread.currentThread().getName(); int i=Integer.parseInt(name); used[i]=false; used[(i+1)%5]=false; notifyAll(); }}public class PhilosopherMain { public static void main(String[] args) { Fork fork=new Fork(); new Philosopher("0",fork).start(); new Philosopher("1",fork).start(); new Philosopher("2",fork).start(); new Philosopher("3",fork).start(); new Philosopher("4",fork).start(); }}输出结果 ...

June 14, 2019 · 1 min · jiezi

全栈之路JAVA基础课程四20190614v11

欢迎进入JAVA基础课程 本系列文章将主要针对JAVA一些基础知识点进行讲解,为平时归纳所总结,不管是刚接触JAVA开发菜鸟还是业界资深人士,都希望对广大同行带来一些帮助。若有问题请及时留言或加QQ:243042162。 谨记:“天眼”之父南仁东,心无旁骛,为崇山峻岭间的中国“天眼”燃尽生命,看似一口“大锅”,天眼是世界上最大、最灵敏的单口径射电望远镜,可以接受百亿光年外的电磁信号。南仁东总工程师执着追求科学梦想的精神,将激励一代又一代科技工作者继续奋斗,勇攀世界科技高峰。作为IT界的从业者,我们需要紧跟时代的步伐,踏过平庸,一生为科技筑梦。生产者消费者问题1. 背景 生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。生产者生成一定量的数据放到缓冲区中,然后重复此过程;与此同时,消费者也在缓冲区消耗这些数据。生产者和消费者之间必须保持同步,要保证生产者不会在缓冲区满时放入数据,消费者也不会在缓冲区空时消耗数据。不够完善的解决方法容易出现死锁的情况,此时进程都在等待唤醒。 2. 条件 生产者仅仅在仓储未满时候生产, 仓满则停止生产.生产者在生产出可消费产品时候, 应该通知等待的消费者去消费.消费者仅仅在仓储有产品时候才能消费, 仓空则等待.消费者发现仓储没产品可消费时候会通知生产者生产.3.实现方式 wait() / notify()方法await() / signal()方法BlockingQueue阻塞队列方法Semaphore方法PipedInputStream / PipedOutputStream下面主要针对前面三种方式做代码实现 (1) wait() / notify()方法 public class ProducerMain { private static Integer count=0; private final Integer full=10; private static String LOCK="LOCK"; class Producer implements Runnable{ @Override public void run() {// for(int i=0;i<10;i++){// try {// Thread.sleep(3000);// } catch (InterruptedException e) {// e.printStackTrace();// }// } synchronized (LOCK){ while(count==full){ try { LOCK.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } count++; System.out.println(Thread.currentThread().getName()+"生产者生产,目前共生产了:"+count); LOCK.notifyAll(); } } } class Consumer implements Runnable{ @Override public void run() { synchronized (LOCK){ while (count==0){ try { LOCK.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } count--; System.out.println(Thread.currentThread().getName()+"生产者消费,目前共剩余:"+count); LOCK.notifyAll(); } } } public static void main(String[] args) { ProducerMain producer = new ProducerMain(); new Thread(producer.new Producer()).start(); new Thread(producer.new Consumer()).start(); new Thread(producer.new Producer()).start(); new Thread(producer.new Consumer()).start(); new Thread(producer.new Producer()).start(); new Thread(producer.new Consumer()).start(); new Thread(producer.new Producer()).start(); new Thread(producer.new Consumer()).start(); }}输出结果 ...

June 14, 2019 · 3 min · jiezi

高性能服务器架构思路不仅是思路

在服务器端程序开发领域,性能问题一直是备受关注的重点。业界有大量的框架、组件、类库都是以性能为卖点而广为人知。然而,服务器端程序在性能问题上应该有何种基本思路,这个却很少被这些项目的文档提及。本文正式希望介绍服务器端解决性能问题的基本策略和经典实践,并分为几个部分来说明: 缓存策略的概念和实例2.缓存策略的难点:不同特点的缓存数据的清理机制 3.分布策略的概念和实例 4.分布策略的难点:共享数据安全性与代码复杂度的平衡 缓存缓存策略的概念我们提到服务器端性能问题的时候,往往会混淆不清。因为当我们访问一个服务器时,出现服务卡住不能得到数据,就会认为是“性能问题”。但是实际上这个性能问题可能是有不同的原因,表现出来都是针对客户请求的延迟很长甚至中断。我们来看看这些原因有哪些:第一个是所谓并发数不足,也就是同时请求的客户过多,导致超过容纳能力的客户被拒绝服务,这种情况往往会因为服务器内存耗尽而导致的;第二个是处理延迟过长,也就是有一些客户的请求处理时间已经超过用户可以忍受的长度,这种情况常常表现为CPU占用满额100%。 我们在服务器开发的时候,最常用到的有下面这几种硬件:CPU、内存、磁盘、网卡。其中CPU是代表计算机处理时间的,硬盘的空间一般很大,主要是读写磁盘会带来比较大的处理延迟,而内存、网卡则是受存储、带宽的容量限制的。所以当我们的服务器出现性能问题的时候,就是这几个硬件某一个甚至几个都出现负荷占满的情况。这四个硬件的资源一般可以抽象成两类:一类是时间资源,比如CPU和磁盘读写;一类是空间资源,比如内存和网卡带宽。所以当我们的服务器出现性能问题,有一个最基本的思路,就是——时间空间转换。我们可以举几个例子来说明这个问题。 当我们访问一个WEB的网站的时候,输入的URL地址会被服务器变成对磁盘上某个文件的读取。如果有大量的用户访问这个网站,每次的请求都会造成对磁盘的读操作,可能会让磁盘不堪重负,导致无法即时读取到文件内容。但是如果我们写的程序,会把读取过一次的文件内容,长时间的保存在内存中,当有另外一个对同样文件的读取时,就直接从内存中把数据返回给客户端,就无需去让磁盘读取了。由于用户访问的文件往往很集中,所以大量的请求可能都能从内存中找到保存的副本,这样就能大大提高服务器能承载的访问量了。这种做法,就是用内存的空间,换取了磁盘的读写时间,属于用空间换时间的策略。 举另外一个例子:我们写一个网络游戏的服务器端程序,通过读写数据库来提供玩家资料存档。如果有大量玩家进入这个服务器,必定有很多玩家的数据资料变化,比如升级、获得武器等等,这些通过读写数据库来实现的操作,可能会让数据库进程负荷过重,导致玩家无法即时完成游戏操作。我们会发现游戏中的读操作,大部分都是针是对一些静态数据的,比如游戏中的关卡数据、武器道具的具体信息;而很多写操作,实际上是会覆盖的,比如我的经验值,可能每打一个怪都会增加几十点,但是最后记录的只是最终的一个经验值,而不会记录下打怪的每个过程。所以我们也可以使用时空转换的策略来提供性能:我们可以用内存,把那些游戏中的静态数据,都一次性读取并保存起来,这样每次读这些数据,都和数据库无关了;而玩家的资料数据,则不是每次变化都去写数据库,而是先在内存中保持一个玩家数据的副本,所有的写操作都先去写内存中的结构,然后定期再由服务器主动写回到数据库中,这样可以把多次的写数据库操作变成一次写操作,也能节省很多写数据库的消耗。这种做法也是用空间换时间的策略。 最后说说用时间换空间的例子:假设我们要开发一个企业通讯录的数据存储系统,客户要求我们能保存下通讯录的每次新增、修改、删除操作,也就是这个数据的所有变更历史,以便可以让数据回退到任何一个过去的时间点。那么我们最简单的做法,就是这个数据在任何变化的时候,都拷贝一份副本。但是这样会非常的浪费磁盘空间,因为这个数据本身变化的部分可能只有很小一部分,但是要拷贝的副本可能很大。这种情况下,我们就可以在每次数据变化的时候,都记下一条记录,内容就是数据变化的情况:插入了一条内容是某某的联系方法、删除了一条某某的联系方法……,这样我们记录的数据,仅仅就是变化的部分,而不需要拷贝很多份副本。当我们需要恢复到任何一个时间点的时候,只需要按这些记录依次对数据修改一遍,直到指定的时间点的记录即可。这个恢复的时间可能会有点长,但是却可以大大节省存储空间。这就是用CPU的时间来换磁盘的存储空间的策略。我们现在常见的MySQL InnoDB日志型数据表,以及SVN源代码存储,都是使用这种策略的。 另外,我们的Web服务器,在发送HTML文件内容的时候,往往也会先用ZIP压缩,然后发送给浏览器,浏览器收到后要先解压,然后才能显示,这个也是用服务器和客户端的CPU时间,来换取网络带宽的空间。 在我们的计算机体系中,缓存的思路几乎无处不在,比如我们的CPU里面就有1级缓存、2级缓存,他们就是为了用这些快速的存储空间,换取对内存这种相对比较慢的存储空间的等待时间。我们的显示卡里面也带有大容量的缓存,他们是用来存储显示图形的运算结果的。 缓存的本质,除了让“已经处理过的数据,不需要重复处理”以外,还有“以快速的数据存储读写,代替较慢速的存储读写”的策略。我们在选择缓存策略进行时空转换的时候,必须明确我们要转换的时间和空间是否合理,是否能达到效果。比如早期有一些人会把WEB文件缓存在分布式磁盘上(例如NFS),但是由于通过网络访问磁盘本身就是一个比较慢的操作,而且还会占用可能就不充裕的网络带宽空间,导致性能可能变得更慢。 在设计缓存机制的时候,我们还容易碰到另外一个风险,就是对缓存数据的编程处理问题。如果我们要缓存的数据,并不是完全无需处理直接读写的,而是需要读入内存后,以某种语言的结构体或者对象来处理的,这就需要涉及到“序列化”和“反序列化”的问题。如果我们采用直接拷贝内存的方式来缓存数据,当我们的这些数据需要跨进程、甚至跨语言访问的时候,会出现那些指针、ID、句柄数据的失效。因为在另外一个进程空间里,这些“标记型”的数据都是不存在的。因此我们需要更深入的对数据缓存的方法,我们可能会使用所谓深拷贝的方案,也就是跟着那些指针去找出目标内存的数据,一并拷贝。一些更现代的做法,则是使用所谓序列化方案来解决这个问题,也就是用一些明确定义了的“拷贝方法”来定义一个结构体,然后用户就能明确的知道这个数据会被拷贝,直接取消了指针之类的内存地址数据的存在。比如著名的Protocol Buffer就能很方便的进行内存、磁盘、网络位置的缓存;现在我们常见的JSON,也被一些系统用来作为缓存的数据格式。 但是我们需要注意的是,缓存的数据和我们程序真正要操作的数据,往往是需要进行一些拷贝和运算的,这就是序列化和反序列化的过程,这个过程很快,也有可能很慢。所以我们在选择数据缓存结构的时候,必须要注意其转换时间,否则你缓存的效果可能被这些数据拷贝、转换消耗去很多,严重的甚至比不缓存更差。一般来说,缓存的数据越解决使用时的内存结构,其转换速度就越快,在这点上,Protocol Buffer采用TLV编码,就比不上直接memcpy的一个C结构体,但是比编码成纯文本的XML或者JSON要来的更快。因为编解码的过程往往要进行复杂的查表映射,列表结构等操作。 缓存策略的难点虽然使用缓存思想似乎是一个很简单的事情,但是缓存机制却有一个核心的难点,就是——缓存清理。我们所说的缓存,都是保存一些数据,但是这些数据往往是会变化的,我们要针对这些变化,清理掉保存的“脏”数据,却可能不是那么容易。 首先我们来看看最简单的缓存数据——静态数据。这种数据往往在程序的运行时是不会变化的,比如Web服务器内存中缓存的HTML文件数据,就是这种。事实上,所有的不是由外部用户上传的数据,都属于这种“运行时静态数据”。一般来说,我们对这种数据,可以采用两种建立缓存的方法:一是程序一启动,就一股脑把所有的静态数据从文件或者数据库读入内存;二就是程序启动的时候并不加载静态数据,而是等有用户访问相关数据的时候,才去加载,这也就是所谓lazy load的做法。第一种方法编程比较简单,程序的内存启动后就稳定了,不太容易出现内存漏洞(如果加载的缓存太多,程序在启动后立刻会因内存不足而退出,比较容易发现问题);第二种方法程序启动很快,但要对缓存占用的空间有所限制或者规划,否则如果要缓存的数据太多,可能会耗尽内存,导致在线服务中断。 一般来说,静态数据是不会“脏”的,因为没有用户会去写缓存中的数据。但是在实际工作中,我们的在线服务往往会需要“立刻”变更一些缓存数据。比如在门户网站上发布了一条新闻,我们会希望立刻让所有访问的用户都看到。按最简单的做法,我们一般只要重启一下服务器进程,内存中的缓存就会消失了。对于静态缓存的变化频率非常低的业务,这样是可以的,但是如果是新闻网站,就不能每隔几分钟就重启一下WEB服务器进程,这样会影响大量在线用户的访问。常见的解决这类问题有两种处理策略: 第一种是使用控制命令。简单来说,就是在服务器进程上,开通一个实时的命令端口,我们可以通过网络数据包(如UDP包),或者Linux系统信号(如kill SIGUSR2进程号)之类的手段,发送一个命令消息给服务器进程,让进程开始清理缓存。这种清理可能执行的是最简单的“全部清理”,也有的可以细致一点的,让命令消息中带有“想清理的数据ID”这样的信息,比如我们发送给WEB服务器的清理消息网络包中会带一个字符串URL,表示要清理哪一个HTML文件的缓存。这种做法的好处是清理的操作很精准,可以明确的控制清理的时间和数据。但是缺点就是比较繁琐,手工去编写发送这种命令很烦人,所以一般我们会把清理缓存命令的工作,编写到上传静态数据的工具当中,比如结合到网站的内容发布系统中,一旦编辑提交了一篇新的新闻,发布系统的程序就自动的发送一个清理消息给WEB服务器。 第二种是使用字段判断逻辑。也就是服务器进程,会在每次读取缓存前,根据一些特征数据,快速的判断内存中的缓存和源数据内容,是否有不一致(是否脏)的地方,如果有不一致的地方,就自动清理这条数据的缓存。这种做法会消耗一部分CPU,但是就不需要人工去处理清理缓存的事情,自动化程度很高。现在我们的浏览器和WEB服务器之间,就有用这种机制:检查文件MD5;或者检查文件最后更新时间。具体的做法,就是每次浏览器发起对WEB服务器的请求时,除了发送URL给服务器外,还会发送一个缓存了此URL对应的文件内容的MD5校验串、或者是此文件在服务器上的“最后更新时间”(这个校验串和“最后更新时间”是第一次获的文件时一并从服务器获得的);服务器收到之后,就会把MD5校验串或者最后更新时间,和磁盘上的目标文件进行对比,如果是一致的,说明这个文件没有被修改过(缓存不是“脏”的),可以直接使用缓存。否则就会读取目标文件返回新的内容给浏览器。这种做法对于服务器性能是有一定消耗的,所以如果往往我们还会搭配其他的缓存清理机制来用,比如我们会在设置一个“超时检查”的机制:就是对于所有的缓存清理检查,我们都简单的看看缓存存在的时间是否“超时”了,如果超过了,才进行下一步的检查,这样就不用每次请求都去算MD5或者看最后更新时间了。但是这样就存在“超时”时间内缓存变脏的可能性。 上面说了运行时静态的缓存清理,现在说说运行时变化的缓存数据。在服务器程序运行期间,如果用户和服务器之间的交互,导致了缓存的数据产生了变化,就是所谓“运行时变化缓存”。比如我们玩网络游戏,登录之后的角色数据就会从数据库里读出来,进入服务器的缓存(可能是堆内存或者memcached、共享内存),在我们不断进行游戏操作的时候,对应的角色数据就会产生修改的操作,这种缓存数据就是“运行时变化的缓存”。这种运行时变化的数据,有读和写两个方面的清理问题:由于缓存的数据会变化,如果另外一个进程从数据库读你的角色数据,就会发现和当前游戏里的数据不一致;如果服务器进程突然结束了,你在游戏里升级,或者捡道具的数据可能会从内存缓存中消失,导致你白忙活了半天,这就是没有回写(缓存写操作的清理)导致的问题。这种情况在电子商务领域也很常见,最典型的就是火车票网上购买的系统,火车票数据缓存在内存必须有合适的清理机制,否则让两个买了同一张票就麻烦了,但如果不缓存,大量用户同时抢票,服务器也应对不过来。因此在运行时变化的数据缓存,应该有一些特别的缓存清理策略。 在实际运行业务中,运行变化的数据往往是根据使用用户的增多而增多的,因此首先要考虑的问题,就是缓存空间不够的可能性。我们不太可能把全部数据都放到缓存的空间里,也不可能清理缓存的时候就全部数据一起清理,所以我们一般要对数据进行分割,这种分割的策略常见的有两种:一种是按重要级来分割,一种是按使用部分分割。 先举例说说“按重要级分割”,在网络游戏中,同样是角色的数据,有些数据的变化可能会每次修改都立刻回写到数据库(清理写缓存),其他一些数据的变化会延迟一段时间,甚至有些数据直到角色退出游戏才回写,如玩家的等级变化(升级了),武器装备的获得和消耗,这些玩家非常看重的数据,基本上会立刻回写,这些就是所谓最重要的缓存数据。而玩家的经验值变化、当前HP、MP的变化,就会延迟一段时间才写,因为就算丢失了缓存,玩家也不会太过关注。最后有些比如玩家在房间(地区)里的X/Y坐标,对话聊天的记录,可能会退出时回写,甚至不回写。这个例子说的是“写缓存”的清理,下面说说“读缓存”的按重要级分割清理。 假如我们写一个网店系统,里面容纳了很多产品,这些产品有一些会被用户频繁检索到,比较热销,而另外一些商品则没那么热销。热销的商品的余额、销量、评价都会比较频繁的变化,而滞销的商品则变化很少。所以我们在设计的时候,就应该按照不同商品的访问频繁程度,来决定缓存哪些商品的数据。我们在设计缓存的结构时,就应该构建一个可以统计缓存读写次数的指标,如果有些数据的读写频率过低,或者空闲(没有人读、写缓存)时间超长,缓存应该主动清理掉这些数据,以便其他新的数据能进入缓存。这种策略也叫做“冷热交换”策略。实现“冷热交换”的策略时,关键是要定义一个合理的冷热统计算法。一些固定的指标和算法,往往并不能很好的应对不同硬件、不同网络情况下的变化,所以现在人们普遍会用一些动态的算法,如Redis就采用了5种,他们是: 1.根据过期时间,清理最长时间没用过的 2.根据过期时间,清理即将过期的 3.根据过期时间,任意清理一个 4.无论是否过期,随机清理 5.无论是否过期,根据LRU原则清理:所谓LRU,就是Least Recently Used,最近最久未使用过。这个原则的思想是:如果一个数据在最近一段时间没有被访问到,那么在将来他被访问的可能性也很小。LRU是在操作系统中很常见的一种原则,比如内存的页面置换算法(也包括FIFO,LFU等),对于LRU的实现,还是非常有技巧的,但是本文就不详细去说明如何实现,留待大家上网搜索“LRU”关键字学习。 数据缓存的清理策略其实远不止上面所说的这些,要用好缓存这个武器,就要仔细研究需要缓存的数据特征,他们的读写分布,数据之中的差别。然后最大化的利用业务领域的知识,来设计最合理的缓存清理策略。这个世界上不存在万能的优化缓存清理策略,只存在针对业务领域最优化的策略,这需要我们程序员深入理解业务领域,去发现数据背后的规律。 分布分布策略的概念任何的服务器的性能都是有极限的,面对海量的互联网访问需求,是不可能单靠一台服务器或者一个CPU来承担的。所以我们一般都会在运行时架构设计之初,就考虑如何能利用多个CPU、多台服务器来分担负载,这就是所谓分布的策略。分布式的服务器概念很简单,但是实现起来却比较复杂。因为我们写的程序,往往都是以一个CPU,一块内存为基础来设计的,所以要让多个程序同时运行,并且协调运作,这需要更多的底层工作。 首先出现能支持分布式概念的技术是多进程。在DOS时代,计算机在一个时间内只能运行一个程序,如果你想一边写程序,同时一边听mp3,都是不可能的。但是,在WIN95操作系统下,你就可以同时开多个窗口,背后就是同时在运行多个程序。在Unix和后来的Linux操作系统里面,都普遍支持了多进程的技术。所谓的多进程,就是操作系统可以同时运行我们编写的多个程序,每个程序运行的时候,都好像自己独占着CPU和内存一样。在计算机只有一个CPU的时候,实际上计算机会分时复用的运行多个进程,CPU在多个进程之间切换。但是如果这个计算机有多个CPU或者多个CPU核,则会真正的有几个进程同时运行。所以进程就好像一个操作系统提供的运行时“程序盒子”,可以用来在运行时,容纳任何我们想运行的程序。当我们掌握了操作系统的多进程技术后,我们就可以把服务器上的运行任务,分为多个部分,然后分别写到不同的程序里,利用上多CPU或者多核,甚至是多个服务器的CPU一起来承担负载。 这种划分多个进程的架构,一般会有两种策略:一种是按功能来划分,比如负责网络处理的一个进程,负责数据库处理的一个进程,负责计算某个业务逻辑的一个进程。另外一种策略是每个进程都是同样的功能,只是分担不同的运算任务而已。使用第一种策略的系统,运行的时候,直接根据操作系统提供的诊断工具,就能直观的监测到每个功能模块的性能消耗,因为操作系统提供进程盒子的同时,也能提供对进程的全方位的监测,比如CPU占用、内存消耗、磁盘和网络I/O等等。但是这种策略的运维部署会稍微复杂一点,因为任何一个进程没有启动,或者和其他进程的通信地址没配置好,都可能导致整个系统无法运作;而第二种分布策略,由于每个进程都是一样的,这样的安装部署就非常简单,性能不够就多找几个机器,多启动几个进程就完成了,这就是所谓的平行扩展。 现在比较复杂的分布式系统,会结合这两种策略,也就是说系统既按一些功能划分出不同的具体功能进程,而这些进程又是可以平行扩展的。当然这样的系统在开发和运维上的复杂度,都是比单独使用“按功能划分”和“平行划分”要更高的。由于要管理大量的进程,传统的依靠配置文件来配置整个集群的做法,会显得越来越不实用:这些运行中的进程,可能和其他很多进程产生通信关系,当其中一个进程变更通信地址时,势必影响所有其他进程的配置。所以我们需要集中的管理所有进程的通信地址,当有变化的时候,只需要修改一个地方。在大量进程构建的集群中,我们还会碰到容灾和扩容的问题:当集群中某个服务器出现故障,可能会有一些进程消失;而当我们需要增加集群的承载能力时,我们又需要增加新的服务器以及进程。这些工作在长期运行的服务器系统中,会是比较常见的任务,如果整个分布系统有一个运行中的中心进程,能自动化的监测所有的进程状态,一旦有进程加入或者退出集群,都能即时的修改所有其他进程的配置,这就形成了一套动态的多进程管理系统。开源的ZooKeeper给我们提供了一个可以充当这种动态集群中心的实现方案。由于ZooKeeper本身是可以平行扩展的,所以它自己也是具备一定容灾能力的。现在越来越多的分布式系统都开始使用以ZooKeeper为集群中心的动态进程管理策略了。 在调用多进程服务的策略上,我们也会有一定的策略选择,其中最著名的策略有三个:一个是动态负载均衡策略;一个是读写分离策略;一个是一致性哈希策略。动态负载均衡策略,一般会搜集多个进程的服务状态,然后挑选一个负载最轻的进程来分发服务,这种策略对于比较同质化的进程是比较合适的。读写分离策略则是关注对持久化数据的性能,比如对数据库的操作,我们会提供一批进程专门用于提供读数据的服务,而另外一个(或多个)进程用于写数据的服务,这些写数据的进程都会每次写多份拷贝到“读服务进程”的数据区(可能就是单独的数据库),这样在对外提供服务的时候,就可以提供更多的硬件资源。一致性哈希策略是针对任何一个任务,看看这个任务所涉及读写的数据,是属于哪一片的,是否有某种可以缓存的特征,然后按这个数据的ID或者特征值,进行“一致性哈希”的计算,分担给对应的处理进程。这种进程调用策略,能非常的利用上进程内的缓存(如果存在),比如我们的一个在线游戏,由100个进程承担服务,那么我们就可以把游戏玩家的ID,作为一致性哈希的数据ID,作为进程调用的KEY,如果目标服务进程有缓存游戏玩家的数据,那么所有这个玩家的操作请求,都会被转到这个目标服务进程上,缓存的命中率大大提高。而使用“一致性哈希”,而不是其他哈希算法,或者取模算法,主要是考虑到,如果服务进程有一部分因故障消失,剩下的服务进程的缓存依然可以有效,而不会整个集群所有进程的缓存都失效。具体有兴趣的读者可以搜索“一致性哈希”一探究竟。 以多进程利用大量的服务器,以及服务器上的多个CPU核心,是一个非常有效的手段。但是使用多进程带来的额外的编程复杂度的问题。一般来说我们认为最好是每个CPU核心一个进程,这样能最好的利用硬件。如果同时运行的进程过多,操作系统会消耗很多CPU时间在不同进程的切换过程上。但是,我们早期所获得的很多API都是阻塞的,比如文件I/O,网络读写,数据库操作等。如果我们只用有限的进程来执行带这些阻塞操作的程序,那么CPU会大量被浪费,因为阻塞的API会让有限的这些进程停着等待结果。那么,如果我们希望能处理更多的任务,就必须要启动更多的进程,以便充分利用那些阻塞的时间,但是由于进程是操作系统提供的“盒子”,这个盒子比较大,切换耗费的时间也比较多,所以大量并行的进程反而会无谓的消耗服务器资源。加上进程之间的内存一般是隔离的,进程间如果要交换一些数据,往往需要使用一些操作系统提供的工具,比如网络socket,这些都会额外消耗服务器性能。因此,我们需要一种切换代价更少,通信方式更便捷,编程方法更简单的并行技术,这个时候,多线程技术出现了。 多线程的特点是切换代价少,可以同时访问内存。我们可以在编程的时候,任意让某个函数放入新的线程去执行,这个函数的参数可以是任何的变量或指针。如果我们希望和这些运行时的线程通信,只要读、写这些指针指向的变量即可。在需要大量阻塞操作的时候,我们可以启动大量的线程,这样就能较好的利用CPU的空闲时间;线程的切换代价比进程低得多,所以我们能利用的CPU也会多很多。线程是一个比进程更小的“程序盒子”,他可以放入某一个函数调用,而不是一个完整的程序。一般来说,如果多个线程只是在一个进程里面运行,那其实是没有利用到多核CPU的并行好处的,仅仅是利用了单个空闲的CPU核心。但是,在JAVA和C#这类带虚拟机的语言中,多线程的实现底层,会根据具体的操作系统的任务调度单位(比如进程),尽量让线程也成为操作系统可以调度的单位,从而利用上多个CPU核心。比如Linux2.6之后,提供了NPTL的内核线程模型,JVM就提供了JAVA线程到NPTL内核线程的映射,从而利用上多核CPU。而Windows系统中,据说本身线程就是系统的最小调度单位,所以多线程也是利用上多核CPU的。所以我们在使用JAVAC#编程的时候,多线程往往已经同时具备了多进程利用多核CPU、以及切换开销低的两个好处。 早期的一些网络聊天室服务,结合了多线程和多进程使用的例子。一开始程序会启动多个广播聊天的进程,每个进程都代表一个房间;每个用户连接到聊天室,就为他启动一个线程,这个线程会阻塞的读取用户的输入流。这种模型在使用阻塞API的环境下,非常简单,但也非常有效。 当我们在广泛使用多线程的时候,我们发现,尽管多线程有很多优点,但是依然会有明显的两个缺点:一个内存占用比较大且不太可控;第二个是多个线程对于用一个数据使用时,需要考虑复杂的“锁”问题。由于多线程是基于对一个函数调用的并行运行,这个函数里面可能会调用很多个子函数,每调用一层子函数,就会要在栈上占用新的内存,大量线程同时在运行的时候,就会同时存在大量的栈,这些栈加在一起,可能会形成很大的内存占用。并且,我们编写服务器端程序,往往希望资源占用尽量可控,而不是动态变化太大,因为你不知道什么时候会因为内存用完而当机,在多线程的程序中,由于程序运行的内容导致栈的伸缩幅度可能很大,有可能超出我们预期的内存占用,导致服务的故障。而对于内存的“锁”问题,一直是多线程中复杂的课题,很多多线程工具库,都推出了大量的“无锁”容器,或者“线程安全”的容器,并且还大量设计了很多协调线程运作的类库。但是这些复杂的工具,无疑都是证明了多线程对于内存使用上的问题。 由于多线程还是有一定的缺点,所以很多程序员想到了一个釜底抽薪的方法:使用多线程往往是因为阻塞式API的存在,比如一个read()操作会一直停止当前线程,那么我们能不能让这些操作变成不阻塞呢?——selector/epoll就是Linux退出的非阻塞式API。如果我们使用了非阻塞的操作函数,那么我们也无需用多线程来并发的等待阻塞结果。我们只需要用一个线程,循环的检查操作的状态,如果有结果就处理,无结果就继续循环。这种程序的结果往往会有一个大的死循环,称为主循环。在主循环体内,程序员可以安排每个操作事件、每个逻辑状态的处理逻辑。这样CPU既无需在多线程间切换,也无需处理复杂的并行数据锁的问题——因为只有一个线程在运行。这种就是被称为“并发”的方案。 实际上计算机底层早就有使用并发的策略,我们知道计算机对于外部设备(比如磁盘、网卡、显卡、声卡、键盘、鼠标),都使用了一种叫“中断”的技术,早期的电脑使用者可能还被要求配置IRQ号。这个中断技术的特点,就是CPU不会阻塞的一直停在等待外部设备数据的状态,而是外部数据准备好后,给CPU发一个“中断信号”,让CPU转去处理这些数据。非阻塞的编程实际上也是类似这种行为,CPU不会一直阻塞的等待某些I/O的API调用,而是先处理其他逻辑,然后每次主循环去主动检查一下这些I/O操作的状态。 多线程和异步的例子,最著名就是Web服务器领域的Apache和Nginx的模型。Apache是多进程/多线程模型的,它会在启动的时候启动一批进程,作为进程池,当用户请求到来的时候,从进程池中分配处理进程给具体的用户请求,这样可以节省多进程/线程的创建和销毁开销,但是如果同时有大量的请求过来,还是需要消耗比较高的进程/线程切换。而Nginx则是采用epoll技术,这种非阻塞的做法,可以让一个进程同时处理大量的并发请求,而无需反复切换。对于大量的用户访问场景下,apache会存在大量的进程,而nginx则可以仅用有限的进程(比如按CPU核心数来启动),这样就会比apache节省了不少“进程切换”的消耗,所以其并发性能会更好。 在现代服务器端软件中,nginx这种模型的运维管理会更简单,性能消耗也会稍微更小一点,所以成为最流行的进程架构。但是这种好处,会付出一些另外的代价:非阻塞代码在编程的复杂度变大。 分布式编程复杂度以前我们的代码,从上往下执行,每一行都会占用一定的CPU时间,这些代码的直接顺序,也是和编写的顺序基本一致,任何一行代码,都是唯一时刻的执行任务。当我们在编写分布式程序的时候,我们的代码将不再好像那些单进程、单线程的程序一样简单。我们要把同时运行的不同代码,在同一段代码中编写。就好像我们要把整个交响乐团的每个乐器的乐谱,全部写到一张纸上。为了解决这种编程的复杂度,业界发展出了多种编码形式。 在多进程的编码模型上,fork()函数可以说一个非常典型的代表。在一段代码中,fork()调用之后的部分,可能会被新的进程中执行。要区分当前代码的所在进程,要靠fork()的返回值变量。这种做法,等于把多个进程的代码都合并到一块,然后通过某些变量作为标志来划分。这样的写法,对于不同进程代码大部份相同的“同质进程”来说,还是比较方便的,最怕就是有大量的不同逻辑要用不同的进程来处理,这种情况下,我们就只能自己通过规范fork()附近的代码,来控制混乱的局面。比较典型的是把fork()附近的代码弄成一个类似分发器(dispatcher)的形式,把不同功能的代码放到不同的函数中,以fork之前的标记变量来决定如何调用。 ...

June 12, 2019 · 1 min · jiezi

Netty服务端和客户端

欢迎关注公众号:【爱编程】如果有需要后台回复2019赠送1T的学习资料哦!! 本文是基于Netty4.1.36进行分析 服务端Netty服务端的启动代码基本都是如下: private void start() throws Exception { final EchoServerHandler serverHandler = new EchoServerHandler(); /** * NioEventLoop并不是一个纯粹的I/O线程,它除了负责I/O的读写之外 * 创建了两个NioEventLoopGroup, * 它们实际是两个独立的Reactor线程池。 * 一个用于接收客户端的TCP连接, * 另一个用于处理I/O相关的读写操作,或者执行系统Task、定时任务Task等。 */ EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup childGroup = new NioEventLoopGroup(); try { //ServerBootstrap负责初始化netty服务器,并且开始监听端口的socket请求 ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, childGroup) .channel(NioServerSocketChannel.class) .localAddress(new InetSocketAddress(port)) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception {// 为监听客户端read/write事件的Channel添加用户自定义的ChannelHandler socketChannel.pipeline().addLast(serverHandler); } }); ChannelFuture f = b.bind().sync(); f.channel().closeFuture().sync(); } catch (InterruptedException e) { e.printStackTrace(); } finally { bossGroup.shutdownGracefully().sync(); childGroup.shutdownGracefully().sync(); } }从上图的代码可以总结为以下几个步骤: ...

June 3, 2019 · 2 min · jiezi

java-Callable与Future模式

在Java中,创建线程一般有两种方式,一种是继承Thread类,一种是实现Runnable接口。然而,这两种方式的缺点是在线程任务执行结束后,无法获取执行结果。我们一般只能采用共享变量或共享存储区以及线程通信的方式实现获得任务结果的目的。不过,Java中,也提供了使用Callable和Future来实现获取任务结果的操作。Callable用来执行任务,产生结果,而Future用来获得结果。 Future常用方法:V get() :获取异步执行的结果,如果没有结果可用,此方法会阻塞直到异步计算完成。V get(Long timeout , TimeUnit unit) :获取异步执行结果,如果没有结果可用,此方法会阻塞,但是会有时间限制,如果阻塞时间超过设定的timeout时间,该方法将抛出异常。boolean isDone() :如果任务执行结束,无论是正常结束或是中途取消还是发生异常,都返回true。boolean isCanceller() :如果任务完成前被取消,则返回true。boolean cancel(boolean mayInterruptRunning) :如果任务还没开始,执行cancel(...)方法将返回false;如果任务已经启动,执行cancel(true)方法将以中断执行此任务线程的方式来试图停止任务,如果停止成功,返回true;当任务已经启动,执行cancel(false)方法将不会对正在执行的任务线程产生影响(让线程正常执行到完成),此时返回false;当任务已经完成,执行cancel(...)方法将返回false。mayInterruptRunning参数表示是否中断执行中的线程。通过方法分析我们也知道实际上Future提供了3种功能:(1)能够中断执行中的任务(2)判断任务是否执行完成(3)获取任务执行完成后额结果。 定义threadpublic class CallThread implements Callable<String> { @Override public String call() throws InterruptedException { Thread.sleep(1000); return "callable and future"; }}测试@RequestMapping("test-call") public void testCall() { CallThread callThread = new CallThread(); ExecutorService executor = Executors.newCachedThreadPool(); Future<String> submit = executor.submit(callThread); try { String s = submit.get(); System.out.println("获取结果:" + s); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } }

June 1, 2019 · 1 min · jiezi

java-锁机制

重入锁锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized(重量级) 和 ReentrantLock(轻量级)等等 ) 。这些已经写好提供的锁为我们开发提供了便利。重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁 读写锁相比Java中的锁(Locks in Java)里Lock实现,读写锁更复杂一些。假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写(译者注:也就是说:读-读能共存,读-写不能共存,写-写不能共存)。这就需要一个读/写锁来解决这个问题。Java5在java.util.concurrent包中已经包含了读写锁(ReentrantReadWriteLock) 乐观锁总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现。 version方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。核心SQL语句update table set x=x+1, version=version+1 where id=#{id} and version=#{version}; CAS操作方式:即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。 悲观锁总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁,在Java中,synchronized的思想也是悲观锁。 原子类java.util.concurrent.atomic包:原子类的小工具包,支持在单个变量上解除锁的线程安全编程原子变量类相当于一种泛化的 volatile 变量,能够支持原子的和有条件的读-改-写操作。AtomicInteger 表示一个int类型的值,并提供了 get 和 set 方法,这些 Volatile 类型的int变量在读取和写入上有着相同的内存语义。它还提供了一个原子的 compareAndSet 方法(如果该方法成功执行,那么将实现与读取/写入一个 volatile 变量相同的内存效果),以及原子的添加、递增和递减等方法。AtomicInteger 表面上非常像一个扩展的 Counter 类,但在发生竞争的情况下能提供更高的可伸缩性,因为它直接利用了硬件对并发的支持。CAS:Compare and Swap,即比较再交换。 jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。常用:AtomicBooleanAtomicIntegerAtomicLongAtomicReference 分布式锁如果想在不同的jvm中保证数据同步,使用分布式锁技术。有数据库实现、缓存实现、Zookeeper分布式锁

June 1, 2019 · 1 min · jiezi

jave-线程池

线程池简述Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处。 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间,从而提高效率。如果一个线程的时间非常长,就没必要用线程池了(不是不能作长时间操作,而是不宜。),况且我们还不能控制线程池中线程的开始、挂起、和中止 线程池的分类ThreadPoolExecutorExecutor框架的最顶层实现是ThreadPoolExecutor类,Executors工厂类中提供的newScheduledThreadPool、newFixedThreadPool、newCachedThreadPool方法其实也只是ThreadPoolExecutor的构造函数参数不同而已。通过传入不同的参数,就可以构造出适用于不同应用场景下的线程池corePoolSize: 核心池的大小。 当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中maximumPoolSize: 线程池最大线程数,它表示在线程池中最多能创建多少个线程;keepAliveTime: 表示线程没有任务执行时最多保持多久时间会终止。unit: 参数keepAliveTime的时间单位,有7种取值 newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程 newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors() newScheduledThreadPool创建一个定长线程池,支持定时及周期性任务执行 newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行 线程池原理提交一个任务到线程池中,线程池的处理流程如下:1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。 合理配置线程池CPU密集CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。 CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总的运算能力就那些。 IO密集IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即时在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间 要想合理的配置线程池的大小,首先得分析任务的特性,可以从以下几个角度分析: 任务的性质:CPU密集型任务、IO密集型任务、混合型任务。任务的优先级:高、中、低。任务的执行时间:长、中、短。任务的依赖性:是否依赖其他系统资源,如数据库连接等。性质不同的任务可以交给不同规模的线程池执行。对于不同性质的任务来说,CPU密集型任务应配置尽可能小的线程,如配置CPU个数+1的线程数,IO密集型任务应配置尽可能多的线程,因为IO操作不占用CPU,不要让CPU闲下来,应加大线程数量,如配置两倍CPU个数+1,而对于混合型的任务,如果可以拆分,拆分成IO密集型和CPU密集型分别处理,前提是两者运行的时间是差不多的,如果处理时间相差很大,则没必要拆分了。若任务对其他系统资源有依赖,如某个任务依赖数据库的连接返回的结果,这时候等待的时间越长,则CPU空闲的时间越长,那么线程数量应设置得越大,才能更好的利用CPU。 当然具体合理线程池值大小,需要结合系统实际情况,在大量的尝试下比较才能得出,以上只是前人总结的规律。 最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目可以得出一个结论: 线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。 以上公式与之前的CPU和IO密集型任务设置线程数基本吻合。CPU密集型时,任务可以少配置线程数,大概和机器的cpu核数相当,这样可以使得每个线程都在执行任务IO密集型时,大部分线程都阻塞,故需要多配置线程数,2*cpu核数操作系统之名称解释:某些进程花费了绝大多数时间在计算上,而其他则在等待I/O上花费了大多是时间,前者称为计算密集型(CPU密集型)computer-bound,后者称为I/O密集型,I/O-bound。 自定义线程池代码@RequestMapping("test-threadpool") public void testThreadPool() { ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 6, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3)); for (int i = 0; i < 6; i++) { TaskThred t1 = new TaskThred("任务" + i); threadPoolExecutor.execute(t1); } threadPoolExecutor.shutdown(); }class TaskThred implements Runnable { private String taskName; public TaskThred(String taskName) { this.taskName = taskName; } @Override public void run() { System.out.println(Thread.currentThread().getName()+taskName); }}结果pool-1-thread-2任务4pool-1-thread-1任务0pool-1-thread-1任务1pool-1-thread-1任务2pool-1-thread-1任务3pool-1-thread-3任务5 ...

June 1, 2019 · 1 min · jiezi

java-Semaphore

Semaphore简介Semaphore是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore可以用来构建一些对象池,资源池之类的,比如数据库连接池,我们也可以创建计数为1的Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态。它的用法如下:availablePermits函数用来获取当前可用的资源数量wc.acquire(); //申请资源wc.release();// 释放资源 public Semaphore(int permits,boolean fair) permits:初始化可用的许可数目。 fair: 若该信号量保证在征用时按FIFO的顺序授予许可,则为true,否则为false; 例子餐厅2个座位,但是有3个人要等位就餐 public class SemaphoreThread extends Thread { private String name; private Semaphore semaphore; public SemaphoreThread(String name, Semaphore semaphore) { this.name = name; this.semaphore = semaphore; } @Override public void run() { if (semaphore.availablePermits() <= 0) { System.out.println(name + "等位中。。。"); } try { semaphore.acquire(); System.out.println(name + "开始就餐了。。"); Thread.sleep(new Random().nextInt(1000)); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(name + "吃完了。。"); semaphore.release(); }}请求: ...

May 30, 2019 · 1 min · jiezi

java-CountDownLatch

CountDownLatch 介绍CountDownLatch 类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他几个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。 定义两个线程线程1public class Thread1 extends Thread { private CountDownLatch countDownLatch; public Thread1(CountDownLatch countDownLatch) { this.countDownLatch = countDownLatch; } @Override public void run() { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("thread-1"); countDownLatch.countDown(); }}线程2public class Thread2 extends Thread { private CountDownLatch countDownLatch; public Thread2(CountDownLatch countDownLatch) { this.countDownLatch = countDownLatch; } @Override public void run() { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("thread-2"); countDownLatch.countDown(); }}请求@RequestMapping("test-countdownlatch") public void testCountDownLatch() { CountDownLatch countDownLatch = new CountDownLatch(2); Thread1 thread1 = new Thread1(countDownLatch); Thread2 thread2 = new Thread2(countDownLatch); thread1.start(); thread2.start(); try { countDownLatch.await(); System.out.println("main..."); } catch (InterruptedException e) { e.printStackTrace(); } }结果"main..."总是等待两个线程完成后打印 ...

May 30, 2019 · 1 min · jiezi

java-多线程-waitnotify

因为涉及到对象锁,Wait、Notify一定要在synchronized里面进行使用。Wait必须暂定当前正在执行的线程,并释放资源锁,让其他线程可以有机会运行notify/notifyall: 唤醒线程共享变量public class ShareEntity { private String name; // 线程通讯标识 private Boolean flag = false; public ShareEntity() { } public String getName() { return name; } public void setName(String name) { this.name = name; } public Boolean getFlag() { return flag; } public void setFlag(Boolean flag) { this.flag = flag; }}线程1(生产者)public class CommunicationThread1 extends Thread{ private ShareEntity shareEntity; public CommunicationThread1(ShareEntity shareEntity) { this.shareEntity = shareEntity; } @Override public void run() { int num = 0; while (true) { synchronized (shareEntity) { if (shareEntity.getFlag()) { try { shareEntity.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } if (num % 2 == 0) shareEntity.setName("thread1-set-name-0"); else shareEntity.setName("thread1-set-name-1"); num++; shareEntity.setFlag(true); shareEntity.notify(); } } }}线程2(消费者)public class CommunicationThread2 extends Thread{ private ShareEntity shareEntity; public CommunicationThread2(ShareEntity shareEntity) { this.shareEntity = shareEntity; } @Override public void run() { while (true) { synchronized (shareEntity) { if (!shareEntity.getFlag()) { try { shareEntity.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(shareEntity.getName()); shareEntity.setFlag(false); shareEntity.notify(); } } }}请求@RequestMapping("test-communication") public void testCommunication() { ShareEntity shareEntity = new ShareEntity(); CommunicationThread1 thread1 = new CommunicationThread1(shareEntity); CommunicationThread2 thread2 = new CommunicationThread2(shareEntity); thread1.start(); thread2.start(); }结果thread1-set-name-0thread1-set-name-1thread1-set-name-0thread1-set-name-1thread1-set-name-0thread1-set-name-1thread1-set-name-0 ...

May 28, 2019 · 1 min · jiezi

java-Threadlocal

ThreadlocalThreadLocal提高一个线程的局部变量,访问某个线程拥有自己局部变量。 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。ThreadLocal的接口方法ThreadLocal类接口很简单,只有4个方法:• void set(Object value)设置当前线程的线程局部变量的值。• public Object get()该方法返回当前线程所对应的线程局部变量。• public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。• protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。 示例public class LocalDemo { // 生成序列号共享变量 public static Integer count = 0; public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() { protected Integer initialValue() { return 0; }; }; public Integer getNum() { int count = threadLocal.get() + 1; threadLocal.set(count); return count; }}public class LocalThread extends Thread{ private LocalDemo localDemo; public LocalThread(LocalDemo localDemo) { this.localDemo = localDemo; } @Override public void run() { for (int i = 0; i < 3; i++) { System.out.println(Thread.currentThread().getName() + "---" + "i---" + i + "--num:" + localDemo.getNum()); } }}@RequestMapping("test-local") public void testLocal() { LocalDemo localDemo = new LocalDemo(); LocalThread lt1 = new LocalThread(localDemo); LocalThread lt2 = new LocalThread(localDemo); LocalThread lt3 = new LocalThread(localDemo); lt1.start(); lt2.start(); lt3.start(); }结果Thread-18---i---0--num:1Thread-18---i---1--num:2Thread-18---i---2--num:3Thread-17---i---0--num:1Thread-17---i---1--num:2Thread-17---i---2--num:3Thread-19---i---0--num:1Thread-19---i---1--num:2Thread-19---i---2--num:3 ...

May 19, 2019 · 1 min · jiezi

java多线程-线程安全问题

当多个线程同时共享,同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题。但是做读操作是不会发生数据冲突问题 模拟线程安全问题public class SafeThread implements Runnable { private int ticketCount = 50; @Override public void run() { while (ticketCount > 0) { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ",出售第" + (50 - ticketCount + 1) + "张票"); ticketCount--; } }}@RequestMapping("test-safe") public void testSafe() { SafeThread safeThread = new SafeThread(); Thread t1 = new Thread(safeThread, "thread-1"); Thread t2 = new Thread(safeThread, "thread-2"); t1.start(); t2.start(); }结果:火车票会重复出售 ...

May 18, 2019 · 2 min · jiezi

AQS同步组件ReentrantLock与锁

ReentrantLock与锁Synchronized和ReentrantLock异同可重入性:两者都具有可重入性锁的实现:Synchronized是依赖jvm实现的,ReentrantLock是jdk实现的。(我们可以理解为一个是操作系统层面的实现另一个是用户自己自己实现的)Synchronized的实现是jvm层面的很难看到其中的实现。而ReentrantLock是通过jvm实现的我们可以通过阅读jvm源码来查看实现。性能区别:在Synchronized优化之前Synchronized的性能相比ReentrantLock差很多,在Synchronized引入了偏向锁,轻量级锁也就是自旋锁之后了,两者的性能相差不大了。在两者都可用的情况下官方更推荐使用Synchronized,因为其写法更简单,Synchronized的优化就是借鉴了ReentrantLock中的cas技术。功能区别:便利性,很明显synchronized的使用更加便利,ReentrantLock在细粒度和灵活性中会优于Synchronized。ReentrantLock独有功能ReentrantLock可指定是公平锁还是非公平锁,Synchronized只能是非公平锁。(公平锁就是先等待的线程先获得锁)ReentrantLock提供一个Condition类,可以分组唤醒需要唤醒的形成。synchronized是要嘛随机唤醒一个线程要嘛唤醒所有的线程。ReentrantLock提供了一种能够中断等待锁的线程的机制lock.locInterruptibly(),ReentrantLock实现是一种自旋锁通过循环调用,通过cas机制来实现加锁。性能较好是因为避免了线程进入内核的阻塞状态@Slf4jpublic class LockExample2 { // 请求总数 public static int clientTotal = 5000; // 同时并发执行的线程数 public static int threadTotal = 200; public static int count = 0; private final static Lock lock = new ReentrantLock(); public static void main(String[] args) throws Exception { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(threadTotal); final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for (int i = 0; i < clientTotal ; i++) { executorService.execute(() -> { try { semaphore.acquire(); add(); semaphore.release(); } catch (Exception e) { log.error("exception", e); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); log.info("count:{}", count); } private static void add() { lock.lock(); try { count++; } finally { lock.unlock(); } }}我们首先使用 private final static Lock lock = new ReentrantLock()声明一个所得实例,然后使用 ...

May 15, 2019 · 3 min · jiezi

AQS同步组件CyclicBarrier

CyclicBarrierCyclicBarrier也是一个同步辅助类,它允许一组线程相互等待直到到达某个工作屏障点,通过他可以完成多线程之间的相互等待。每个线程都就绪之后才能执行后面的操作。和CountLatch有相似的地方都是通过计数器来实现的。当某个线程执行了await()方法后就进入等待状态,计数器进行加1操作,当增加后的值达到我们设定的值后,线程被唤醒,继续执行后续操作。CyclicBarrier是可重用的计数器,CyclicBarrier的使用场景和CountDownLatch的使用场景很相似,可以用于多线程计算数据最后总计结果。CyclicBarrier和CountDownLatch的使用区别: CountDownLatch的计数器只能使用一次,CyclicBarrier可以用reset方法重置。CountDownLatch是一个线程等待其他线程完成某个操作后才能继续执行。也就是一个或个多线程等待其他的关系,而CyclicBarrier是实现了多个线程之间的相互等待,所有线程都满足了条件之后才能继续使用。演示代码 @Slf4jpublic class CyclicBarrierExample1 { private static CyclicBarrier barrier = new CyclicBarrier(5); public static void main(String[] args) throws Exception { ExecutorService executor = Executors.newCachedThreadPool(); for (int i = 0; i < 10; i++) { final int threadNum = i; Thread.sleep(1000); executor.execute(() -> { try { race(threadNum); } catch (Exception e) { log.error("exception", e); } }); } executor.shutdown(); } private static void race(int threadNum) throws Exception { Thread.sleep(1000); log.info("{} is ready", threadNum); barrier.await(); log.info("{} continue", threadNum); }}输出结果如下: ...

May 14, 2019 · 2 min · jiezi

java多线程synchronized

synchronize可以在多个线程操作同一个成员变量或者方法时,实现同步(或者互斥)的效果。synchronized可以作用于方法,以及方法内部的代码块。 //1 synchronized void method(){} //2static synchronized void method(){}//3synchronized void method(){ synchronized(锁对象){ }}//4static synchronized void method(){ synchronized(锁对象){ }}锁对象那么在上面的示例中,它们分别持有的锁对象是谁?synchronized作用于非静态方法以及非静态方法内部的代码块,持有的是当前类的对象的锁,并且是同一个锁。作用于静态方法及其内部的代码块,持有的是当前类的Class对象的锁,并且和非静态方法不是同一个锁。通过代码来验证。 public class SynchronizedTest { private synchronized void test1(){ for (int x = 0; x < 5; x++) { System.out.println("test1---"+x); } } private void test2(){ synchronized(this) { for (int x = 0; x < 5; x++) { System.out.println("---test2---"+x); } } } private static synchronized void test3(){ for (int x = 0; x < 5; x++) { System.out.println("------test3---"+x); } } private static void test4(){ synchronized (SynchronizedTest.class){ for (int x = 0; x < 5; x++) { System.out.println("---------test4---"+x); } } } public static void main(String[] args) { SynchronizedTest synchronizedTest = new SynchronizedTest(); new Thread(new Runnable() { @Override public void run() { synchronizedTest.test1(); } }).start(); new Thread(new Runnable() { @Override public void run() { synchronizedTest.test2(); } }).start(); new Thread(new Runnable() { @Override public void run() { test3(); } }).start(); new Thread(new Runnable() { @Override public void run() { test4(); } }).start(); }}执行结果 ...

May 14, 2019 · 2 min · jiezi

AQS同步组件Semaphore

Semaphore什么是Semaphore?是用于控制某个资源同一时间被线程访问的个数,提供acquire()和release()方法,acquire获取一个许可,如果没有获取的到就等待,release是在操作完成后释放一个许可,Semaphore维护了当前访问的个数,通过同步机制来控制同时访问的个数,在数据结构里链表中的节点是可以无限个的,而Semaphore里维护的是一个有大小的限链表。Semaphore的使用场景Semaphore用于仅能提供有限访问的资源,比如数据库中的链接数只有20但是我们上层应用数可能远大于20,如果同时都对数据库链接进行获取,那很定会因为链接获取不到而报错,所以我们就要对数据库链接的访问进行控制。演示代码@Slf4jpublic class SemaphoreExample1 { private final static int threadCount = 20; public static void main(String[] args) throws Exception { ExecutorService exec = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(3); for (int i = 0; i < threadCount; i++) { final int threadNum = i; exec.execute(() -> { try { semaphore.acquire(); // 获取一个许可 test(threadNum); semaphore.release(); // 释放一个许可 } catch (Exception e) { log.error("exception", e); } }); } exec.shutdown(); } private static void test(int threadNum) throws Exception { log.info("{}", threadNum); Thread.sleep(1000); }}我们在执行 test(threadNum)方式前后包裹上acquire和release,这样其实我们就相当于一个单线程在执行。当执行acquire后就只能等待执行release后再执行新的线程,然后我们在acquire()和release()都是没有传参也就是1,每次只允许一个线程执行,如果我们改成 ...

May 13, 2019 · 2 min · jiezi

java多线程-如何停止线程

和线程停止相关的三个方法 /*中断线程。如果线程被wait(),join(),sleep()等方法阻塞,调用interrupt()会清除线程中断状态,并收到InterruptedException异常。另外interrupt();对于isAlive()返回false的线程不起作用。*/ public void interrupt(); /* 静态方法,判断线程中断状态,并且会清除线程的中断状态。所以连续多次调用该方法,第二次之后必定返回false。另外,isAlive()用于判断线程是否处于存活状态,如果isAlive()返回false,interrupted()也必定返回false。 */ public static boolean interrupted();/*判断线程中断状态,但不会清除线程中断状态。另外,isAlive()用于判断线程是否处于存活状态,如果isAlive()返回false,interrupted()也必定返回false。*/ public boolean isInterrupted();线程停止的几种情况:1: 使用退出标记,run方法执行完毕,线程正常退出。2: 使用stop()方法,已过时的方法,不推荐。3: 使用interrupt()方法中断线程。 interrupt()单独调用这个方法并不能中断线程,只是打了一个中断状态的标记。或者说是将线程状态更改为中断状态。中断线程可以通过以下几种方法。 在线程内部抛出异常。在线程内部使用return结束线程。

May 13, 2019 · 1 min · jiezi

AQS同步组件CountDownLatch

CountDownLatchCountDownLatch是在java1.5被引入的,跟它一起被引入的并发工具类还有CyclicBarrier、Semaphore、ConcurrentHashMap和BlockingQueue,它们都存在于java.util.concurrent包下。CountDownLatch这个类能够使一个线程等待其他线程完成各自的工作后再执行。例如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有的框架服务之后再执行。CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。 调用CountDownLatch类的await()方法会一直阻塞,直到其他线程调用CountDown()方法使计数器的值减1,当计数器的值等于0则当因调用await()方法处于阻塞状态的线程会被唤醒继续执行。计数器是不能被重置的。这个类使用线程在达到某个条件后继续执行的情况。比如并行计算,计算量特别大,我可以将计算量拆分成多个线程进行计算,最后将结果汇总。 一个CountdownLatch 例子 @Slf4jpublic class CountDownLatchExample1 { private final static int threadCount = 200; public static void main(String[] args) throws Exception { ExecutorService exec = Executors.newCachedThreadPool(); final CountDownLatch countDownLatch = new CountDownLatch(threadCount); for (int i = 0; i < threadCount; i++) { final int threadNum = i; exec.execute(() -> { try { test(threadNum); } catch (Exception e) { log.error("exception", e); } finally { countDownLatch.countDown(); } }); } countDownLatch.await(); log.info("finish"); exec.shutdown(); } private static void test(int threadNum) throws Exception { Thread.sleep(100); log.info("{}", threadNum); Thread.sleep(100); }}我们在线程之后都调用了countDown方法,在执行log之前调用了await方法,从而来保证打印日志时一定是在所有线程执行完。假设我们不使用CountDownLatch时结果会怎么样? ...

May 12, 2019 · 1 min · jiezi

线程封闭

我们可以通过把对象设计成不可变对象来躲避并发,我们还可以通过使用线程封闭来实现线程安全,所谓线程封闭 就是将数据都封装到一个线程里,不让其他线程访问。Ad-hoc 线程封闭程序控制实现,比较脆弱,尽量少用堆栈封闭:局部变量,无并发问题,在项目中使用最多,简单说就是局部变量,方法的变量都拷贝到线程的堆栈中,只有这个线程能访问到。尽量少使用全局变量(变量不是常量)ThreadLocal线程封闭:比较好的封闭方法ThreadLocal维护的是一个map 这个map是线程的名称多为key 我们所有封闭的值作为value。我们做使用Filter做登录操作都做过,我们现在来使用ThreadLoad来存储一下用户信息。public class RequestHolder { private final static ThreadLocal<Long> requestHolder = new ThreadLocal<>(); public static void add(Long id) { requestHolder.set(id); } public static Long getId() { return requestHolder.get(); } public static void remove() { requestHolder.remove(); }}声明一个ThreadLoad对象用来存储ThreadLoad信息。 @Slf4jpublic class HttpFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; log.info("do filter, {}, {}", Thread.currentThread().getId(), request.getServletPath()); RequestHolder.add(Thread.currentThread().getId()); filterChain.doFilter(servletRequest, servletResponse); } @Override public void destroy() { }@Slf4jpublic class HttpInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.info("preHandle"); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { RequestHolder.remove(); log.info("afterCompletion"); return; }}定义filter的内容,我们在filter中将线程id加入到ThreadLoad中,然后在controller中获取,看看能不能获取的到。在线程执行完之后将ThreadLoad中的数据清空防止内存溢出。 ...

May 9, 2019 · 1 min · jiezi

3分钟干货之多线程有什么用

一个可能在很多人看来很扯淡的一个问题:我会用多线程就好了,还管它有什么用?在我看来,这个回答更扯淡。所谓"知其然知其所以然","会用"只是"知其然","为什么用"才是"知其所以然",只有达到"知其然知其所以然"的程度才可以说是把一个知识点运用自如。OK,下面说说我对这个问题的看法:1)发挥多核CPU的优势随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4核、8核甚至16核的也都不少见,如果是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费了75%。单核CPU上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了。多核CPU上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的。 2)防止阻塞从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU我们还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。 3)便于建模这是另外一个没有这么明显的优点了。假设有一个大的任务A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。

May 9, 2019 · 1 min · jiezi

线程安全可见性

共享变量在线程间不可见的原因线程的交叉执行重排序结合线程交叉执行共享变量更新后的值没有在工作内存与主内存间及时更新使用synchronized的来保证可见性使用synchronized的两条规定: 线程解锁前,必须把共享变量的最新值刷新到主内存线程加锁锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意加锁与解锁是同一把锁)volatile 来实现可见性通过加入内存屏障和禁止重拍讯优化来实现可见性。对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存对volatile变量进行读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。也就是说使用volatile关键字在读和写操作时都会强迫从主内存中获取变量值。下图是使用volatile写操作的示意图 使用volatile写操作前会插入一条StoreStore指令来禁止在volatile写之前的普通写对volatile写的指令重排序优化,在写之后会插入一条StoreLoad屏障指令来防止上面的volatile写操作和下面可能有的读或者写进行指令重排序。下图是volatile读操作示意图 volatile操作都是cpu指令级别的下面看一段演示代码 @Slf4jpublic class CountExample4 { // 请求总数 public static int clientTotal = 5000; // 同时并发执行的线程数 public static int threadTotal = 200; public static volatile int count = 0; public static void main(String[] args) throws Exception { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(threadTotal); final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for (int i = 0; i < clientTotal ; i++) { executorService.execute(() -> { try { semaphore.acquire(); add(); semaphore.release(); } catch (Exception e) { log.error("exception", e); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); log.info("count:{}", count); } private static void add() { count++; // 1、count // 2、+1 // 3、count }}我们多次运行个这段代码,发现结果并不是我们预期5000,volatile只能保证可见性并不能保证原子性。 ...

May 6, 2019 · 1 min · jiezi

线程安全性原子性

线程安全性定义当多个线程访问同一个类时,不管运行时环境采用何种调度方式,不论线程如何交替执行,在主调代码中不需要额外的协同或者同步代码时,这个类都可以表现出正确的行为,我们则称这个类为线程安全的。线程安全性原子性:提供了互斥访问,同一时刻只能有一个线程来对他进行操作。可见性:一个线程对主内存的修改可以及时被其他线程观察到。有序性:一个线程观察其他线程中的指令顺序,由于指令重排序的存在,该结果一般杂乱无序。原子性 - Atomic包AtomicXXX 是通过 CAS(CompareAndSwap)来保证线程原子性 通过比较操作的对象的值(工作内存的值)与底层的值(共享内存中的值)对比是否相同来判断是否进行处理,如果不相同则重新获取。如此循环操作,直至获取到期望的值。(关于什么是主内存什么事工作内存在上篇博客中进行介绍了,不懂的同学可以翻一下)示例代码: @Slf4jpublic class AtomicExample2 { // 请求总数 public static int clientTotal = 5000; // 同时并发执行的线程数 public static int threadTotal = 200; public static AtomicLong count = new AtomicLong(0); public static void main(String[] args) throws Exception { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(threadTotal); final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for (int i = 0; i < clientTotal ; i++) { executorService.execute(() -> { try { semaphore.acquire(); add(); semaphore.release(); } catch (Exception e) { log.error("exception", e); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); log.info("count:{}", count.get()); } private static void add() { count.incrementAndGet(); // count.getAndIncrement(); }}LongAdder和DoubleAdderjdk8中新增的保证同步操作的类,我们之前介绍了AtomicXXX来保证原子性,那么为什么还有有LongAdder呢?说AtomicXXX的实现是通过死循环来判断值的,在低并发的情况下AtomicXXX进行更改值的命中率还是很高的。但是在高并发下进行命中率可能没有那么高,从而一直执行循环操作,此时存在一定的性能消耗,在jvm中我们允许将64位的数值拆分成2个32位的数进行储存的,LongAdder的思想就是将热点数据分离,将AtomicXXX中的核心数据分离,热点数据会被分离成多个数组,每个数据都单独维护各自的值,将单点的并行压力发散到了各个节点,这样就提高了并行,在低并发的时候性能基本和AtomicXXX相同,在高并发时具有较好的性能,缺点是在并发更新时统计时可能会出现误差。在低并发,需要全局唯一,准确的比如id等使用AtomicXXX,要求性能使用LongAdder ...

May 5, 2019 · 2 min · jiezi

java并发编程实战学习三

线程封闭 当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不要同步。这种技术成为线程封闭(Thread Confinement)。Ad-hoc 线程封闭Ad-hoc 线程封闭是指维护线程封闭的职责完全是由程序自己来承担。栈封闭栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。ThreadLocal类ThreadLocal提供了set和get等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set设置的最新值。import java.sql.Connection;import java.sql.DriverManager;import java.sql.SQLException;/** * ConnectionDispenser * <p/> * Using ThreadLocal to ensure thread confinement * * @author Brian Goetz and Tim Peierls */public class ConnectionDispenser { static String DB_URL = "jdbc:mysql://localhost/mydatabase"; private ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { public Connection initialValue() { try { return DriverManager.getConnection(DB_URL); } catch (SQLException e) { throw new RuntimeException("Unable to acquire Connection, e"); } }; }; public Connection getConnection() { return connectionHolder.get(); }}不变性满足同步需求的另一种方法是使用不可变对象。1,对象创建以后其状态就不能修改。2,对象的所有域都是final类型。3,对象是正确创建的。 ...

May 3, 2019 · 1 min · jiezi

java并发编程实战学习二

对象的共享上一章介绍了如何通过同步来避免多个线程在同一时刻访问相同的数据,而本章将介绍如何共享和发布对象,从而使它们能够安全地由多个线程同时访问。 列同步代码块和同步方法可以确保以原子的方式执行操作,但一种常见的误解是,认为关键字synchronized只能用于实现原子性或者确定“临界区”。同步还有另一重要的方面;内存可见性。我们不仅希望防止某个线程正在使用对象状态而另一个线程在同时修改该状态,而希望确保当一个线程修改了对象状态之后,其他线程能够看到发生的状态变化。如果没有同步,那么这种情况就无法实现。3.1 可见性通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值。有时甚至是不可能的事情。为了确保多个线程的之间对内存写入操作的可见性,必须使用同步机制。/** * NoVisibility * <p/> * Sharing variables without synchronization * * @author Brian Goetz and Tim Peierls */public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { while (!ready) Thread.yield(); System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); number = 42; ready = true; }}主线程启动读线程,然后将number设为42,并将ready设为true。读线程一直循环知道发现ready的值为true,然后输出number的值。虽然NoVisibility看起来会输出42,但事实上很可能输出0,或者根本无法终止。这是因为在代码中没有使用足够的同步机制,因此无法保证主线程写入的ready值和number值对于读线程来说是可见的。 3.1.1 失效数据查看变脸时,可能会得到一个已经失效的值。下方代码中,如果某线程调用了set,那么另一个正在调用get的线程可能会看到更新后的value值,也可能看不到。 ...

May 3, 2019 · 3 min · jiezi

Java并发之线程组ThreadGroup介绍

线程组介绍线程组的构造ThreadGroup方法介绍 查看线程组信息终止线程组中的所有线程总结Links 作者资源相关资源线程组介绍线程组(ThreadGroup)简单来说就是一个线程集合。线程组的出现是为了更方便地管理线程。 线程组是父子结构的,一个线程组可以集成其他线程组,同时也可以拥有其他子线程组。从结构上看,线程组是一个树形结构,每个线程都隶属于一个线程组,线程组又有父线程组,这样追溯下去,可以追溯到一个根线程组——System线程组。 <div align=center> </div> 下面介绍一下线程组树的结构: JVM创建的system线程组是用来处理JVM的系统任务的线程组,例如对象的销毁等。system线程组的直接子线程组是main线程组,这个线程组至少包含一个main线程,用于执行main方法。main线程组的子线程组就是应用程序创建的线程组。你可以在main方法中看到JVM创建的system线程组和main线程组: public static void main(String[] args) { ThreadGroup mainThreadGroup=Thread.currentThread().getThreadGroup(); ThreadGroup systenThreadGroup=mainThreadGroup.getParent(); System.out.println("systenThreadGroup name = "+systenThreadGroup.getName()); System.out.println("mainThreadGroup name = "+mainThreadGroup.getName()); }console输出: systenThreadGroup name = systemmainThreadGroup name = main一个线程可以访问其所属线程组的信息,但不能访问其所属线程组的父线程组或者其他线程组的信息。 线程组的构造java.lang.ThreadGroup提供了两个构造函数: ConstructorDescriptionThreadGroup(String name)根据线程组名称创建线程组,其父线程组为main线程组ThreadGroup(ThreadGroup parent, String name)根据线程组名称创建线程组,其父线程组为指定的parent线程组下面演示一下这两个构造函数的用法: public static void main(String[] args) { ThreadGroup subThreadGroup1 = new ThreadGroup("subThreadGroup1"); ThreadGroup subThreadGroup2 = new ThreadGroup(subThreadGroup1, "subThreadGroup2"); System.out.println("subThreadGroup1 parent name = " + subThreadGroup1.getParent().getName()); System.out.println("subThreadGroup2 parent name = " + subThreadGroup2.getParent().getName());}console输出: ...

April 29, 2019 · 3 min · jiezi

线程池系列-1-让多线程不再坑爹的线程池

背景线程池的来由服务端的程序,例如数据库服务器和Web服务器,每次收到客户端的请求,都会创建一个线程来处理这些请求。 创建线程的方式又很多,例如继承Thread类、实现Runnable或者Callable接口等。 通过创建新的线程来处理客户端的请求,这种看起来很容易的方法,其实是有很大弊端且有很高的风险的。 俗话说,简单的路越走越困难,困难的路越走越简单,就是这个道理。 创建和销毁线程,会消耗大量的服务器资源,甚至创建和销毁线程消耗的时间比线程本身处理任务的时间还要长。 由于启动线程需要消耗大量的服务器资源,如果创建过多的线程会造成系统内存不足(run out of memory),因此限制线程创建的数量十分必要。 什么是线程池线程池通俗来讲就是一个取出和放回提前创建好的线程的池子,概念上,类似数据库的连接池。 那么线程池是如何发挥作用的呢? 实际上,线程池是通过重用之前创建好线程来处理当前任务,来达到大大降低线程频繁创建和销毁导致的资源消耗的目的。 A thread pool reuses previously created threads to execute current tasks and offers a solution to the problem of thread cycle overhead and resource thrashing. Since the thread is already existing when the request arrives, the delay introduced by thread creation is eliminated, making the application more responsive. 背景总结下面总结一下开篇对于线程池的一些介绍。 线程是程序的组成部分,可以帮助我们搞事情。多个线程同时帮我们搞事情,可以通过更大限度地利用服务器资源,用来大大提高我们搞事情的效率。我们创建的每个线程都不是省油的灯,线程越多就会占用越多的系统资源,因此小弟虽好使但不要贪多哦,在有限的系统资源下,线程并不是“韩信点兵,多多益善”的,要限制线程的数量。请记住这一条,因为下面“批判”Java提供的线程池创建解决方案的时候,这就是“罪魁祸首”。创建和销毁线程会耗费大量系统资源,就像大佬招募和遣散小弟,都是要大费周章的。因此聪明的大佬就想到了“池”,把线程缓存起来,用的时候拿出来不用的时候还放回去,这就可以既享受多线程的乐趣,又可以避免使用多线程的痛苦了。但到底怎么使用线程池呢?线程池真的这么简单好用吗?线程池使用的过程中有没有什么坑? 不要着急,下面就结合具体的示例,跟你讲解各种使用线程池的姿势,以及这些姿势爽在哪里,痛在哪里。 准备好纸巾,咳咳...,是笔记本,涛哥要跟你开讲啦! ...

April 29, 2019 · 3 min · jiezi

『并发包入坑指北』之向大佬汇报任务

前言在面试过程中聊到并发相关的内容时,不少面试官都喜欢问这类问题: 当 N 个线程同时完成某项任务时,如何知道他们都已经执行完毕了。这也是本次讨论的话题之一,所以本篇为『并发包入坑指北』的第二篇;来聊聊常见的并发工具。 <!--more--> 自己实现其实这类问题的核心论点都是:如何在一个线程中得知其他线程是否执行完毕。 假设现在有 3 个线程在运行,需要在主线程中得知他们的运行结果;可以分为以下几步: 定义一个计数器为 3。每个线程完成任务后计数减一。一旦计数器减为 0 则通知等待的线程。所以也很容易想到可以利用等待通知机制来实现,和上文的『并发包入坑指北』之阻塞队列的类似。 按照这个思路自定义了一个 MultipleThreadCountDownKit 工具,构造函数如下: 考虑到并发的前提,这个计数器自然需要保证线程安全,所以采用了 AtomicInteger。 所以在初始化时需要根据线程数量来构建对象。 计数器减一当其中一个业务线程完成后需要将这个计数器减一,直到减为0为止。 /** * 线程完成后计数 -1 */ public void countDown(){ if (counter.get() <= 0){ return; } int count = this.counter.decrementAndGet(); if (count < 0){ throw new RuntimeException("concurrent error") ; } if (count == 0){ synchronized (notify){ notify.notify(); } } }利用 counter.decrementAndGet() 来保证多线程的原子性,当减为 0 时则利用等待通知机制来 notify 其他线程。 ...

April 29, 2019 · 2 min · jiezi

JUC可重入锁ReentrantLock

一、写在前面前几篇我们具体的聊了AQS原理以及底层源码的实现,具体参见 《J.U.C|一文搞懂AQS》《J.U.C|同步队列(CLH)》《J.U.C|AQS独占式源码分析》《J.U.C|AQS共享式源码分析》 本章我们来聊一聊其实现之一 可重入锁ReentrantLock的实现原理以及源码分析。 注 :本章主要讲解非公平锁的实现流程和源码解析,其中涉及到AQS底层的实现因在前面几章都已经详细聊过在这会一笔带过。 二、什么是重入锁可重入锁 ReentrantLock ,顾名思义,支持重新进入的锁,其表示该锁能支持一个线程对资源的重复加锁。 Java API 描述 一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。ReentrantLock 将由最近成功获得锁,并且还没有释放该锁的线程所拥有。当锁没有被另一个线程所拥有时,调用 lock 的线程将成功获取该锁并返回。如果当前线程已经拥有该锁,此方法将立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法来检查此情况是否发生。 ReentrantLock还提供了公平锁和非公平锁的选择, 其构造方法接受一个公平参数(默认是非公平方式),当传入ture时表示公平锁, 否则为非公平锁。其两者的主要区别在于公平锁获取锁是有顺序的。但是其效率往往没有非公平锁的效率高,在多线程的访问时往往表现很低的吞吐量(即速度慢,常常急慢)。 来张图缓解下 三、源码分析我们先来看一段代码 ReentrantLock lock = new ReentrantLock(); try { lock.lock(); // 业务代码 } finally { lock.unlock(); }这一段代码相信学过Java的同学都非常熟悉了,今天我们就以此为入口一步一步的带你深入其底层世界。 共享状态的获取(锁的获取)lock()方法// ReentrantLock --> lokc() 实现Lock 接口的方法public void lock() { // 调用内部类sync 的lock方法, 这里有两种实现,公平锁(FairSync)非公平锁(NonfairSync)这里我们来主要说 NonfairSync sync.lock(); }ReentrantLock 的lock 方法, sync 为ReentrantLock的一个内部类,其继承了AbstractQueuedSynchronizer(AQS), 他有两个子类公平锁FairSync 和非公平锁NonfairSync ReentrantLock 中其中大部分的功能的实现都是委托给内部类Sync实现的,在Sync 中定义了abstract void lock() 留给子类去实现, 默认实现了final boolean nonfairTryAcquire(int acquires) 方法,可以看出其为非公平锁默认实现方式,下面我讲下给看下非公平锁lock方法。 ...

April 26, 2019 · 2 min · jiezi

开发小记-Java-线程池-之-被吃掉的线程异常附源码分析和解决方法

前言今天遇到了一个bug,现象是,一个任务放入线程池中,似乎“没有被执行”,日志也没有打。 经过本地代码调试之后,发现在任务逻辑的前半段,抛出了NPE,但是代码外层没有try-catch,导致这个异常被吃掉。 这个问题解决起来是很简单的,外层加个try-catch就好了,但是这个异常如果没有被catch,线程池内部逻辑是怎么处理这个异常的呢?这个异常最后会跑到哪里呢? 带着疑问和好奇心,我研究了一下线程池那一块的源码,并且做了以下的总结。 源码分析项目中出问题的代码差不多就是下面这个样子 ExecutorService threadPool = Executors.newFixedThreadPool(3);threadPool.submit(() -> { String pennyStr = null; Double penny = Double.valueOf(pennyStr); ...})先进到newFixedThreadPool这个工厂方法中看生成的具体实现类,发现是ThreadPoolExecutor public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }再看这个类的继承关系, 再进到submit方法,这个方法在ExecutorService接口中约定,其实是在AbstractExectorService中实现,ThreadPoolExecutor并没有override这个方法。 public Future<?> submit(Runnable task) { if (task == null) throw new NullPointerException(); RunnableFuture<Void> ftask = newTaskFor(task, null); execute(ftask); return ftask; }protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) { return new FutureTask<T>(runnable, value); }对应的FutureTask对象的构造方法 ...

April 25, 2019 · 4 min · jiezi

从单例模式到HappensBefore

目录 双重检测锁的演变过程利用HappensBefore分析并发问题无volatile的双重检测锁双重检测锁的演变过程synchronized修饰方法的单例模式双重检测锁的最初形态是通过在方法声明的部分加上synchronized进行同步,保证同一时间调用方法的线程只有一个,从而保证new Singlton()的线程安全: public class Singleton { private static Singleton instance; private Singleton() { } public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }}这样做的好处是代码简单、并且JVM保证new Singlton()这行代码线程安全。但是付出的代价有点高昂: 所有的线程的每一次调用都是同步调用,性能开销很大,而且new Singlton()只会执行一次,不需要每一次都进行同步。 既然只需要在new Singlton()时进行同步,那么把synchronized的同步范围缩小呢? 线程不安全的双重检测锁public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; }}把synchronized同步的范围缩小以后,貌似是解决了每次调用都需要进行同步而导致的性能开销的问题。但是有引入了新的问题:线程不安全,返回的对象可能还没有初始化。 ...

April 24, 2019 · 2 min · jiezi

再次认识ReentrantReadWriteLock读写锁

前言最近研究了一下juc包的源码。在研究ReentrantReadWriteLock读写锁的时候,对于其中一些细节的思考和处理以及关于提升效率的设计感到十分的震撼,难以遏制想要分享这份心得的念头,因此在这里写一篇小文章作为记录。 本片文章建立在已经了解并发相关基础概念的基础上,可能不会涉及很多源码,以思路为主。如果文章有什么纰漏或者错误,还请务必指正,预谢。 1. 从零开始考虑如何实现读写锁首先我们需要知道独占锁(RenentractLock)这种基础的锁,在juc中是如何实现的:它基于java.util.concurrent.locks.AbstractQueuedSynchronizer(AQS)这个抽象类所搭建的同步框架,利用AQS中的一个volatile int类型变量的CAS操作来表示锁的占用情况,以及一个双向链表的数据结构来存储排队中的线程。 简单地说:一个线程如果想要获取锁,就需要尝试对AQS中的这个volatile int变量(下面简称state)执行类似comapre 0 and swap 1的操作,如果不成功就进入同步队列排队(自旋和重入之类的细节不展开说了)同时休眠自己(LockSupport.park),等待占有锁的线程释放锁时再唤醒它。 那么,如果不考虑重入,state就是一个简单的状态标识:0表示锁未被占用,1表示锁被占用,同步性由volatile和CAS来保证。 上面说的是独占锁,state可以不严谨地认为只有两个状态位。 但是如果是读写锁,那这个锁的基本逻辑应该是:读和读共享、读和写互斥、写和写互斥 如何实现锁的共享呢?如果我们不再把state当做一个状态,而是当做一个计数器,那仿佛就可以说得通了:获取锁时compare n and swap n+1,释放锁时compare n and swap n-1,这样就可以让锁不被独占了。 因此,要实现读写锁,我们可能需要两个锁,一个共享锁(读锁),一个独占锁(写锁),而且这两个锁还需要协作,写锁需要知道读锁的占用情况,读锁需要知道写锁的占用情况。 设想中的简单流程大概如下: 设想中的流程很简单,然而存在一些问题: 关于读写互斥: 对于某个线程,当它先获得读锁,然后在执行代码的过程中,又想获得写锁,这种情况是否应该允许?如果允许,会有什么问题?如果不允许,当真的存在这种需求时怎么办?关于写写互斥是否也存在上面两条提到的情况和问题呢?我们知道一般而言,读的操作数量要远远大于写的操作,那么很有可能读锁一旦被获取就长时间处于被占有的情况(因为新来的读操作只需要进去+1就好了,不需要等待state回到0,这样的话state可能永远不会有回到0的一天),这会导致极端情况下写锁根本没有机会上位,该如何解决这种情况?对于上面用计数器来实现共享锁的假设,当任意一个线程想要释放锁(即使它并未获取锁,因为解锁的方法是开放的,任何获取锁对象的线程都可以执行lock.unlock())时,如何判断它是否有权限执行compare n and swap n-1? 是否应该使用ThreadLocal来实现这种权限控制?如果使用ThreadLocal来控制,如何保证性能和效率?2. 带着问题研究ReentrantReadWriteLock在开始研究ReentrantReadWriteLock之前,应当先了解两个概念: 重入性一个很现实的问题是:我们时常需要在锁中加锁。这可能是由代码复用产生的需求,也可能业务的逻辑就是这样。但是不管怎样,在一个线程已经获取锁后,在释放前再次获取锁是一个合理的需求,而且并不生硬。上文在说独占锁时说到如果不考虑重入的情况,state会像boolean一样只有两个状态位。那么如果考虑重入,也很简单,在加锁时将state的值累加即可,表示同一个线程重入此锁的次数,当state归零,即表示释放完毕。 公平、非公平这里的公平和非公平是指线程在获取锁时的机会是否公平。我们知道AQS中有一个FIFO的线程排队队列,那么如果所有线程想要获取锁时都来排队,大家先来后到井然有序,这就是公平;而如果每个线程都不守秩序,选择插队,而且还插队成功了,那这就是不公平。 但是为什么需要不公平呢?因为效率。有两个因素会制约公平机制下的效率: 上下文切换带来的消耗依赖同步队列造成的消耗我们之所以会使用锁、使用并发,可能很大一部分原因是想要挖掘程序的效率,那么相应的,对于性能和效率的影响需要更加敏感。简单地说,上述的两点由于公平带来的性能损耗很可能让你的并发失去高效的初衷。当然这也是和场景密切关联的,比如说你非常需要避免ABA问题,那么公平模式很适合你。具体的不再展开,可以参考这篇文章:深入剖析ReentrantLock公平锁与非公平锁源码实现 回到我们之前提的问题: 对于某个线程,当它先获得读锁,然后在执行代码的过程中,又想获得写锁,这种情况是否应该允许?我们先考虑这种情况是否实际存在:假设我们有一个对象,它有两个实例变量a和b,我们需要在实现:if a = 1 then set b = 2,或者换个例子,如果有个用户名字叫张三就给他打钱。 这看上去仿佛是个CAS操作,然而它并不是,因为它涉及了两个变量,CAS操作并不支持这种针对多个变量的疑似CAS的操作。为什么不支持呢?因为cpu不提供这种针对多个变量的CAS操作指令(至少x86不提供),代码层面的CAS只是对cpu指令的封装而已。为什么cpu不支持呢?可以,但没必要鄙人也不是特别清楚(逃)。 总而言之这种情况是存在的,但是在并发情况下如果不加锁就会有问题:比如先判断得到这个用户确实名叫张三,正准备打钱,突然中途有人把他的名字改了,那再打这笔钱就有问题了,我们判断名字和打钱这两个行为中间应当是没有空隙的。那么为了保证这个操作的正确性,我们或许可以在读之前加一个读锁,在我释放锁之前,其他人不得改变任何内容,这就是之前所说的读写互斥:读的期间不准写。但是如果照着这个想法,就产生了自相矛盾的地方:都说了读期间不能写,那你自己怎么在写(打钱)呢? 如果我们顺着这个思路去尝试解释“自己读的期间还在写”的行为的正当性,我们也许可以设立一个规则:如果读锁是我自己持有,则我可以写。然而这会出现其他的问题:因为读锁是共享的,你加了读锁,其他人仍然可以读,这是否会有问题呢?假如我们的打钱操作涉及更多的值的改变,只有这些值全部改变完毕,才能说此时的整体状态正确,否则在改变完毕之前,读到的东西都有可能是错的。再去延伸这个思路似乎会变得非常艰难,也许会陷入耦合的地狱。 但是实际上我们不需要这样做,我们只需要反过来使用读写互斥的概念即可:因为写写互斥(写锁是独占锁),所以我们在执行这个先读后写的行为之前,加一个写锁,这同样能防止其他人来写,同时还可以阻止其他人来读,从而实现我们在单线程中读写并存的需求。 这就是ReentrantReadWriteLock中一个重要的概念:锁降级 对于另一个子问题:如果在已经获取写锁的期间还要再获取写锁的时候怎么办?这种情况还是很常见的,多数是由于代码的复用导致,不过相应的处理也很简单:对写锁这个独占锁增加允许单线程重入的规则即可。 极端情况下写操作根本没有机会上位,该如何解决这种情况?如果我们有两把锁,一把读锁,一把写锁,它们之间想要互通各自加锁的情况很简单——只要去get对方的state就行了。但是只知道state是不够的,对于读的操作来说,它如果只看到写锁没被占用,也不管有多少个写操作还在排队,就去在读锁上+1,那很可能发展成为问题所说的场景:写操作永远没机会上位。 那么我们理想的情况应该是:读操作如果发现写锁空闲,最好再看看写操作的排队情况如何,酌情考虑放弃这一次竞争,让写操作有机会上位。这也是我理解的,为什么ReentrantReadWriteLock不设计成两个互相沟通的、独立的锁,而是公用一个锁(class Sync extends AbstractQueuedSynchronizer)——因为它们看似独立,实际上对于耦合的需求很大,它们不仅需要沟通锁的情况,还要沟通队列的情况。 公用一个锁的具体实现是:使用int state的高16位表示读锁的state,低16位表示写锁的state,而队列公用的方式是给每个节点增加一个标记,表明该节点是一个共享锁的节点(读操作)还是一个独占锁的节点(写操作)。 上面说到的“酌情放弃这一次竞争”,ReentrantReadWriteLock中体现在boolean readerShouldBlock()这个方法里,这个方法有两个模式:公平和非公平,我们来稍微看一点源码先看公平模式的实现: final boolean readerShouldBlock() { return hasQueuedPredecessors();}当线程发现自己可以获取读锁时(写锁未被占用),会调用这个方法,来判断自己是否应该放弃此次获取。hasQueuedPredecessors()这个方法我们不去看源码,因为它的意思很显而易见(实际代码也是):是否存在排队中的线程(Predecessor先驱者可以理解为先来的)。如果有,那就放弃竞争去排队。在公平模式下,无论读写操作,只需要大家都遵守FIFO的秩序,就不会出现问题描述的情况 ...

April 23, 2019 · 3 min · jiezi

解决死锁的100种方法

死锁是多线程编程或者说是并发编程中的一个经典问题,也是我们在实际工作中很可能会碰到的问题。相信大部分读者对“死锁”这个词都是略有耳闻的,但从我对后端开发岗位的面试情况来看很多同学往往对死锁都还没有系统的了解。虽然“死锁”听起来很高深,但是实际上已经被研究得比较透彻,大部分的解决方法都非常成熟和清晰,所以大家完全不用担心这篇文章的难度。 虽然本文是一篇介绍死锁及其解决方式的文章,但是对于多线程程序中的非死锁问题我们也应该有所了解,这样才能写出正确且高效的多线程程序。多线程程序中的非死锁问题主要分为两类: 违反原子性问题 一些语句在底层会被分为多个底层指令运行,所以在多个线程之间这些指令就可能会存在穿插,这样程序的行为就可能会与预期不符造成bug。违反执行顺序问题 一些程序语句可能会因为子线程立即启动早于父线程中的后续代码,或者是多个线程并发执行等情况,造成程序运行顺序和期望不符导致产生bug。这两大非死锁多线程问题及其解决方案在之前的文章《多线程中那些看不到的陷阱》里都有详细的介绍,感兴趣的读者可以了解一下。 接下来就让我们开始消灭死锁吧! 初识死锁什么是死锁?死锁,顾名思义就是导致线程卡死的锁冲突,例如下面的这种情况: 线程t1线程t2获取锁A 获取锁B获取锁B(等待线程t2释放锁B) 获取锁A(等待线程t1释放锁A)可以看出,上面的两个线程已经互相卡死了,线程t1在等待线程t2释放锁B,而线程t2在等待线程t1释放锁A。两个线程互不相让也就没有一个线程可以继续往下执行了。这种情况下就发生了死锁。 死锁的四个必要条件上面的情况只是死锁的一个例子,我们可以用更精确的方式描述死锁出现的条件: 互斥。资源被竞争性地访问,这里的资源可以理解为锁;持有并等待。线程持有已经分配给他们的资源,同时等待其他的资源;不抢占。线程已经获取到的资源不会被其他线程强制抢占;环路等待。线程之间存在资源的环形依赖链,每个线程都依赖于链条中的下一个线程释放必要的资源,而链条的末尾又依赖了链条头部的线程,进入了一个循环等待的状态。上面这四个都是死锁出现的必要条件,如果其中任何一个条件不满足都不会出现死锁。虽然这四个条件的定义看起来非常的理论和官方,但是在实际的编程实践中,我们正是在死锁的这四个必要条件基础上构建出解决方案的。所以这里不妨思考一下这四个条件各自的含义,想一想如果去掉其中的一个条件死锁是否还能发生,或者为什么不能发生。 阻止死锁的发生了解了死锁的概念和四个必要条件之后,我们下面就正式开始解决死锁问题了。对于死锁问题,我们最希望能够达到的当然是完全不发生死锁问题,也就是在死锁发生之前就阻止它。 那么想要阻止死锁的发生,我们自然是要让死锁无法成立,最直接的方法当然是破坏掉死锁出现的必要条件。只要有任何一个必要条件无法成立,那么死锁也就没办法发生了。 破坏环路等待条件实践中最有效也是最常用的一种死锁阻止技术就是锁排序,通过对加锁的操作进行排序我们就能够破坏环路等待条件。例如当我们需要获取数组中某一个位置对应的锁来修改这个位置上保存的值时,如果需要同时获取多个位置对应的锁,那么我们就可以按位置在数组中的排列先后顺序统一从前往后加锁。 试想一下如果程序中所有需要加锁的代码都按照一个统一的固定顺序加锁,那么我们就可以想象锁被放在了一条不断向前延伸的直线上,而因为加锁的顺序一定是沿着这条线向下走的,所以每条线程都只能向前加锁,而不能再回头获取已经在后面的锁了。这样一来,线程只会向前单向等待锁释放,自然也就无法形成一个环路了。 其实大部分死锁解决方法不止可以用于多线程编程领域,还可以扩展到更多的并发场景下。比如在数据库操作中,如果我们要对某几行数据执行更新操作,那么就会获取这几行数据所对应的锁,我们同样可以通过对数据库更新语句进行排序来阻止在数据库层面发生的死锁。 但是这种方案也存在它的缺点,比如在大型系统当中,不同模块直接解耦和隔离得非常彻底,不同模块的研发同学之间都不清楚具体的实现细节,在这样的情况下就很难做到整个系统层面的全局锁排序了。在这种情况下,我们可以对方案进行扩充,例如Linux在内存映射代码就使用了一种锁分组排序的方式来解决这个问题。锁分组排序首先按模块将锁分为了不同的组,每个组之间定义了严格的加锁顺序,然后再在组内对具体的锁按规则进行排序,这样就保证了全局的加锁顺序一致。在Linux的对应的源码顶部,我们可以看到有非常详尽的注释定义了明确的锁排序规则。 这种解决方案如果规模过大的话即使可以实现也会非常的脆弱,只要有一个加锁操作没有遵守锁排序规则就有可能会引发死锁。不过在像微服务之类解耦比较充分的场景下,只要架构拆分合理,任务模块尽可能小且不会将加锁范围扩大到模块之外,那么锁排序将是一种非常实用和便捷的死锁阻止技术。 破坏持有并等待条件想要破坏持有并等待条件,我们可以一次性原子性地获取所有需要的锁,比如通过一个专门的全局锁作为加锁令牌控制加锁操作,只有获取了这个锁才能对其他锁执行加锁操作。这样对于一个线程来说就相当于一次性获取到了所有需要的锁,且除非等待加锁令牌否则在获取其他锁的过程中不会发生锁等待。 这样的解决方案虽然简单粗暴,但这种简单粗暴也带来了一些问题: 这种实现会降低系统的并发性,因为所有需要获取锁的线程都要去竞争同一个加锁令牌锁;并且因为要在程序的一开始就获取所有需要的锁,这就导致了线程持有锁的时间超出了实际需要,很多锁资源被长时间的持有所浪费,而其他线程只能等待之前的线程执行结束后统一释放所有锁;另一方面,现代程序设计理念要求我们提高程序的封装性,不同模块之间的细节要互相隐藏,这就使得在一个统一的位置一次性获取所有锁变得不再可能。破坏不抢占条件如果一个线程已经获取到了一些锁,那么在这个线程释放锁之前这些锁是不会被强制抢占的。但是为了防止死锁的发生,我们可以选择让线程在获取后续的锁失败时主动放弃自己已经持有的锁并在之后重试整个任务,这样其他等待这些锁的线程就可以继续执行了。 同样的,这个方案也会有自己的缺陷: 虽然这种方式可以避免死锁,但是如果几个互相存在竞争的线程不断地放弃、重试、放弃,那么就会导致活锁问题(livelock)。在这种情况下,虽然线程没有因为锁冲突被卡死,但是仍然会被阻塞相当长的时间甚至一直处于重试当中。 这个问题的一种解决方式是给任务重试添加一个随机的延迟时间,这样就能大大降低任务冲突的概率了。在一些接口请求框架中也使用了这种技巧来分散服务高峰期的请求重试操作,防止服务陷入阻塞、崩溃、阻塞的恶性循环。还是因为程序的封装性,在一个模块中难以释放其他模块中已经获取到的锁。虽然每一个方案都有自己的缺陷,但是在适合它们的场景下,它们都能发挥出巨大的作用。 破坏互斥条件在之前的文章中,我们已经了解了一种与锁完全不同的同步方式CAS。通过CAS提供的原子性支持,我们可以实现各种无锁数据结构,不仅避免了互斥锁所带来的开销和复杂性,也由此避开了我们一直在讨论的死锁问题。 AtomicInteger类中就大量使用了CAS操作来实现并发安全,例如incrementAndGet()方法就是用Unsafe类中基于CAS的原子累加方法getAndAddInt来实现的。下面是Unsafe类的getAndAddInt方法实现: /** * 增加指定字段值并返回原值 * * @param obj 目标对象 * @param valueOffset 目标字段的内存偏移量 * @param increment 增加值 * @return 字段原值 */public final int getAndAddInt(Object obj, long valueOffset, int increment) { // 保存字段原值的变量 int oldValue; do { // 获取字段原值 oldValue = this.getIntVolatile(obj, valueOffset); // obj和valueOffset唯一指定了目标字段所对应的内存区域 // while条件中不断调用CAS方法来对目标字段值进行增加,并保证字段的值没有被其他线程修改 // 如果在修改过程中其他线程修改了这个字段的值,那么CAS操作失败,循环语句会重试操作 } while(!this.compareAndSwapInt(obj, valueOffset, oldValue, oldValue + increment)); // 返回字段的原值 return oldValue;}上面代码中的compareAndSwapInt方法就是我们说的CAS操作(Compare And Swap),我们可以看到,CAS在每次执行时不一定会成功。如果执行CAS操作时目标字段的值已经被别的线程修改了,那么这次CAS操作就会失败,循环语句将会在CAS操作失败的情况下不断重试同样的操作。这种不断重试的方式就被称为自旋,在jvm当中对互斥锁的等待也会通过少量的自旋操作来进行优化。 ...

April 21, 2019 · 1 min · jiezi

Java并发编程之CountDownLatch源码解析

一、导语最近在学习并发编程原理,所以准备整理一下自己学到的知识,先写一篇CountDownLatch的源码分析,之后希望可以慢慢写完整个并发编程。二、什么是CountDownLatchCountDownLatch是java的JUC并发包里的一个工具类,可以理解为一个倒计时器,主要是用来控制多个线程之间的通信。 比如有一个主线程A,它要等待其他4个子线程执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。三、简单使用public static void main(String[] args){ System.out.println(“主线程和他的两个小兄弟约好去吃火锅”); System.out.println(“主线程进入了饭店”); System.out.println(“主线程想要开始动筷子吃饭”); //new一个计数器,初始值为2,当计数器为0时,主线程开始执行 CountDownLatch latch = new CountDownLatch(2); new Thread(){ public void run() { try { System.out.println(“子线程1——小兄弟A 正在到饭店的路上”); Thread.sleep(3000); System.out.println(“子线程1——小兄弟A 到饭店了”); //一个小兄弟到了,计数器-1 latch.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } }; }.start(); new Thread(){ public void run() { try { System.out.println(“子线程2——小兄弟B 正在到饭店的路上”); Thread.sleep(3000); System.out.println(“子线程2——小兄弟B 到饭店了”); //另一个小兄弟到了,计数器-1 latch.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } }; }.start(); //主线程等待,直到其他两个小兄弟也进入饭店(计数器==0),主线程才能吃饭 latch.await(); System.out.println(“主线程终于可以开始吃饭了~”);}四、源码分析核心代码:CountDownLatch latch = new CountDownLatch(1); latch.await(); latch.countDown();其中构造函数的参数是计数器的值; await()方法是用来阻塞线程,直到计数器的值为0 countDown()方法是执行计数器-1操作1、首先来看构造函数的代码public CountDownLatch(int count) { if (count < 0) throw new IllegalArgumentException(“count < 0”); this.sync = new Sync(count); }这段代码很简单,首先if判断传入的count是否<0,如果小于0直接抛异常。 然后new一个类Sync,这个Sync是什么呢?我们一起来看下private static final class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 4982264981922014374L; Sync(int count) { setState(count); } int getCount() { return getState(); } //尝试获取共享锁 protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; } //尝试释放共享锁 protected boolean tryReleaseShared(int releases) { // Decrement count; signal when transition to zero for (;;) { int c = getState(); if (c == 0) return false; int nextc = c-1; if (compareAndSetState(c, nextc)) return nextc == 0; } } }可以看到Sync是一个内部类,继承了AQS,AQS是一个同步器,之后我们会详细讲。 其中有几个核心点:变量 state是父类AQS里面的变量,在这里的语义是计数器的值getState()方法也是父类AQS里的方法,很简单,就是获取state的值tryAcquireShared和tryReleaseShared也是父类AQS里面的方法,在这里CountDownLatch对他们进行了重写,先有个印象,之后详讲。2、了解了CountDownLatch的构造函数之后,我们再来看它的核心代码,首先是await()。public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); }可以看到,其实是通过内部类Sync调用了父类AQS的acquireSharedInterruptibly()方法。public final void acquireSharedInterruptibly(int arg) throws InterruptedException { //判断线程是否是中断状态 if (Thread.interrupted()) throw new InterruptedException(); //尝试获取state的值 if (tryAcquireShared(arg) < 0)//step1 doAcquireSharedInterruptibly(arg);//step2 }tryAcquireShared(arg)这个方法就是我们刚才在Sync内看到的重写父类AQS的方法,意思就是判断是否getState() == 0,如果state为0,返回1,则step1处不进入if体内acquireSharedInterruptibly(int arg)方法执行完毕。若state!=0,则返回-1,进入if体内step2处。 下面我们来看acquireSharedInterruptibly(int arg)方法:private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { //step1、把当前线程封装为共享类型的Node,加入队列尾部 final Node node = addWaiter(Node.SHARED); boolean failed = true; try { for (;;) { //step2、获取当前node的前一个元素 final Node p = node.predecessor(); //step3、如果前一个元素是队首 if (p == head) { //step4、再次调用tryAcquireShared()方法,判断state的值是否为0 int r = tryAcquireShared(arg); //step5、如果state的值==0 if (r >= 0) { //step6、设置当前node为队首,并尝试释放共享锁 setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; return; } } //step7、是否可以安心挂起当前线程,是就挂起;并且判断当前线程是否中断 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { //step8、如果出现异常,failed没有更新为false,则把当前node从队列中取消 if (failed) cancelAcquire(node); } }按照代码中的注释,我们可以大概了解该方法的内容,下面我们来仔细看下其中调用的一些方法是干什么的。 1、首先看addWaiter()//step1private Node addWaiter(Node mode) { //把当前线程封装为node Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure //获取当前队列的队尾tail,并赋值给pred Node pred = tail; //如果pred!=null,即当前队尾不为null if (pred != null) { //把当前队尾tail,变成当前node的前继节点 node.prev = pred; //cas更新当前node为新的队尾 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //如果队尾为空,走enq方法 enq(node);//step1.1 return node; }—————————————————————–//step1.1private Node enq(final Node node) { for (;;) { Node t = tail; //如果队尾tail为null,初始化队列 if (t == null) { // Must initialize //cas设置一个新的空node为队首 if (compareAndSetHead(new Node())) tail = head; } else { //cas把当前node设置为新队尾,把前队尾设置成当前node的前继节点 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }2、接下来我们在来看setHeadAndPropagate()方法,看其内部实现//step6private void setHeadAndPropagate(Node node, int propagate) { //获取队首head Node h = head; // Record old head for check below //设置当前node为队首,并取消node所关联的线程 setHead(node); // if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; //如果当前node的后继节点为null或者是shared类型的 if (s == null || s.isShared()) //释放锁,唤醒下一个线程 doReleaseShared();//step6.1 } }——————————————————————–//step6.1private void doReleaseShared() { for (;;) { //找到头节点 Node h = head; if (h != null && h != tail) { //获取头节点状态 int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases //唤醒head节点的next节点 unparkSuccessor(h); } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } }3、接下来我们来看countDown()方法。public void countDown() { sync.releaseShared(1); }可以看到调用的是父类AQS的releaseShared 方法public final boolean releaseShared(int arg) { //state-1 if (tryReleaseShared(arg)) {//step1 //唤醒等待线程,内部调用的是LockSupport.unpark方法 doReleaseShared();//step2 return true; } return false; }——————————————————————//step1protected boolean tryReleaseShared(int releases) { // Decrement count; signal when transition to zero for (;;) { //获取当前state的值 int c = getState(); if (c == 0) return false; int nextc = c-1; //cas操作来进行原子减1 if (compareAndSetState(c, nextc)) return nextc == 0; } }五、总结CountDownLatch主要是通过计数器state来控制是否可以执行其他操作,如果不能就通过LockSupport.park()方法挂起线程,直到其他线程执行完毕后唤醒它。下面我们通过一个简单的图来帮助我们理解一下:PS:本人也是还在学习的路上,理解的也不是特别透彻,如有错误,愿倾听教诲。^_^ ...

April 18, 2019 · 3 min · jiezi

Java并发编程之线程间通讯(上)wait/notify机制

线程间通信如果一个线程从头到尾执行完也不和别的线程打交道的话,那就不会有各种安全性问题了。但是协作越来越成为社会发展的大势,一个大任务拆成若干个小任务之后,各个小任务之间可能也需要相互协作最终才能执行完整个大任务。所以各个线程在执行过程中可以相互通信,所谓通信就是指相互交换一些数据或者发送一些控制指令,比如一个线程给另一个暂停执行的线程发送一个恢复执行的指令,下边详细看都有哪些通信方式。volatile和synchronized可变共享变量是天然的通信媒介,也就是说一个线程如果想和另一个线程通信的话,可以修改某个在多线程间共享的变量,另一个线程通过读取这个共享变量来获取通信的内容。由于原子性操作、内存可见性和指令重排序的存在,java提供了volatile和synchronized的同步手段来保证通信内容的正确性,假如没有这些同步手段,一个线程的写入不能被另一个线程立即观测到,那这种通信就是不靠谱的~wait/notify机制故事背景也不知道是那个遭天杀的给我们学校厕所的坑里塞了个塑料瓶,导致楼道里如黄河泛滥一般,臭味熏天。更加悲催的是整个楼只有这么一个厕所,比这个更悲催的是这个厕所里只有一个坑!!!!!好吧,让我们用java来描述一下这个厕所:public class Washroom { private volatile boolean isAvailable = false; //表示厕所是否是可用的状态 private Object lock = new Object(); //厕所门的锁 public boolean isAvailable() { return isAvailable; } public void setAvailable(boolean available) { this.isAvailable = available; } public Object getLock() { return lock; }}isAvailable字段代表厕所是否可用,由于厕所损坏,默认是false的,lock字段代表这个厕所门的锁。需要注意的是 isAvailable字段被volatile修饰,也就是说有一个线程修改了它的值,它可以立即对别的线程可见~由于厕所资源宝贵,英明的学校领导立即拟定了一个修复任务:public class RepairTask implements Runnable { private Washroom washroom; public RepairTask(Washroom washroom) { this.washroom = washroom; } @Override public void run() { synchronized (washroom.getLock()) { System.out.println(“维修工 获取了厕所的锁”); System.out.println(“厕所维修中,维修厕所是一件辛苦活,需要很长时间。。。”); try { Thread.sleep(5000L); //用线程sleep表示维修的过程 } catch (InterruptedException e) { throw new RuntimeException(e); } washroom.setAvailable(true); //维修结束把厕所置为可用状态 System.out.println(“维修工把厕所修好了,准备释放锁了”); } }}这个维修计划的内容就是当维修工进入厕所之后,先把门锁上,然后开始维修,维修结束之后把Washroom的isAvailable字段设置为true,以表示厕所可用。与此同时,一群急得像热锅上的蚂蚁的家伙在厕所门前打转转,他们想做神马不用我明说了吧????????:public class ShitTask implements Runnable { private Washroom washroom; private String name; public ShitTask(Washroom washroom, String name) { this.washroom = washroom; this.name = name; } @Override public void run() { synchronized (washroom.getLock()) { System.out.println(name + " 获取了厕所的锁"); while (!washroom.isAvailable()) { // 一直等 } System.out.println(name + " 上完了厕所"); } }}这个ShitTask描述了上厕所的一个流程,先获取到厕所的锁,然后判断厕所是否可用,如果不可用,则在一个死循环里不断的判断厕所是否可用,直到厕所可用为止,然后上完厕所释放锁走人。然后我们看看现实世界都发生了什么吧:public class Test { public static void main(String[] args) { Washroom washroom = new Washroom(); new Thread(new RepairTask(washroom), “REPAIR-THREAD”).start(); try { Thread.sleep(1000L); } catch (InterruptedException e) { throw new RuntimeException(e); } new Thread(new ShitTask(washroom, “狗哥”), “BROTHER-DOG-THREAD”).start(); new Thread(new ShitTask(washroom, “猫爷”), “GRANDPA-CAT-THREAD”).start(); new Thread(new ShitTask(washroom, “王尼妹”), “WANG-NI-MEI-THREAD”).start(); }}学校先让维修工进入厕所维修,然后包括狗哥、猫爷、王尼妹在内的上厕所大军就开始围着厕所打转转的旅程,我们看一下执行结果:维修工 获取了厕所的锁厕所维修中,维修厕所是一件辛苦活,需要很长时间。。。维修工把厕所修好了,准备释放锁了王尼妹 获取了厕所的锁王尼妹 上完了厕所猫爷 获取了厕所的锁猫爷 上完了厕所狗哥 获取了厕所的锁狗哥 上完了厕所看起来没有神马问题,但是再回头看看代码,发现有两处特别别扭的地方:在main线程开启REPAIR-THREAD线程后,必须调用sleep方法等待一段时间才允许上厕所线程开启。如果REPAIR-THREAD线程和其他上厕所线程一块儿开启的话,就有可能上厕所的人,比如狗哥先获取到厕所的锁,然后维修工压根儿连厕所也进不去。但是真实情况可能真的这样的,狗哥先到了厕所,然后维修工才到。不过狗哥的处理应该不是一直待在厕所里,而是先出来等着,啥时候维修工说修好了他再进去。所以这点有些别扭~在一个上厕所的人获取到厕所的锁的时候,必须不断判断Washroom的isAvailable字段是否为true。如果一个人进入到厕所发现厕所仍然处在不可用状态的话,那它应该在某个地方休息,啥时候维修工把厕所修好了,再叫一下等着上厕所的人就好了嘛,没必要自己不停的去检查厕所是否被修好了。总结一下,就是一个线程在获取到锁之后,如果指定条件不满足的话,应该主动让出锁,然后到专门的等待区等待,直到某个线程完成了指定的条件,再通知一下在等待这个条件完成的线程,让它们继续执行。如果你觉得上边这句话比较绕的话,我来给你翻译一下:当上狗哥获取到厕所门锁之后,如果厕所处于不可用状态,那就主动让出锁,然后到等待上厕所的队伍里排队等待,直到维修工把厕所修理好,把厕所的状态置为可用后,维修工再通知需要上厕所的人,然他们正常上厕所。具体使用方式为了实现这个构想,java里提出了一套叫wait/notify的机制。当一个线程获取到锁之后,如果发现条件不满足,那就主动让出锁,然后把这个线程放到一个等待队列里等待去,等到某个线程把这个条件完成后,就通知等待队列里的线程他们等待的条件满足了,可以继续运行啦!如果不同线程有不同的等待条件肿么办,总不能都塞到同一个等待队列里吧?是的,java里规定了每一个锁都对应了一个等待队列,也就是说如果一个线程在获取到锁之后发现某个条件不满足,就主动让出锁然后把这个线程放到与它获取到的锁对应的那个等待队列里,另一个线程在完成对应条件时需要获取同一个锁,在条件完成后通知它获取的锁对应的等待队列。这个过程意味着锁和等待队列建立了一对一关联。怎么让出锁并且把线程放到与锁关联的等待队列中以及怎么通知等待队列中的线程相关条件已经完成java已经为我们规定好了。我们知道,锁其实就是个对象而已,在所有对象的老祖宗类Object中定义了这么几个方法:public final void wait() throws InterruptedExceptionpublic final void wait(long timeout) throwsInterruptedExceptionpublic final void wait(long timeout, int nanos) throws InterruptedExceptionpublic final void notify();public final void notifyAll();了解了这些方法的意思以后我们再来改写一下ShitTask:public class ShitTask implements Runnable { // … 为节省篇幅,省略相关字段和构造方法 @Override public void run() { synchronized (washroom.getLock()) { System.out.println(name + " 获取了厕所的锁"); while (!washroom.isAvailable()) { try { washroom.getLock().wait(); //调用锁对象的wait()方法,让出锁,并把当前线程放到与锁关联的等待队列 } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println(name + " 上完了厕所"); } }}看,原来我们在判断厕所是否可用的死循环里加了这么一段代码:washroom.getLock().wait(); 这段代码的意思就是让出厕所的锁,并且把当前线程放到与厕所的锁相关联的等待队列里。然后我们也需要修改一下维修任务:public class RepairTask implements Runnable { // … 为节省篇幅,省略相关字段和构造方法 @Override public void run() { synchronized (washroom.getLock()) { System.out.println(“维修工 获取了厕所的锁”); System.out.println(“厕所维修中,维修厕所是一件辛苦活,需要很长时间。。。”); try { Thread.sleep(5000L); //用线程sleep表示维修的过程 } catch (InterruptedException e) { throw new RuntimeException(e); } washroom.setAvailable(true); //维修结束把厕所置为可用状态 washroom.getLock().notifyAll(); //通知所有在与锁对象关联的等待队列里的线程,它们可以继续执行了 System.out.println(“维修工把厕所修好了,准备释放锁了”); } }}大家可以看出来,我们在维修结束后加了这么一行代码:washroom.getLock().notifyAll();这个代码表示将通知所有在与锁对象关联的等待队列里的线程,它们可以继续执行了。在使用java的wait/notify机制修改了ShitTask和RepairTask后,我们在复原一下整个现实场景:public class Test { public static void main(String[] args) { Washroom washroom = new Washroom(); new Thread(new ShitTask(washroom, “狗哥”), “BROTHER-DOG-THREAD”).start(); new Thread(new ShitTask(washroom, “猫爷”), “GRANDPA-CAT-THREAD”).start(); new Thread(new ShitTask(washroom, “王尼妹”), “WANG-NI-MEI-THREAD”).start(); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } new Thread(new RepairTask(washroom), “REPAIR-THREAD”).start(); }}在这个场景中,我们可以刻意让着急上厕所的先到达了厕所,维修工最后抵达厕所,来看一下加了wait/notify机制的代码的执行结果是:狗哥 获取了厕所的锁猫爷 获取了厕所的锁王尼妹 获取了厕所的锁维修工 获取了厕所的锁厕所维修中,维修厕所是一件辛苦活,需要很长时间。。。维修工把厕所修好了,准备释放锁了王尼妹 上完了厕所猫爷 上完了厕所狗哥 上完了厕所从执行结果可以看出来,狗哥、猫爷、王尼妹虽然先到达了厕所并且获取到锁,但是由于厕所处于不可用状态,所以都先调用wait()方法让出了自己获得的锁,然后躲到与这个锁关联的等待队列里,直到维修工修完了厕所,通知了在等待队列中的狗哥、猫爷、王尼妹,他们才又开始继续执行上厕所的程序~通用模式经过上边的厕所案例,大家应该对wait/notify机制有了大致了解,下边我们总结一下这个机制的通用模式。首先看一下等待线程的通用模式:获取对象锁。如果某个条件不满足的话,调用锁对象的wait方法,被通知后仍要检查条件是否满足。条件满足则继续执行代码。通用的代码如下:synchronized (对象) { 处理逻辑(可选) while(条件不满足) { 对象.wait(); } 处理逻辑(可选)}除了判断条件是否满足和调用wait方法以外的代码,其他的处理逻辑是可选的。下边再来看通知线程的通用模式:获得对象的锁。完成条件。通知在等待队列中的等待线程。synchronized (对象) { 完成条件 对象.notifyAll();、}小贴士:别忘了同步方法也是使用锁的喔,静态同步方法的锁对象是该类的Class对象,成员同步方法的锁对象是this对象。所以如果没有刻意强调,下边所说的同步代码块也包含同步方法。了解了wait/notify的通用模式之后,使用的时候需要特别小心,需要注意下边这些方面:必须在同步代码块中调用wait、 notify或者notifyAll方法。有的童鞋会有疑问,为啥wait/notify机制的这些方法必须都放在同步代码块中才能调用呢?wait方法的意思只是让当前线程停止执行,把当前线程放在等待队列里,notify方法的意思只是从等待队列里移除一个线程而已,跟加锁有什么关系?答:因为wait方法是运行在等待线程里的,notify或者notifyAll是运行在通知线程里的。而执行wait方法前需要判断一下某个条件是否满足,如果不满足才会执行wait方法,这是一个先检查后执行的操作,不是一个原子性操作,所以如果不加锁的话,在多线程环境下等待线程和通知线程的执行顺序可能是这样的:也就是说当等待线程已经判断条件不满足,正要执行wait方法,此时通知线程抢先把条件完成并且调用了notify方法,之后等待线程才执行到wait方法,这会导致等待线程永远停留在等待队列而没有人再去notify它。所以等待线程中的判断条件是否满足、调用wait方法和通知线程中完成条件、调用notify方法都应该是原子性操作,彼此之间是互斥的,所以用同一个锁来对这两个原子性操作进行同步,从而避免出现等待线程永久等待的尴尬局面。如果不在同步代码块中调用wait、notify或者notifyAll方法,也就是说没有获取锁就调用wait方法,就像这样:对象.wait();是会抛出IllegalMonitorStateException异常的。在同步代码块中,必须调用获取的锁对象的wait、 notify或者notifyAll方法。也就是说不能随便调用一个对象的wait、notify或者notifyAll方法。比如等待线程中的代码是这样的:synchronized (对象1) { while(条件不满足) { 对象2.wait(); //随便调用一个对象的wait方法 }}通知线程中的代码是这样的:synchronized (对象1) { 完成条件 对象2.notifyAll();}对于代码对象2.wait(),表示让出当前线程持有的对象2的锁,而当前线程持有的是对象1的锁,所以这么写是错误的,也会抛出IllegalMonitorStateException异常的。意思就是如果当前线程不持有某个对象的锁,那它就不能调用该对象的wait方法来让出该锁。所以如果想让等待线程让出当前持有的锁,只能调用对象1.wait()。然后这个线程就被放置到与对象1相关联的等待队列中,在通知线程中只能调用对象1.notifyAll()来通知这些等待的线程了。在等待线程判断条件是否满足时,应该使用while,而不是if。也就是说在判断条件是否满足的时候要使用while:while(条件不满足) { //正确✅ 对象.wait();}而不是使用if:if(条件不满足) { //错误❌ 对象.wait();}这个是因为在多线程条件下,可能在一个线程调用notify之后立即又有一个线程把条件改成了不满足的状态,比如在维修工把厕所修好之后通知大家上厕所吧的瞬间,有一个小屁孩以迅雷不及掩耳之势又给厕所坑里塞了个瓶子,厕所又被置为不可用状态,等待上厕所的还是需要再判断一下条件是否满足才能继续执行。在调用完锁对象的notify或者notifyAll方法后,等待线程并不会立即从wait()方法返回,需要调用notify()或者notifyAll()的线程释放锁之后,等待线程才从wait()返回继续执行。。也就是说如果通知线程在调用完锁对象的notify或者notifyAll方法后还有需要执行的代码,就像这样:synchronized (对象) { 完成条件 对象.notifyAll(); … 通知后的处理逻辑}需要把通知后的处理逻辑执行完成后,把锁释放掉,其他线程才可以从wait状态恢复过来,重新竞争锁来执行代码。比方说在维修工修好厕所并通知了等待上厕所的人们之后,他还没有从厕所出来,而是在厕所的墙上写了 “XXX到此一游"之类的话之后才从厕所出来,从厕所出来才代表着释放了锁,狗哥、猫爷、王尼妹才开始争抢进入厕所的机会。notify方法只会将等待队列中的一个线程移出,而notifyAll方法会将等待队列中的所有线程移出。大家可以把上边代码中的notifyAll方法替换称notify方法,看看执行结果~wait和sleep的区别眼尖的小伙伴肯定发现,wait和sleep这两个方法都可以让线程暂停执行,而且都有InterruptedException的异常说明,那么它们的区别是啥呢?wait是Object的成员方法,而sleep是Thread的静态方法。只要是作为锁的对象都可以在同步代码块中调用自己的wait方法,sleep是Thread的静态方法,表示的是让当前线程休眠指定的时间。调用wait方法需要先获得锁,而调用sleep方法是不需要的。在一次强调,一定要在同步代码块中调用锁对象的wait方法,前提是要获得锁!前提是要获得锁!前提是要获得锁!而sleep方法随时调用~调用wait方法的线程需要用notify来唤醒,而sleep必须设置超时值。线程在调用wait方法之后会先释放锁,而sleep不会释放锁。这一点可能是最重要的一点不同点了吧,狗哥、猫爷、王尼妹这些线程一开始是获取到厕所的锁了,但是调用了wait方法之后主动把锁让出,从而让维修工得以进入厕所维修。如果狗哥在发现厕所是不可用的条件时选择调用sleep方法的话,线程是不会释放锁的,也就是说维修工无法获得厕所的锁,也就修不了厕所了~ 大家一定要谨记这一点啊!总结线程间需要通过通信才能协作解决某个复杂的问题。可变共享变量是天然的通信媒介,但是使用的时候一定要保证线程安全性,通常使用volatile变量或synchronized来保证线程安全性。一个线程在获取到锁之后,如果指定条件不满足的话,应该主动让出锁,然后到专门的等待区等待,直到某个线程完成了指定的条件,再通知一下在等待这个条件完成的线程,让它们继续执行。这个机制就是wait/notify机制。等待线程的通用模式:synchronized (对象) { 处理逻辑(可选) while(条件不满足) { 对象.wait(); } 处理逻辑(可选)}可以分为下边几个步骤:获取对象锁。如果某个条件不满足的话,调用锁对象的wait方法,被通知后仍要检查条件是否满足。条件满足则继续执行代码。通知线程的通用模式:synchronized (对象) {完成条件对象.notifyAll();、}可以分为下边几个步骤:获得对象的锁。完成条件。通知在等待队列中的等待线程。wait和sleep的区别wait是Object的成员方法,而sleep是Thread的静态方法。调用wait方法需要先获得锁,而调用sleep方法是不需要的。调用wait方法的线程需要用notify来唤醒,而sleep必须设置超时值。线程在调用wait方法之后会先释放锁,而sleep不会释放锁。 ...

April 18, 2019 · 2 min · jiezi

Java并发编程之设计线程安全的类

设计线程安全的类前边我们对线程安全性的分析都停留在一两个可变共享变量的基础上,真实并发程序中可变共享变量会非常多,在出现安全性问题的时候很难准确定位是哪块儿出了问题,而且修复问题的难度也会随着程序规模的扩大而提升(因为在程序的各个位置都可以随便使用可变共享变量,每个操作都可能导致安全性问题的发生)。比方说我们设计了一个这样的类:public class Increment { private int i; public void increase() { i++; } public int getI() { return i; }}然后有很多客户端程序员在多线程环境下都使用到了这个类,有的程序员很聪明,他在调用increase方法时使用了适当的同步操作:public class RightUsageOfIncrement { public static void main(String[] args) { Increment increment = new Increment(); Thread[] threads = new Thread[20]; //创建20个线程 for (int i = 0; i < threads.length; i++) { Thread t = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100000; i++) { synchronized (RightUsageOfIncrement.class) { // 使用Class对象加锁 increment.increase(); } } } }); threads[i] = t; t.start(); } for (int i = 0; i < threads.length; i++) { try { threads[i].join(); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println(increment.getI()); }}在调用Increment的increase方法的时候,使用RightUsageOfIncrement.class这个对象作为锁,有效的对i++操作进行了同步,的确不错,执行之后的结果是:2000000可是并不是每个客户端程序员都会这么聪明,有的客户端程序员压根儿不知道啥叫个同步,所以写成了这样:public class WrongUsageOfIncrement { public static void main(String[] args) { Increment increment = new Increment(); Thread[] threads = new Thread[20]; //创建20个线程 for (int i = 0; i < threads.length; i++) { Thread t = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100000; i++) { increment.increase(); //没有进行有效的同步 } } }); threads[i] = t; t.start(); } for (int i = 0; i < threads.length; i++) { try { threads[i].join(); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println(increment.getI()); }}没有进行有效同步的执行结果是(每次执行都可能不一样):1815025其实对于Increment这个类的开发者来说,本质上是把对可变共享变量的必要同步操作转嫁给客户端程序员处理。有的情况下我们希望自己设计的类可以让客户端程序员们不需要使用额外的同步操作就可以放心的在多线程环境下使用,我们就把这种类成为线程安全类。其实就是类库设计者把一些在多线程环境下可能导致安全性问题的操作封装到类里边儿,比如Increment的increase方法,我们可以写成这样:public synchronized void increase() { i++;}也就是说把对可变共享变量i可能造成多线程安全性问题的i++操作在Increment类内就封装好,其他人直接调用也不会出现安全性问题。使用封装也是无奈之举:你无法控制其他人对你的代码调用,风险始终存在,封装使无意中破坏设计约束条件变得更难。封装变量访问找出共享、可变的字段设计线程安全类的第一步就是要找出所有的字段,这里的字段包括静态变量也包括成员变量,然后再分析这些字段是否是共享并且可变的。首先辨别一下字段是否是共享的。由于我们无法控制客户端程序员以怎样的方式来使用这个类,所以我们可以通过访问权限,也就是public权限、protected权限、 默认权限以及private权限来控制哪些代码是可以被客户端程序员调用的,哪些是不可以调用的。一般情况下,我们需要把所有字段都声明为 private 的,把对它们的访问都封装到方法中,对这些方法再进行必要的同步控制,也就是说我们只暴露给客户端程序员一些可以调用的方法来间接的访问到字段,因为如果直接把字段暴露给客户端程序员的话,我们无法控制客户端程序员如何使用该字段,比如他可以随意的在多线程环境下对字段进行累加操作,从而不能保证把所有同步逻辑都封装到类中。所以如果一个字段是可以通过对外暴露的方法访问到,那这个字段就是共享的。然后再看一下字段是否是可变的。如果该字段的类型是基本数据类型,可以看一下类所有对外暴露的方法中是否有修改该字段值的操作,如果有,那这个字段就是可变的。如果该字段的类型是非基本数据类型的,那这个字段可变就有两层意思了,第一是在对外暴露的方法中有直接修改引用的操作,第二是在对外暴露的方法中有直接修改该对象中字段的操作。比如一个类长这样:public class MyObj { private List<String> list; public void m1() { list = new ArrayList<>(); //直接修改字段指向的对象 } public void m2() { list[0] = “aa”; //修改该字段指向对象的字段 }}代码中的m1和m2都可以算做是修改字段list,如果类暴露的方法中有这两种修改方式中的任意一种,就可以算作这个字段是可变的。小贴士:是不是把字段声明成final类型,该字段就不可变了呢?如果该字段是基本数据类型,那声明为final的确可以保证在程序运行过程中不可变,但是如果该字段是非基本数据类型,那么需要让该字段代表的对象中的所有字段都是不可变字段才能保证该final字段不可变。所以在使用字段的过程中,应该尽可能的让字段不共享或者不可变,不共享或者不可变的字段才不会引起安全性问题哈哈。这让我想起了一句老话:只有死人才不会说话~用锁来保护访问确定了哪些字段必须是共享、可变的之后,就要分析在哪些对外暴露的方法中访问了这些字段,我们需要在所有的访问位置都进行必要的同步处理,这样才可以保证这个类是一个线程安全类。通常,我们会使用锁来保证多线程在访问共享可变字段时是串行访问的。但是一种常见的错误就是:只有在写入共享可变字段时才需要使用同步,就像这样:public class Test { private int i; public int getI() { return i; } public synchronized void setI(int i) { this.i = i; }}为了使Test类变为线程安全类,也就是需要保证共享可变字段i在所有外界能访问的位置都是线程安全的,而上边getI方法可以访问到字段i,却没有进行有效的同步处理,由于内存可见性问题的存在,在调用getI方法时仍有可能获取的是旧的字段值。所以再次强调一遍:我们需要在所有的访问位置都进行必要的同步处理。使用同一个锁还有一点需要强调的是:如果使用锁来保护共享可变字段的访问的话,对于同一个字段来说,在多个访问位置需要使用同一个锁。我们知道如果多个线程竞争同一个锁的话,在一个线程获取到锁后其他线程将被阻塞,如果是使用多个锁来保护同一个共享可变字段的话,多个线程并不会在一个线程访问的时候阻塞等待,而是会同时访问这个字段,我们的保护措施就变得无效了。一般情况下,在一个线程安全类中,我们使用同步方法,也就是使用this对象作为锁来保护字段的访问就OK了~。封不封装取决于你的心情虽然面向对象技术封装了安全性,但是打破这种封装也没啥不可以,只不过安全性会更脆弱,增加开发成本和风险。也就是说你把字段声明为public访问权限也没人拦得住你,当然你也可能因为某种性能问题而打破封装,不过对于我们实现业务的人来说,还是建议先使代码正确运行,再考虑提高代码执行速度吧~。不变性条件现实中有些字段之间是有实际联系的,比如说下边这个类:public class SquareGetter { private int numberCache; //数字缓存 private int squareCache; //平方值缓存 public int getSquare(int i) { if (i == numberCache) { return squareCache; } int result = ii; numberCache = i; squareCache = result; return result; } public int[] getCache() { return new int[] {numberCache, squareCache}; }}这个类提供了一个很简单的getSquare功能,可以获取指定参数的平方值。但是它的实现过程使用了缓存,就是说如果指定参数和缓存的numberCache的值一样的话,直接返回缓存的squareCache,如果不是的话,计算参数的平方,然后把该参数和计算结果分别缓存到numberCache和squareCache中。从上边的描述中我们可以知道,squareCache不论在任何情况下都是numberCache平方值,这就是SquareGetter类的一个不变性条件,如果违背了这个不变性条件的话,就可能会获得错误的结果。在单线程环境中,getSquare方法并不会有什么问题,但是在多线程环境中,numberCache和squareCache都属于共享的可变字段,而getSquare方法并没有提供任何同步措施,所以可能造成错误的结果。假设现在numberCache的值是2,squareCache的值是3,一个线程调用getSquare(3),另一个线程调用getSquare(4),这两个线程的一个可能的执行时序是:两个线程执行过后,最后numberCache的值是4,而squareCache的值竟然是9,也就意味着多线程会破坏不变性条件。为了保持不变性条件,我们需要把保持不变性条件的多个操作定义为一个原子操作,即用锁给保护起来。我们可以这样修改getSquare方法的代码:public synchronized int getSquare(int i) { if (i == numberCache) { return squareCache; } int result = ii; numberCache = i; squareCache = result; return result;}但是不要忘了将代码都放在同步代码块是会造成阻塞的,能不进行同步,就不进行同步,所以我们修改一下上边的代码:public int getSquare(int i) { synchronized(this) { if (i == numberCache) { // numberCache字段的读取需要进行同步 return squareCache; } } int result = i*i; //计算过程不需要同步 synchronized(this) { // numberCache和squareCache字段的写入需要进行同步 numberCache = i; squareCache = result; } return result;}虽然getSquare方法同步操作已经做好了,但是别忘了SquareGetter类的getCache方法也访问了numberCache和squareCache字段,所以对于每个包含多个字段的不变性条件,其中涉及的所有字段都需要被同一个锁来保护,所以我们再修改一下getCache方法:public synchronized int[] getCache() { return new int[] {numberCache, squareCache};}这样修改后的SquareGetter类才属于一个线程安全类。使用volatile修饰状态使用锁来保护共享可变字段虽然好,但是开销大。使用volatile修饰字段来替换掉锁是一种可能的考虑,但是一定要记住volatile是不能保证一系列操作的原子性的,所以只有我们的业务场景符合下边这两个情况的话,才可以考虑:对变量的写入操作不依赖当前值,或者保证只有单个线程进行更新。该变量不需要和其他共享变量组成不变性条件。比方说下边的这个类:public class VolatileDemo { private volatile int i; public int getI() { return i; } public void setI(int i) { this.i = i; }}VolatileDemo中的字段i并不和其他字段组成不变性条件,而且对于可以访问这个字段的方法getI和setI来说,并不需要以来i的当前值,所以可以使用volatile来修饰字段i,而不用在getI和setI的方法上使用锁。避免this引用逸出我们先来看一段代码:public class ExplicitThisEscape { private final int i; public static ThisEscape INSTANCE; public ThisEscape() { INSTANCE = this; i = 1; }}在构造方法中就把this引用给赋值到了静态变量INSTANCE中,而别的线程是可以随时访问INSTANCE的,我们把这种在对象创建完成之前就把this引用赋值给别的线程可以访问的变量的这种情况称为 this引用逸出,这种方式是极其危险的!,这意味着在ThisEscape对象创建完成之前,别的线程就可以通过访问INSTANCE来获取到i字段的信息,也就是说别的线程可能获取到字段i的值为0,与我们期望的final类型字段值不会改变的结果是相违背的。所以千万不要在对象构造过程中使this引用逸出。上边的this引用逸出是通过显式将this引用赋值的方式导致逸出的,也可能通过内部类的方式神不知鬼不觉的造成this引用逸出:public class ImplicitThisEscape { private final int i; private Thread t; public ThisEscape() { t = new Thread(new Runnable() { @Override public void run() { // … 具体的任务 } }); i = 1; }}虽然在ImplicitThisEscape的构造方法中并没有显式的将this引用赋值,但是由于Runnable内部类的存在,作为外部类的ImplicitThisEscape,内部类对象可以轻松的获取到外部类的引用,这种情况下也算this引用逸出。this引用逸出意味着创建对象的过程是不安全的,在对象尚未创建好的时候别的线程就可以来访问这个对象。虽然我们不确定客户端程序员会怎么使用这个逸出的this引用,但是风险始终存在,所以强烈建议千万不要在对象构造过程中使this引用逸出。总结客户端程序员不靠谱,我们有必要把线程安全性封装到类中,只给客户端程序员提供线程安全的方法。认真找出代码中既共享又可变的变量,并把它们使用锁来保护起来,同一个字段的多个访问位置需要使用同一个锁来保护。对于每个包含多个字段的不变性条件,其中涉及的所有字段都需要被同一个锁来保护。在对变量的写入操作不依赖当前值以及该变量不需要和其他共享变量组成不变性条件的情况下可以考虑使用volatile变量来保证并发安全。千万不要在对象构造过程中使this引用逸出。 ...

April 18, 2019 · 3 min · jiezi

Java并发编程之原子性操作

上头一直在说以线程为基础的并发编程的好处了,什么提高处理器利用率啦,简化编程模型啦。但是砖家们还是认为并发编程是程序开发中最不可捉摸、最诡异、最扯犊子、最麻烦、最恶心、最心烦、最容易出错、最不符合社会主义核心价值观的一个部分~ 造成这么多最的原因其实很简单:进程中的各种资源,比如内存和I/O,在代码里以变量的形式展现,而某些变量在多线程间是共享、可变的,共享意味着这个变量可以被多个线程同时访问,可变意味着变量的值可能被访问它的线程修改。围绕这些共享、可变的变量形成了并发编程的三大杀手:安全性、活跃性、性能,下边我们来详细唠叨这些风险~共享变量的含义并不是所有内存变量都可以被多个线程共享,在一个线程调用一个方法的时候,会在栈内存上为局部变量以及方法参数申请一些内存,在方法调用结束的时候,这些内存便被释放。不同线程调用同一个方法都会为局部变量和方法参数拷贝一个副本(如果你忘了,需要重新学习一下方法的调用过程),所以这个栈内存是线程私有的,也就是说局部变量和方法参数是不可以共享的。但是对象或者数组是在堆内存上创建的,堆内存是所有线程都可以访问的,所以包括成员变量、静态变量和数组元素是可共享的,我们之后讨论的就是这些可以被共享的变量对并发编程造成的风险~ 如果不强调的话,我们下边所说的变量都代表成员变量、静态变量或者数组元素。安全性原子性操作、内存可见性和指令重排序是构成线程安全性的三个主题,下边我们详细看哈~原子性操作我们先拿一个例子开场:public class Increment { private int i; public void increase() { i++; } public int getI() { return i; } public static void test(int threadNum, int loopTimes) { Increment increment = new Increment(); Thread[] threads = new Thread[threadNum]; for (int i = 0; i < threads.length; i++) { Thread t = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < loopTimes; i++) { increment.increase(); } } }); threads[i] = t; t.start(); } for (Thread t : threads) { //main线程等待其他线程都执行完成 try { t.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println(threadNum + “个线程,循环” + loopTimes + “次结果:” + increment.getI()); } public static void main(String[] args) { test(20, 1); test(20, 10); test(20, 100); test(20, 1000); test(20, 10000); test(20, 100000); }}其中,increase方法的作用是给成员变量i增1,test方法接受两个参数,一个是线程的数量,一个是循环的次数,每个线程中都有一个将成员变量i增1给定循环次数的任务,在所有线程的任务都完成之后,输出成员变量i的值,如果没有什么问题的话,程序执行完成后成员变量i的值都是threadNum*loopTimes。大家看一下执行结果:20个线程,循环1次结果:2020个线程,循环10次结果:20020个线程,循环100次结果:200020个线程,循环1000次结果:1992620个线程,循环10000次结果:11990320个线程,循环100000次结果:1864988咦,貌似有点儿不对劲唉~再次执行一遍的结果:20个线程,循环1次结果:2020个线程,循环10次结果:20020个线程,循环100次结果:200020个线程,循环1000次结果:1950220个线程,循环10000次结果:10015720个线程,循环100000次结果:1833170这就更令人奇怪了~~ 当循环次数增加时,执行结果与我们预期不一致,而且每次执行貌似都是不一样的结果,这个是个什么鬼?答:这个就是多线程的非原子性操作导致的一个不确定结果。啥叫个原子性操作呢?就是一个或某几个操作只能在一个线程执行完之后,另一个线程才能开始执行该操作,也就是说这些操作是不可分割的,线程不能在这些操作上交替执行。java中自带了一些原子性操作,比如给一个非long、double基本数据类型变量或者引用的赋值或者读取操作。为什么强调非long、double类型的变量?我们稍后看哈~那i++这个操作不是一个原子性操作么?答:还真不是,这个操作其实相当于执行了i = i + 1,也就是三个原子性操作:读取变量i的值将变量i的值加1将结果写入i变量中由于线程是基于处理器分配的时间片执行的,在这个过程中,这三个步骤可能让多个线程交叉执行,为简化过程,我们以两个线程交叉执行为例,看下图:这个图的意思就是:线程1执行increase方法先读取变量i的值,发现是5,此时切换到线程2执行increase方法读取变量i的值,发现也是5。线程1执行将变量i的值加1的操作,得到结果是6,线程二也执行这个操作。线程1将结果赋值给变量i,线程2也将结果赋值给变量i。在这两个线程都执行了一次increase方法之后,最后的结果竟然是变量i从5变到了6,而不是我们想象中的7。。。另外,由于CPU的速度非常快,这种交叉执行在执行次数较低的时候体现的并不明显,但是在执行次数多的时候就十分明显了,从我们上边测试的结果上就能看出。在真实编程环境中,我们往往需要某些涉及共享、可变变量的一系列操作具有原子性,我们可以从下边三个角度来保证这些操作具有原子性。从共享性解决如果一个变量变得不可以被多线程共享,不就可以随便访问了呗哈哈,大致有下面这么两种改进方案。尽量使用局部变量解决问题因为方法中的局部变量(包括方法参数和方法体中创建的变量)是线程私有的,所以无论多少线程调用某个不涉及共享变量的方法都是安全的。所以如果能将问题转换为使用局部变量解决问题而不是共享变量解决,那将是极好的哈~。不过我貌似想不出什么案例来说明一下,等想到了再说哈,各位想到了也可以告诉我哈。使用ThreadLocal类为了维护一些线程内可以共享的数据,java提出了一个ThreadLocal类,它提供了下边这些方法:public class ThreadLocal<T> { protected T initialValue() { return null; } public void set(T value) { … } public T get() { … } public void remove() { … }}其中,类型参数T就代表了在同一个线程中共享数据的类型,它的各个方法的含义是:T initialValue():当某个线程初次调用get方法时,就会调用initialValue方法来获取初始值。void set(T value):调用当前线程将指定的value参数与该线程建立一对一关系(会覆盖initialValue的值),以便后续get方法获取该值。T get():获取与当前线程建立一对一关系的值。void remove():将与当前线程建立一对一关系的值移除。我们可以在同一个线程里的任何代码处存取该类型的值:public class ThreadLocalDemo { public static ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<String>(){ @Override protected String initialValue() { return “调用initialValue方法初始化的值”; } }; public static void main(String[] args) { ThreadLocalDemo.THREAD_LOCAL.set(“与main线程关联的字符串”); new Thread(new Runnable() { @Override public void run() { System.out.println(“t1线程从ThreadLocal中获取的值:” + ThreadLocalDemo.THREAD_LOCAL.get()); ThreadLocalDemo.THREAD_LOCAL.set(“与t1线程关联的字符串”); System.out.println(“t1线程再次从ThreadLocal中获取的值:” + ThreadLocalDemo.THREAD_LOCAL.get()); } }, “t1”).start(); System.out.println(“main线程从ThreadLocal中获取的值:” + ThreadLocalDemo.THREAD_LOCAL.get()); }}执行结果是:main线程从ThreadLocal中获取的值:与main线程关联的字符串t1线程从ThreadLocal中获取的值:调用initialValue方法初始化的值t1线程再次从ThreadLocal中获取的值:与t1线程关联的字符串从这个执行结果我们也可以看出来,不同线程操作同一个 ThreadLocal 对象执行各种操作而不会影响其他线程里的值。这一点非常有用,比如对于一个网络程序,通常每一个请求都分配一个线程去处理,可以在ThreadLocal里记录一下这个请求对应的用户信息,比如用户名,登录失效时间什么的,这样就很有用了。虽然ThreadLocal很有用,但是它作为一种线程级别的全局变量,如果某些代码依赖它的话,会造成耦合,从而影响了代码的可重用性,所以设计的时候还是要权衡一下子滴。从可变性解决如果一个变量可以被共享,但是它自打被创建之后就不能被修改,那么随意哪个线程去访问都可以哈,反正又不能改变它的值,随便读啦~再强调一遍,我们写的程序可能不仅我们自己会用,所以我们不能靠猜、靠直觉、靠信任其他使用我们写的代码的客户端程序猿,所以如果我们想通过让对象不可变的方式来保证线程安全,那就把该变量声明为 final 的吧 :public class FinalDemo { private final int finalField; public FinalDemo(int finalField) { this.finalField = finalField; }}然后就可以随便在多线程间共享finalField这个变量喽~加锁解决锁的概念如果我们的需求确实是需要共享并且可变的变量,又想让某些关于这个变量的操作是原子性的,还是以上边的increase方法为例,我们现在面临的困境是increase方法其实是由下边3个原子性操作累积起来的一个操作:读变量i;运算;写变量i;针对同一个变量i,不同线程可能交叉执行上边的三个步骤,导致两个线程读到同样的变量i的值,从而导致结果比预期的小。为了让increase方法里的操作具有原子性,也就是在一个线程执行这一系列操作的同时禁止其他线程执行这些操作,java提出了锁的概念。我们拿上厕所做一个例子,比如我们上厕所需要这几步:脱裤子干正事儿擦屁股提裤子上厕所的时候必须把这些步骤都执行完了,才能圆满的完成上厕所这个事儿,要不然执行到擦屁股环节被别人赶出来岂不是贼尴尬????,所以为了能安全的完成上厕所这个事儿,我们不得不在进入厕所之后,就拿一把锁把厕所门给锁了,等提完裤子走出厕所的时候再把锁给打开,让其他人来上厕所。同步代码块java语言里把锁给做了个抽象,任何一个对象都可以作为一个锁,也称为内置锁,某个线程在进入某个代码块的时候去获取一个锁,在退出该代码块的时候把锁给释放掉,我们来修改一下Increment的代码:public class Increment { private int i; private Object lock = new Object(); public void increase() { synchronized (lock) { i++; } } public int getI() { synchronized (lock) { return i; } } public static void test(int threadNum, int loopTimes) { // … 为节省篇幅,省略此处代码,与上边的一样 } public static void main(String[] args) { test(20, 1); test(20, 10); test(20, 100); test(20, 1000); test(20, 10000); test(20, 100000); }}对i++加锁之后的代码执行结果是:20个线程,循环1次结果:2020个线程,循环10次结果:20020个线程,循环100次结果:200020个线程,循环1000次结果:2000020个线程,循环10000次结果:20000020个线程,循环100000次结果:2000000哈哈,这回就符合预期了,如果你不信可以多执行几遍试试。我们再回过头来看这个加锁的语法:synchronized (锁对象) { 需要保持原子性的一系列代码}如果一个线程获取某个锁之后,就相当于把厕所门儿给锁上了,其他的线程就不能获取该锁了,进不去厕所只能干等着,也就是这些线程处于一种阻塞状态,直到已经获取锁的线程把该锁给释放掉,也就是把厕所门再打开,某个线程就可以再次获得锁了。这样线程们按照获取锁的顺序执行的方式也叫做同步执行(英文名就是synchronized),这个被锁保护的代码块也叫做同步代码块,我们也会说这段代码被这个锁保护。由于如果线程没有获得锁就会阻塞在同步代码块这,所以我们需要格外注意的是,在同步代码块中的代码要尽量的短,不要把不需要同步的代码也加入到同步代码块,在同步代码块中千万不要执行特别耗时或者可能发生阻塞的一些操作,比如I/O操作啥的。为什么一个对象就可以当作一个锁呢?我们知道一个对象会占据一些内存,这些内存地址可是唯一的,也就是说两个对象不能占用相同的内存。真实的对象在内存中的表示其实有对象头和数据区组成的,数据区就是我们声明的各种字段占用的内存部分,而对象头里存储了一系列的有用信息,其中就有几个位代表锁信息,也就是这个对象有没有作为某个线程的锁的信息。详细情况我们会在JVM里详细说明,现在大家看个乐呵,了解用一个对象作为锁是有底层依据的哈~锁的重入我们前边说过,当一个线程请求获得已经被其他线程获得的锁的时候,它就会被阻塞,但是如果一个线程请求一个它已经获得的锁,那么这个请求就会成功。public class SynchronizedDemo { private Object lock = new Object(); public void m1() { synchronized (lock) { System.out.println(“这是第一个方法”); m2(); } } public void m2() { synchronized (lock) { System.out.println(“这是第二个方法”); } } public static void main(String[] args) { SynchronizedDemo synchronizedDemo = new SynchronizedDemo(); synchronizedDemo.m1(); }}执行结果是:这是第一个方法这是第二个方法也就是说只要一个线程持有了某个锁,那么它就可以进入任何被这个锁保护的代码块。小贴士:这样的重入锁实现起来也简单,可以给每个锁关联一个持有的线程和获取锁的次数,初始状态下锁的计数值是0,也就是没有被任何线程持有锁,当某个线程获取这个锁的时候,计数值为1,如果继续获取该锁,那么计数值继续递增,每次退出一个同步代码块时,计数值递减,直到递减到0为止。同步方法我们前边说为了创建一个同步代码块,必须显式的指定一个对象作为锁,有时候我们想把整个方法的操作都写入同步代码块,就像我们上边说过的increase方法,这种情况下其实有个偷懒的办法,因为我们的程序中默默的藏着某些对象~对于成员方法来说,我们可以直接用this作为锁。对于静态方法来说,我们可以直接用Class对象作为锁(Class对象可以直接在任何地方访问,如果不知道的话需要重新学一下反射了亲)。就像这样:public class Increment { private int i; public void increase() { synchronized (this) { //使用this作为锁 i++; } } public static void anotherStaticMethod() { synchronized (Increment.class) { //使用Class对象作为锁 // 此处填写需要同步的代码块 } }}为了简便起见,设计java的大叔们规定整个方法的操作都需要被同步,而且使用this作为锁的成员方法,使用Class对象作为锁的静态方法,就可以被简写成这样:public class Increment { private int i; public synchronized increase() { //使用this作为锁 i++; } public synchronized static void anotherStaticMethod() { //使用Class对象作为锁 // 此处填写需要同步的代码块 }}再写一遍通用格式,大家长长记性:public synchronized 返回类型 方法名(参数列表) { 需要被同步执行的代码}public synchronized static 返回类型 方法名(参数列表) { 需要被同步执行的代码}上述的两种方法也被称为同步方法,也就是说整个方法都需要被同步执行,而且使用的锁是this对象或者Class对象。注意,同步方法只不过是同步代码块的另一种写法,没什么稀奇的~。总结共享、可变的变量形成了并发编程的三大杀手:安全性、活跃性、性能,本章详细讨论安全性问题。本文中的共享变量指的是在堆内存上创建的对象或者数组,包括成员变量、静态变量和数组元素。安全性问题包括三个方面,原子性操作、内存可见性和指令重排序,本篇文章主要对原子性操作进行详细讨论。原子性操作就是一个或某几个操作只能在一个线程执行完之后,另一个线程才能开始执行该操作,也就是说这些操作是不可分割的,线程不能在这些操作上交替执行。为了保证某些操作的原子性,提出了下边几种解决方案:尽量使用局部变量解决问题使用ThreadLocal类解决问题从共享性解决,在编程时,最好使用下边这两种方案解决问题:从可变性解决,最好让某个变量在程序运行过程中不可变,把它使用final修饰。加锁解决任何一个对象都可以作为一个锁,也称为内置锁。某个线程在进入某个同步代码块的时候去获取一个锁,在退出该代码块的时候把锁给释放掉。锁的重入是指只要一个线程持有了某个锁,那么它就可以进入任何被这个锁保护的代码块。同步方法是一种比较特殊的同步代码块,对于成员方法来讲,使用this作为锁对象,对于静态方法来说,使用Class对象作为锁对象。 ...

April 18, 2019 · 2 min · jiezi

多线程、锁和线程同步方案

多线程多线程技术大家都很了解,而且在项目中也比较常用。比如开启一个子线程来处理一些耗时的计算,然后返回主线程刷新UI等。首先我们先简单的梳理一下常用到的多线程方案。具体的用法这里我就不说了,每一种方案大家可以去查一下,网上教程很多。常见的多线程方案我们比较常用的是GCD和NSOperation,当然还有NSThread,pthread。他们的具体区别我们不详细说,给出下面这一个表格,大家自行对比一下。容易混淆的术语提到多线程,有一个术语是经常能听到的,同步,异步,串行,并发。同步和异步的区别,就是是否有开启新的线程的能力。异步具备开启线程的能力,同步不具备开启线程的能力。注意,异步只是具备开始新线程的能力,具体开启与否还要跟队列的属性有关系。串行和并发,是指的任务的执行方式。并发是任务可以多个同时执行,串行之能是一个执行完成后在执行下一个。在面试的过程中可能被问到什么网情况下会出现死锁的问题,总结一下就是使用sync函数(同步)往当前的串行对列中添加任务时,会出现死锁。锁多线程的安全隐患多线程和安全问题是分不开的,因为在使用多个线程访问同一块数据的时候,如果同时有读写操作,就可能产生数据安全问题。所以这时候我们就用到了锁这个东西。其实使用锁也是为了在使用多线程的过程中保障数据安全,除了锁,然后一些其他的实现线程同步来保证数据安全的方案,我们一起来了解一下。线程同步方案下面这些是我们常用来实现线程同步方案的。OSSpinLockos_unfair_lockpthread_mutexNSLockNSRecursiveLockNSConditionNSConditinLockdispatch_semaphoredispatch_queue(DISPATCH_QUEUE_SERIAL)@synchronized可以看出来,实现线程同步的方案包括各种锁,还有信号量,串行队列。我们只挑其中不常用的来说一下使用方法。下面是我们模拟了存钱取钱的场景,下面是加锁之前的代码,运行之后肯定是有数据问题的。/** 存钱、取钱演示 /- (void)moneyTest { self.money = 100; dispatch_queue_t queue = dispatch_get_global_queue(0, 0); dispatch_async(queue, ^{ for (int i = 0; i < 10; i++) { [self __saveMoney]; } }); dispatch_async(queue, ^{ for (int i = 0; i < 10; i++) { [self __drawMoney]; } });}/* 存钱 /- (void)__saveMoney { int oldMoney = self.money; sleep(.2); oldMoney += 50; self.money = oldMoney; NSLog(@“存50,还剩%d元 - %@”, oldMoney, [NSThread currentThread]); }/* 取钱 /- (void)__drawMoney { int oldMoney = self.money; sleep(.2); oldMoney -= 20; self.money = oldMoney; NSLog(@“取20,还剩%d元 - %@”, oldMoney, [NSThread currentThread]); }加锁的代码,涉及到锁的初始化、加锁、解锁这么三部分。我们从OSSpinLock开始说。OSSpinLock自旋锁OSSpinLock叫做自旋锁。那什么叫自旋锁呢?其实我们可以从大类上面把锁分为两类,一类是自旋锁,一类是互斥锁。我们通过一个例子来区分这两类锁。如果线程A率先到达加锁的部分,并成功加锁,线程B到达的时候会因为已经被A加锁而等待。如果是自旋锁,线程B会通过执行一个循环来实现等待,我们不用管它循环执行了什么,只要知道他在那"转圈圈"等着就行。如果是互斥锁,那线程B在等待的时候会休眠。使用OSSpinLock需要导入头文件#import <libkern/OSAtomic.h>//声明一个锁@property (nonatomic, assign) OSSpinLock lock;// 锁的初始化self.lock = OS_SPINLOCK_INIT;在我们这个例子中,存钱取钱都是访问了money,所以我们要在存和取的操作中使用同一个锁。/* 存钱 /- (void)__saveMoney { OSSpinLockLock(&_lock); //….省去中间的逻辑代码 OSSpinLockUnlock(&_lock);}/* 取钱 */- (void)__drawMoney { OSSpinLockLock(&_lock); //….省去中间的逻辑代码 OSSpinLockUnlock(&_lock);}这就是简单的自旋锁的使用,我们发现在使用的过程中,Xcode一直提醒我们这个OSSpinLock被废弃了,让我们使用os_unfair_lock代替。OSSpinLock之所以会被废弃是因为它可能会产生一个优先级反转的问题。具体来说,如果一个低优先级的线程获得了锁并访问共享资源,那高优先级的线程只能忙等,从而占用大量的CPU。低优先级的线程无法和高优先级的线程竞争(CPU会给高优先级的线程分配更多的时间片),所以会导致低优先级的线程的任务一直完不成,从而无法释放锁。os_unfair_lock的用法跟OSSpinLock很像,就不单独说了。pthread_mutexDefault一看到这个pthread我们应该就能知道这是一种跨平台的方案了。首先还是来看用法。//声明一个锁@property (nonatomic, assign) pthread_mutex_t lock;//初始化pthread_mutex_init(pthread_mutex_t *restrict _Nonnull, const pthread_mutexattr_t *restrict _Nullable)我们可以看到在初始化锁的时候,第一个参数是锁的地址,第二个参数是一个pthread_mutexattr_t类型的地址,如果我们不传pthread_mutexattr_t,直接传一个NULL,相当于创建一个默认的互斥锁。//方式一pthread_mutex_init(mutex, NULL);//方式二// - 创建attrpthread_mutexattr_t attr;// - 初始化attrpthread_mutexattr_init(&attr);// - 设置attr类型pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_DEFAULT);// - 使用attr初始化锁pthread_mutex_init(&_lock, &attr);// - 销毁attrpthread_mutexattr_destroy(&attr);上面两个方式是一个效果,那为什么使用attr,那就说明除了default类型的还有其他类型,我们后面再说。在使用的时候用pthread_mutex_lock(&_lock); 和 pthread_mutex_unlock(&_lock);加锁解锁。NSLock就是对这种普通互斥锁的OC层面的封装。RECURSIVE 递归锁调用pthread_mutexattr_settype的时候如果类型传入PTHREAD_MUTEX_RECURSIVE,会创建一个递归锁。举个例子吧。// 伪代码-(void)test { lock; [self test]; unlock;}如果是普通的锁,当我们在test方法中,递归调用test,应该会出现死锁,因为被lock,在递归调用时无法调用,一直等待。但是如果锁是递归锁,他会允许同一个线程多次加锁和解锁,就可以解决这个问题了。NSRecursiveLock是对递归锁的封装。Condition 条件锁我们直接上这种锁的使用方法,- (void)otherTest{ [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start]; [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];}// 线程1// 删除数组中的元素- (void)__remove { pthread_mutex_lock(&_mutex); NSLog(@"__remove - begin"); if (self.data.count == 0) { // 等待 pthread_cond_wait(&_cond, &_mutex); } [self.data removeLastObject]; NSLog(@“删除了元素”); pthread_mutex_unlock(&_mutex);}// 线程2// 往数组中添加元素- (void)__add { pthread_mutex_lock(&_mutex); sleep(1); [self.data addObject:@“Test”]; NSLog(@“添加了元素”); // 信号 pthread_cond_signal(&_cond); // 广播// pthread_cond_broadcast(&_cond); pthread_mutex_unlock(&_mutex);}我们创建了两个线程,一个往数组中添加数据,一个删除数据,我们通过这个条件锁实现的效果就是在数组中还没有数据的时候等待,数组中添加了一个数据之后在进行删除。条件锁就是互斥锁+条件。我们声明一个条件并初始化。@property (assign, nonatomic) pthread_cond_t cond;//使用完后也要pthread_cond_destroy(&_cond);pthread_cond_init(&_cond, NULL);在__remove方法中if (self.data.count == 0) { // 等待 pthread_cond_wait(&_cond, &_mutex);}如果线程1率先拿到所并加锁,执行到上面代码这里发现数组中还没有数据,就执行pthread_cond_wait,此时线程1会暂时放开_mutex这个锁,并在这休眠等待。线程2在__add方法中最开始因为拿不到锁,所以等待,在线程1休眠放开锁之后拿到锁,加锁,并执行为数组添加数据的代码。添加完了之后会发个信号通知等待条件的线程,并解锁。 pthread_cond_signal(&_cond); pthread_mutex_unlock(&_mutex);线程2执行了pthread_cond_signal之后,线程1就收到了通知,退出休眠状态,继续执行下面的代码。这个地方可能有人会有疑问,是不是线程2应该先unlock再cond_dingnal,其实这个地方顺序没有太大差别,因为线程2执行了pthread_cond_signal之后,会继续执行unlock代码,线程1收到signal通知后会推出休眠状态,同时线程1需要再一次持有这个锁,就算此时线程2还没有unlock,线程1等到线程2 unlock 的时间间隔很短,等到线程2 unlock 后线程1会再去持有这个锁,并加锁。NSCondition就是OC层面的条件锁,内部把mutex互斥锁和条件封装到了一起。NSConditionLock其实也差不多,NSConditionLock可以指定具体的条件,这两个OC层面的类的用法大家可以自行上网搜索。dispatch_semaphore 信号量@property (strong, nonatomic) dispatch_semaphore_t semaphore;//初始化self.semaphore = dispatch_semaphore_create(5);在初始化一个信号的的过程中传入dispatch_semaphore_create的值,其实就代表了允许几个线程同时访问。再回到之前我们存钱取钱这个例子。self.moneySemaphore = dispatch_semaphore_create(1);我们一次只允许一个线程访问,所以在初始化的时候传1。下面就是使用方法。- (void)__drawMoney{ dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER); // … 省略代码 dispatch_semaphore_signal(self.moneySemaphore);}- (void)__saveMoney{ dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER); // … 省略代码 dispatch_semaphore_signal(self.moneySemaphore);}dispatch_semaphore_wait是怎么上锁的呢?如果信号量>0的时候,让信号量-1,并继续往下执行。如果信号量<=0的时候,休眠等待。就这么简单。dispatch_semaphore_signal让信号量+1。小提示在我们平时使用这种方法的时候,可以把信号量的代码提取出来定义一个宏。#define SemaphoreBegin \static dispatch_semaphore_t semaphore; \static dispatch_once_t onceToken; \dispatch_once(&onceToken, ^{ \ semaphore = dispatch_semaphore_create(1); }); \dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);#define SemaphoreEnd \dispatch_semaphore_signal(semaphore);读写安全方案上面我们讲到的线程同步方案都是每次只允许一个线程访问,在实际的情况中,读写的同步方案应该下面这样:每次只能有一个线程写可以有多个线程同时读读和写不能同时进行这就是多读单写,用于文件读写的操作。在我们的iOS中可以用下面这两种解决方案。pthread_rwlock 读写锁这个读写锁的用法很简单,跟之前的普通互斥锁都差不多,大家随便搜一下应该就能搜到,我就不拿出来写了,这里主要是提一下这种锁,大家以后有需要的时候可以用。dispatch_barrier_async 异步栅栏首先在使用这个函数的时候,我们要用自己创建的并发队列。如果传入的是一个串行队列或者全局的并发队列,那dispatch_barrier_async等同于dispatch_async的效果。self.queue = dispatch_queue_create(“rw_queue”, DISPATCH_QUEUE_CONCURRENT);dispatch_async(self.queue, ^{ [self read];}); dispatch_barrier_async(self.queue, ^{ [self write];});在读取数据的时候,使用dispatch_async往对列中添加任务,在写数据时,用dispatch_barrier_async添加任务。dispatch_barrier_async添加的任务会等前面所有的任务都执行完,他再执行,而且他执行的时候,不允许有别的任务同时执行。atomic我们都知道这个atomic是原子性的意思。他保证了属性setter和getter的原子性操作,相当于在set和get方法内部加锁。atomic修饰的属性是读/写安全的,但不是线程安全。假设有一个 atomic 的属性 “name”,如果线程 A 调用 [self setName:@“A”],线程 B 调用 [self setName:@“B”],线程 C 调用 [self name],那么所有这些不同线程上的操作都将依次顺序执行——也就是说,如果一个线程正在执行 getter/setter,其他线程就得等待。因此,属性 name 是读/写安全的。但是,如果有另一个线程 D 同时在调[name release],那可能就会crash,因为 release 不受 getter/setter 操作的限制。也就是说,这个属性只能说是读/写安全的,但并不是线程安全的,因为别的线程还能进行读写之外的其他操作。线程安全需要开发者自己来保证。 ...

April 1, 2019 · 2 min · jiezi

多线程+代理池爬取天天基金网、股票数据(无需使用爬虫框架)

@[TOC]简介提到爬虫,大部分人都会想到使用Scrapy工具,但是仅仅停留在会使用的阶段。为了增加对爬虫机制的理解,我们可以手动实现多线程的爬虫过程,同时,引入IP代理池进行基本的反爬操作。本次使用天天基金网进行爬虫,该网站具有反爬机制,同时数量足够大,多线程效果较为明显。技术路线IP代理池多线程爬虫与反爬编写思路首先,开始分析天天基金网的一些数据。经过抓包分析,可知:./fundcode_search.js包含所有基金的数据,同时,该地址具有反爬机制,多次访问将会失败的情况。同时,经过分析可知某只基金的相关信息地址为:fundgz.1234567.com.cn/js/ + 基金代码 + .js分析完天天基金网的数据后,搭建IP代理池,用于反爬作用。点击这里搭建代理池,由于该作者提供了一个例子,所以本代码里面直接使用的是作者提供的接口。如果你需要更快速的获取到普匿IP,则可以自行搭建一个本地IP代理池。 # 返回一个可用代理,格式为ip:端口 # 该接口直接调用github代理池项目给的例子,故不保证该接口实时可用 # 建议自己搭建一个本地代理池,这样获取代理的速度更快 # 代理池搭建github地址https://github.com/1again/ProxyPool # 搭建完毕后,把下方的proxy.1again.cc改成你的your_server_ip,本地搭建的话可以写成127.0.0.1或者localhost def get_proxy(): data_json = requests.get(“http://proxy.1again.cc:35050/api/v1/proxy/?type=2").text data = json.loads(data_json) return data[‘data’][‘proxy’]搭建完IP代理池后,我们开始着手多线程爬取数据的工作。一旦使用多线程,则需要考虑到数据的读写顺序问题。这里使用python中的队列queue进行存储基金代码,不同线程分别从这个queue中获取基金代码,并访问指定基金的数据。由于queue的读取和写入是阻塞的,所以可以确保该过程不会出现读取重复和读取丢失基金代码的情况。 # 将所有基金代码放入先进先出FIFO队列中 # 队列的写入和读取都是阻塞的,故在多线程情况下不会乱 # 在不使用框架的前提下,引入多线程,提高爬取效率 # 创建一个队列 fund_code_queue = queue.Queue(len(fund_code_list)) # 写入基金代码数据到队列 for i in range(len(fund_code_list)): #fund_code_list[i]也是list类型,其中该list中的第0个元素存放基金代码 fund_code_queue.put(fund_code_list[i][0])现在,开始编写如何获取指定基金的代码。首先,该函数必须先判断queue是否为空,当不为空的时候才可进行获取基金数据。同时,当发现访问失败时,则必须将我们刚刚取出的基金代码重新放回到队列中去,这样才不会导致基金代码丢失。 # 获取基金数据 def get_fund_data(): # 当队列不为空时 while (not fund_code_queue.empty()): # 从队列读取一个基金代码 # 读取是阻塞操作 fund_code = fund_code_queue.get() # 获取一个代理,格式为ip:端口 proxy = get_proxy() # 获取一个随机user_agent和Referer header = {‘User-Agent’: random.choice(user_agent_list), ‘Referer’: random.choice(referer_list) } try: req = requests.get(“http://fundgz.1234567.com.cn/js/" + str(fund_code) + “.js”, proxies={“http”: proxy}, timeout=3, headers=header) except Exception: # 访问失败了,所以要把我们刚才取出的数据再放回去队列中 fund_code_queue.put(fund_code) print(“访问失败,尝试使用其他代理访问”)当访问成功时,则说明能够成功获得基金的相关数据。当我们在将这些数据存入到一个.csv文件中,会发现数据出现错误。这是由于多线程导致,由于多个线程同时对该文件进行写入,导致出错。所以需要引入一个线程锁,确保每次只有一个线程写入。 # 申请获取锁,此过程为阻塞等待状态,直到获取锁完毕 mutex_lock.acquire() # 追加数据写入csv文件,若文件不存在则自动创建 with open(’./fund_data.csv’, ‘a+’, encoding=‘utf-8’) as csv_file: csv_writer = csv.writer(csv_file) data_list = [x for x in data_dict.values()] csv_writer.writerow(data_list) # 释放锁 mutex_lock.release()至此,大部分工作已经完成了。为了更好地实现伪装效果,我们对header进行随机选择。 # user_agent列表 user_agent_list = [ ‘Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.71 Safari/537.1 LBBROWSER’, ‘Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E)’, ‘Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.84 Safari/535.11 SE 2.X MetaSr 1.0’, ‘Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Maxthon/4.4.3.4000 Chrome/30.0.1599.101 Safari/537.36’, ‘Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 UBrowser/4.0.3214.0 Safari/537.36’ ] # referer列表 referer_list = [ ‘http://fund.eastmoney.com/110022.html’, ‘http://fund.eastmoney.com/110023.html’, ‘http://fund.eastmoney.com/110024.html’, ‘http://fund.eastmoney.com/110025.html’ ] # 获取一个随机user_agent和Referer header = {‘User-Agent’: random.choice(user_agent_list), ‘Referer’: random.choice(referer_list) }最后,在main中,开启线程即可。 # 创建一个线程锁,防止多线程写入文件时发生错乱 mutex_lock = threading.Lock() # 线程数为50,在一定范围内,线程数越多,速度越快 for i in range(50): t = threading.Thread(target=get_fund_data,name=‘LoopThread’+str(i)) t.start()通过对多线程和IP代理池的实践操作,能够更加深入了解多线程和爬虫的工作原理。当你在使用一些爬虫框架的时候,就能够做到快速定位错误并解决错误。数据格式000056,建信消费升级混合,2019-03-26,1.7740,1.7914,0.98,2019-03-27 15:00000031,华夏复兴混合,2019-03-26,1.5650,1.5709,0.38,2019-03-27 15:00000048,华夏双债增强债券C,2019-03-26,1.2230,1.2236,0.05,2019-03-27 15:00000008,嘉实中证500ETF联接A,2019-03-26,1.4417,1.4552,0.93,2019-03-27 15:00000024,大摩双利增强债券A,2019-03-26,1.1670,1.1674,0.04,2019-03-27 15:00000054,鹏华双债增利债券,2019-03-26,1.1697,1.1693,-0.03,2019-03-27 15:00000016,华夏纯债债券C,2019-03-26,1.1790,1.1793,0.03,2019-03-27 15:00功能截图配置说明# 确保安装以下库,如果没有,请在python3环境下执行pip install 模块名 import requests import random import re import queue import threading import csv import json补充完整版源代码存放在github上,有需要的可以下载项目持续更新,欢迎您star本项目 ...

March 27, 2019 · 2 min · jiezi

线程池中你不容错过的一些细节

背景上周分享了一篇《一个线程罢工的诡异事件》,最近也在公司内部分享了这个案例。无独有偶,在内部分享的时候也有小伙伴问了之前分享时所提出的一类问题:这其实是一类共性问题,我认为主要还是两个原因:我自己确实也没讲清楚,之前画的那张图还需要再完善,有些误导。第二还是大家对线程池的理解不够深刻,比如今天要探讨的内容。线程池的工作原理首先还是来复习下线程池的基本原理。我认为线程池它就是一个调度任务的工具。众所周知在初始化线程池会给定线程池的大小,假设现在我们有 1000 个线程任务需要运行,而线程池的大小为 1020,在真正运行任务的过程中他肯定不会创建这1000个线程同时运行,而是充分利用线程池里这 1020 个线程来调度这1000个任务。而这里的 10~20 个线程最后会由线程池封装为 ThreadPoolExecutor.Worker 对象,而这个 Worker 是实现了 Runnable 接口的,所以他自己本身就是一个线程。深入分析这里我们来做一个模拟,创建了一个核心线程、最大线程数、阻塞队列都为2的线程池。这里假设线程池已经完成了预热,也就是线程池内部已经创建好了两个线程 Worker。当我们往一个线程池丢一个任务会发生什么事呢?第一步是生产者,也就是任务提供者他执行了一个 execute() 方法,本质上就是往这个内部队列里放了一个任务。之前已经创建好了的 Worker 线程会执行一个 while 循环 —> 不停的从这个内部队列里获取任务。(这一步是竞争的关系,都会抢着从队列里获取任务,由这个队列内部实现了线程安全。)获取得到一个任务后,其实也就是拿到了一个 Runnable 对象(也就是 execute(Runnable task) 这里所提交的任务),接着执行这个 Runnable 的 run() 方法,而不是 start(),这点需要注意后文分析原因。结合源码来看:从图中其实就对应了刚才提到的二三两步:while 循环,从 getTask() 方法中一直不停的获取任务。拿到任务后,执行它的 run() 方法。这样一个线程就调度完毕,然后再次进入循环从队列里取任务并不断的进行调度。再次解释之前的问题接下来回顾一下我们上一篇文章所提到的,导致一个线程没有运行的根本原因是:在单个线程的线程池中一但抛出了未被捕获的异常时,线程池会回收当前的线程并创建一个新的 Worker;它也会一直不断的从队列里获取任务来执行,但由于这是一个消费线程,根本没有生产者往里边丢任务,所以它会一直 waiting 在从队列里获取任务处,所以也就造成了线上的队列没有消费,业务线程池没有执行的问题。结合之前的那张图来看:这里大家问的最多的一个点是,为什么会没有是根本没有生产者往里边丢任务,图中不是明明画的有一个 product 嘛?这里确实是有些不太清楚,再次强调一次:图中的 product 是往内部队列里写消息的生产者,并不是往这个 Consumer 所在的线程池中写任务的生产者。因为即便 Consumer 是一个单线程的线程池,它依然具有一个常规线程池所具备的所有条件:Worker 调度线程,也就是线程池运行的线程;虽然只有一个。内部的阻塞队列;虽然长度只有1。再次结合图来看:所以之前提到的【没有生产者往里边丢任务】是指右图放大后的那一块,也就是内部队列并没有其他线程往里边丢任务执行 execute() 方法。而一旦发生未捕获的异常后,Worker1 被回收,顺带的它所调度的线程 task1(这个task1 也就是在执行一个 while 循环消费左图中的那个队列) 也会被回收掉。新创建的 Worker2 会取代 Worker1 继续执行 while 循环从内部队列里获取任务,但此时这个队列就一直会是空的,所以也就是处于 Waiting 状态。我觉得这波解释应该还是讲清楚了,欢迎还没搞明白的朋友留言讨论。为什是 run() 而不是 start()问题搞清楚后来想想为什么线程池在调度的时候执行的是 Runnable 的 run() 方法,而不是 start() 方法呢?我相信大部分没有看过源码的同学心中第一个印象就应该是执行的 start() 方法;因为不管是学校老师,还是网上大牛讲的都是只有执行了 start() 方法后操作系统才会给我们创建一个独立的线程来运行,而 run() 方法只是一个普通的方法调用。而在线程池这个场景中却恰好就是要利用它只是一个普通方法调用。回到我在文初中所提到的:我认为线程池它就是一个调度任务的工具。假设这里是调用的 Runnable 的 start 方法,那会发生什么事情。如果我们往一个核心、最大线程数为 2 的线程池里丢了 1000 个任务,那么它会额外的创建 1000 个线程,同时每个任务都是异步执行的,一下子就执行完毕了。从而没法做到由这两个 Worker 线程来调度这 1000 个任务,而只有当做一个同步阻塞的 run() 方法调用时才能满足这个要求。这事也让我发现一个奇特的现象:就是网上几乎没人讲过为什么在线程池里是 run 而不是 start,不知道是大家都觉得这是基操还是没人仔细考虑过。总结针对之前线上事故的总结上次已经写得差不多了,感兴趣的可以翻回去看看。这次呢可能更多是我自己的总结,比如写一篇技术博客时如果大部分人对某一个知识点讨论的比较热烈时,那一定是作者要么讲错了,要么没讲清楚。这点确实是要把自己作为一个读者的角度来看,不然很容易出现之前的一些误解。在这之外呢,我觉得对于线程池把这两篇都看完同时也理解后对于大家理解线程池,利用线程池完成工作也是有很大好处的。如果有在面试中加分的记得回来点赞、分享啊。你的点赞与分享是对我最大的支持 ...

March 26, 2019 · 1 min · jiezi

JAVA多线程使用场景和注意事项

我曾经对自己的小弟说,如果你实在搞不清楚什么时候用HashMap,什么时候用ConcurrentHashMap,那么就用后者,你的代码bug会很少。他问我:ConcurrentHashMap是什么? -.-编程不是炫技。大多数情况下,怎么把代码写简单,才是能力。多线程生来就是复杂的,也是容易出错的。一些难以理解的概念,要规避。本文不讲基础知识,因为你手里就有jdk的源码。线程Thread第一类就是Thread类。大家都知道有两种实现方式。第一可以继承Thread覆盖它的run方法;第二种是实现Runnable接口,实现它的run方法;而第三种创建线程的方法,就是通过线程池。我们的具体代码实现,就放在run方法中。我们关注两种情况。一个是线程退出条件,一个是异常处理情况。线程退出有的run方法执行完成后,线程就会退出。但有的run方法是永远不会结束的。结束一个线程肯定不是通过Thread.stop()方法,这个方法已经在java1.2版本就废弃了。所以我们大体有两种方式控制线程。定义退出标志放在while中代码一般长这样。private volatile boolean flag= true;public void run() { while (flag) { }}标志一般使用volatile进行修饰,使其读可见,然后通过设置这个值来控制线程的运行,这已经成了约定俗成的套路。使用interrupt方法终止线程类似这种。while(!isInterrupted()){……}对于InterruptedException,比如Thread.sleep所抛出的,我们一般是补获它,然后静悄悄的忽略。中断允许一个可取消任务来清理正在进行的工作,然后通知其他任务它要被取消,最后才终止,在这种情况下,此类异常需要被仔细处理。interrupt方法不一定会真正”中断”线程,它只是一种协作机制。interrupt方法通常不能中断一些处于阻塞状态的I/O操作。比如写文件,或者socket传输等。这种情况,需要同时调用正在阻塞操作的close方法,才能够正常退出。interrupt系列使用时候一定要注意,会引入bug,甚至死锁。异常处理java中会抛出两种异常。一种是必须要捕获的,比如InterruptedException,否则无法通过编译;另外一种是可以处理也可以不处理的,比如NullPointerException等。在我们的任务运行中,很有可能抛出这两种异常。对于第一种异常,是必须放在try,catch中的。但第二种异常如果不去处理的话,会影响任务的正常运行。有很多同学在处理循环的任务时,没有捕获一些隐式的异常,造成任务在遇到异常的情况下,并不能继续执行下去。如果不能确定异常的种类,可以直接捕获Exception或者更通用的Throwable。while(!isInterrupted()){ try{ …… }catch(Exception ex){ …… }}同步方式java中实现同步的方式有很多,大体分为以下几种。synchronized 关键字wait、notify等Concurrent包中的ReentrantLockvolatile关键字ThreadLocal局部变量生产者、消费者是wait、notify最典型的应用场景,这些函数的调用,是必须要放在synchronized代码块里才能够正常运行的。它们同信号量一样,大多数情况下属于炫技,对代码的可读性影响较大,不推荐。关于ObjectMonitor相关的几个函数,只要搞懂下面的图,就基本ok了。使用ReentrantLock最容易发生错误的就是忘记在finally代码块里关闭锁。大多数同步场景下,使用Lock就足够了,而且它还有读写锁的概念进行粒度上的控制。我们一般都使用非公平锁,让任务自由竞争。非公平锁性能高于公平锁性能,非公平锁能更充分的利用cpu的时间片,尽量的减少cpu空闲的状态时间。非公平锁还会造成饿死现象:有些任务一直获取不到锁。synchronized通过锁升级机制,速度不见得就比lock慢。而且,通过jstack,能够方便的看到其堆栈,使用还是比较广泛。volatile总是能保证变量的读可见,但它的目标是基本类型和它锁的基本对象。假如是它修饰的是集合类,比如Map,那么它保证的读可见是map的引用,而不是map对象,这点一定要注意。synchronized和volatile都体现在字节码上(monitorenter、monitorexit),主要是加入了内存屏障。而Lock,是纯粹的java api。ThreadLocal很方便,每个线程一份数据,也很安全,但要注意内存泄露。假如线程存活时间长,我们要保证每次使用完ThreadLocal,都调用它的remove()方法(具体来说是expungeStaleEntry),来清除数据。关于Concurrent包concurrent包是在AQS的基础上搭建起来的,AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架。线程池最全的线程池大概有7个参数,想要合理使用线程池,肯定不会不会放过这些参数的优化。线程池参数concurrent包最常用的就是线程池,平常工作建议直接使用线程池,Thread类就可以降低优先级了。我们常用的主要有newSingleThreadExecutor、newFixedThreadPool、newCachedThreadPool、调度等,使用Executors工厂类创建。newSingleThreadExecutor可以用于快速创建一个异步线程,非常方便。而newCachedThreadPool永远不要用在高并发的线上环境,它用的是无界队列对任务进行缓冲,可能会挤爆你的内存。我习惯性自定义ThreadPoolExecutor,也就是参数最全的那个。public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) 假如我的任务可以预估,corePoolSize,maximumPoolSize一般都设成一样大的,然后存活时间设的特别的长。可以避免线程频繁创建、关闭的开销。I/O密集型和CPU密集型的应用线程开的大小是不一样的,一般I/O密集型的应用线程就可以开的多一些。threadFactory我一般也会定义一个,主要是给线程们起一个名字。这样,在使用jstack等一些工具的时候,能够直观的看到我所创建的线程。监控高并发下的线程池,最好能够监控起来。可以使用日志、存储等方式保存下来,对后续的问题排查帮助很大。通常,可以通过继承ThreadPoolExecutor,覆盖beforeExecute、afterExecute、terminated方法,达到对线程行为的控制和监控。线程池饱和策略最容易被遗忘的可能就是线程的饱和策略了。也就是线程和缓冲队列的空间全部用完了,新加入的任务将如何处置。jdk默认实现了4种策略,默认实现的是AbortPolicy,也就是直接抛出异常。下面介绍其他几种。DiscardPolicy 比abort更加激进,直接丢掉任务,连异常信息都没有。CallerRunsPolicy 由调用的线程来处理这个任务。比如一个web应用中,线程池资源占满后,新进的任务将会在tomcat线程中运行。这种方式能够延缓部分任务的执行压力,但在更多情况下,会直接阻塞主线程的运行。 DiscardOldestPolicy 丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)。很多情况下,这些饱和策略可能并不能满足你的需求,你可以自定义自己的策略,比如将任务持久化到一些存储中。阻塞队列阻塞队列会对当前的线程进行阻塞。当队列中有元素后,被阻塞的线程会自动被唤醒,这极大的提高的编码的灵活性,非常方便。在并发编程中,一般推荐使用阻塞队列,这样实现可以尽量地避免程序出现意外的错误。阻塞队列使用最经典的场景就是socket数据的读取、解析,读数据的线程不断将数据放入队列,解析线程不断从队列取数据进行处理。ArrayBlockingQueue对访问者的调用默认是不公平的,我们可以通过设置构造方法参数将其改成公平阻塞队列。LinkedBlockingQueue队列的默认最大长度为Integer.MAX_VALUE,这在用做线程池队列的时候,会比较危险。SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。队列本身不存储任何元素,吞吐量非常高。对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务”。它更像是一个管道,在一些通讯框架中(比如rpc),通常用来快速处理某个请求,应用较为广泛。DelayQueue是一个支持延时获取元素的无界阻塞队列。放入DelayQueue的对象需要实现Delayed接口,主要是提供一个延迟的时间,以及用于延迟队列内部比较排序。这种方式通常能够比大多数非阻塞的while循环更加节省cpu资源。另外还有PriorityBlockingQueue和LinkedTransferQueue等,根据字面意思就能猜测它的用途。在线程池的构造参数中,我们使用的队列,一定要注意其特性和边界。比如,即使是最简单的newFixedThreadPool,在某些场景下,也是不安全的,因为它使用了无界队列。CountDownLatch假如有一堆接口A-Y,每个接口的耗时最大是200ms,最小是100ms。我的一个服务,需要提供一个接口Z,调用A-Y接口对结果进行聚合。接口的调用没有顺序需求,接口Z如何在300ms内返回这些数据?此类问题典型的还有赛马问题,只有通过并行计算才能完成问题。归结起来可以分为两类:实现任务的并行性开始执行前等待n个线程完成任务在concurrent包出现之前,需要手工的编写这些同步过程,非常复杂。现在就可以使用CountDownLatch和CyclicBarrier进行便捷的编码。CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。CyclicBarrier与其类似,可以实现同样的功能。不过在日常的工作中,使用CountDownLatch会更频繁一些。信号量Semaphore虽然有一些应用场景,但大部分属于炫技,在编码中应该尽量少用。信号量可以实现限流的功能,但它只是常用限流方式的一种。其他两种是漏桶算法、令牌桶算法。hystrix的熔断功能,也有使用信号量进行资源的控制。Lock && Condition在Java中,对于Lock和Condition可以理解为对传统的synchronized和wait/notify机制的替代。concurrent包中的许多阻塞队列,就是使用Condition实现的。但这些类和函数对于初中级码农来说,难以理解,容易产生bug,应该在业务代码中严格禁止。但在网络编程、或者一些框架类工程中,这些功能是必须的,万不可将这部分的工作随便分配给某个小弟。End不管是wait、notify,还是同步关键字或者锁,能不用就不用,因为它们会引发程序的复杂性。最好的方式,是直接使用concurrent包所提供的机制,来规避一些编码方面的问题。concurrent包中的CAS概念,在一定程度上算是无锁的一种实现。更专业的有类似disruptor的无锁队列框架,但它依然是建立在CAS的编程模型上的。近些年,类似AKKA这样的事件驱动模型正在走红,但编程模型简单,不代表实现简单,背后的工作依然需要多线程去协调。golang引入协程(coroutine)概念以后,对多线程加入了更加轻量级的补充。java中可以通过javaagent技术加载quasar补充一些功能,但我觉得你不会为了这丁点效率去牺牲编码的可读性。

March 15, 2019 · 1 min · jiezi

synchronized锁了什么

前言synchronized翻译为中文的意思是同步的,它是Java中处理线程安全问题常用的关键字。也有人称其为同步锁。既然是锁,其必然有锁的东西,下面先会简单介绍一下synchronized,再通过一个示例代码展示synchronized锁了什么。(这里先提前透露答案synchronized锁的是代码)介绍定义synchronized提供的同步机制确保了同一个时刻,被修饰的代码块或方法只会有一个线程执行。用法synchronized可以修饰方法和代码块:修饰普通方法修饰静态方法修饰代码块根据修饰情况,分为对象锁和类锁:对象锁:普通方法(等价于代码块修饰this)代码块修饰的是是类的一个对象类锁类方法(等价于代码块修饰当前类Class对象)代码块修饰的是是类Class对象原理synchronized底层原理是使用了对象持有的监视器(monitor)。但是同步代码块和同步方法的原理存在一点差异:同步代码块是使用monitorenter和monitorexit指令实现的同步方法是由方法调用指令读取运行时常量池中方法的ACC_SYNCHRONIZED 标识隐式实现,实际上还是调用了monitorenter和monitorexit指令测试示例计数器一个特殊的计数器,自增方法increase()被synchronized修饰,而获取当前值方法getCurrent()则没有被synchronized修饰。/** * 计数器 * @author RJH * create at 2019-03-13 /public class Counter { /* * 全局对象,总数 / private static int i = 0; /* * 自增 * @return / public synchronized int increase() { try { //使用休眠让结果更明显 Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } return ++i; } /* * 获取当前值 * @return / public int getCurrent() { return i; }}测试代码使用自增线程和获取当前值的线程来验证synchronized锁的是代码,而不是全局变量/* * synchronized锁了什么 * @author RJH * create at 2019-03-02 /public class LockWhatTest { public static void main(String[] args) { Counter counter =new Counter(); IncreaseThread increaseThread1=new IncreaseThread(counter); IncreaseThread increaseThread2=new IncreaseThread(counter); GetThread getThread=new GetThread(counter); increaseThread1.start(); increaseThread2.start(); //直到increaseThread的线程启动才执行下一步 while (increaseThread1.getState().compareTo(Thread.State.NEW)==0 && increaseThread1.getState().compareTo(Thread.State.NEW)==0){ } getThread.start(); } /* * 自增线程 / static class IncreaseThread extends Thread{ private Counter counter; public IncreaseThread(Counter counter) { this.counter = counter; } @Override public void run() { System.out.println(“After increase:” + counter.increase()+",trigger time:"+System.currentTimeMillis()); } } /* * 获取当前值的线程 */ static class GetThread extends Thread{ private Counter counter; public GetThread(Counter counter) { this.counter = counter; } @Override public void run() { System.out.println(“Current:"+ counter.getCurrent()+",trigger time:"+System.currentTimeMillis()); } }}执行结果Current:0,trigger time:1552487003845After increase:1,trigger time:1552487008846After increase:2,trigger time:1552487013848结果分析从测试结果可以得知在两个自增线程启动后,获取当前值的线程才启动,但是获取当前值的线程是先被执行完成了。根据自增线程执行完成的时间戳间隔可以得知,两个自增线程是依次执行的。从而可以证明synchronized并不是锁定方法内访问的变量synchronized锁定的是同一个监视器对象监视的代码 ...

March 14, 2019 · 1 min · jiezi

一个线程罢工的诡异事件

背景事情(事故)是这样的,突然收到报警,线上某个应用里业务逻辑没有执行,导致的结果是数据库里的某些数据没有更新。虽然是前人写的代码,但作为 Bug maker&killer 只能咬着牙上了。<!–more–>因为之前没有接触过出问题这块的逻辑,所以简单理了下如图:有一个生产线程一直源源不断的往队列写数据。消费线程也一直不停的取出数据后写入后续的业务线程池。业务线程池里的线程会对每个任务进行入库操作。整个过程还是比较清晰的,就是一个典型的生产者消费者模型。尝试定位接下来便是尝试定位这个问题,首先例行检查了以下几项:是否内存有内存溢出?应用 GC 是否有异常?通过日志以及监控发现以上两项都是正常的。紧接着便 dump 了线程快照查看业务线程池中的线程都在干啥。结果发现所有业务线程池都处于 waiting 状态,队列也是空的。同时生产者使用的队列却已经满了,没有任何消费迹象。结合上面的流程图不难发现应该是消费队列的 Consumer 出问题了,导致上游的队列不能消费,下有的业务线程池没事可做。review 代码于是查看了消费代码的业务逻辑,同时也发现消费线程是一个单线程。结合之前的线程快照,我发现这个消费线程也是处于 waiting 状态,和后面的业务线程池一模一样。他做的事情基本上就是对消息解析,之后丢到后面的业务线程池中,没有发现什么特别的地方。但是由于里面的分支特别多(switch case),看着有点头疼;所以我与写这个业务代码的同学沟通后他告诉我确实也只是入口处解析了一下数据,后续所有的业务逻辑都是丢到线程池中处理的,于是我便带着这个前提去排查了(埋下了伏笔)。因为这里消费的队列其实是一个 disruptor 队列;它和我们常用的 BlockQueue 不太一样,不是由开发者自定义一个消费逻辑进行处理的;而是在初始化队列时直接丢一个线程池进去,它会在内部使用这个线程池进行消费,同时回调一个方法,在这个方法里我们写自己的消费逻辑。所以对于开发者而言,这个消费逻辑其实是一个黑盒。于是在我反复 review 了消费代码中的数据解析逻辑发现不太可能出现问题后,便开始疯狂怀疑是不是 disruptor 自身的问题导致这个消费线程罢工了。再翻了一阵 disruptor 的源码后依旧没发现什么问题后我咨询对 disruptor 较熟的@咖啡拿铁,在他的帮助下在本地模拟出来和生产一样的情况。本地模拟本地也是创建了一个单线程的线程池,分别执行了两个任务。第一个任务没啥好说的,就是简单的打印。第二个任务会对一个数进行累加,加到 10 之后就抛出一个未捕获的异常。接着我们来运行一下。发现当任务中抛出一个没有捕获的异常时,线程池中的线程就会处于 waiting 状态,同时所有的堆栈都和生产相符。细心的朋友会发现正常运行的线程名称和异常后处于 waiting 状态的线程名称是不一样的,这个后续分析。解决问题当加入异常捕获后又如何呢?程序肯定会正常运行。同时会发现所有的任务都是由一个线程完成的。虽说就是加了一行代码,但我们还是要搞清楚这里面的门门道道。源码分析于是只有直接 debug 线程池的源码最快了;通过刚才的异常堆栈我们进入到 ThreadPoolExecutor.java:1142 处。发现线程池已经帮我们做了异常捕获,但依然会往上抛。在 finally 块中会执行 processWorkerExit(w, completedAbruptly) 方法。看过之前《如何优雅的使用和理解线程池》的朋友应该还会有印象。线程池中的任务都会被包装为一个内部 Worker 对象执行。processWorkerExit 可以简单的理解为是把当前运行的线程销毁(workers.remove(w))、同时新增(addWorker())一个 Worker 对象接着处理;就像是哪个零件坏掉后重新换了一个新的接着工作,但是旧零件负责的任务就没有了。接下来看看 addWorker() 做了什么事情:只看这次比较关心的部分;添加成功后会直接执行他的 start() 的方法。由于 Worker 实现了 Runnable 接口,所以本质上就是调用了 runWorker() 方法。在 runWorker() 其实就是上文 ThreadPoolExecutor 抛出异常时的那个方法。它会从队列里一直不停的获取待执行的任务,也就是 getTask();在 getTask 也能看出它会一直从内置的队列取出任务。而一旦队列是空的,它就会 waiting 在 workQueue.take(),也就是我们从堆栈中发现的 1067 行代码。线程名字的变化上文还提到了异常后的线程名称发生了改变,其实在 addWorker() 方法中可以看到 new Worker()时就会重新命名线程的名称,默认就是把后缀的计数+1。这样一切都能解释得通了,真相只有一个:在单个线程的线程池中一但抛出了未被捕获的异常时,线程池会回收当前的线程并创建一个新的 Worker;它也会一直不断的从队列里获取任务来执行,但由于这是一个消费线程,根本没有生产者往里边丢任务,所以它会一直 waiting 在从队列里获取任务处,所以也就造成了线上的队列没有消费,业务线程池没有执行的问题。总结所以之后线上的那个问题加上异常捕获之后也变得正常了,但我还是有点纳闷的是:既然后续所有的任务都是在线程池中执行的,也就是纯异步了,那即便是出现异常也不会抛到消费线程中啊。这不是把我之前储备的知识点推翻了嘛?不信邪!之后我让运维给了加上异常捕获后的线上错误日志。结果发现在上文提到的众多 switch case 中,最后一个竟然是直接操作的数据库,导致一个非空字段报错了????!!这事也给我个教训,还是得眼见为实啊。虽然这个问题改动很小解决了,但复盘整个过程还是有许多需要改进的:消费队列的线程名称竟然和业务线程的前缀一样,导致我光找它就花了许多时间,命名必须得调整。开发规范,防御式编程大家需要养成习惯。未知的技术栈需要谨慎,比如 disruptor,之前的团队应该只是看了个高性能的介绍就直接使用,并没有深究其原理;导致出现问题后对它拿不准。实例代码:https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/thread/ThreadExceptionTest.java你的点赞与分享是对我最大的支持 ...

March 13, 2019 · 1 min · jiezi

这一次,让我们完全掌握Java多线程(2/10)

多线程不仅是Java后端开发面试中非常热门的一个问题,也是各种高级工具、框架与分布式的核心基石。但是这个领域相关的知识点涉及到了线程调度、线程同步,甚至在一些关键点上还涉及到了硬件原语、操作系统等更底层的知识。想要背背面试题很容易,但是如果面试官一追问就很容易露馅,更不用说真正想搞明白这个问题并应用在实际的代码实践中了。不用担心!在接下来的一系列文章中将会由浅入深地贯穿这个问题的方方面面,虽然不如一些面试大全来得直接和速成。但是真正搞明白多线程编程不仅能够一劳永逸地解决面试中的尴尬,而且还能打开通往底层知识的大门,不止是搞明白一个孤立的知识点,更是一个将以前曾经了解过的理论知识融会贯通连点成面的好机会。虽然阅读本文不需要事先了解并发相关的概念,但是如果已经掌握了一些大概的概念将会大大降低理解的难度。有兴趣的读者可以参考本系列的第一篇文章来了解一下并发相关的基本概念——当我们在说“并发、多线程”,说的是什么?。这一系列文章将会包含10篇文章,本文是其中的第二篇,相信只要有耐心看完所有内容一定能轻松地玩转多线程编程,不止是游刃有余地通过面试,更是能熟练掌握多线程编程的实践技巧与并发实践这一Java高级工具与框架的共同核心。前五篇包含以下内容,将会在近期发布:并发基本概念——当我们在说“并发、多线程”,说的是什么?多线程入门——本文线程池剖析线程同步机制解析并发常见问题为什么要有多线程?多线程程序和一般的单线程程序相比引入了同步、线程调度、内存可见性等一大堆复杂的问题,大大提高了开发者开发程序的难度,那么为什么现在多线程在各个邻域中还被如此趋之若鹜呢?一种场景在我大学的时候宿舍边上有一家盖浇饭,也提供炒菜。老板非常地耿直,非要按点菜的顺序一桌一桌地烧,如果前一桌的菜没上完后一桌一个菜都别想吃到。结果就是每天这家店里都是怨声载道,顾客们常常等了半个小时也等不来一个菜填填肚子。你问我为什么还会有人去吃,受这罪,那肯定是因为好吃啊????。不过仔细想想,好像一般的店里好像并没有这种情况,因为大部分饭店都是混合着上的,就算前一桌没上完好歹会给几个菜垫垫肚子。这在程序中也是一样,不同的程序之间可以交替运行,不至于在我们的电脑上打开了开发工具就不能接收微信消息。这就是多线程的一个应用场景:通过任务的交替执行使一台计算机上可以同时运行多个程序。另一种场景还是在小饭馆里,一个服务员在给一桌点完菜之后肯定不会等到这桌菜上完了才去给另外一桌点菜。一般都是点完菜就把订单给了厨房,之后就继续给下一桌点菜了。在这里,我们可以把服务员想象成我们的计算机,把厨房想象成远程的服务器。那么在我们的电脑下载音乐的时候同时继续播放音乐,这就能更高效地利用我们的电脑了。这种场景可以描述为:在等待网络请求、磁盘I/O等耗时操作完成时,可以用多线程来让CPU继续运转,以达到有效利用CPU资源的目的。最后一种场景然后我们来到了厨房,竟然看到了一个大神,能一个人烧2个灶台。如果这个厨师大神是一个多核处理器,那么两个灶台就是两个线程,如果只给一个灶台,那就浪费他的才能了,这绝对是一种损失。这就是多线程应用的最后一种场景:将计算量比较大的任务拆分到两个CPU上执行可以减少执行完成的时间,而多线程就是拆分和执行任务的载体,没有多线程就没办法把任务放到多个CPU上执行了。什么是多线程?多线程就是很多线程的意思,嗯,是不是很简单?线程是操作系统中的一个执行单元,同样的执行单元还有进程,所有的代码都要在进程/线程中执行。线程是从属于进程的,一个进程可以包含多个线程。进程和线程之间还有一个区别就是,每个进程有自己独立的内存空间,互相直接不能直接访问;但是同一个进程中的多个线程都共享进程的内存空间,所以可以直接访问同一块内存,其中最典型的就是Java中的堆。初识多线程编程了解了这么多理论概念,终于到了实际上手写写代码的时候了。创建线程Java中的线程使用Thread类表示,Thread类的构造器可以传入一个实现了Runnable接口的对象,这个Runnable对象中的void run()方法就代表了线程中会执行的任务。例如如果要创建一个对整型变量进行自增的Runnable任务就可以写为:// 静态变量,用于自增private static int count = 0;// 创建Runnable对象(匿名内部类对象)Runnable task = new Runnable() { public void run() { for (int i = 0; i < 1e6; ++i) { count += 1; }}有了Runnable对象代表的待执行任务之后,我们就可以创建两个线程来运行它了。Thread t1 = new Thread(task);Thread t2 = new Thread(task);但是这时候只是创建了线程对象,实际上线程还没有被执行,想要执行线程还需要调用线程对象的start()方法。t1.start();t2.start();这时候线程就能开始执行了,完整的代码如下所示:public class SimpleThread { private static int count = 0; public static void main(String[] args) throws Exception { Runnable task = new Runnable() { public void run() { for (int i = 0; i < 1000000; ++i) { count = count + 1; } } }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); // 等待t1和t2执行完成// t1.join();// t2.join(); System.out.println(“count = " + count); }}最后输出的结果是8251,你执行的时候应该会与这个值不同,但是一样会远远小于一百万。这好像离我们期望的结果有点远,毕竟每个任务都累加了至少一百万次。这是因为我们在main方法中创建线程并运行之后并没有等待线程完成,使用t1.join()可以使当前线程等待t1线程执行完成后再继续执行。让我们去掉两个join方法调用前面的双斜杠试一试效果。线程同步在我的电脑上执行的结果是1753490,你执行的结果会有不同,但是同样达不到我们所期望的两百万。具体的原因可以从下面的执行顺序图中找到答案。t1t2获取count值为0 获取count值为0计算0+1的结果为2 将2保存到count 计算0+1的结果为2 将2保存到count可以看到,t1和t2两个线程之间的并发运行会导致互相自己的结果覆盖,最后的结果就会在一百万与两百万之间,但是离两百万会有比较大的距离。这样的多线程共同读取并修改同一个共享数据的代码区块就被称为临界区,临界区同一时刻只允许一个线程进入,如果同时有多个线程进入就会导致数据竞争问题。如果有读者对这里提到的临界区和数据竞争概念还不清楚的,可以参考本系列的第一篇介绍并发基本概念的文章——当我们在说“并发、多线程”,说的是什么?。在Java 5之前,我们最常用的线程同步方式就是关键字synchronized,这个关键字既可以标在方法上,也可以作为独立的块结构使用。方法声明形式的synchronized关键字可以在方法定义时如此使用:public synchronized static void methodName()。因为我们的累加操作在继承自Runnable接口的run()方法中,所以没办法改变方法的声明,那么就可以使用如下的块结构形式使用synchronized关键字:Runnable task = new Runnable() { public void run() { for (int i = 0; i < 1000000; ++i) { synchronized (SimpleThread.class) { count += 1; } } }};synchronized是一种对象锁,采用的锁和具体的对象有关,如果是同一个对象就是同一个锁;如果是不同的对象则是不同的锁。同一时刻只能有一个线程持有锁,也就意味着其他想要获取同一个锁的线程会被阻塞,直到持有锁的线程释放这个锁为止。这里可以把对象锁对应的对象看做是锁的名称,实现同步的并不是对象本身,而是与对象对应的对象锁。在块结构的synchronized关键字后的括号中的就是对象锁所对应的对象,在上面的代码中,我们使用了SimpleThread类的类对象对应的锁作为同步工具。而如果synchronized关键字被用在方法声明中,那么如果是实例方法(非static方法)对应的对象就是this指针所指向的对象,如果是static方法,那么对应的对象就是所处类的类对象。这次我们可以看到输出的结果每次都是稳定的两百万了,我们成功完成了我们的第一个完整的多线程程序????????????后记但是一般在实际编写多线程代码时,我们一般不会直接创建Thread对象,而是使用线程池管理任务的执行。相信读者们也在很多地方看见过“线程池”这个词,如果希望了解线程池相关的使用与具体实现,可以关注一下将会在近期发布的下一篇文章。到目前为止,我们都只是涉及了并发与多线程相关的概念和简单的多线程程序实现。接下来我们就会进入更深入与复杂的多线程实现当中了,包括但不限于volatile关键字、CAS、AQS、内存可见性、常用线程池、阻塞队列、死锁、非死锁并发问题、事件驱动模型等等知识点的应用和串联,最后大家都可以逐步实现在各种工具中常用的一系列并发数据结构与程序,例如AtomicInteger、阻塞队列、事件驱动Web服务器。相信大家通过这一系列多线程编程的冒险历程之后一定可以做到对多线程这个话题举重若轻、有条不紊了。 ...

March 10, 2019 · 1 min · jiezi

Java 线程池的认识和使用

多线程编程很难,难点在于多线程代码的执行不是按照我们直觉上的执行顺序。所以多线程编程必须要建立起一个宏观的认识。线程池是多线程编程中的一个重要概念。为了能够更好地使用多线程,学习好线程池当然是必须的。为什么要使用线程池?平时我们在使用多线程的时候,通常都是架构师配置好了线程池的 Bean,我们需要使用的时候,提交一个线程即可,不需要过多关注其内部原理。在学习一门新的技术之前,我们还是先了解下为什么要使用它,使用它能够解决什么问题:创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处理效率例如:记创建线程消耗时间T1,执行任务消耗时间T2,销毁线程消耗时间T3如果T1+T3>T2,那么是不是说开启一个线程来执行这个任务太不划算了!正好,线程池缓存线程,可用已有的闲置线程来执行新任务,避免了T1+T3带来的系统开销线程并发数量过多,抢占系统资源从而导致阻塞我们知道线程能共享系统资源,如果同时执行的线程过多,就有可能导致系统资源不足而产生阻塞的情况运用线程池能有效的控制线程最大并发数,避免以上的问题对线程进行一些简单的管理比如:延时执行、定时循环执行的策略等运用线程池都能进行很好的实现创建一个线程池在 Java 中,新建一个线程池对象非常简单,Java 本身提供了工具类java.util.concurrent.Executors,可以使用如下代码创建一个固定数量线程的线程池:ExecutorService service = Executors.newFixedThreadPool(10);注意:以上代码用来测试还可以,实际使用中最好能够显示地指定相关参数。我们可以看下其内部源码实现:public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }在阿里巴巴代码规范中,建议我们自己指定线程池的相关参数,为的是让开发人员能够自行理解线程池创建中的每个参数,根据实际情况,创建出合理的线程池。接下来,我们来剖析下java.util.concurrent.ThreadPoolExecutor的构造方法参数。ThreadPoolExecutor 浅析java.util.concurrent.ThreadPoolExecutor有多个构造方法,我们拿参数最多的构造方法来举例,以下是阿里巴巴代码规范中给出的创建线程池的范例:ThreadPoolExecutor service = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024), new ThreadFactoryBuilder().setNameFormat(“demo-pool-%d”).build(), new ThreadPoolExecutor.AbortPolicy());贴一张IDEA中的图更方便看:首先最重要的几个参数,可能就是:corePoolSize,maximumPoolSize,workQueue了,先看下这几个参数的解释:corePoolSize用于设定 thread pool 需要时刻保持的最小 core threads 的数量,即便这些 core threads 处于空闲状态啥事都不做也不会将它们回收掉,当然前提是你没有设置 allowCoreThreadTimeOut 为 true。至于 pool 是如何做到保持这些个 threads 不死的,我们稍后再说。maximumPoolSize用于限定 pool 中线程数的最大值。如果你自己构造了 pool 且传入了一个 Unbounded 的 queue 且没有设置它的 capacity,那么不好意思,最大线程数会永远 <= corePoolSize,maximumPoolSize 变成了无效的。workQueue该线程池中的任务队列:维护着等待执行的 Runnable 对象。当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务由于本文是初步了解线程池,所以先理解这几个参数,上文对于这三个参数的解释,基本上跟JDK源码中的注释一致(java.util.concurrent.ThreadPoolExecutor#execute里的代码)。我们编写个程序来方便理解:// 创建线程池ThreadPoolExecutor service = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024), new ThreadFactoryBuilder().setNameFormat(“demo-pool-%d”).build(), new ThreadPoolExecutor.AbortPolicy());// 等待执行的runnable Runnable runnable = () -> { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }};// 启动的任务数量int counts = 1224;for (int i = 0; i < counts; i++) { service.execute(runnable);}// 监控线程池执行情况的代码 ThreadPoolExecutor tpe = ((ThreadPoolExecutor) service);while (true) { System.out.println(); int queueSize = tpe.getQueue().size(); System.out.println(“当前排队线程数:” + queueSize); int activeCount = tpe.getActiveCount(); System.out.println(“当前活动线程数:” + activeCount); long completedTaskCount = tpe.getCompletedTaskCount(); System.out.println(“执行完成线程数:” + completedTaskCount); long taskCount = tpe.getTaskCount(); System.out.println(“总线程数:” + taskCount); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); }} 线程池的容量与我们启动的任务数量息息相关。已知:corePoolSize = 5maximumPoolSize = 200workQueue.size() = 1024我们修改同时 execute 添加到线程池的 Runnable 数量 counts:counts <= corePoolSize:所有的任务均为核心线程执行,没有任何 Runnable 被添加到 workQueue中当前排队线程数:0当前活动线程数:3执行完成线程数:0总线程数:3corePoolSize < counts <= corePoolSize + workQueue.size():所有任务均为核心线程执行,当核心线程处于繁忙状态,则将任务添加到 workQueue 中等待当前排队线程数:15当前活动线程数:5执行完成线程数:0总线程数:20corePoolSize + workQueue.size() < counts <= maximumPoolSize + workQueue.size():corePoolSize 个线程由核心线程执行,超出队列长度 workQueue.size() 的任务,将另启动非核心线程执行当前排队线程数:1024当前活动线程数:105执行完成线程数:0总线程数:1129counts > maximumPoolSize + workQueue.size():将会报异常java.util.concurrent.RejectedExecutionExceptionjava.util.concurrent.RejectedExecutionException: Task com.bwjava.util.ExecutorServiceUtilTest$$Lambda$1/314265080@725bef66 rejected from java.util.concurrent.ThreadPoolExecutor@2aaf7cc2[Running, pool size = 200, active threads = 200, queued tasks = 1024, completed tasks = 0]线程池踩坑:线程嵌套导致阻塞这次的踩坑才是我写这篇文章的初衷,借此机会好好了解下线程池的各个概念。本身这段时间在研究爬虫,为了尽量提高爬虫的效率,用到了多线程处理。由于代码写得比较随性,所以遇到了一个阻塞的问题,研究了一下才搞明白,模拟的代码如下:ThreadPoolExecutor service = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024), new ThreadFactoryBuilder().setNameFormat(“demo-pool-%d”).build(), new ThreadPoolExecutor.AbortPolicy());@Testpublic void testBlock() { Runnable runnableOuter = () -> { try { Runnable runnableInner1 = () -> { try { TimeUnit.SECONDS.sleep(3); // 模拟比较耗时的爬虫操作 } catch (InterruptedException e) { e.printStackTrace(); } }; Future<?> submit = service.submit(runnableInner1); submit.get(); // 实际业务中,runnableInner2需要用到此处返回的参数,所以必须get Runnable runnableInner2 = () -> { try { TimeUnit.SECONDS.sleep(5); // 模拟比较耗时的爬虫操作 } catch (InterruptedException e) { e.printStackTrace(); } }; Future<?> submit2 = service.submit(runnableInner2); submit2.get(); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } }; for (int i = 0; i < 20; i++) { service.execute(runnableOuter); } ThreadPoolExecutor tpe = ((ThreadPoolExecutor) service); while (true) { System.out.println(); int queueSize = tpe.getQueue().size(); System.out.println(“当前排队线程数:” + queueSize); int activeCount = tpe.getActiveCount(); System.out.println(“当前活动线程数:” + activeCount); long completedTaskCount = tpe.getCompletedTaskCount(); System.out.println(“执行完成线程数:” + completedTaskCount); long taskCount = tpe.getTaskCount(); System.out.println(“总线程数:” + taskCount); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } }}线程池是前文的线程池,参数完全不变。线程的监控代码也一致。当我们运行这个单元测试的时候,会发现打印出来的结果一直是如下:当前排队线程数:15当前活动线程数:5执行完成线程数:0总线程数:20当前排队线程数:20当前活动线程数:5执行完成线程数:0总线程数:25当前排队线程数:20当前活动线程数:5执行完成线程数:0总线程数:25……略根本问题是 Runnable 内部还嵌套了 Runnable ,且他们都提交到了一个线程池。下面分步骤说明问题:runnableOuter 被提交到了线程池runnableOuter 开始执行,runnableInner1 被提交到线程池,对 runnableInner1 的结果进行 get,导致runnableOuter 被阻塞于此同时,更多的 runnableOuter 被提交到线程池,核心线程被 runnableOuter 和 runnableInner1 占满,多余的线程 runnableInner2 被加入 workQueue 中等待执行runnableInner2 被提交到线程池,但是因为核心线程已满,被提交到了 workQueue ,也处于阻塞状态,此时对 runnableInner2 的结果进行 get,导致 runnableOuter 被阻塞runnableOuter 被阻塞,无法释放核心线程资源,而 runnableInner2 又因为无法得到核心线程资源,只能呆在 workQueue 里,导致整个程序卡死,无法返回。(有点类似死锁,互相占有了资源,对方不释放,我也不释放)用图表示大概为:既然明白了出错的原因,那么解决起来就简单了。这个案例告诉我们,设计一个多线程程序,一定要自顶向下有一个良好的设计,然后再开始编码,不能够盲目地使用多线程、线程池,这样只会导致程序出现莫名其妙的错误。动态修改 corePoolSize & maximumPoolSize其实这个我没怎么关注过,曾经在一次面试中被问到过。很简单,java.util.concurrent.ThreadPoolExecutor提供了Setter方法,可以直接设置相关参数。按我目前的实践经验,几乎没有用到过,但是知道这个聊胜于无吧。特定的复杂场景下应该很有用。线程池和消息队列笔者在实际工程应用中,使用过多线程和消息队列处理过异步任务。很多新手工程师往往弄不清楚这两者的区别。按笔者的浅见:多线程是用来充分利用多核 CPU 以提高程序性能的一种开发技术,线程池可以维持一个队列保存等待处理的多线程任务,但是由于此队列是内存控制的,所以断电或系统故障后未执行的任务会丢失。消息队列是为消息处理而生的一门技术。其根据消费者的自身消费能力进行消费的特性使其广泛用于削峰的高并发任务处理。此外利用其去耦合的特性也可以实现代码上的解耦。消息队列大多可以对其消息进行持久化,即使断电也能够恢复未被消费的任务并继续处理。以上是笔者在学习实践之后对于多线程和消息队列的粗浅认识,初学者切莫混淆两者的作用。参考文献:Deep thinking in Java thread pool线程池,这一篇或许就够了 ...

March 6, 2019 · 2 min · jiezi

JavaScript多线程编程

远离浏览器卡顿,提高用户体验,提升代码运行效率,使用多线程编程方法。浏览器端JavaScript是以单线程的方式执行的,也就是说JavaScript和UI渲染占用同一个主线程,那就意味着,如果JavaScript进行高负载的数据处理,UI渲染就很有可能被阻断,浏览器就会出现卡顿,降低了用户体验。为此,JavaScript提供了异步操作,比如定时器(setTimeout、setInterval)事件、Ajax请求、I/O回调等。我们可以把高负载的任务使用异步处理,它们将会被放入浏览器的事件任务队列(event loop)中去,等到JavaScript运行时执行线程空闲时候,事件队列才会按照先进先出的原则被一一执行。通过类似定时器,回调函数等异步编程方式在平常的工作中已经足够,但是如果做复杂运算,这种方式的不足就逐渐体现出来,比如settimeout拿到的值并不正确,或者页面有复杂运算的时候很容易触发假死状态,异步代码会影响主线程的代码执行,异步终究还是单线程,不能从根本上解决问题。多线程(Web Worker)就应运而生,它是HTML5标准的一部分,这一规范定义了一套 API,允许一段JavaScript程序运行在主线程之外的另外一个线程中。将一些任务分配给后者运行。在主线程运行的同时,Worker(子)线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。什么是web workerworker是window对象的一个方法,就是用它来创建多线程。可以通过以下方式来检测你的浏览器是否支持workerif (window.Worker) {…… your code ……}一个worker是使用一个构造函数(Worker())创建的一个对象,这个构造函数需要传入一个的JavaScript文件,这个文件包含将在工作线程中运行的代码。类似于这样:let myWorker = new Worker(‘worker.js’);主线程和子线程的数据不是共享的,worker通过postMessage() 方法和onmessage事件进行数据通信。主线程和子线程是双向的,都可以发送和监听事件。向一个worker发送消息需要这样做(main.js):myWorker.postMessage(‘hello, world’); // 发送worker.onmessage = function (event) { // 接收 console.log(‘Received message ’ + event.data); doSomething();} postMessage所传的数据都是拷贝传递(ArrayBuffer类型除外),所以子线程也是类似传递(worker.js)addEventListener(‘message’, function (e) { postMessage(‘You said: ’ + e.data);}, false); 当子线程运行结束后,使用完毕,为了节省系统资源,可以手动关闭子线程。如果worker没有监听消息,那么当所有任务执行完毕(包括计数器)后,它就会自动关闭。// 在主线程中关闭worker.terminate();// 在子线程里线程close();Worker也提供了错误处理机制,当出错时会触发error事件。// 监听 error 事件worker.addEventListener(’error’, function (e) { console.log(‘ERROR’, e);});web worker本身很简单,但是它的限制特别多。使用的问题1、同源限制分配给Worker 线程运行的脚本文件(worker.js),必须与主线程的脚本文件(main.js)同源。这里的同源限制包括协议、域名和端口,不支持本地地址(file://)。这会带来一个问题,我们经常使用CDN来存储js文件,主线程的worker.js的域名指的是html文件所在的域,通过new Worker(url)加载的url属于CDN的域,会带来跨域的问题,实际开发中我们不会吧所有的代码都放在一个文件中让子线程加载,肯定会选择模块化开发。通过工具或库把代码合并到一个文件中,然后把子线程的代码生成一个文件url。解决方法:(1)将动态生成的脚本转换成Blob对象。(2)然后给这个Blob对象创建一个URL。(3)最后将这个创建好的URL作为地址传给Worker的构造函数。let script = ‘console.log(“hello world!”);’let workerBlob = new Blob([script], { type: “text/javascript” });let url = URL.createObjectURL(workerBlob);let worker = new Worker(url);2、访问限制Worker子线程所在的全局对象,与主线程不在同一个上下文环境,无法读取主线程所在网页的 DOM 对象,也无法使用document、window、parent这些对象,global对象的指向有变更,window需要改写成self,不能执行alert()方法和confirm()等方法,只能读取部分navigator对象内的数据。另外chrome的console.log()倒是可以使用,也支持debugger断点,增加调试的便利性。3、使用异步Worker子线程中可以使用XMLHttpRequest 对象发出 AJAX 请求,可以使用setTimeout() setInterval()方法,也可使用websocket进行持续链接。也可以通过importScripts(url)加载另外的脚本文件,但是仍然不能跨域。应用场景:1、使用专用线程进行数学运算Web Worke设计的初衷就是用来做计算耗时任务,大数据的处理,而这种计算放在worker中并不会中断前台用户的操作,避免代码卡顿带来不必要的用户体验。例如处理ajax返回的大批量数据,读取用户上传文件,计算MD5,canvas的位图的过滤,分析视频和声频文件等。worker中除了缺失了DOM和BOM操作能力以外,还是拥有非常强大的js逻辑运算处理的能力的,相当于nodejs一个级别的的运行环境。2、高频的用户交互高频的用户交互适用于根据用户的输入习惯、历史记录以及缓存等信息来协助用户完成输入的纠错、校正功能等类似场景,用户频繁输入的响应处理同样可以考虑放在web worker中执行。例如,我们可以 做一个像Word一样的应用:当用户打字时,后台立即在词典中进行查找,帮助用户自动纠错等等。3、数据的预取对于一些有大量数据的前后台交互产品,可以新开一个线程专门用来进行数据的预取和缓冲数据,worker可以用在本地web数据库的行写入和更改,长时间持续的运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断,也有利于随时响应主线程的通信。也可以配合XMLHttpRequest和websocket进行不断开的通信,实现守卫进程。兼容性总体来说,兼容性还是不错的, 移动端可以放心使用,桌面端要求不高的话,也可以使用。superWorker为了更方便快捷的使用web worker,我们封装了一个工具,可以通过模块化的方式编写运行在web worker中的脚本,避免同源策略,减少服务端发送一个额外的url请求,无需了解web worker,就像使用setTimeout一样,快速使用superWorker,提升你的编码效率和运行效率,它有以下优点:1、原生JS实现,无任何依赖库。2、简单快速,摈弃繁琐的创建文件、绑定事件,实现无侵入、无感知运行新线程的代码。3、返回Promise类型的数据,支持链式调用,清晰明了。4、支持多种方式新建worker,包括匿名函数、函数列表、文本文件、html片段、url、类,方便快捷。5、gzipped压缩后仅仅 1.2kb。使用教程:import superWorker from ‘superWorker’let worker = superWorker(function (a, b) { // 子线程中要运行的代码 return a + b;});worker.start(1, 2).then((r)=>console.log(r)); // 3用法superWorker(code, [type])参数code:运行的代码, type(非必须):代码类型,目前支持0、1、2、3、4。实现原理:先进行源代码转文件:let workerBlob = new Blob(code, { type: “text/javascript” });let url = URL.createObjectURL(workerBlob);对类型拆分,code参数支持传入匿名函数、函数列表、文本文件、url、HTML内嵌标签、类等功能,首先对传入的代码进行分类匹配,字符串化,然后进行拼接运行code = (${Function.prototype.toString.call(code)})(${exportsObjName}); 对于传入的方法,分别在主线程中的exports对象进行标记,和worker子线程中的exportsObjName对象中进行赋值。对于ES6 模块化的代码,进行过滤转译。// 处理 \nexport default function xxx(){} => exports.default = true; exportsObjName.default = function xx(){}code = code.replace(/^(\s*)export\s+default\s+/m, (s, before) => { exports.default = true; return ${before}${exportsObjName}.default=;}); 形成主线程exports和子线程exportsObjName中的方法进行一一对应。worker主线程与主线程进行通讯则是仍然需要通过postMessage方法和onmessage回调事件来进行,这个我们统一进行了双向绑定,分别对主线程和子线程执行setup。function setup(ctx, pmMethods, callbacks) { ctx.addEventListener(‘message’, ({ data }) => { // …… })} 在主线程中对worker封装了一些快捷的方法,比如关闭线程:worker.terminate = () => { URL.revokeObjectURL(url); term.call(this);}; 并把子线程拥有的方法、属性,暴露出来,方便主线程通过传递参数调用。worker.expose = methodName => { worker[i] = function () { return worker[‘call’](methodName, [].slice.call(arguments)); };}; 大致如下图:欢迎小伙伴们使用以及批评指正。有问题多多反馈,多多交流。小结对于web worker这项新技术,无论在PC还是在移动web,都很实用,腾讯新闻前端组进行了广泛的尝试,Web Worker 的实现为前端程序带来了后台计算的能力,实现了主 UI 线程与复杂计运算线程的分离,从而极大减轻了因计算量大而造成 UI 阻塞而出现的界面渲染卡、掉帧的情况,并且更大程度地利用了终端硬件的性能。superWorker能解决掉事件绑定,同源策略等繁琐的问题,它目前最大的问题在于不兼容IE9,在兼容性要求不是那么严格的地方,尽可能的使用吧! ...

March 5, 2019 · 1 min · jiezi

MySQL8.0.14 - 新特性 - InnoDB Parallel Read简述

最近的MySQL8.0.14版本增加了其第一个并行查询特性,可以支持在聚集索引上做SELECT COUNT()和check table操作。本文简单的介绍下这个特性。用法增加了一个session级别参数: innodb_parallel_read_threads要执行并行查询,需要满足如下条件(ref: row_scan_index_for_mysql)无锁查询聚集索引不是Insert…select需要参数设置为>1相关代码入口函数:row_scan_index_for_mysql parallel_select_count_star // for select count() parallel_check_table // for check tableInnoDB里实现了两种查询方式,一种是基于key的(key reader), 根据叶子节点上的值做分区,需要判断可见性;另外一种是基于page的(physical read),根据page no来做分区,无需判断可见性。目前支持的两种查询都是key reader的方式。使用如下代码创建一个reader,并调用接口函数,read()函数里的回调函数包含了如何对获取到的行数据进行处理:Key_reader reader(prebuilt->table, trx, index, prebuilt, n_threads);reader.read(func), 其中func是回调函数,用于告诉线程怎么处理得到的每一行分区并计算线程数分区入口:template <typename T, typename R>typename Reader<T, R>::Ranges Reader<T, R>::partition()流程:搜集btree的最左节点page no从root page开始向下,尝试构建子树:如果该level的page个数不足线程数,继续往下走否则,使用该level, 搜集该level的每个page的最左记录向下直到叶子节点的最左链表如上搜集到的是多条代表自上而下的page no数组,需要根据这些数组创建分区range,这里有两种创建方式:Key_reader::Ranges Key_reader::create_ranges: 基于键值创建分区找到每个链表的叶子节点的第一条记录,存储其cursor作为当前range的起点和上一个range的终点Phy_reader::Ranges Phy_reader::create_ranges:基于物理页创建分区找到每个链表的叶子节点,相邻链表的叶子节点组成一个range线程数取分区数和配置线程数的最小值启动线程启动线程各自扫描: start_parallel_load为每个分区创建context(class Reader::Ctx),加入到队列中实现了一个Lock-free的队列模型,多线程可以并发的从队列中取context: 实现细节在文件include/ut0mpmcbq.h中,对应类 class mpmc_bq, 实现思路见链接线程函数:dberr_t Reader<T, R>::worker(size_t id, Queue &ctxq, Function &f)每取一个分区,调用处理函数去遍历分区:Key_reader::traverse对于获得的每条记录,判断其可见性(共享事务对象trx_t),调用回调函数处理记录(在Key_reader::read()作为参数传递),对于select count(), 就是累加记录的计数器Phy_reader::traverse读取每条非标记删除的记录并调用回调函数处理,无需判断可见性对于异常情况,只返回最后一个context的错误码。该特性只是MySQL在并行查询的第一步,甚至定义了一些接口还没有使用,例如接口函数pread_adapter_scan_get_num_threads, 估计是给未来server层做并行查询使用的。代码里对应两个适配类:Parallel_reader_adapterParallel_partition_reader_adapter另外一个可以用到的地方是创建二级索引,我们知道InnoDB创建二级索引,是先从聚集索引读取记录,生成多个merge file,然后再做归并排序,但无论是生成merge file,还是排序,都可以做到并行化。官方也提到这是未来的一个优化点,相信不久的将来,我们就能看到MySQL更为强大的并行查询功能。ReferenceWL#11720: InnoDB: Parallel read of indexMySQL 8.0.14: A Road to Parallel Query Execution is Wide Open!本文作者:zhaiwx_yinfeng.阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

February 28, 2019 · 1 min · jiezi

多线程并发-计算机基础

CPU缓存一致性协议MESICPU在摩尔定律的指导下以每18个月翻一番的速度在发展,然而内存和硬盘的发展速度远远不及CPU。这就造成了高性能能的内存和硬盘价格及其昂贵。然而CPU的高度运算需要高速的数据。为了解决这个问题,CPU厂商在CPU中内置了少量的高速缓存以解决IO速度和CPU运算速度之间的不匹配问题。在CPU访问存储设备时,无论是存取数据抑或存取指令,都趋于聚集在一片连续的区域中,这就被称为局部性原理。时间局部性(Temporal Locality):如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。比如循环、递归、方法的反复调用等。空间局部性(Spatial Locality):如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。比如顺序执行的代码、连续创建的两个对象、数组等带有高速缓存的CPU执行计算的流程程序以及数据被加载到主内存指令和数据被加载到CPU的高速缓存CPU执行指令,把结果写到高速缓存高速缓存中的数据写回主内存多核CPU多级缓存一致性协议MESIMESI协议缓存状态缓存行(Cache line):缓存存储数据的单元。状态描述监听任务M 修改 (Modified)该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S(共享)状态之前被延迟执行。E 独享、互斥 (Exclusive)该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中。缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S(共享)状态。S 共享 (Shared)该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。I 无效 (Invalid)该Cache line无效。无注意:对于M和E状态而言总是精确的,他们在和该缓存行的真正状态是一致的,而S状态可能是非一致的。如果一个缓存将处于S状态的缓存行作废了,而另一个缓存实际上可能已经独享了该缓存行,但是该缓存却不会将该缓存行升迁为E状态,这是因为其它缓存不会广播他们作废掉该缓存行的通知,同样由于缓存并没有保存该缓存行的copy的数量,因此(即使有这种通知)也没有办法确定自己是否已经独享了该缓存行。从上面的意义看来E状态是一种投机性的优化:如果一个CPU想修改一个处于S状态的缓存行,总线事务需要将所有该缓存行的copy变成invalid状态,而修改E状态的缓存不需要使用总线事务。cpu多级缓存 - 乱序执行优化处理器为提高运算速度儿做出违背代码原有顺序的优化。在单核处理器时代处理器的乱序执行优化不会影响执行结果。在多核处理中,某个核心执行写入操作时,将某个标志当做写入完成,进行重排优化,可能会先执行标志指令导致其他核心以为改核心已经执行完成写入操作。从而拿到错误的值。java内存模型(java memory model, JMM)堆heap堆是运行时确定的内存,由java GC来维护大小,优点是可以动态的确定大小,缺点是运行时动态确定内存所以速度相对栈小一点。对象存放在堆上。静态变量跟随类一起存放在堆上。栈stack栈内存的速度相对堆内存更快,仅次于寄存器,缺点是大小必须是编译期确定的。缺乏一定的灵活性,存放一些基本的数据变量(int double。。。)java内存要求本地变量(Local Variable),调用栈必须存放在线程栈(Thead Stack)中。本地变量可能存放的是对象的引用。当两个线程同时引用一个对象时,那么这两个线程的本地引用存放的是这个对象的私有拷贝。硬件内存模型如图硬件内存模型和java内存模型的对应模型如图:java内存抽象模型结构看图,本地内存:本地内存是java抽象的概念,涵盖了缓存,写缓存区,寄存器,其他硬件和编译器优化。本地内存储存了共享变量的副本,从硬件的角度上讲主内存就是硬件内存,但是为了获取更好的速度,java可能会将数据存储在寄存器或者高速缓存区。如果线程要通信必须要经过主内存,流程是先在主内存中获取共享变量,存储在本地内存中经由进程计算,然后刷新至主内存,再经由其他线程访问。java内存模型- 同步操作与规则lock和Unlack:作用在主内存上只有在Unlock的情况下内存才可以被其他线程锁定。Read:作用在主内存上,把主内存中的变量输送在工作内存中。Load:作用工作内存中,把主内存中的值放入到工作内存副本中。use:作用于工作内存,把数据给执行引擎。每当执行器需要使用到变量时或者执行字节码指令时会执行这个操作。assign:赋值,在执行赋值操作时执行,将执行引擎中的值赋值给工作内存。store:存储,把工作内存中的值传递到主内存中。write:写入,将工作内存中的值写入到主内存中。下面介绍一下规则,规则是用来限制每一步是如何操作的。不允许read和load、store和write单一出现,因为他们是一个连贯的操作。而且必须是按顺序执行的。load必须是read之后,write必须是store之后,但是不一定是连续操作,在他们之间可以插入其他的指令。不允许线程丢弃assign操作,也就是说执行完了之后必须放入工作内存中。不允许线程不经过Assign操作直接把数据给主内存。一个新的变量只能在主内存中诞生。一个变量只允许一个线程对其lack操作,但是可以被一个线程lack多次,lack多次之后只有执行相同次数的unlack才能被解锁。如果一个变量执行了lack操作之后将会清楚工作内存中该变量的值。执行引擎在使用变量时需要重新执行read-load-use等操作。如果没有执行一个lack操作的变量不能执行unlack操作。或者被其他线程执行了lack操作的线程也不能被改线程执行unlack。多线程并发的优势和缺点

February 20, 2019 · 1 min · jiezi

Java多线程001——一图读懂线程与进程

本博客 猫叔的博客,转载请申明出处前言本系列将由浅入深,学习Java并发多线程。一图读懂线程与进程1、一个进程可以包含一个或多个线程。(其实你经常听到“多线程”,没有听过“多进程”嘛)2、进程存在堆和方法区3、线程存在程序计数器和栈4、堆占最大内存,其为创建时分配的,是多线程共享的,主要存放new创建的对象5、方法区也是多线程共享的,主要存放类、常量、静态变量6、CPU的基本执行单位是线程(注意!不是进程)7、由此,线程需要一个程序计数器记录当前线程要执行的指令地址8、当CPU的时间片用完,让出后记录当前执行地址,下次继续执行(时间片轮询)9、只有执行Java代码时pc技数器记录的才是下一条指令的地址,执行native方法,则记录的是undefined地址10、线程中的栈,只要存储线程局部变量、调用栈帧栈帧:C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。公众号:Java猫说现架构设计(码农)兼创业技术顾问,不羁平庸,热爱开源,杂谈程序人生与不定期干货。

February 19, 2019 · 1 min · jiezi

Java多线程编程实战:模拟大量数据同步

背景最近对于 Java 多线程做了一段时间的学习,笔者一直认为,学习东西就是要应用到实际的业务需求中的。否则要么无法深入理解,要么硬生生地套用技术只是达到炫技的效果。不过笔者仍旧认为自己对于多线程掌握不够熟练,不敢轻易应用到生产代码中。这就按照平时工作中遇到的实际问题,脑补了一个很可能存在的业务场景:已知某公司管理着 1000 个微信服务号,每个服务号有 1w ~ 50w 粉丝不等。假设该公司每天都需要将所有微信服务号的粉丝数据通过调用微信 API 的方式更新到本地数据库。需求分析对此需求进行分析,主要存在以下问题:单个服务号获取粉丝 id,只能每次 1w 按顺序拉取微信的 API 对于服务商的并发请求数量有限制单个服务号获取粉丝 id,只能每次 1w 按顺序拉取。这个问题决定了单个公众号在拉取粉丝 id 上,无法分配给多个线程执行。微信的 API 对于服务商的并发请求数量有限制。这点最容易被忽略,如果我们同时有过多的请求,则会导致接口被封禁。这里可以通过信号量来控制同时执行的线程数量。为了尽快完成数据同步,根据实际情况:整个数据同步可分为读数据和写数据两个部分。读数据是通过 API 获取,走网络 IO,速度较慢;写数据是写到数据库,速度较快。所以得出结论:需要分配较多的线程进行读数据,较少的线程进行写数据。设计要点首先,我们需要确定开启多少个线程(在生产中往往是使用线程池),线程数量需要根据服务器性能来决定,这里我们定为 40 个读取数据线程(将 1000 个公众号分为 40 份,分别在 40 个线程中执行),1个写入数据线程。(具体开多少个线程,取决于线程池的容量,以及可以分配给此业务的数量。具体的数字需要根据实际情况测试得出,比服务器阈值低一些较好。当然,配置允许范围内越大越好)其次,考虑到微信对于 API 并发请求的限制,需要限制同时执行的线程数,使用java.util.concurrent.Semaphore进行控制,这里我们限制为 20 个(具体的信号量凭证数,取决于同一时间能够执行的线程,跟 API 限制,服务器性能有关)。然后,我们需要知道数据何时读取、写入完毕,以控制程序逻辑以及终止程序,这里我们使用java.util.concurrent.CountDownLatch进行控制。最后,我们需要一个数据结构,用来在多个线程中共享处理的数据,此处同步数据的场景非常适合使用队列,这里我们使用线程安全的java.util.concurrent.ConcurrentLinkedQueue来进行处理。(需要注意的是,在实际开发中,队列不能够无限制地增长,这将会很快消耗掉内存,我们需要根据实际情况对队列长度做控制。例如,可以通过控制读取线程数和写入线程数的比例来控制队列的长度)模拟代码由于本文重点关注多线程的使用,模拟代码只体现多线程操作的方法。代码里添加了大量的注释,方便各位读者阅读理解。JDK:1.8import java.util.Arrays;import java.util.List;import java.util.Queue;import java.util.concurrent.ConcurrentLinkedQueue;import java.util.concurrent.CountDownLatch;import java.util.concurrent.Semaphore;import java.util.concurrent.TimeUnit;/** * N个线程向队列添加数据 * 一个线程消费队列数据 */public class QueueTest { private static List<String> data = Arrays.asList(“a”, “b”, “c”, “d”, “e”); private static final int OFFER_COUNT = 40; // 开启的线程数量 private static Semaphore semaphore = new Semaphore(20); // 同一时间执行的线程数量(大多用于控制API调用次数或数据库查询连接数) public static void main(String[] args) throws InterruptedException { Queue<String> queue = new ConcurrentLinkedQueue<>(); // 处理队列,需要处理的数据,放置到此队列中 CountDownLatch offerLatch = new CountDownLatch(OFFER_COUNT); // offer线程latch,每完成一个,latch减一,lacth的count为0时表示offer处理完毕 CountDownLatch pollLatch = new CountDownLatch(1); // poll线程latch,latch的count为0时,表示poll处理完毕 Runnable offerRunnable = () -> { try { semaphore.acquire(); // 信号量控制 } catch (InterruptedException e) { e.printStackTrace(); } try { for (String datum : data) { queue.offer(datum); TimeUnit.SECONDS.sleep(2); // 模拟取数据很慢的情况 } } catch (InterruptedException e) { e.printStackTrace(); } finally { // 在finally中执行latch.countDown()以及信号量释放,避免因异常导致没有正常释放 offerLatch.countDown(); semaphore.release(); } }; Runnable pollRunnable = () -> { int count = 0; try { while (offerLatch.getCount() > 0 || queue.size() > 0) { // 只要offer的latch未执行完,或queue仍旧有数据,则继续循环 String poll = queue.poll(); if (poll != null) { System.out.println(poll); count++; } // 无论是否poll到数据,均暂停一小段时间,可降低CPU消耗 TimeUnit.MILLISECONDS.sleep(100); } System.out.println(“total count:” + count); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 在finally中执行latch.countDown(),避免因异常导致没有正常释放 pollLatch.countDown(); } }; // 启动线程(生产环境中建议使用线程池) new Thread(pollRunnable).start(); // 启动一个poll线程 for (int i = 0; i < OFFER_COUNT; i++) { new Thread(offerRunnable).start(); } // 模拟取数据很慢,需要开启40个线程处理 // latch等待,会block主线程直到latch的count为0 offerLatch.await(); pollLatch.await(); System.out.println("===the end==="); }}到这里,本文结束。以上是笔者脑补的一个常见需求的解决方案。注意:多线程编程对实际环境和需求有很大的依赖,需要根据实际的需求情况对各个参数做调整。实际在使用中,需要尽量模拟生产环境的数据情况来进行测试,对服务器执行期间的并发数,CPU、内存、网络 IO、磁盘 IO 做好观察。并适当地调低并发数,以给服务器留有处理其他请求的余量。 ...

February 14, 2019 · 2 min · jiezi

Java多线程-Callable和Future

Callable和Future出现的原因创建线程的2种方式,一种是直接继承Thread,另外一种就是实现Runnable接口。 这2种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。 如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。自从Java 1.5开始,就提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。Callable和Future介绍Callable接口代表一段可以调用并返回结果的代码;Future接口表示异步任务,是还没有完成的任务给出的未来结果。所以说Callable用于产生结果,Future用于获取结果。Callable接口使用泛型去定义它的返回类型。Executors类提供了一些有用的方法在线程池中执行Callable内的任务。由于Callable任务是并行的(并行就是整体看上去是并行的,其实在某个时间点只有一个线程在执行),我们必须等待它返回的结果。 java.util.concurrent.Future对象为我们解决了这个问题。在线程池提交Callable任务后返回了一个Future对象,使用它可以知道Callable任务的状态和得到Callable返回的执行结果。Future提供了get()方法让我们可以等待Callable结束并获取它的执行结果。Callable与Runnablejava.lang.Runnable吧,它是一个接口,在它里面只声明了一个run()方法:public interface Runnable { public abstract void run();} 由于run()方法返回值为void类型,所以在执行完任务之后无法返回任何结果。 Callable位于java.util.concurrent包下,它也是一个接口,在它里面也只声明了一个方法,只不过这个方法叫做call():public interface Callable<V> { /** * Computes a result, or throws an exception if unable to do so. * * @return computed result * @throws Exception if unable to compute a result */ V call() throws Exception;}这是一个泛型接口,call()函数返回的类型就是传递进来的V类型。Callable的使用一般情况下是配合ExecutorService来使用的,在ExecutorService接口中声明了若干个submit方法的重载版本。<T> Future<T> submit(Callable<T> task);<T> Future<T> submit(Runnable task, T result);Future<?> submit(Runnable task);第一个submit方法里面的参数类型就是Callable。暂时只需要知道Callable一般是和ExecutorService配合来使用的,具体的使用方法讲在后面讲述。一般情况下我们使用第一个submit方法和第三个submit方法,第二个submit方法很少使用。FutureFuture就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。Future类位于java.util.concurrent包下,它是一个接口:public interface Future<V> { boolean cancel(boolean mayInterruptIfRunning); boolean isCancelled(); boolean isDone(); V get() throws InterruptedException, ExecutionException; V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;}在Future接口中声明了5个方法,下面依次解释每个方法的作用cancel方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。isCancelled方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。isDone方法表示任务是否已经完成,若任务完成,则返回true;get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null。Future提供了三种功能:判断任务是否完成;能够中断任务;能够获取任务执行结果。因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的FutureTask。FutureTaskFutureTask实现了RunnableFuture接口,这个接口的定义如下:public interface RunnableFuture<V> extends Runnable, Future<V> { void run(); }可以看到这个接口实现了Runnable和Future接口,接口中的具体实现由FutureTask来实现。这个类的两个构造方法如下 :public FutureTask(Callable<V> callable) { if (callable == null) throw new NullPointerException(); sync = new Sync(callable); } public FutureTask(Runnable runnable, V result) { sync = new Sync(Executors.callable(runnable, result)); }如上提供了两个构造函数,一个以Callable为参数,另外一个以Runnable为参数。这些类之间的关联对于任务建模的办法非常灵活,允许你基于FutureTask的Runnable特性(因为它实现了Runnable接口),把任务写成Callable,然后封装进一个由执行者调度并在必要时可以取消的FutureTask。FutureTask可以由执行者调度,这一点很关键。它对外提供的方法基本上就是Future和Runnable接口的组合:get()、cancel、isDone()、isCancelled()和run(),而run()方法通常都是由执行者调用,我们基本上不需要直接调用它。FutureTask的例子public class MyCallable implements Callable<String> { private long waitTime; public MyCallable(int timeInMillis){ this.waitTime=timeInMillis; } @Override public String call() throws Exception { Thread.sleep(waitTime); //return the thread name executing this callable task return Thread.currentThread().getName(); } }public class FutureTaskExample { public static void main(String[] args) { MyCallable callable1 = new MyCallable(1000); // 要执行的任务 MyCallable callable2 = new MyCallable(2000); FutureTask<String> futureTask1 = new FutureTask<String>(callable1);// 将Callable写的任务封装到一个由执行者调度的FutureTask对象 FutureTask<String> futureTask2 = new FutureTask<String>(callable2); ExecutorService executor = Executors.newFixedThreadPool(2); // 创建线程池并返回ExecutorService实例 executor.execute(futureTask1); // 执行任务 executor.execute(futureTask2); while (true) { try { if(futureTask1.isDone() && futureTask2.isDone()){// 两个任务都完成 System.out.println(“Done”); executor.shutdown(); // 关闭线程池和服务 return; } if(!futureTask1.isDone()){ // 任务1没有完成,会等待,直到任务完成 System.out.println(“FutureTask1 output="+futureTask1.get()); } System.out.println(“Waiting for FutureTask2 to complete”); String s = futureTask2.get(200L, TimeUnit.MILLISECONDS); if(s !=null){ System.out.println(“FutureTask2 output="+s); } } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); }catch(TimeoutException e){ //do nothing } } } } ...

February 11, 2019 · 2 min · jiezi

OC基础-单例的实现 & 提醒自己注意多线程问题

做客户端开发应当时刻考虑多线程问题。我最初是做前端开发的,在这方面考虑得往往不够。谨记。单例的常见写法单例的常见写法其实就两种1. 依赖锁+ (id)sharedInstance { static testClass *sharedInstance = nil; @synchronized(self) { if (!sharedInstance) { sharedInstance = [[self alloc] init]; } } return sharedInstance; }2. 依赖dispatch_once+ (id)sharedInstance { static testClass *sharedInstance = nil; static dispatch_once_t once; dispatch_once(&once, ^{ sharedInstance = [[self alloc] init]; }); return sharedInstance; }dispatch_once的写法更推荐一些。一方面是性能上好一点,另一方面是语义上更直观。once,执行一次嘛。不管是用锁还是dispatch_once,本质上都是为了避免单例创建过程出现线程安全问题。更进一步,我们经常会有懒加载某些属性的写法:- (id<InterfaceEngineA>)engineA{ if (_engineA == nil) { _engineA = [EngineA new]; } return _engineA;}其实跟单例的实现是类似的,这种时候要格外注意线程安全问题。如果存在多线程场景,一定要做好保护- (id<InterfaceEngineA>)engineA{ @synchronized(self) { if (_engineA == nil) { _engineA = [EngineA new]; } } return _engineA;}一些废话多线程问题的表现可能是各种各样难以预料的。这里我遇到的是,_engineA在多线程场景下小概率被重复创建,其实例1在init时注册了网络层命令字cmd1的回包,而这个网络层框架的实现是,只接受第一个注册这一命令字的对象。导致实例2注册失败。后面调用实例2发送请求,回包都被实例1接收了。从日志上看,一切都挺正常的。但是下次取数据就是取不到。这个bug第一次提过来的时候,没分析出根本原因,只在表面上做了保护。结果第二次提过来才真正改掉。丢人呐。还是要好好学习才是。 ...

January 24, 2019 · 1 min · jiezi

JavaScript多线程-Web Worker

JS组成ECMAScriptECMAScript规定了JavaScript脚本的核心语法,如 数据类型、关键字、保留字、运算符、对象和语句等,它不属于任何浏览器。Document Object Model文档对象模型(DOM)将web页面与到脚本或编程语言连接起来。通常是指 JavaScript,但将HTML、SVG或XML文档建模为对象并不是JavaScript语言的一部分。DOM模型用一个逻辑树来表示一个文档,树的每个分支的终点都是一个节点(node),每个节点都包含着对象(objects)。DOM的方法(methods)让你可以用特定方式操作这个树,用这些方法你可以改变文档的结构、样式或者内容。节点可以关联上事件处理器,一旦某一事件被触发了,那些事件处理器就会被执行。Browser Object Model浏览器对象模型(BOM),是用于描述这种对象与对象之间层次关系的模型,浏览器对象模型提供了独立于内容的、可以与浏览器窗口进行互动的对象结构。BOM由多个对象组成,其中代表浏览器窗口的Window对象是BOM的顶层对象,其他对象都是该对象的子对象.线程与进程进程(Process)是系统资源分配和调度的单元。一个运行着的程序就对应了一个进程。一个进程包括了运行中的程序和程序所使用到的内存和系统资源。如果是单核CPU的话,在同一时间内,有且只有一个进程在运行。但是,单核CPU也能实现多任务同时运行,比如你边听网易云音乐的每日推荐歌曲,边在网易有道云笔记上写博文。这算开了两个进程(多进程),那运行的机制就是一会儿播放一下歌,一会儿响应一下你的打字,但由于CPU切换的速度很快,你根本感觉不到,以至于你认为这两个进程是在同时运行的。进程之间是资源隔离的。那线程(Thread)是什么?线程是进程下的执行者,一个进程至少会开启一个线程(主线程),也可以开启多个线程。比如网易云音乐一边播放音频,一边显示歌词。多进程的运行其实也就是通过进程中的线程来执行的。一个进程下的线程是可以共享资源的,所以在多线程的情况下,需要特别注意对临界资源的访问控制.浏览器目前最为流行的浏览器为:`Chrome,IE,Safari,FireFox,Opera.一个浏览器通常由以下几个常驻的线程:渲染引擎线程:负责页面的渲染JS引擎线程:负责JS的解析和执行定时触发器线程:处理定时事件,比如setTimeout, setInterval事件触发线程:处理DOM事件异步http请求线程:处理http请求需要注意的是,渲染线程和JS引擎线程是不能同时进行的。渲染线程在执行任务的时候,JS引擎线程会被挂起。因为JS可以操作DOM,若在渲染中JS处理了DOM,浏览器可能就懵逼了。Web Worker简介Web Worker (工作线程) 是 HTML5 中提出的概念,Web Workers 使得一个Web应用程序可以在与主执行线程分离的后台线程中运行一个脚本操作。这样做的好处是可以在一个单独的线程中执行费时的处理任务,从而允许主(通常是UI)线程运行而不被阻塞/放慢.Web Worker可以分为一下几类:专用线程(Dedicated Worker)专用线程仅能被创建它的脚本所使用(一个专用线程对应一个主线程)共享线程(Shared Worker)共享线程能够在不同的脚本中使用(一个共享线程对应多个主线程)服务工作线程(Service Workers)注册在指定源和路径下的事件驱动worker, 可以控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源.Chrome Workers一种仅适用于firefox的worker.Aduio Workers可以在网络worker上下文中直接完成脚本化音频处理浏览器兼容性可以通过caniuse 查看兼容性Dedicated Worker 兼容性Shared Worker 兼容性使用场景懒加载文本分析流媒体数据处理canvas图形绘制图像处理…限制同源限制无法访问DOM有自己的上下文,无法使用Window对象workerType上下文Dedicated WorkerDedicatedWorkerGlobalScopeShared WorkerSharedWorkerGlobalScope创建线程检查浏览器是否支持if (window.Worker) { // some code}专用线程@params {String} url 表示worker将执行的脚本的URL,必须遵守同源策略@params {Object} [options] 创建对象实例时设置的选项属性的对象@params {Object} [options.type]@params {Object} [options.name]@params {Object} [options.credentials]@returns 创建的workerconst myWorker = new Worker(url[, options]);示例// main.jsconst myDedicatedWorker = new Worker(’./dedicated_worker/worker.js’, { name: ‘dedicatedWorker’ });// worker.jsconsole.log(‘success’);共享线程@params {String} url 表示worker将执行的脚本的URL,必须遵守同源策略@params {Object} [options] 创建对象实例时设置的选项属性的对象@params {Object} [options.type]@params {Object} [options.name]@params {Object} [options.credentials]@returns 创建的workerconst myWorker = new SharedWorker(url[, options]);示例// main.jsconst mySharedWorker = new SharedWorker(’./shared_worker/worker.js’, { name: ‘sharedWorker’ });// worker.jsconsole.log(‘success’);线程通信发送信息@params {Object} message 传递的数据对象@params {Object} [options] 一个可选的Transferable对象的数组,用于传递所有权.如果一个对象的所有权被转移,在发送它的上下文中将变为不可用(中止),并且只有在它被发送到的worker中可用。myWorker.postMessage(message, transferList);接收信息myWorker.onmessage = function(event) { const data = event.data; // 接收到的消息数据}专用线程示例// main.jsconst myWorker = new Worker(‘worker.js’);myWorker.postMessage([10, 20]);myWorker.onmessage = function (event) { console.log(event.data);}// worker.jsonmessage(event) { console.log(event.data); postMessage(event.data[1] - event.data[0]);}共享线程示例// main.jsconst myWorker = new SharedWorker(‘worker.js’);myWorker.port.start();myWorker.port.postMessage([10, 20]);myWorker.port.onmessage = function (event) { console.log(event.data);}// worker.jsconnect(event) { const port = event.port[0]; port.onmessage(event) { port.postMessage(event.data[1] - event.data[0]); }}在SharedWorker的使用中,我们发现对于SharedWorker实例对象,我们需要通过port属性来访问到主要方法;同时在Worker脚本中,多了个全局的 connect()函数,同时在函数中也需要去获取一个port对象来进行启动以及操作;这是因为,多页页面共享一个SharedWorker线程时,在线程中需要去判断和区,分来自不同页面的信息,这是最主要的区别和原因。在Worker线程中,self和this都代表子线程的全局对象。对于监听 message事件,以下的四种写法是等同的。// 写法 1self.addEventListener(‘message’, function (e) { // …})// 写法 2this.addEventListener(‘message’, function (e) { // …})// 写法 3addEventListener(‘message’, function (e) { // …})// 写法 4onmessage = function (e) { // …}示例关闭线程myWorker.terminate(); // 主线程中使用close(); worker线程中使用(推荐)错误处理// 主线程myWorker.onerror = function(event) { const lineno = event.lineno; // 出错的脚本名称 const filename = event.filename; // 发生错误的行号 const message = event.message; // 对错误的描述} // 不能进行反序列化时触发myWorker.onmessageerror = function() { … } // 专用线程myWorker.port.onmessageerror function() {…} // 共享线程外部加载脚本提供importScript()方法,导入一条或者以上脚本到当前worker的作用域里.每个脚本中的全局对象都能够被 worker 使用.importScript(‘first.js’, ‘second.js’);子进程Worker可以生成子进程存在同源限制子Worker中的URL相对于父级Woker所在位置进行解析嵌入Worker<script type=“text/js-worker”> onmessage = (event) => { postMessage(event.data + 1); }</script><script> const workerScript = document.querySelector(‘script[type=“text/js-worker”]’); const blob = new Bolb(workerScript, { type: ’text/javascript’ }); const myWorker = new Worker(window.URL.createObjectURL(blob)); myWorker.postMessage(1); myWorker.onmessage = (event) => { console.log(‘来自子线程消息:’, event.data); }</script>结构化克隆算法结构化克隆算法是由HTML5规范定义的用于复制复杂JavaScript对象的算法。通过来自Workers的postMessage()或使用IndexedDB存储对象时在内部使用。它通过递归输入对象来构建克隆,同时保持先前访问过的引用的映射,以避免无限遍历循环。这一过程可以理解为,在发送方使用类似JSON.stringfy()的方法将参数序列化,在接收方采用类JSON.parse()的方法反序列化。Error以及Function对象是不能被结构化克隆算法复制的;如果你尝试这样子去做,这会导致抛出DATA_CLONE_ERR的异常无法克隆DOM对象的某些特定参数也不会被保留RegExp对象的lastIndex字段不会被保留属性描述符,setters 以及 getters(以及其他类似元数据的功能)同样不会被复制。例如,如果一个对象用属性描述符标记为 read-only,它将会被复制为 read-write,因为这是默认的情况下原形链上的属性也不会被追踪以及复制Web Worker中可以使用的函数和类时间相关clearInterval()clearTimeout()setInterval()setTimeoutWorker 相关importScripts()close()postMessage()存储相关CacheIndexedDB网络相关FetchWebSocketXMLHttpRequest更多 ...

January 24, 2019 · 2 min · jiezi

在Java中怎样实现多线程?Java线程的四种状态

一、在java中怎样实现多线程?extends Threadimplement Runnable方法一:继承 Thread 类,覆盖方法 run(),我们在创建的 Thread 类的子类中重写 run() ,加入线程所要执行的代码即可。下面是一个例子:public class MyThread extends Thread { int count= 1, number; public MyThread(int num) { number = num; System.out.println (“创建线程 " + number); } public void run() { while(true) { System.out.println (“线程 " + number + “:计数 " + count); if(++count== 6) return; } } public static void main(String args[]) { for(int i = 0;i 〈 5; i++) new MyThread(i+1).start(); } }*上海尚学堂Java培训 shsxt.com 这种方法简单明了,符合大家的习惯,但是,它也有一个很大的缺点,那就是如果我们的类已经从一个类继承(如小程序必须继承自 Applet 类),则无法再继承 Thread 类,这时如果我们又不想建立一个新的类,应该怎么办呢? 我们不妨来探索一种新的方法:我们不创建Thread类的子类,而是直接使用它,那么我们只能将我们的方法作为参数传递给 Thread 类的实例,有点类似回调函数。但是 Java 没有指针,我们只能传递一个包含这个方法的类的实例。 那么如何限制这个类必须包含这一方法呢?当然是使用接口!(虽然抽象类也可满足,但是需要继承,而我们之所以要采用这种新方法,不就是为了避免继承带来的限制吗?) Java 提供了接口 java.lang.Runnable 来支持这种方法。方法二:实现 Runnable 接口 Runnable接口只有一个方法run(),我们声明自己的类实现Runnable接口并提供这一方法,将我们的线程代码写入其中,就完成了这一部分的任务。但是Runnable接口并没有任何对线程的支持,我们还必须创建Thread类的实例,这一点通过Thread类的构造函数 public Thread(Runnable target);来实现。下面是一个例子:public class MyThread implements Runnable { int count= 1, number; public MyThread(int num) { number = num; System.out.println(“创建线程 " + number); } public void run() { while(true) { System.out.println (“线程 " + number + “:计数 " + count); if(++count== 6) return; } } public static void main(String args[]) { for(int i = 0; i 〈 5;i++) new Thread(new MyThread(i+1)).start(); } }**上海尚学堂Java培训 shsxt.com 严格地说,创建Thread子类的实例也是可行的,但是必须注意的是,该子类必须没有覆盖 Thread 类的 run 方法,否则该线程执行的将是子类的 run 方法,而不是我们用以实现Runnable 接口的类的 run 方法,对此大家不妨试验一下。 使用 Runnable 接口来实现多线程使得我们能够在一个类中包容所有的代码,有利于封装,它的缺点在于,我们只能使用一套代码,若想创建多个线程并使各个线程执行不同的代码,则仍必须额外创建类,如果这样的话,在大多数情况下也许还不如直接用多个类分别继承 Thread 来得紧凑。 综上所述,两种方法各有千秋,大家可以灵活运用。 下面让我们一起来研究一下多线程使用中的一些问题。 二、线程的四种状态 1. 新状态:线程已被创建但尚未执行(start() 尚未被调用)。 2. 可执行状态:线程可以执行,虽然不一定正在执行。CPU 时间随时可能被分配给该线程,从而使得它执行。 3. 死亡状态:正常情况下 run() 返回使得线程死亡。调用 stop()或 destroy() 亦有同样效果,但是不被推荐,前者会产生异常,后者是强制终止,不会释放锁。 4. 阻塞状态:线程不会被分配 CPU 时间,无法执行。 三、线程的优先级 线程的优先级代表该线程的重要程度,当有多个线程同时处于可执行状态并等待获得 CPU 时间时,线程调度系统根据各个线程的优先级来决定给谁分配 CPU 时间,优先级高的线程有更大的机会获得 CPU 时间,优先级低的线程也不是没有机会,只是机会要小一些罢了。关于Java线程就介绍到这,更多Java学习资料教程请点 上海尚学堂java学习视频 ...

January 24, 2019 · 1 min · jiezi

阿里重磅开源首款自研科学计算引擎Mars,揭秘超大规模科学计算

摘要: 由阿里巴巴统一大数据计算平台MaxCompute研发团队,历经1年多研发,打破大数据、科学计算领域边界,完成第一个版本并开源。 Mars,一个基于张量的统一分布式计算框架。使用 Mars 进行科学计算,不仅使得完成大规模科学计算任务从MapReduce实现上千行代码降低到Mars数行代码,更在性能上有大幅提升。日前,阿里巴巴正式对外发布了分布式科学计算引擎 Mars 的开源代码地址,开发者们可以在pypi上自主下载安装,或在Github上获取源代码并参与开发。此前,早在2018年9月的杭州云栖大会上,阿里巴巴就公布了这项开源计划。Mars 突破了现有大数据计算引擎的关系代数为主的计算模型,将分布式技术引入科学计算/数值计算领域,极大地扩展了科学计算的计算规模和效率。目前已应用于阿里巴巴及其云上客户的业务和生产场景。本文将为大家详细介绍Mars的设计初衷和技术架构。*概述科学计算即数值计算,是指应用计算机处理科学研究和工程技术中所遇到的数学计算问题。比如图像处理、机器学习、深度学习等很多领域都会用到科学计算。有很多语言和库都提供了科学计算工具。这其中,Numpy以其简洁易用的语法和强大的性能成为佼佼者,并以此为基础形成了庞大的技术栈。(下图所示)Numpy的核心概念多维数组是各种上层工具的基础。多维数组也被称为张量,相较于二维表/矩阵,张量具有更强大的表达能力。因此,现在流行的深度学习框架也都广泛的基于张量的数据结构。随着机器学习/深度学习的热潮,张量的概念已逐渐为人所熟知,对张量进行通用计算的规模需求也与日俱增。但现实是如Numpy这样优秀的科学计算库仍旧停留在单机时代,无法突破规模瓶颈。当下流行的分布式计算引擎也并非为科学计算而生,上层接口不匹配导致科学计算任务很难用传统的SQL/MapReduce编写,执行引擎本身没有针对科学计算优化更使得计算效率难以令人满意。基于以上科学计算现状,由阿里巴巴统一大数据计算平台MaxCompute研发团队,历经1年多研发,打破大数据、科学计算领域边界,完成第一个版本并开源。 Mars,一个基于张量的统一分布式计算框架。使用 Mars 进行科学计算,不仅使得完成大规模科学计算任务从MapReduce实现上千行代码降低到Mars数行代码,更在性能上有大幅提升。目前,Mars 实现了 tensor 的部分,即numpy 分布式化, 实现了 70% 常见的 numpy 接口。后续,在 Mars 0.2 的版本中, 正在将 pandas 分布式化,即将提供完全兼容 pandas 的接口,以构建整个生态。 Mars作为新一代超大规模科学计算引擎,不仅普惠科学计算进入分布式时代,更让大数据进行高效的科学计算成为可能。Mars的核心能力符合使用习惯的接口Mars 通过 tensor 模块提供兼容 Numpy 的接口,用户可以将已有的基于 Numpy 编写的代码,只需替换 import,就可将代码逻辑移植到 Mars,并直接获得比原来大数万倍规模,同时处理能力提高数十倍的能力。目前,Mars 实现了大约 70% 的常见 Numpy 接口。充分利用GPU加速除此之外,Mars 还扩展了 Numpy,充分利用了GPU在科学计算领域的已有成果。创建张量时,通过指定 gpu=True 就可以让后续计算在GPU上执行。比如:a = mt.random.rand(1000, 2000, gpu=True) # 指定在 GPU 上创建(a + 1).sum(axis=1).execute()稀疏矩阵Mars 还支持二维稀疏矩阵,创建稀疏矩阵的时候,通过指定 sparse=True 即可。以eye 接口为例,它创建了一个单位对角矩阵,这个矩阵只有对角线上有值,其他位置上都是 0,所以,我们可以用稀疏的方式存储。a = mt.eye(1000, sparse=True) # 指定创建稀疏矩阵(a + 1).sum(axis=1).execute()系统设计接下来介绍 Mars 的系统设计,让大家了解 Mars 是如何让科学计算任务自动并行化并拥有强大的性能。分而治之—tileMars 通常对科学计算任务采用分而治之的方式。给定一个张量,Mars 会自动将其在各个维度上切分成小的 Chunk 来分别处理。对于 Mars 实现的所有的算子,都支持自动切分任务并行。这个自动切分的过程在Mars里被称为 tile。比如,给定一个 1000 2000 的张量,如果每个维度上的 chunk 大小为 500,那么这个张量就会被 tile 成 2 4 一共 8 个 chunk。对于后续的算子,比如加法(Add)和求和(SUM),也都会自动执行 tile 操作。一个张量的运算的 tile 过程如下图所示。延迟执行和 Fusion 优化目前 Mars 编写的代码需要显式调用 execute 触发,这是基于 Mars 的延迟执行机制。用户在写中间代码时,并不会需要任何的实际数据计算。这样的好处是可以对中间过程做更多优化,让整个任务的执行更优。目前 Mars 里主要用到了 fusion 优化,即把多个操作合并成一个执行。对于前面一个图的例子,在 tile 完成之后,Mars 会对细粒度的 Chunk 级别图进行 fusion 优化,比如8个 RAND+ADD+SUM,每个可以被分别合并成一个节点,一方面可以通过调用如 numexpr 库来生成加速代码,另一方面,减少实际运行节点的数量也可以有效减少调度执行图的开销。多种调度方式Mars 支持多种调度方式:| 多线程模式:Mars 可以使用多线程来在本地调度执行 Chunk 级别的图。对于 Numpy 来说,大部分算子都是使用单线程执行,仅使用这种调度方式,也可以使得 Mars 在单机即可获得 tile 化的执行图的能力,突破 Numpy 的单机内存限制,同时充分利用单机所有 CPU/GPU 资源,获得比 Numpy 快数倍的性能。| 单机集群模式: Mars 可以在单机启动整个分布式运行时,利用多进程来加速任务的执行;这种模式适合模拟面向分布式环境的开发调试。| 分布式 : Mars 可以启动一个或者多个 scheduler,以及多个 worker,scheduler 会调度 Chunk 级别的算子到各个 worker 去执行。下图是 Mars 分布式的执行架构:Mars 分布式执行时会启动多个 scheduler 和 多个 worker,图中是3个 scheduler 和5个 worker,这些 scheduler 组成一致性哈希环。用户在客户端显式或隐式创建一个 session,会根据一致性哈希在其中一个 scheduler 上分配 SessionActor,然后用户通过 execute 提交了一个张量的计算,会创建 GraphActor 来管理这个张量的执行,这个张量会在 GraphActor 中被 tile 成 chunk 级别的图。这里假设有3个 chunk,那么会在 scheduler 上创建3个 OperandActor 分别对应。这些 OperandActor 会根据自己的依赖是否完成、以及集群资源是否足够来提交到各个 worker 上执行。在所有 OperandActor 都完成后会通知 GraphActor 任务完成,然后客户端就可以拉取数据来展示或者绘图。向内和向外伸缩Mars 灵活的 tile 化执行图配合多种调度模式,可以使得相同的 Mars 编写的代码随意向内(scale in)和向外(scale out)伸缩。向内伸缩到单机,可以利用多核来并行执行科学计算任务;向外伸缩到分布式集群,可以支持到上千台 worker 规模来完成单机无论如何都难以完成的任务。Benchmark在一个真实的场景中,我们遇到了巨型矩阵乘法的计算需求,需要完成两个均为千亿元素,大小约为2.25T的矩阵相乘。Mars通过5行代码,使用1600 CU(200个 worker,每 worker 为 8核 32G内存),在2个半小时内完成计算。在此之前,同类计算只能使用 MapReduce 编写千余行代码模拟进行,完成同样的任务需要动用 9000 CU 并耗时10个小时。让我们再看两个对比。下图是对36亿数据矩阵的每个元素加一再乘以二,红色的叉表示 Numpy 的计算时间,绿色的实线是 Mars 的计算时间,蓝色虚线是理论计算时间。可以看到单机 Mars 就比 Numpy 快数倍,随着 Worker 的增加,可以获得几乎线性的加速比。下图是进一步扩大计算规模,把数据扩大到144亿元素,对这些元素加一乘以二以后再求和。这时候输入数据就有 115G,单机的 Numpy 已经无法完成运算,Mars 依然可以完成运算,且随着机器的增多可以获得还不错的加速比。开源地址Mars 已经在 Github 开源:https://github.com/mars-project/mars ,且后续会全部在 Github 上使用标准开源软件的方式来进行开发,欢迎大家使用 Mars,并成为 Mars 的 contributor。Mars科学计算引擎产品发布会发布直播回放>>发布活动页>> 大数据计算服务MaxCompute官网>>MaxCompute试用申请页面>>聚能聊>>本文作者:晋恒阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 17, 2019 · 2 min · jiezi

Java 并发方案全面学习总结

并发与并行的概念并发(Concurrency): 问题域中的概念—— 程序需要被设计成能够处理多个同时(或者几乎同时)发生的事件并行(Parallelism): 方法域中的概念——通过将问题中的多个部分 并行执行,来加速解决问题。进程、线程与协程它们都是并行机制的解决方案。进程: 进程是什么呢?直白地讲,进程就是应用程序的启动实例。比如我们运行一个游戏,打开一个软件,就是开启了一个进程。进程拥有代码和打开的文件资源、数据资源、独立的内存空间。启动一个进程非常消耗资源,一般一台机器最多启动数百个进程。线程: 线程从属于进程,是程序的实际执行者。一个进程至少包含一个主线程,也可以有更多的子线程。线程拥有自己的栈空间。在进程内启动线程也要消耗一定的资源,一般一个进程最多启动数千个线程。操作系统能够调度的最小单位就是线程了。协程: 协程又从属于线程,它不属于操作系统管辖,完全由程序控制,一个线程内可以启动数万甚至数百万协程。但也正是因为它由程序控制,它对编写代码的风格改变也最多。Java的并行执行实现JVM中的线程主线程: 独立生命周期的线程守护线程: 被主线程创建,随着创建线程结束而结束线程状态要注意的是,线程不是调用start之后马上进入运行中的状态,而是在"可运行"状态,由操作系统来决定调度哪个线程来运行。Jetty中的线程Web服务器都有自己管理的线程池, 比如轻量级的Jetty, 就有以下三种类型的线程:AcceptorSelectorWorker最原始的多线程——Thread类继承类 vs 实现接口继承Thread类实现Runnable接口实际使用中显然实现接口更好, 避免了单继承限制。Runnable vs CallableRunnable:实现run方法,无法抛出受检查的异常,运行时异常会中断主线程,但主线程无法捕获,所以子线程应该自己处理所有异常Callable:实现call方法,可以抛出受检查的异常,可以被主线程捕获,但主线程无法捕获运行时异常,也不会被打断。需要返回值的话,就用Callable接口一个实现了Callable接口的对象,需要被包装为RunnableFuture对象, 然后才能被新线程执行, 而RunnableFuture其实还是实现了Runnable接口。Future, Runnable 和FutureTask的关系如下:可以看出FutureTask其实是RunnableFuture接口的实现类,下面是使用Future的示例代码public class Callee implements Callable { AtomicInteger counter = new AtomicInteger(0); private Integer seq=null; public Callee() { super(); } public Callee(int seq) { this.seq = seq; } /** * call接口可以抛出受检查的异常 * @return * @throws InterruptedException / @Override public Person call() throws InterruptedException { Person p = new Person(“person”+ counter.incrementAndGet(), RandomUtil.random(0,150)); System.out.println(“In thread("+seq+”), create a Person: “+p.toString()); Thread.sleep(1000); return p; }}Callee callee1 = new Callee();FutureTask<Person> ft= new FutureTask<Person>(callee1);Thread thread = new Thread(ft);thread.start();try { thread.join();} catch (InterruptedException e) { e.printStackTrace(); return;}System.out.println(“ft.isDone: “+ft.isDone());Person result1;try { result1 = ((Future<Person>) ft).get();} catch (InterruptedException e) { e.printStackTrace(); result1 = null;} catch (ExecutionException e) { e.printStackTrace(); result1 = null;}Person result = result1;System.out.println(“main thread get result: “+result.toString());线程调度Thread.yield() 方法:调用这个方法,会让当前线程退回到可运行状态,而不是阻塞状态,这样就留给其他同级线程一些运行机会Thread.sleep(long millis):调用这个方法,真的会让当前线程进入阻塞状态,直到时间结束线程对象的join():这个方法让当前线程进入阻塞状态,直到要等待的线程结束。线程对象的interrupt():不要以为它是中断某个线程!它只是线线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出异常,从而结束线程,但是如果你吃掉了这个异常,那么这个线程还是不会中断的!Object类中的wait():线程进入等待状态,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个状态跟加锁有关,所以是Object的方法。Object类中的notify():唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。同步与锁内存一致性错误public class Counter { private int c = 0; public void increment() { c++; } public void decrement() { c–; } public int value() { return c; }}volatilepublic class Foo { private int x = -1; private volatile boolean v = false; public void setX(int x) { this.x = x; v = true; } public int getX() { if (v == true) { return x; } return 0; }}volatile关键字实际上指定了变量不使用寄存器, 并且对变量的访问不会乱序执行。但仅仅对原始类型变量本身生效,如果是++或者–这种“非原子”操作,则不能保证多线程操作的正确性了原子类型JDK提供了一系列对基本类型的封装,形成原子类型(Atomic Variables),特别适合用来做计数器import java.util.concurrent.atomic.AtomicInteger;class AtomicCounter { private AtomicInteger c = new AtomicInteger(0); public void increment() { c.incrementAndGet(); } public void decrement() { c.decrementAndGet(); } public int value() { return c.get(); }}原子操作的实现原理,在Java8之前和之后不同Java7public final int getAndIncrement() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return current; }}Java8public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1);}至于Compare-and-Swap,以及Fetch-and-Add两种算法,是依赖机器底层机制实现的。线程安全的集合类BlockingQueue: 定义了一个先进先出的数据结构,当你尝试往满队列中添加元素,或者从空队列中获取元素时,将会阻塞或者超时ConcurrentMap: 是 java.util.Map 的子接口,定义了一些有用的原子操作。移除或者替换键值对的操作只有当 key 存在时才能进行,而新增操作只有当 key 不存在时。使这些操作原子化,可以避免同步。ConcurrentMap 的标准实现是 ConcurrentHashMap,它是 HashMap 的并发模式。ConcurrentNavigableMap: 是 ConcurrentMap 的子接口,支持近似匹配。ConcurrentNavigableMap 的标准实现是 ConcurrentSkipListMap,它是 TreeMap 的并发模式。ThreadLocal-只有本线程才能访问的变量ThreadLoal 变量,它的基本原理是,同一个 ThreadLocal 所包含的对象(对ThreadLocal< String >而言即为 String 类型变量),在不同的 Thread 中有不同的副本(实际是不同的实例,后文会详细阐述)。这里有几点需要注意因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。后文会通过实例详细阐述该观点。另外,该场景下,并非必须使用 ThreadLocal ,其它方式完全可以实现同样的效果,只是 ThreadLocal 使得实现更简洁。synchronized关键字方法加锁:其实不是加在指定的方法上,而是在指定的对象上,只不过在方法开始前会检查这个锁静态方法锁:加在类上,它和加在对象上的锁互补干扰代码区块锁:其实不是加在指定的代码块上,而是加在指定的对象上,只不过在代码块开始前会检查这个锁。一个对象只会有一个锁,所以代码块锁和实例方法锁是会互相影响的需要注意的是:无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问,每个对象只有一个锁(lock)与之相关联加锁不慎可能会造成死锁线程池(Java 5)用途真正的多线程使用,是从线程池开始的,Callable接口,基本上也是被线程池调用的。线程池全景图线程池的使用 ExecutorService pool = Executors.newFixedThreadPool(3); Callable<Person> worker1 = new Callee(); Future ft1 = pool.submit(worker1); Callable<Person> worker2 = new Callee(); Future ft2 = pool.submit(worker2); Callable<Person> worker3 = new Callee(); Future ft3 = pool.submit(worker3); System.out.println(“准备通知线程池shutdown…”); pool.shutdown(); System.out.println(“已通知线程池shutdown”); try { pool.awaitTermination(2L, TimeUnit.SECONDS); System.out.println(“线程池完全结束”); } catch (InterruptedException e) { e.printStackTrace(); }线程池要解决的问题任务排队:当前能并发执行的线程数总是有限的,但任务数可以很大线程调度:线程的创建是比较消耗资源的,需要一个池来维持活跃线程结果收集:每个任务完成以后,其结果需要统一采集线程池类型newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。newSingleThreadScheduledExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。线程池状态线程池在构造前(new操作)是初始状态,一旦构造完成线程池就进入了执行状态RUNNING。严格意义上讲线程池构造完成后并没有线程被立即启动,只有进行“预启动”或者接收到任务的时候才会启动线程。这个会后面线程池的原理会详细分析。但是线程池是出于运行状态,随时准备接受任务来执行。线程池运行中可以通过shutdown()和shutdownNow()来改变运行状态。shutdown()是一个平缓的关闭过程,线程池停止接受新的任务,同时等待已经提交的任务执行完毕,包括那些进入队列还没有开始的任务,这时候线程池处于SHUTDOWN状态;shutdownNow()是一个立即关闭过程,线程池停止接受新的任务,同时线程池取消所有执行的任务和已经进入队列但是还没有执行的任务,这时候线程池处于STOP状态。一旦shutdown()或者shutdownNow()执行完毕,线程池就进入TERMINATED状态,此时线程池就结束了。isTerminating()描述的是SHUTDOWN和STOP两种状态。isShutdown()描述的是非RUNNING状态,也就是SHUTDOWN/STOP/TERMINATED三种状态。任务拒绝策略Fork/Join模型(Java7)用途计算密集型的任务,最好很少有IO等待,也没有Sleep之类的,最好是本身就适合递归处理的算法分析在给定的线程数内,尽可能地最大化利用CPU资源,但又不会导致其他资源过载(比如内存),或者大量空线程等待。ForkJoinPool主要用来使用分治法(Divide-and-Conquer Algorithm)来解决问题。典型的应用比如快速排序算法。这里的要点在于,ForkJoinPool需要使用相对少的线程来处理大量的任务。比如要对1000万个数据进行排序,那么会将这个任务分割成两个500万的排序任务和一个针对这两组500万数据的合并任务。以此类推,对于500万的数据也会做出同样的分割处理,到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。比如,当元素的数量小于10时,会停止分割,转而使用插入排序对它们进行排序。那么到最后,所有的任务加起来会有大概2000000+个。问题的关键在于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行。所以当使用ThreadPoolExecutor时,使用分治法会存在问题,因为ThreadPoolExecutor中的线程无法像任务队列中再添加一个任务并且在等待该任务完成之后再继续执行。而使用ForkJoinPool时,就能够让其中的线程创建新的任务,并挂起当前的任务,此时线程就能够从队列中选择子任务执行。以上程序的关键是fork()和join()方法。在ForkJoinPool使用的线程中,会使用一个内部队列来对需要执行的任务以及子任务进行操作来保证它们的执行顺序。那么使用ThreadPoolExecutor或者ForkJoinPool,会有什么性能的差异呢?首先,使用ForkJoinPool能够使用数量有限的线程来完成非常多的具有父子关系的任务,比如使用4个线程来完成超过200万个任务。但是,使用ThreadPoolExecutor时,是不可能完成的,因为ThreadPoolExecutor中的Thread无法选择优先执行子任务,需要完成200万个具有父子关系的任务时,也需要200万个线程,显然这是不可行的。ps:ForkJoinPool在执行过程中,会创建大量的子任务,导致GC进行垃圾回收,这些是需要注意的。原理与使用ForkJoinPool首先是ExecutorService的实现类,因此是特殊的线程池。创建了ForkJoinPool实例之后,就可以调用ForkJoinPool的submit(ForkJoinTask<T> task) 或invoke(ForkJoinTask<T> task)方法来执行指定任务了。其中ForkJoinTask代表一个可以并行、合并的任务。ForkJoinTask是一个抽象类,它还有两个抽象子类:RecusiveAction和RecusiveTask。其中RecusiveTask代表有返回值的任务,而RecusiveAction代表没有返回值的任务。个人认为ForkJoinPool设计不太好的地方在于,ForkJoinTask不是个接口,而是抽象类,实际使用时基本上不是继承RecursiveAction就是继承RecursiveTask,对业务类有限制。示例典型的一个例子,就是一串数组求和public interface Calculator { long sumUp(long[] numbers);}public class ForkJoinCalculator implements Calculator { private ForkJoinPool pool; private static class SumTask extends RecursiveTask<Long> { private long[] numbers; private int from; private int to; public SumTask(long[] numbers, int from, int to) { this.numbers = numbers; this.from = from; this.to = to; } @Override protected Long compute() { // 当需要计算的数字小于6时,直接计算结果 if (to - from < 6) { long total = 0; for (int i = from; i <= to; i++) { total += numbers[i]; } return total; // 否则,把任务一分为二,递归计算 } else { int middle = (from + to) / 2; SumTask taskLeft = new SumTask(numbers, from, middle); SumTask taskRight = new SumTask(numbers, middle+1, to); taskLeft.fork(); taskRight.fork(); return taskLeft.join() + taskRight.join(); } } } public ForkJoinCalculator() { // 也可以使用公用的 ForkJoinPool: // pool = ForkJoinPool.commonPool() pool = new ForkJoinPool(); } @Override public long sumUp(long[] numbers) { return pool.invoke(new SumTask(numbers, 0, numbers.length-1)); }}这个例子展示了当数组被拆分得足够小(<6)之后,就不需要并行处理了,而更大的数组就拆为两半,分别处理。Stream(Java 8)概念别搞混了,跟IO的Stream完全不是一回事,可以把它看做是集合处理的声明式语法,类似数据库操作语言SQL。当然也有跟IO类似的地方,就是Stream只能消费一次,不能重复使用。看个例子:int sum = widgets.stream().filter(w -> w.getColor() == RED) .mapToInt(w -> w.getWeight()) .sum();流提供了一个能力,任何一个流,只要获取一次并行流,后面的操作就都可以并行了。例如:Stream<String> stream = Stream.of(“a”, “b”, “c”,“d”,“e”,“f”,“g”);String str = stream.parallel().reduce((a, b) -> a + “,” + b).get();System.out.println(str);流操作生成流Collection.stream()Collection.parallelStream()Arrays.stream(T array) or Stream.of()java.io.BufferedReader.lines()java.util.stream.IntStream.range()java.nio.file.Files.walk()java.util.SpliteratorRandom.ints()BitSet.stream()Pattern.splitAsStream(java.lang.CharSequence)JarFile.stream()示例// 1. Individual valuesStream stream = Stream.of(“a”, “b”, “c”);// 2. ArraysString [] strArray = new String[] {“a”, “b”, “c”};stream = Stream.of(strArray);stream = Arrays.stream(strArray);// 3. CollectionsList<String> list = Arrays.asList(strArray);stream = list.stream();需要注意的是,对于基本数值型,目前有三种对应的包装类型 Stream:IntStream、LongStream、DoubleStream。当然我们也可以用 Stream<Integer>、Stream<Long> >、Stream<Double>,但是 boxing 和 unboxing 会很耗时,所以特别为这三种基本数值型提供了对应的 Stream。Intermediate一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。已知的Intermediate操作包括:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered。Terminal一个流只能有一个 terminal操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。已知的Terminal操作包括:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iteratorreduce解析: reduce本质上是个聚合方法,它的作用是用流里面的元素生成一个结果,所以用来做累加,字符串拼接之类的都非常合适。它有三个参数初始值:最终结果的初始化值,可以是一个空的对象聚合函数:一个二元函数(有两个参数),第一个参数是上一次聚合的结果,第二个参数是某个元素多个部分结果的合并函数:如果流并发了,那么聚合操作会分为多段进行,这里显示了多段之间如何配合collect: collect比reduce更强大:reduce最终只能得到一个跟流里数据类型相同的值, 但collect的结果可以是任何对象。简单的collect也有三个参数:最终要返回的数据容器把元素并入返回值的方法多个部分结果的合并两个collect示例//和reduce相同的合并字符操作String concat = stringStream.collect(StringBuilder::new, StringBuilder::append,StringBuilder::append).toString();//等价于上面,这样看起来应该更加清晰String concat = stringStream.collect(() -> new StringBuilder(),(l, x) -> l.append(x), (r1, r2) -> r1.append(r2)).toString();//把stream转成mapStream stream = Stream.of(1, 2, 3, 4).filter(p -> p > 2);List result = stream.collect(() -> new ArrayList<>(), (list, item) -> list.add(item), (one, two) -> one.addAll(two));/ 或者使用方法引用 */result = stream.collect(ArrayList::new, List::add, List::addAll);协程协程,英文Coroutines,也叫纤程(Fiber)是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。协程实际上是在语言底层(或者框架)对需要等待的程序进行调度,从而充分利用CPU的方法, 其实这完全可以通过回调来实现, 但是深层回调的代码太变态了,所以发明了协程的写法。理论上多个协程不会真的"同时"执行,也就不会引起共享变量操作的不确定性,不需要加锁(待确认)。pythone协程示例Pythone, Golang和C#都内置了协程的语法,但Java没有,只能通过框架实现,常见的框架包括:Quasar,kilim和ea-async。Java ea-async 协程示例import static com.ea.async.Async.await;import static java.util.concurrent.CompletableFuture.completedFuture;public class Store{ //购物操作, 传一个商品id和一个价格 public CompletableFuture<Boolean> buyItem(String itemTypeId, int cost) { //银行扣款(长时间操作) if(!await(bank.decrement(cost))) { return completedFuture(false); } try { //商品出库(长时间操作) await(inventory.giveItem(itemTypeId)); return completedFuture(true); } catch (Exception ex) { await(bank.refund(cost)); throw new AppException(ex); } }}参考资料《七周七并发模型》电子书深入浅出Java Concurrency——线程池Java多线程学习(吐血超详细总结)Jetty基础之线程模型Jetty-server高性能,多线程特性的源码分析Java 编程要点之并发(Concurrency)详解Java Concurrency in Depth (Part 1)Java进阶(七)正确理解Thread Local的原理与适用场景Java 并发编程笔记:如何使用 ForkJoinPool 以及原理ForkJoinPool简介多线程 ForkJoinPoolJava 8 中的 Streams API 详解Java中的协程实现漫画:什么是协程学习源码 ...

January 4, 2019 · 4 min · jiezi

一次生产 CPU 100% 排查优化实践

前言到了年底果然都不太平,最近又收到了运维报警:表示有些服务器负载非常高,让我们定位问题。还真是想什么来什么,前些天还故意把某些服务器的负载提高(没错,老板让我写个 BUG!),不过还好是不同的环境互相没有影响。定位问题拿到问题后首先去服务器上看了看,发现运行的只有我们的 Java 应用。于是先用 ps 命令拿到了应用的 PID。接着使用 ps -Hp pid 将这个进程的线程显示出来。输入大写的 P 可以将线程按照 CPU 使用比例排序,于是得到以下结果。果然某些线程的 CPU 使用率非常高。为了方便定位问题我立马使用 jstack pid > pid.log 将线程栈 dump 到日志文件中。我在上面 100% 的线程中随机选了一个 pid=194283 转换为 16 进制(2f6eb)后在线程快照中查询:因为线程快照中线程 ID 都是16进制存放。发现这是 Disruptor 的一个堆栈,前段时间正好解决过一个由于 Disruptor 队列引起的一次 OOM:强如 Disruptor 也发生内存溢出?没想到又来一出。为了更加直观的查看线程的状态信息,我将快照信息上传到专门分析的平台上。http://fastthread.io/其中有一项菜单展示了所有消耗 CPU 的线程,我仔细看了下发现几乎都是和上面的堆栈一样。也就是说都是 Disruptor 队列的堆栈,同时都在执行 java.lang.Thread.yield 函数。众所周知 yield 函数会让当前线程让出 CPU 资源,再让其他线程来竞争。根据刚才的线程快照发现处于 RUNNABLE 状态并且都在执行 yield 函数的线程大概有 30几个。因此初步判断为大量线程执行 yield 函数之后互相竞争导致 CPU 使用率增高,而通过对堆栈发现是和使用 Disruptor 有关。解决问题而后我查看了代码,发现是根据每一个业务场景在内部都会使用 2 个 Disruptor 队列来解耦。假设现在有 7 个业务类型,那就等于是创建 2*7=14 个 Disruptor 队列,同时每个队列有一个消费者,也就是总共有 14 个消费者(生产环境更多)。同时发现配置的消费等待策略为 YieldingWaitStrategy 这种等待策略确实会执行 yield 来让出 CPU。代码如下:初步看来和这个等待策略有很大的关系。本地模拟为了验证,我在本地创建了 15 个 Disruptor 队列同时结合监控观察 CPU 的使用情况。创建了 15 个 Disruptor 队列,同时每个队列都用线程池来往 Disruptor队列 里面发送 100W 条数据。消费程序仅仅只是打印一下。跑了一段时间发现 CPU 使用率确实很高。同时 dump 线程发现和生产的现象也是一致的:消费线程都处于 RUNNABLE 状态,同时都在执行 yield。通过查询 Disruptor 官方文档发现:YieldingWaitStrategy 是一种充分压榨 CPU 的策略,使用自旋 + yield的方式来提高性能。当消费线程(Event Handler threads)的数量小于 CPU 核心数时推荐使用该策略。同时查阅到其他的等待策略 BlockingWaitStrategy (也是默认的策略),它使用的是锁的机制,对 CPU 的使用率不高。于是在和之前同样的条件下将等待策略换为 BlockingWaitStrategy。和刚才的 CPU 对比会发现到后面使用率的会有明显的降低;同时 dump 线程后会发现大部分线程都处于 waiting 状态。优化解决看样子将等待策略换为 BlockingWaitStrategy 可以减缓 CPU 的使用,但留意到官方对 YieldingWaitStrategy 的描述里谈道:当消费线程(Event Handler threads)的数量小于 CPU 核心数时推荐使用该策略。而现有的使用场景很明显消费线程数已经大大的超过了核心 CPU 数了,因为我的使用方式是一个 Disruptor 队列一个消费者,所以我将队列调整为只有 1 个再试试(策略依然是 YieldingWaitStrategy)。跑了一分钟,发现 CPU 的使用率一直都比较平稳而且不高。总结所以排查到此可以有一个结论了,想要根本解决这个问题需要将我们现有的业务拆分;现在是一个应用里同时处理了 N 个业务,每个业务都会使用好几个 Disruptor 队列。由于是在一台服务器上运行,所以 CPU 资源都是共享的,这就会导致 CPU 的使用率居高不下。所以我们的调整方式如下:为了快速缓解这个问题,先将等待策略换为 BlockingWaitStrategy,可以有效降低 CPU 的使用率(业务上也还能接受)。第二步就需要将应用拆分(上文模拟的一个 Disruptor 队列),一个应用处理一种业务类型;然后分别单独部署,这样也可以互相隔离互不影响。当然还有其他的一些优化,因为这也是一个老系统了,这次 dump 线程居然发现创建了 800+ 的线程。创建线程池的方式也是核心线程数、最大线程数是一样的,导致一些空闲的线程也得不到回收;这样会有很多无意义的资源消耗。所以也会结合业务将创建线程池的方式调整一下,将线程数降下来,尽量的物尽其用。本文的演示代码已上传至 GitHub:https://github.com/crossoverJie/JCSprout你的点赞与分享是对我最大的支持 ...

December 17, 2018 · 1 min · jiezi

阿里云梁楹:这样的青春,别样的精彩

人的青春应该怎样度过?相信一千个人心中,有一千个答案。我是郭嘉梁,花名梁楹,在不少人眼中,我是一个来自北方的大男孩,一个自带“古典气质的少年”,其实我是一个喜欢晋级打怪,热爱挑战自我的阿里云工程师。1024程序员节之际,分享我的成长经历,且看别样的“青春修炼手册”。学生时代:热爱、执著、前进早在读书的时候,我就一直很喜欢接触一些新的技术。本科毕业后,我被保送到中科院计算所读研,机缘巧合,我接触到了很有前瞻性的光网络互连技术。当时,在国内做光网络研究的人还是很少的。在导师的指导下,我专注于根据高性能数据中心流量模型,利用光交换机对数据中心的网络拓扑进行快速重构。通过 RYU 控制器完成了控制层的拓扑发现,路由计算等工作。在模拟系统中,实现并验证了 HyperX、Torus、DragonFly 等高性能网络常见拓扑结构 的重构算法 FHTR(fast and hitless data center topology reconfiguration)。在基于 AWGR 的光网络中利用该算法达到了微秒级的拓扑重构,并在小规模拓扑的评测中比之前的最新研究成果降低了 50%的丢包率。终于在2017年投中了欧洲光通信领域顶会ECOC的文章。虽然实验室内接触的技术大多偏重计算机硬件,但当时实验室的同学也喜欢利用业余时间探讨一些互联网的相关技术。研二时,我看到了阿里云正在举办中间件性能挑战赛,我和实验室的小伙伴一拍即合,决定以赛代练,多接触接触工业界的先进技术。当时的赛题是需要实现自定制数据库,满足双十一脱敏数据的高并发写入和查询需求。于是在两个多月的时间里,我们几乎从0开始调研数据库的索引机制,整个暑假的时间都泡在实验室里。最终,在索引阶段,我们通过 TeraSort 的排序算法对 4 亿订单进行聚集索引,并采用多线程同步的方式控制磁盘 I/O。 在查询阶段,通过多线程完成 Join 操作,充分利用了 CPU 资源。同时,利用 AVRO 实现了数据的压缩,将原始数据压 缩到了 46%。使用 LRU 算法完成了基于块的缓存机制,查询的命中率达到 83%。日常学习的沉淀积累、平时练就的细致全面的解题思路、敢打硬仗的勇气,终于帮助我们克服了重重困难,翻越高山和大海,我们拿到了决赛冠军的好成绩!从此,我也结下了与阿里巴巴的缘分。阿里体验:我挑战,我能行2017年,我参加了阿里巴巴的校园招聘,了解到当时正在打算开辟新的业务,也是国内第一个和Elasticsearch官方合作的项目。当时内心就十分向往,虽然对全文搜索技术了解不多,但我依然觉得这是一个不错的挑战机遇。心里有个声音告诉我,如果刚工作的时候,能把一件未知的事情干好,以后职场上没有什么事情是做不好的!十分幸运,我加入阿里就赶上了Elasticsearch项目的启动,以及长达三个月的封闭开发。“一个新人+ 一个新项目”,挑战模式全面升级,而这正是我加入阿里所期待的。还记得刚入职的时候,很多问题搞不清楚,阿里的“老员工”濒湖同学,就像高年级的学长一样,耐心与我共同探讨问题、结对开发,极大的缩短了我融入团队的时间。但毕竟是新项目,压力和焦虑感也随之而来,漫长的封闭开发期,需要我用最快的速度了解阿里云的相关业务,以及适应阿里的开发节奏,这种“折磨”感让我无论是在技术方面还是对公司文化的理解方面,感觉都是经历了一场脱胎换骨式的洗礼。记忆里,几乎所有的场景都是与时间赛跑的拼搏画面,项目也终于在进入封闭开发室两个月后,进入了公测阶段。阿里的工程师每个人都肩负着重要的开发任务,以及相应的责任。主管万喜对我说的一句话,至今记忆犹新,“阿里云上的业务很重要,对待每一行代码都要非常认真,这是客户沉甸甸的信任。”每一次开发新功能时,每一次版本迭代时,我都心怀敬畏。如今,我参与开发的产品和相关技术在国内同行业中已经处于领先位置,获得行业认可和用户的好评。马老师说过,阿里人要有家国情怀。阿里云的业务涉及到的中小型企业非常多,因此我们每一天要做的,就是要完成好这一份重托,这份嘱托,支撑我迎接挑战、面对困难、赢得胜利!不忘初心,迎接未来来到阿里已经一年多了,在这个欢乐的大家庭,我收获很多,不但认识了新的同事,开发了新的产品,身份也从一名学生,正是转变成了工程师,我的“青春”再升级。对于工程师的身份,我感到十分骄傲。目前,我参与的阿里云Elasticsearch产品,提供基于开源Elasticsearch及商业版X-Pack插件,致力于数据分析、数据搜索等场景服务。在开源Elasticsearch基础上提供企业级权限管控、安全监控告警、自动报表生成等功能。Elasticsearch公有云,目前已经部署了4个国内区域以及6个国际区域,在线的弹性调度,配置管理,词典更新,集群监控,集群诊断,集群网络管理等功能均已提供服务。如果有志同道合的小伙伴,欢迎加入我们的团队。从学生时代到阿里巴巴,所有获得的成绩,都来源于对未知的好奇心。所有事情都是这样,做了不一定有机会,但不做一定没机会。未来,我感到身上的责任更重了,我会认真写好每一行代码,做好每一个云产品。认真生活,快乐工作!点击了解阿里云Elasticsearchhttps://data.aliyun.com/product/elasticsearch本文作者:山哥在这里阅读原文本文为云栖社区原创内容,未经允许不得转载。

November 16, 2018 · 1 min · jiezi

BATJ都爱问的多线程面试题

下面最近发的一些并发编程的文章汇总,通过阅读这些文章大家再看大厂面试中的并发编程问题就没有那么头疼了。今天给大家总结一下,面试中出镜率很高的几个多线程面试题,希望对大家学习和面试都能有所帮助。备注:文中的代码自己实现一遍的话效果会更佳哦!并发编程面试必备:synchronized 关键字使用、底层原理、JDK1.6 之后的底层优化以及 和ReenTrantLock 的对比并发编程面试必备:JUC 中的 Atomic 原子类总结并发编程面试必备:AQS 原理以及 AQS 同步组件总结该文已加入开源文档:JavaGuide(一份涵盖大部分Java程序员所需要掌握的核心知识)。地址:https://github.com/Snailclimb… 【强烈推荐!非广告!】阿里云双11褥羊毛活动:https://m.aliyun.com/act/team1111/#/share?params=N.FF7yxCciiM.hf47liqn 差不多一折,不过仅限阿里云新人购买,不是新人的朋友自己找方法买哦!一 面试中关于 synchronized 关键字的 5 连击1.1 说一说自己对于 synchronized 关键字的了解synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。1.2 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗synchronized关键字最主要的三种使用方式:修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 。也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 和 synchronized 方法一样,synchronized(this)代码块也是锁定当前对象的。synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。这里再提一下:synchronized关键字加到非 static 静态方法上是给对象实例上锁。另外需要注意的是:尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓冲功能!下面我已一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单利模式的原理呗!”双重校验锁实现对象单例(线程安全)public class Singleton { private volatile static Singleton uniqueInstance; private Singleton() { } public static Singleton getUniqueInstance() { //先判断对象是否已经实例过,没有实例化过才进入加锁代码 if (uniqueInstance == null) { //类对象加锁 synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; }}另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:为 uniqueInstance 分配内存空间初始化 uniqueInstance将 uniqueInstance 指向分配的内存地址但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出先问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。1.3 讲一下 synchronized 关键字的底层原理synchronized 关键字底层原理属于 JVM 层面。① synchronized 同步语句块的情况public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println(“synchronized 代码块”); } }}通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行javap -c -s -v -l SynchronizedDemo.class。从上面我们可以看出:synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。② synchronized 修饰方法的的情况public class SynchronizedDemo2 { public synchronized void method() { System.out.println(“synchronized 方法”); }}synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。1.4 说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。关于这几种优化的详细信息可以查看:synchronized 关键字使用、底层原理、JDK1.6 之后的底层优化以及 和ReenTrantLock 的对比1.5 谈谈 synchronized和ReenTrantLock 的区别① 两者都是可重入锁两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。② synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 APIsynchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。③ ReenTrantLock 比 synchronized 增加了一些高级功能相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。④ 性能已不是选择标准二 面试中关于线程池的 4 连击2.1 讲一下Java内存模型在 JDK1.2 之前,Java的内存模型实现总是从<font color=“red”>主存(即共享内存)读取变量</font>,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存<font color=“red”>本地内存</font>(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成<font color=“red”>数据的不一致</font>。要解决这个问题,就需要把变量声明为<font color=“red”> volatile</font>,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。说白了,<font color=“red”> volatile</font> 关键字的主要作用就是保证变量的可见性然后还有一个作用是防止指令重排序。2.2 说说 synchronized 关键字和 volatile 关键字的区别synchronized关键字和volatile关键字比较volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。三 面试中关于 线程池的 2 连击3.1 为什么要用线程池?线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。 这里借用《Java并发编程的艺术》提到的来说一下使用线程池的好处:降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。3.2 实现Runnable接口和Callable接口的区别如果想让线程池执行任务的话需要实现的Runnable接口或Callable接口。 Runnable接口或Callable接口实现类都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。两者的区别在于 Runnable 接口不会返回结果但是 Callable 接口可以返回结果。备注: 工具类Executors可以实现Runnable对象和Callable对象之间的相互转换。(Executors.callable(Runnable task)或Executors.callable(Runnable task,Object resule))。3.3 执行execute()方法和submit()方法的区别是什么呢?1)execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;2)submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。3.4 如何创建线程池《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险**Executors 返回线程池对象的弊端如下:FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。方式一:通过构造方法实现方式二:通过Executor 框架的工具类Executors来实现我们可以创建三种类型的ThreadPoolExecutor:FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。对应Executors工具类中的方法如图所示:四 面试中关于 Atomic 原子类的 4 连击4.1 介绍一下Atomic 原子类Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。所以,所谓原子类说简单点就是具有原子/原子操作特征的类。并发包 java.util.concurrent 的原子类都存放在java.util.concurrent.atomic下,如下图所示。4.2 JUC 包中的原子类是哪4类?基本类型 使用原子的方式更新基本类型AtomicInteger:整形原子类AtomicLong:长整型原子类AtomicBoolean :布尔型原子类数组类型使用原子的方式更新数组里的某个元素AtomicIntegerArray:整形数组原子类AtomicLongArray:长整形数组原子类AtomicReferenceArray :引用类型数组原子类引用类型AtomicReference:引用类型原子类AtomicStampedRerence:原子更新引用类型里的字段原子类AtomicMarkableReference :原子更新带有标记位的引用类型对象的属性修改类型AtomicIntegerFieldUpdater:原子更新整形字段的更新器AtomicLongFieldUpdater:原子更新长整形字段的更新器AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。4.3 讲讲 AtomicInteger 的使用AtomicInteger 类常用方法public final int get() //获取当前的值public final int getAndSet(int newValue)//获取当前的值,并设置新的值public final int getAndIncrement()//获取当前的值,并自增public final int getAndDecrement() //获取当前的值,并自减public final int getAndAdd(int delta) //获取当前的值,并加上预期的值boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。AtomicInteger 类的使用示例使用 AtomicInteger 之后,不用对 increment() 方法加锁也可以保证线程安全。class AtomicIntegerTest { private AtomicInteger count = new AtomicInteger(); //使用AtomicInteger之后,不需要对该方法加锁,也可以实现线程安全。 public void increment() { count.incrementAndGet(); } public int getCount() { return count.get(); }}4.4 能不能给我简单介绍一下 AtomicInteger 类的原理AtomicInteger 线程安全原理简单分析AtomicInteger 类的部分源码: // setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用) private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField(“value”)); } catch (Exception ex) { throw new Error(ex); } } private volatile int value;AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。关于 Atomic 原子类这部分更多内容可以查看我的这篇文章:并发编程面试必备:JUC 中的 Atomic 原子类总结五 AQS5.1 AQS 介绍AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。5.2 AQS 原理分析AQS 原理这部分参考了部分博客,在5.2节末尾放了链接。在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于AQS原理的理解”。下面给大家一个示例供大家参加,面试不是背题,大家一定要假如自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。下面大部分内容其实在AQS类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话可以看看源码。5.2.1 AQS 原理概览AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。看个AQS(AbstractQueuedSynchronizer)原理图:AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。private volatile int state;//共享变量,使用volatile修饰保证线程可见性状态信息通过procted类型的getState,setState,compareAndSetState进行操作//返回同步状态的当前值protected final int getState() { return state;} // 设置同步状态的值protected final void setState(int newState) { state = newState;}//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update);}5.2.2 AQS 对资源的共享方式AQS定义两种资源共享方式Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:公平锁:按照线程在队列中的排队顺序,先到者先拿到锁非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。5.2.3 AQS底层使用了模板方法模式同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。默认情况下,每个方法都抛出 UnsupportedOperationException。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。 以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。推荐两篇 AQS 原理和相关源码分析的文章:http://www.cnblogs.com/watery…https://www.cnblogs.com/cheng...5.3 AQS 组件总结Semaphore(信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。CountDownLatch (倒计时器): CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。关于AQS这部分的更多内容可以查看我的这篇文章:并发编程面试必备:AQS 原理以及 AQS 同步组件总结Reference《深入理解 Java 虚拟机》《实战 Java 高并发程序设计》《Java并发编程的艺术》http://www.cnblogs.com/watery…https://www.cnblogs.com/cheng…【强烈推荐!非广告!】阿里云双11褥羊毛活动(10.29-11.12):https://m.aliyun.com/act/team1111/#/share?params=N.FF7yxCciiM.hf47liqn 。一句话解析该次活动:新用户低至一折购买(1核2g服务器仅8.3/月,比学生机还便宜,真的强烈推荐屯3年)。老用户可以加入我的战队,然后分享自己的链接,可以获得红包和25%的返现,我们的战队目前300位新人,所以可以排进前100,后面可以瓜分百万现金(按拉新人数瓜分现金,拉的越多分的越多!不要自己重新开战队,后面不能参与瓜分现金)。你若盛开,清风自来。 欢迎关注我的微信公众号:“Java面试通关手册”,一个有温度的微信公众号。公众号后台回复关键字“1”,可以免费获取一份我精心准备的小礼物哦! ...

November 2, 2018 · 3 min · jiezi

并发编程面试必备:AQS 原理以及 AQS 同步组件总结

常见问题:AQS 原理?;CountDownLatch和CyclicBarrier了解吗,两者的区别是什么?用过Semaphore吗?本节思维导图:【强烈推荐!非广告!】阿里云双11褥羊毛活动:https://m.aliyun.com/act/team1111/#/share?params=N.FF7yxCciiM.hf47liqn 差不多一折,不过仅限阿里云新人购买,不是新人的朋友自己找方法买哦!1 AQS 简单介绍AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。2 AQS 原理在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于AQS原理的理解”。下面给大家一个示例供大家参加,面试不是背题,大家一定要假如自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。下面大部分内容其实在AQS类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话可以看看源码。2.1 AQS 原理概览AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。看个AQS(AbstractQueuedSynchronizer)原理图:AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。private volatile int state;//共享变量,使用volatile修饰保证线程可见性状态信息通过procted类型的getState,setState,compareAndSetState进行操作//返回同步状态的当前值protected final int getState() { return state;} // 设置同步状态的值protected final void setState(int newState) { state = newState;}//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update);}2.2 AQS 对资源的共享方式AQS定义两种资源共享方式Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:公平锁:按照线程在队列中的排队顺序,先到者先拿到锁非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在上层已经帮我们实现好了。2.3 AQS底层使用了模板方法模式同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用,下面简单的给大家介绍一下模板方法模式,模板方法模式是一个很容易理解的设计模式之一。模板方法模式是基于”继承“的,主要是为了在不改变模板结构的前提下在子类中重新定义模板中的内容以实现复用代码。举个很简单的例子假如我们要去一个地方的步骤是:购票buyTicket()->安检securityCheck()->乘坐某某工具回家ride()->到达目的地arrive()。我们可能乘坐不同的交通工具回家比如飞机或者火车,所以除了ride()方法,其他方法的实现几乎相同。我们可以定义一个包含了这些方法的抽象类,然后用户根据自己的需要继承该抽象类然后修改 ride()方法。AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。默认情况下,每个方法都抛出 UnsupportedOperationException。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。 以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。推荐两篇 AQS 原理和相关源码分析的文章:http://www.cnblogs.com/watery…https://www.cnblogs.com/cheng...3 Semaphore(信号量)-允许多个线程同时访问synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。示例代码如下:/** * * @author Snailclimb * @date 2018年9月30日 * @Description: 需要一次性拿一个许可的情况 /public class SemaphoreExample1 { // 请求的数量 private static final int threadCount = 550; public static void main(String[] args) throws InterruptedException { // 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢) ExecutorService threadPool = Executors.newFixedThreadPool(300); // 一次只能允许执行的线程数量。 final Semaphore semaphore = new Semaphore(20); for (int i = 0; i < threadCount; i++) { final int threadnum = i; threadPool.execute(() -> {// Lambda 表达式的运用 try { semaphore.acquire();// 获取一个许可,所以可运行线程数量为20/1=20 test(threadnum); semaphore.release();// 释放一个许可 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } }); } threadPool.shutdown(); System.out.println(“finish”); } public static void test(int threadnum) throws InterruptedException { Thread.sleep(1000);// 模拟请求的耗时操作 System.out.println(“threadnum:” + threadnum); Thread.sleep(1000);// 模拟请求的耗时操作 }}执行 acquire 方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个 release 方法增加一个许可证,这可能会释放一个阻塞的acquire方法。然而,其实并没有实际的许可证这个对象,Semaphore只是维持了一个可获得许可证的数量。 Semaphore经常用于限制获取某种资源的线程数量。当然一次也可以一次拿取和释放多个许可,不过一般没有必要这样做: semaphore.acquire(5);// 获取5个许可,所以可运行线程数量为20/5=4 test(threadnum); semaphore.release(5);// 获取5个许可,所以可运行线程数量为20/5=4除了 acquire方法之外,另一个比较常用的与之对应的方法是tryAcquire方法,该方法如果获取不到许可就立即返回false。Semaphore 有两种模式,公平模式和非公平模式。公平模式: 调用acquire的顺序就是获取许可证的顺序,遵循FIFO;非公平模式: 抢占式的。Semaphore 对应的两个构造方法如下: public Semaphore(int permits) { sync = new NonfairSync(permits); } public Semaphore(int permits, boolean fair) { sync = fair ? new FairSync(permits) : new NonfairSync(permits); }这两个构造方法,都必须提供许可的数量,第二个构造方法可以指定是公平模式还是非公平模式,默认非公平模式。 由于篇幅问题,如果对 Semaphore 源码感兴趣的朋友可以看下面这篇文章:https://blog.csdn.net/qq_1943…4 CountDownLatch (倒计时器)CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。4.1 CountDownLatch 的两种典型用法①某一线程在开始运行前等待n个线程执行完毕。将 CountDownLatch 的计数器初始化为n :new CountDownLatch(n) ,每当一个任务线程执行完毕,就将计数器减1 countdownlatch.countDown(),当计数器的值变为0时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。②实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1 :new CountDownLatch(1) ,多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为0,多个线程同时被唤醒。4.2 CountDownLatch 的使用示例/* * * @author SnailClimb * @date 2018年10月1日 * @Description: CountDownLatch 使用方法示例 /public class CountDownLatchExample1 { // 请求的数量 private static final int threadCount = 550; public static void main(String[] args) throws InterruptedException { // 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢) ExecutorService threadPool = Executors.newFixedThreadPool(300); final CountDownLatch countDownLatch = new CountDownLatch(threadCount); for (int i = 0; i < threadCount; i++) { final int threadnum = i; threadPool.execute(() -> {// Lambda 表达式的运用 try { test(threadnum); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { countDownLatch.countDown();// 表示一个请求已经被完成 } }); } countDownLatch.await(); threadPool.shutdown(); System.out.println(“finish”); } public static void test(int threadnum) throws InterruptedException { Thread.sleep(1000);// 模拟请求的耗时操作 System.out.println(“threadnum:” + threadnum); Thread.sleep(1000);// 模拟请求的耗时操作 }}上面的代码中,我们定义了请求的数量为550,当这550个请求被处理完成之后,才会执行System.out.println(“finish”);。4.3 CountDownLatch 的不足CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。5 CyclicBarrier(循环栅栏)CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。5.1 CyclicBarrier 的应用场景CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个Excel保存了用户所有银行流水,每个Sheet保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日均银行流水,最后,再用barrierAction用这些线程的计算结果,计算出整个Excel的日均银行流水。5.2 CyclicBarrier 的使用示例示例1:/* * * @author Snailclimb * @date 2018年10月1日 * @Description: 测试 CyclicBarrier 类中带参数的 await() 方法 /public class CyclicBarrierExample2 { // 请求的数量 private static final int threadCount = 550; // 需要同步的线程数量 private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5); public static void main(String[] args) throws InterruptedException { // 创建线程池 ExecutorService threadPool = Executors.newFixedThreadPool(10); for (int i = 0; i < threadCount; i++) { final int threadNum = i; Thread.sleep(1000); threadPool.execute(() -> { try { test(threadNum); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (BrokenBarrierException e) { // TODO Auto-generated catch block e.printStackTrace(); } }); } threadPool.shutdown(); } public static void test(int threadnum) throws InterruptedException, BrokenBarrierException { System.out.println(“threadnum:” + threadnum + “is ready”); try { cyclicBarrier.await(2000, TimeUnit.MILLISECONDS); } catch (Exception e) { System.out.println("—–CyclicBarrierException——"); } System.out.println(“threadnum:” + threadnum + “is finish”); }}运行结果,如下:threadnum:0is readythreadnum:1is readythreadnum:2is readythreadnum:3is readythreadnum:4is readythreadnum:4is finishthreadnum:0is finishthreadnum:1is finishthreadnum:2is finishthreadnum:3is finishthreadnum:5is readythreadnum:6is readythreadnum:7is readythreadnum:8is readythreadnum:9is readythreadnum:9is finishthreadnum:5is finishthreadnum:8is finishthreadnum:7is finishthreadnum:6is finish……可以看到当线程数量也就是请求数量达到我们定义的 5 个的时候, await方法之后的方法才被执行。 另外,CyclicBarrier还提供一个更高级的构造函数CyclicBarrier(int parties, Runnable barrierAction),用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景。示例代码如下:/* * * @author SnailClimb * @date 2018年10月1日 * @Description: 新建 CyclicBarrier 的时候指定一个 Runnable */public class CyclicBarrierExample3 { // 请求的数量 private static final int threadCount = 550; // 需要同步的线程数量 private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> { System.out.println("——当线程数达到之后,优先执行——"); }); public static void main(String[] args) throws InterruptedException { // 创建线程池 ExecutorService threadPool = Executors.newFixedThreadPool(10); for (int i = 0; i < threadCount; i++) { final int threadNum = i; Thread.sleep(1000); threadPool.execute(() -> { try { test(threadNum); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (BrokenBarrierException e) { // TODO Auto-generated catch block e.printStackTrace(); } }); } threadPool.shutdown(); } public static void test(int threadnum) throws InterruptedException, BrokenBarrierException { System.out.println(“threadnum:” + threadnum + “is ready”); cyclicBarrier.await(); System.out.println(“threadnum:” + threadnum + “is finish”); }}运行结果,如下:threadnum:0is readythreadnum:1is readythreadnum:2is readythreadnum:3is readythreadnum:4is ready——当线程数达到之后,优先执行——threadnum:4is finishthreadnum:0is finishthreadnum:2is finishthreadnum:1is finishthreadnum:3is finishthreadnum:5is readythreadnum:6is readythreadnum:7is readythreadnum:8is readythreadnum:9is ready——当线程数达到之后,优先执行——threadnum:9is finishthreadnum:5is finishthreadnum:6is finishthreadnum:8is finishthreadnum:7is finish……5.3 CyclicBarrier和CountDownLatch的区别CountDownLatch是计数器,只能使用一次,而CyclicBarrier的计数器提供reset功能,可以多次使用。但是我不那么认为它们之间的区别仅仅就是这么简单的一点。我们来从jdk作者设计的目的来看,javadoc是这么描述它们的:CountDownLatch: A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.(CountDownLatch: 一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;)CyclicBarrier : A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point.(CyclicBarrier : 多个线程互相等待,直到到达同一个同步点,再继续一起执行。)对于CountDownLatch来说,重点是“一个线程(多个线程)等待”,而其他的N个线程在完成“某件事情”之后,可以终止,也可以等待。而对于CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待。CountDownLatch是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而CyclicBarrier更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。CyclicBarrier和CountDownLatch的区别这部分内容参考了如下两篇文章:https://blog.csdn.net/u010185…https://blog.csdn.net/tolcf/a...6 ReentrantLock 和 ReentrantReadWriteLockReentrantLock 和 synchronized 的区别在上面已经讲过了这里就不多做讲解。另外,需要注意的是:读写锁 ReentrantReadWriteLock 可以保证多个线程可以同时读,所以在读操作远大于写操作的时候,读写锁就非常有用了。由于篇幅问题,关于 ReentrantLock 和 ReentrantReadWriteLock 详细内容可以查看我的这篇原创文章。ReentrantLock 和 ReentrantReadWriteLock ...

November 2, 2018 · 4 min · jiezi

一份针对于新手的多线程实践--进阶篇

前言在上文《一份针对于新手的多线程实践》留下了一个问题:这只是多线程其中的一个用法,相信看到这里的朋友应该多它的理解更进一步了。再给大家留个阅后练习,场景也是类似的:在 Redis 或者其他存储介质中存放有上千万的手机号码数据,每个号码都是唯一的,需要在最快的时间内把这些号码全部都遍历一遍。有想法感兴趣的朋友欢迎在文末留言参与讨论????????。网友们的方案<!–more–>我在公众号以及其他一些平台收到了大家的回复,果然是众人拾柴火焰高啊。感谢每一位参与的朋友。其实看了大家的方案大多都想到了数据肯定要分段,因为大量的数据肯定没法一次性 load 到内存。但怎么加载就要考虑清楚了,有些人说放在数据库中通过分页的方式进行加载,然后将每页的数据丢到一个线程里去做遍历。其实想法挺不错的,但有个问题就是:这样肯定会导致有一个主线程去遍历所有的号码,即便是分页查询的那也得全部查询一遍,效率还是很低。即便是分页加载号码用多线程,那就会涉及到锁的问题,怎么保证每个线程读取的数据是互不冲突的。但如果存储换成 Redis 的 String 结构这样就更行不通了。遍历数据方案有没有一种利用多线程加载效率高,并且线程之间互相不需要竞争锁的方案呢?下面来看看这个方案:首先在存储这千万号码的时候我们把它的号段单独提出来并冗余存储一次。比如有个号码是 18523981123 那么就还需要存储一个号段:1852398。这样当我们有以下这些号码时:18523981123 18523981124 18523981125 13123874321 13123874322 13123874323我们就还会维护一个号段数据为:1852398 1312387这样我想大家应该明白下一步应当怎么做了吧。在需要遍历时:通过主线程先把所有的号段加载到内存,即便是千万的号码号段也顶多几千条数据。遍历这个号段,将每个号段提交到一个 task 线程中。由这个线程通过号段再去查询真正的号码进行遍历。最后所有的号段都提交完毕再等待所有的线程执行完毕即可遍历所有的号码。这样做的根本原因其实是避免了线程之间加锁,通过号段可以让每个线程只取自己那一部分数据。可能会有人说,如果号码持续增多导致号段的数据也达到了上万甚至几十万这怎么办呢?那其实也是同样的思路,可以再把号段进行拆分。比如之前是 1852398 的号段,那我继续拆分为 1852 。这样只需要在之前的基础上再启动一个线程去查询子号段即可,有点 fork/join 的味道。这样的思路其实也和 JDK1.7 中的 ConcurrentHashMap 类似,定位一个真正的数据需要两次定位。分布式方案上面的方案也是由局限性的,毕竟说到底还是一个单机应用。没法扩展;处理的数据始终是有上限。这个上限就和服务器的配置以及线程数这些相关,说的高大上一点其实就是垂直扩展增加单机的处理性能。因此随着数据量的提升我们肯定得需要通过水平扩展的方式才能达到最好的性能,这就是分布式的方案。假设我现在有上亿的数据需要遍历,但我当前的服务器配置只能支撑一个应用启动 N 个线程 5 分钟跑5000W 的数据。于是我水平扩展,在三台服务器上启动了三个独立的进程。假设一个应用能跑 5000W ,那么理论上来说三个应用就可以跑1.5亿的数据了。但这个的前提还是和上文一样:每个应用只能处理自己的数据,不能出现加锁的情况(这样会大大的降低性能)。所以我们得对刚才的号段进行分组。先通过一张图来直观的表示这个逻辑:假设现在我有 9 个号段,那么我就得按照图中的方式把数据隔离开来。第一个数据给应用0,第二个数据给应用1,第三个数据给应用2。后面的数据以此类推(就是一个简单的取模运算)。这样就可以将号段均匀的分配给不同的应用来进行处理,然后每个应用再按照上文提到的将分配给自己的号段丢到线程池中由具体的线程去查询、遍历即可。分布式带来的问题这样看似没啥问题,但一旦引入了分布式之后就不可避免的会出现 CAP 的取舍,这里不做过多讨论,感兴趣的朋友可以自行搜索。首先要解决的一个问题就是:这三个应用怎么知道它自己应该取哪些号段的数据呢?比如 0 号应用就取 0 3 6(这个相当于号段的下标),难道在配置文件里配置嘛?那如果数据量又增大了,对应的机器数也增加到了 5 台,那自然 0 号应用就不是取 0 3 6 了(取模之后数据会变)。所以我们得需要一个统一的调度来分配各个应用他们应当取哪些号段,这也就是数据分片。假设我这里有一个统一的分配中心,他知道现在有多少个应用来处理数据。还是假设上文的三个应用吧。在真正开始遍历数据的时候,这个分配中心就会去告诉这三个应用:你们要开始工作了啊,0 号应用你的工作内容是 0 3 6,1 号应用你的工作内容是 1 4 7,2 号应用你的工作内容是 2 5 8。这样各个应用就知道他们所应当处理的数据了。当我们新增了一个应用来处理数据时也很简单,同样这个分配中心知道现在有多少台应用会工作。他会再拿着现有的号段对 4(3+1台应用) 进行取模然后对数据进行重新分配,这样就可以再次保证数据分配均匀了。只是分配中心如何知道有多少应用呢,其实也简单,只要中心和应用之间通信就可以了。比如启动的时候调用分配中心的接口即可。上面提到的这个分配中心其实就是一个常见的定时任务的分布式调度中心,由它来统一发起调度,当然分片只是它其中的一个功能而已(关于调度中心之后有兴趣再细说)。总结本次探讨了多线程的更多应用方式,如要是如何高效的运行。最主要的一点其实就是尽量的避免加锁。同时对分布式水平扩展谈了一些处理建议,本次也是难得的一行代码都没贴,大家感兴趣的话在后面更新相关代码。也欢迎大家留言讨论。????你的点赞与转发是最大的支持。

November 1, 2018 · 1 min · jiezi

如何看待Spring下单例模式与线程安全的矛盾

前言有多少人在使用Spring框架时,很多时候不知道或者忽视了多线程的问题? 因为写程序时,或做单元测试时,很难有机会碰到多线程的问题,因为没有那么容易模拟多线程测试的环境。那么当多个线程调用同一个bean的时候就会存在线程安全问题。如果是Spring中bean的创建模式为非单例的,也就不存在这样的问题了。 但如果不去考虑潜在的漏洞,它就会变成程序的隐形杀手,在你不知道的时候爆发。而且,通常是程序交付使用时,在生产环境下触发,会是很麻烦的事。Spring使用ThreadLocal解决线程安全问题 我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。 一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程 ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。 或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。线程安全问题都是由全局变量及静态变量引起的。 若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。 1) 常量始终是线程安全的,因为只存在读操作。 2)每次调用方法前都新建一个实例是线程安全的,因为不会访问共享的资源。 3)局部变量是线程安全的。因为每执行一个方法,都会在独立的空间创建局部变量,它不是共享的资源。局部变量包括方法的参数变量和方法内变量。 有状态就是有数据存储功能。有状态对象(Stateful Bean),就是有实例变量的对象 ,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。 无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象 .不能保存数据,是不变类,是线程安全的。 有状态对象: 无状态的Bean适合用不变模式,技术就是单例模式,这样可以共享实例,提高性能。有状态的Bean,多线程环境下不安全,那么适合用Prototype原型模式。Prototype: 每次对bean的请求都会创建一个新的bean实例。 Struts2默认的实现是Prototype模式。也就是每个请求都新生成一个Action实例,所以不存在线程安全问题。需要注意的是,如果由Spring管理action的生命周期, scope要配成prototype作用域线程安全案例 SimpleDateFormat( 下面简称 sdf) 类内部有一个 Calendar 对象引用 , 它用来储存和这个 sdf 相关的日期信息 , 例如 sdf.parse(dateStr), sdf.format(date) 诸如此类的方法参数传入的日期相关 String, Date 等等 , 都是交友 Calendar 引用来储存的 . 这样就会导致一个问题 , 如果你的 sdf 是个 static 的 , 那么多个 thread 之间就会共享这个 sdf, 同时也是共享这个 Calendar 引用 , 并且 , 观察 sdf.parse() 方法 , 你会发现有如下的调用 : Date parse() { calendar.clear(); // 清理calendar … // 执行一些操作, 设置 calendar 的日期什么的 calendar.getTime(); // 获取calendar的时间 } 这里会导致的问题就是 , 如果 线程 A 调用了 sdf.parse(), 并且进行了 calendar.clear() 后还未执行 calendar.getTime() 的时候 , 线程 B 又调用了 sdf.parse(), 这时候线程 B 也执行了 sdf.clear() 方法 , 这样就导致线程 A 的的 calendar 数据被清空了 ( 实际上 A,B 的同时被清空了 ). 又或者当 A 执行了 calendar.clear() 后被挂起 , 这时候 B 开始调用 sdf.parse() 并顺利 i 结束 , 这样 A 的 calendar 内存储的的 date 变成了后来 B 设置的 calendar 的 date 这个问题背后隐藏着一个更为重要的问题 – 无状态:无状态方法的好处之一,就是它在各种环境下,都可以安全的调用。衡量一个方法是否是有状态的,就看它是否改动了其它的东西,比如全局变量,比如实例的字段。 format 方法在运行过程中改动了SimpleDateFormat 的 calendar 字段,所以,它是有状态的。 这也同时提醒我们在开发和设计系统的时候注意下以下三点 :自己写公用类的时候,要对多线程调用情况下的后果在注释里进行明确说明对线程环境下,对每一个共享的可变变量都要注意其线程安全性我们的类和方法在做设计的时候,要尽量设计成无状态的解决办法1. 需要的时候创建新实例: 说明:在需要用到 SimpleDateFormat 的地方新建一个实例,不管什么时候,将有线程安全问题的对象由共享变为局部私有都能避免多线程问题,不过也加重了创建对象的负担。在一般情况下,这样其实对性能影响比不是很明显的。2. 使用同步:同步 SimpleDateFormat 对象public class DateSyncUtil { private static SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”); public static String formatDate(Date date)throws ParseException{ synchronized(sdf){ return sdf.format(date); } } public static Date parse(String strDate) throws ParseException{ synchronized(sdf){ return sdf.parse(strDate); } } } 说明:当线程较多时,当一个线程调用该方法时,其他想要调用此方法的线程就要block ,多线程并发量大的时候会对性能有一定的影响。3. 使用 ThreadLocal :public class ConcurrentDateUtil { private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”); } }; public static Date parse(String dateStr) throws ParseException { return threadLocal.get().parse(dateStr); } public static String format(Date date) { return threadLocal.get().format(date); }} 或ThreadLocal<DateFormat>(); public static DateFormat getDateFormat() { DateFormat df = threadLocal.get(); if(df==null){ df = new SimpleDateFormat(date_format); threadLocal.set(df); } return df; } public static String formatDate(Date date) throws ParseException { return getDateFormat().format(date); } public static Date parse(String strDate) throws ParseException { return getDateFormat().parse(strDate); } } 说明:使用 ThreadLocal, 也是将共享变量变为独享,线程独享肯定能比方法独享在并发环境中能减少不少创建对象的开销。如果对性能要求比较高的情况下,一般推荐使用这种方法。4. 抛弃 JDK ,使用其他类库中的时间格式化类:使用 Apache commons 里的 FastDateFormat ,宣称是既快又线程安全的SimpleDateFormat, 可惜它只能对日期进行 format, 不能对日期串进行解析。使用 Joda-Time 类库来处理时间相关问题 做一个简单的压力测试,方法一最慢,方法三最快,但是就算是最慢的方法一性能也不差,一般系统方法一和方法二就可以满足,所以说在这个点很难成为你系统的瓶颈所在。从简单的角度来说,建议使用方法一或者方法二,如果在必要的时候,追求那么一点性能提升的话,可以考虑用方法三,用 ThreadLocal 做缓存。 Joda-Time 类库对时间处理方式比较完美,建议使用。总结 回到文章开头的问题:《有多少人在使用Spring框架时,很多时候不知道或者忽视了多线程的问题?》 其实代码谁都会写,为什么架构师写的代码效果和你的天差地别呢?应该就是此类你没考虑到的小问题而架构师都考虑到了。 架构师知识面更广,见识到的具体情况更多,解决各类问题的经验更丰富。只要你养成架构师的思维和习惯,那你离架构师还会远吗? ...

October 23, 2018 · 2 min · jiezi

值得保存的 synchronized 关键字总结

该文已加入开源文档:JavaGuide(一份涵盖大部分Java程序员所需要掌握的核心知识)。地址:https://github.com/Snailclimb…本文是对 synchronized 关键字使用、底层原理、JDK1.6之后的底层优化以及和ReenTrantLock对比做的总结。如果没有学过 synchronized 关键字使用的话,阅读起来可能比较费力。两篇比较基础的讲解 synchronized 关键字的文章:《Java多线程学习(二)synchronized关键字(1)》《Java多线程学习(二)synchronized关键字(2)》synchronized 关键字的总结synchronized关键字最主要的三种使用方式的总结修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 。也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。所以如果一个线程A调用一个实例对象的非静态synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 和 synchronized 方法一样,synchronized(this)代码块也是锁定当前对象的。synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。这里再提一下:synchronized关键字加到非 static 静态方法上是给对象实例上锁。另外需要注意的是:尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓冲功能!synchronized 关键字底层实现原理总结synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 在 Java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。所有用户程序都是运行在用户态的, 但是有时候程序确实需要做一些内核态的事情, 例如从硬盘读取数据, 或者从键盘获取输入等. 而唯一可以做这些事情的就是操作系统,synchronized关键字底层优化总结JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。偏向锁引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉。偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步!关于偏向锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。轻量级锁倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。 关于轻量级锁的加锁和解锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁!自旋锁和自适应自旋轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋。百度百科对自旋锁的解释:何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,“自旋"一词就是因此而得名。自旋锁在 JDK1.6 之前其实就已经引入了,不过是默认关闭的,需要通过–XX:+UseSpinning参数来开启。JDK1.6及1.6之后,就改为默认开启的了。需要注意的是:自旋等待不能完全替代阻塞,因为它还是要占用处理器时间。如果锁被占用的时间短,那么效果当然就很好了!反之,相反!自旋等待的时间必须要有限度。如果自旋超过了限定次数任然没有获得锁,就应该挂起线程。自旋次数的默认值是10次,用户可以修改–XX:PreBlockSpin来更改。另外,在 JDK1.6 中引入了自适应的自旋锁。自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定,虚拟机变得越来越“聪明”了。锁消除锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。锁粗化原则上,我们再编写代码的时候,总是推荐将同步快的作用范围限制得尽量小——只在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。ReenTrantLock 和 synchronized 关键字的总结推荐一篇讲解 ReenTrantLock 的使用比较基础的文章:《Java多线程学习(六)Lock锁的使用》两者都是可重入锁两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 APIsynchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。ReenTrantLock 比 synchronized 增加了一些高级功能相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。性能已不是选择标准在JDK1.6之前,synchronized 的性能是比 ReenTrantLock 差很多。具体表示为:synchronized 关键字吞吐量岁线程数的增加,下降得非常严重。而ReenTrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。JDK1.6 之后,synchronized 和 ReenTrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReenTrantLock 的文章都是错的!JDK1.6之后,性能已经不是选择synchronized和ReenTrantLock的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作。参考《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版第13章《实战Java虚拟机》https://blog.csdn.net/javazej…https://blog.csdn.net/qq83864...http://cmsblogs.com/?p=2071你若盛开,清风自来。 欢迎关注我的微信公众号:“乐趣区”,一个有温度的微信公众号。公众号后台回复关键字“1”,你可能看到想要的东西哦! ...

September 7, 2018 · 1 min · jiezi