最近在github上写了几个关于多线程的练习, 项目地址:https://github.com/jndf/multithreading-pratice

需要的朋友可以看看,代码如有错误,请多提出指正意见。

下面说一下最近的一些心得。

Thread和Runnable两种实现方式。

这两种方法都是最基础的实现线程的方法,声明线程对象后,通过调用对象的start()方法,来执行线程内部的run()方法。

其中Runnable是由Thread实现的,因此在执行Runnable的任务时,一般将这个对象作为Thread的一个tag来使用。

使用Thread进行实现多线程:

JAVA多线程聊天室实验 java多线程实验总结_java

使用Runnable进行实现多线程:

JAVA多线程聊天室实验 java多线程实验总结_java_02

进行一个生产者消费者模型的构建:

生产者消费者模型是一个较为经典的多线程应用场景,在该场景中,引用到了(wait/notify)等待/通知机制

等待/通知机制:

wait/notify是对象的方法,而不是线程的。体现在资源上,同一时刻,对象的资源只能被同一个线程使用,线程运行完毕后,需要释放资源,并进入wait状态,对象再启动notify方法,唤醒其他线程运行,之后这个线程再次wait,一直循环到线程运行结束。

举个例子:有一群人在一扇门外等着,每个人都想进到门里去,但同时最多只能进去一个人,在这个例子里,门就是对象,人们就是线程;一个人进门去,相当于占用对象资源,其他人在门外等着(wait),当这个人从屋里出来,门开着(notify),就又有人可以进去,而门外的人再次进入wait状态,一直循环到事情结束。

这里提一下notify,和notifyAll,一个是门开着然后只有一个人看到了,另一个是所有人都看到了门开着。

也就是说notify只会随机唤醒一个等待(也可以叫做阻塞)线程,而notify All唤醒了所有等待线程,前者会导致线程效率不高,后者会引起线程线程竞争,这时要做的就是确保线程安全;

关于线程安全的部分留到下一篇文章讲

当然还有另一种办法,Thread.join,是让线程依次执行的一个方法具体表现为当一个线程执行完毕,才能执行下一个线程。

/**
 *写一个生产者消费者模型
 * (发生了死锁,情况如下):
 * 1x已无产品可消费,等待生产
 * 4x已无产品可消费,等待生产
 * 2x已无产品可消费,等待生产
 * 3x已无产品可消费,等待生产
 *
 */
public class Demo3 implements Runnable{

    private String name;
    private List<String> list = new ArrayList<String>();
    private final int size = 10;

    public void produce(int num) throws Exception {
        while (true) {
            synchronized (list) {
                while (list.size() + num > size) {
                    System.out.println(Thread.currentThread().getName()+"生产过剩,等待消费");
                    list.wait();
                }
                System.out.println(Thread.currentThread().getName()+"正在生产");
                for (int i = 0; i < num; i++) {
                    list.add("hello, world");
                }
                list.notifyAll();
            }
            Thread.sleep(1000);
        }
    }

    public void consume() throws Exception {
        while (true) {
            synchronized (list) {
                while (list.size() == 0) {
                    System.out.println(Thread.currentThread().getName()+"已无产品可消费,等待生产");
                    list.wait();
                }
                System.out.println(Thread.currentThread().getName()+"正在消费");
                list.remove(0);
                list.notifyAll();
            }
            Thread.sleep(1000);
        }
    }

    public void setName(String name) {
        this.name = name;
    }

    public void run() {
        try {
            while("producer".equals(name)){
                produce(1);
            }
            while("consumer".equals(name)){
                consume();
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        try {
            Demo3 myThread = new Demo3();
            myThread.setName("producer");
            Thread t1 = new Thread(myThread,"1x");
            Thread t4 = new Thread(myThread,"4x");
            t1.start();
            t4.start();
            Thread.sleep(1);
            myThread.setName("consumer");
            Thread t2 = new Thread(myThread,"2x");
            Thread t3 = new Thread(myThread,"3x");
            t2.start();
            t3.start();
        }
        catch (Exception e) {

        }
    }
}

线程框架Executor,以及对Callable的使用:

这个框架主要是可以创建一个线程池,也不需要写原生的线程对象来进行操作,比较方便。

使用方式见代码注释

/**
 * 写一个Executor 多线程框架
 *
 */
public class Demo4{

    /**
     * Executor包括四种创建线程池的对象
     * Java通过Executors提供四种线程池,分别为:
     * newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
     * newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
     * newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
     * newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
     */



    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建一个定长的线程池 例子是10个线程的
            ExecutorService executorService = Executors.newFixedThreadPool(10);

        //1·框架执行execute方法,来执行一个线程类,但是没有返回值
            //executorService.execute(new Demo2.Runner1());

        //2·使用submit执行线程,可以得到返回值
            //Future<String> submit = executorService.submit(new Call1());
            //System.out.println(submit.get());

        //3·CompletionService 可以将已完成任务与未完成的任务分离出来 ExecutorCompletionService此类将安排那些完成时提交的任务,把它们放置在可使用 take 访问的队列上
             CompletionService<String> completionService = new ExecutorCompletionService<String>(executorService);
             completionService.submit(new Call1());
             Future<String> future =completionService.take();
             System.out.println(future.get());

        //4·submit一个runnable和一个callable的区别
        //主要是靠futureTask区别,具体在下面


        //可以使用lambda表达式进行代码优化
    }


    static class Call1 implements  Callable {
        public String call() {
            return "返回callable线程";
        }
    }

    static class Run1 implements  Runnable {
        public void run() {
            //return "返回callable线程";
        }
    }
}

FutureTask的使用

它实现了Runnable和Future,可以用来执行Runnable和Callable,都能获得返回值:

Callable当然能返回,这里说一下void泛型的Runnable,具体返回方法,我把源码放出来:

JAVA多线程聊天室实验 java多线程实验总结_JAVA多线程聊天室实验_03

上面是Callable下面是Runnable,而result是作为参数输入的,这里我们看到调用了

Executors.callable(runnable, result)

JAVA多线程聊天室实验 java多线程实验总结_java_04

JAVA多线程聊天室实验 java多线程实验总结_线程池_05

于是明白了,Runnable返回值被执行并有返回值是因为,返回值是一个预期结果,我们去执行这个线程并输入预期结果,执行成功后,这个result会被返回回来。

FutureTask还适用于执行多任务计算的使用场景,在高并发环境下确保任务只执行一次,以及通过cancel方法来取消掉线程等功能。