最近在github上写了几个关于多线程的练习, 项目地址:https://github.com/jndf/multithreading-pratice
需要的朋友可以看看,代码如有错误,请多提出指正意见。
下面说一下最近的一些心得。
Thread和Runnable两种实现方式。
这两种方法都是最基础的实现线程的方法,声明线程对象后,通过调用对象的start()方法,来执行线程内部的run()方法。
其中Runnable是由Thread实现的,因此在执行Runnable的任务时,一般将这个对象作为Thread的一个tag来使用。
使用Thread进行实现多线程:
使用Runnable进行实现多线程:
进行一个生产者消费者模型的构建:
生产者消费者模型是一个较为经典的多线程应用场景,在该场景中,引用到了(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,具体返回方法,我把源码放出来:
上面是Callable下面是Runnable,而result是作为参数输入的,这里我们看到调用了
Executors.callable(runnable, result)
于是明白了,Runnable返回值被执行并有返回值是因为,返回值是一个预期结果,我们去执行这个线程并输入预期结果,执行成功后,这个result会被返回回来。
FutureTask还适用于执行多任务计算的使用场景,在高并发环境下确保任务只执行一次,以及通过cancel方法来取消掉线程等功能。