天天看点

一个小demo告诉你如何通过Future多线程并发编程

1.java多线程实现的几种方式:

  1. 继承Thread类创建线程
  2. 实现Runnable接口创建线程
  3. 实现Callable接口通过FutureTask包装器来创建Thread线程

2.给一个demo样例主要根据多线程 Future模式角度来讲解多线程并发编程:

       至于为什么选择第3种实现方式FutureTask,当然是因为这种方式可以有返回结果啊。上述1,2两种方式中线程run()方法返回类型是void,那要它何用?溜了溜了。

package threadTest;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

/**
 * @author hz
 * @Description: 测试多线程并行调用两个接口数据。
 * @create 2019-05-28
 */
public class FutureTaskTest {
    public Map<String,String> getUserInfo() throws Exception{
        Map<String, String> map = new HashMap<String,String>();
        //1.获取用户基本信息
        Callable<String> userInfoCallable=new Callable<String>() {
            @Override
            public String call() throws Exception {
                Thread thread0 = Thread.currentThread();
                System.out.println("thread0启动:"+thread0.getName());
                thread0.sleep(3000);
                //模拟调用获取用户mapper接口
                return "返回用户信息数据";
            }
        };
        FutureTask userInfoFutureTask = new FutureTask(userInfoCallable);
       new Thread(userInfoFutureTask).start();

        //2.获取页面展示数据信息
        Callable<String> indexDataCallable=new Callable<String>() {
            @Override
            public String call() throws Exception {
                Thread thread1 = Thread.currentThread();
                System.out.println("thread1启动:"+thread1.getName());
                thread1.sleep(5000);
                //模拟调用获取页面数据mapper接口
                return "返回页面数据信息";
            }
        };
        FutureTask indexDatafutureTask = new FutureTask(indexDataCallable);
        new Thread(indexDatafutureTask).start();//开启线程

        //3.获取对象并合并数据(调用get()方法,该线程会进入阻塞状态,直到线程返回结果才会继续执行下面代码)
        String userInfo = userInfoFutureTask.get().toString();
        map.put("userInfo",userInfo);

        String indexData = indexDatafutureTask.get().toString();
        map.put("indexData",indexData);
        return map;
    }

    public static void main(String[] args) {
        System.out.println("主线程main:"+Thread.currentThread().getName());
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");

        System.out.println("开始"+sdf.format(new Date()));
        FutureTaskTest futureTaskTest = new FutureTaskTest();
        Map<String, String> userInfo=null;
        try {
            userInfo = futureTaskTest.getUserInfo();
            if(userInfo!=null){
                System.out.println(userInfo.get("userInfo"));
                System.out.println(userInfo.get("indexData"));
            }
            System.out.println("结束"+sdf.format(new Date()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
           

    输出结果打印:

一个小demo告诉你如何通过Future多线程并发编程

3.讲解一下上述demo中几个重要的点

     3.1.Callable<String> userInfoCallable=new Callable<String>() ,String 代表的是调用接口后返回你所需要的数据类型。

     3.2.userInfoFutureTask.get() //获取该线程执行后的返回结果 (重点)

     FutureTask类中的get()方法大家可以关注一下源码,水有点小深。因为当时获取调用用户接口线程内容的这一段代码

     String userInfo = userInfoFutureTask.get().toString();

     我没有写在示例中那个位置,而是第二个获取页面展示数据接口Callable之前,然后发现,运行时间差永远是两个休眠时间相加。wtf?那我并发编程意义在哪?当时就蒙了,难道我玩的不是多线程?最后把这个代码放到后面去才运行正常,刚好是休眠时间最长的那个。于是翻开get源码:

public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        return report(s);
    }
           

继续点进awaitDone方法:

private int awaitDone(boolean timed, long nanos)
        throws InterruptedException {
        final long deadline = timed ? System.nanoTime() + nanos : 0L;
        WaitNode q = null;
        boolean queued = false;
        for (;;) {
            if (Thread.interrupted()) {
                removeWaiter(q);
                throw new InterruptedException();
            }

            int s = state;
            if (s > COMPLETING) {
                if (q != null)
                    q.thread = null;
                return s;
            }
            else if (s == COMPLETING) // cannot time out yet
                Thread.yield();
            else if (q == null)
                q = new WaitNode();
            else if (!queued)
                queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                     q.next = waiters, q);
            else if (timed) {
                nanos = deadline - System.nanoTime();
                if (nanos <= 0L) {
                    removeWaiter(q);
                    return state;
                }
                LockSupport.parkNanos(this, nanos);
            }
            else
                LockSupport.park(this);
        }
    }
           

       这一段源码简而言之意思就是:死循环判断该线程是否拿到结果,如果没拿到就执行LockSupport.park(),使该线程挂起,等到返回数据的线程执行完毕,通过unpark()唤醒该线程,则就可以拿到数据。

        3.3  synchronized与notifyAll和wait搭配使用的机制与park机制的区别:

       synchronized与notifyAll和wait:意思就是给线程加锁,只有获得对象锁才能进入,当线程调用wait()后就会释放对象锁,线程进入阻塞状态,需要等待另一个线程获得对象锁并notifyAll唤醒它才能继续执行。

而park机制就是我前面讲到过的两个方法,分别是park()挂起,unpark唤醒。

        当线程使用notify()/wait()或notifyAll()/wait()进行协作时,在wait()的一方可能会错失notify()/notifyAll()的信号,而一直处于wait()中造成死锁。

那park机制优点就来了,他不会管你线程哪个先执行后执行,都会通过unpark()方法去唤醒。避免了死锁现象。

4. 多线程并发编程的意义

       以前单线程中某一响应需要调用多个接口拿到数据,我们只能等上个接口调用完了才能执行下个接口调用(一般大多数人公司里写的小项目都是这种吧)。而多线程就可以开启多个线程分别调用接口拿到数据,例如一个页面需要用户信息即用户接口(2s),以及页面展示内容即页面数据内容接口(4s)。那我们就开启两个线程,分别调用两个接口,最后只要两个数据合在一起,一起返回就行了。然后产生的收益就是:以前单线程调用两个接口分别需要2+4 s才能执行完。现在只需要最大的接口调用时间即4s就好了。