为啥子要用多线程
提高程序执行效率,比如多线程读取、写入文档等等(摊牌了,我生产中没用过,但这不妨碍我吹牛逼);
怎么使用
两个方法:
1、继承Thread类,重写run方法;
2、实现Runnable或者Callable接口,重写run方法;(其实Thread底层也是通过实现Runnable接口)
通常使用实现后者,因为java是单继承,万一你的线程类还需要继承其他类的话就搞不了了鸭,用后者相对灵活一些;
继承Thread类demo
可以看到线程执行的顺序是随机的
还可以给你自己的线程取名字
小啊giao运行截图
demo代码
package com.jd.clouddemo.test; public class MyThreadDemo extends Thread { public static void main(String[] args) { for(int i = 0;i<10;i++){ MyThreadDemo myThreadDemo = new MyThreadDemo(); myThreadDemo.setName("小啊giao"+i+"号"); myThreadDemo.start(); } } @Override public void run() { // 这里写你需要执行的业务逻辑 System.out.println(Thread.currentThread().getName()+"开始执行"); } }
实现Runnable接口demo
我们再看看上面通过继承Thread类的方法,最好亲自敲一下,感受感受
通过实现Runnable搞的线程类只能通过构造方法来命名,传参也是一样
简单说下实现多线程两种方式的区别
实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
4):线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类
线程池
线程池的作用
怎么用
1、使用现成的
JUC包下的Executors,打开你的idea随便new个类,轻轻敲出Executors,再加个“.”就能看到它的方法
但是阿里巴巴开发规范不推荐用现成的,因为这几种都可能造成内存溢出,所以最好自己创建,这样也能更好的理解线程池原理;
2、自己创建怎么搞
除了下面这几个参数,还有个参数是线程工厂,我们用默认的就行,不用传
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( 5, //核心线程数,可以同时运行的线程数量 10, //最大线程数,任务队列装满的时候,当前可以同时运行的线程数变为这个数 1L,//等待时间,当线程池中的线程数量大于核心线程数,如果没有新的任务过来,核心线程数以外的线程不会立刻被回收,而是等待这个时间到了才进行回收,作用的话我想就是避免线程创建销毁带来的内存消耗吧 TimeUnit.SECONDS,//等待时间的单位 new ArrayBlockingQueue<>(100),//任务队列,100是设置队列容量 new ThreadPoolExecutor.CallerRunsPolicy());//饱和策略,任务队列满了并且当前线程数达到最大线程数会触发这个机制,有四种,先别问,下面会讲
四种饱和策略:
任务队列:
如果当前运行的线程小于corePoolSize,则新任务会直接运行,不会存入队列
如果当前运行的线程大于等于 corePoolSize,则 Executor始终首选将请求加入队列,而不添加新的线程。
如果无法将任务加入队列,则创建新的线程,除非创建此线程后线程数会超出 maximumPoolSize,在这种情况下,会触发饱和策略。
1、无界队列(慎用)
队列大小无限制,常用的为无界的LinkedBlockingQueue,使用该队列做为阻塞队列时要尤其当心,当任务耗时较长时可能会导致大量新任务在队列中堆积最终导致OOM。阅读代码发现,Executors.newFixedThreadPool 采用就是 LinkedBlockingQueue,当QPS很高,发送数据很大,大量的任务被添加到这个无界LinkedBlockingQueue 中,导致cpu和内存飙升服务器挂掉。
2、有界队列
常用的有两类,一类是遵循FIFO原则的队列如ArrayBlockingQueue,另一类是优先级队列如PriorityBlockingQueue。PriorityBlockingQueue中的优先级由任务的Comparator决定。
使用有界队列时队列大小需和线程池大小互相配合,线程池较小有界队列较大时可减少内存消耗,降低cpu使用率和上下文切换,但是可能会限制系统吞吐量。
这个队列会导致部分任务丢失
3、同步移交队列
如果不希望任务在队列中等待而是希望将任务直接移交给工作线程,可使用SynchronousQueue作为等待队列。SynchronousQueue不是一个真正的队列,而是一种线程之间移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接收这个元素。只有在使用无界线程池或者有饱和策略时才建议使用该队列。
再分享个小知识
如果想让多线程的操作完了再执行主线程,在.shutDown后调用threadPoolExecutor的isTerminated方法来判断线程池的任务是否执行完毕;(也可以使用CountDownLatch实现,这个后面单独写一篇博客来讲;)
threadPoolExecutor.shutdown(); while (!threadPoolExecutor.isTerminated()) System.out.println("执行完毕");
引发的血案
多线程虽然牛逼,但是会引起并发问题(线程安全的典故也由此而来),比如有个变量a=1,现在有A、B两个线程去操作它,对它进行+1操作,那讲道理这两个线程执行完后a的值应该为3,但是如果不对变量或方法进行处理,a往往会等于2,线程要对变量值进行修改,就必须先读取他的当前值,问题就出在这儿,A线程先过来读取a=1,兴冲冲的准备进行+1操作,但是在+1操作执行之前,B线程也过来了,A线程的操作对B是不可见的,它也读取到a=1,哦豁,它又不知道A也在进行操作,所以在它看来只进行+1操作就万事大吉了,于是AB都对值为1的a进行+1操作,最终结果也就等于2了;当然也有可能等于3(线程的调度具有随机性,B在A操作完了之后再读取a,此时a=2,+1=3听懂掌声);
如何避免并发问题
对于如何保证线程安全这个话题,完全可以写一本书了,这里面牵涉到java内存模型、jvm内存模型、c++等等可怕的东西,但是好在java为我们提供了一些大宝贝用来保证线程安全,对于初学者,不用太在意底成实现,搞理科一定到先知其然后知其所以然,知道怎么用这些大宝贝即可;
1、synchronized
不墨迹,先看看不加synchronized的运行效果
package com.jd.clouddemo.test; public class MyRunnableDemo implements Runnable { private static int count; public MyRunnableDemo() { count = 0; } public static void main(String[] args) { MyRunnableDemo syncThread = new MyRunnableDemo(); Thread thread1 = new Thread(syncThread, "小啊giao"); Thread thread2 = new Thread(syncThread, "大啊giao"); thread1.start(); thread2.start(); } @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + ":" + (count++)); } /** 虽然不明显,但是可以看出是乱序的 小啊giao:1 大啊giao:0 小啊giao:2 大啊giao:3 大啊giao:5 大啊giao:6 大啊giao:7 小啊giao:4 小啊giao:8 小啊giao:9 */ } }
下面说说synchronized三种用法(还有其它使用场景,这里就不一一列出)
1.1 修饰类
public void run() { for (int i = 0; i < 5; i++) { synchronized (MyRunnableDemo.class) { System.out.println(Thread.currentThread().getName() + ":" + (count++)); } } /** 不管执行多少次,永远保持有序,永远年轻,永远热泪盈眶,嘤嘤嘤 小啊giao:0 小啊giao:1 小啊giao:2 小啊giao:3 小啊giao:4 大啊giao:5 大啊giao:6 大啊giao:7 大啊giao:8 大啊giao:9 */ }
1.2 修饰代码块
public void run() { for (int i = 0; i < 5; i++) { synchronized (this) { System.out.println(Thread.currentThread().getName() + ":" + (count++)); } } /** 不管执行多少次,永远保持有序,永远年轻,永远热泪盈眶,嘤嘤嘤 小啊giao:0 小啊giao:1 小啊giao:2 小啊giao:3 小啊giao:4 大啊giao:5 大啊giao:6 大啊giao:7 大啊giao:8 大啊giao:9 */ }
1.3 修饰方法
//这里加上synchronized public synchronized void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + ":" + (count++)); } /** 不管执行多少次,永远保持有序 小啊giao:0 小啊giao:1 小啊giao:2 小啊giao:3 小啊giao:4 大啊giao:5 大啊giao:6 大啊giao:7 大啊giao:8 大啊giao:9 */ }
2、lock
lock是一个接口,它有很多实现类,我们这里使用ReentrantLock;
public void run() { // 这里写你需要执行的业务逻辑 ReentrantLock reentrantLock = new ReentrantLock(); reentrantLock.lock(); System.out.println(Thread.currentThread().getName()+"读取到的值为:"+param); param = param + 1; System.out.println(Thread.currentThread().getName()+"进行+1操作后的值为:"+param); reentrantLock.unlock(); } /** * Thread-0读取到的值为:1 * Thread-1读取到的值为:1 * Thread-0进行+1操作后的值为:2 * Thread-1进行+1操作后的值为:3 */
3、volatile关键字
这个就牛逼了,很多同步框架的底层都是这玩意儿实现的,因为它能保证变量在内存中的可见性,用人话说就是线程能及时拿到这个变量最新的值,就拿上面那个例子,如果变量a用volatile修饰,对于a来讲就不存在线程不安全的问题了,因为volatile能及时刷新a在内存中的值,A线程执行+1操作后B线程能及时发现;
这玩意儿平时不怎么用,想了一上午也没想到一个很好的例子来证明演示效果,后续想到了再来补充,我觉得这个懂原理即可,嘤嘤嘤;
关于volatile有一道很不错的面试题,就是为什么volatile只能保证可见性,不能保证原子性,可能有些叼毛连题都读不懂,简单举个例:
就拿上面的例子来说,a用volatile修饰,如果你对a进行a++操作,他是保证同步的,因为a++实际上不是一个原子操作,是两个,两个原子操作加起来他就不是原子操作了!!!如果面试问到就这么说,面试官不会再细问,因为他连自己都不知道;
web开发中能用到多线程的场景很少(一般搞app用的多一点),即使跟面试官说没用过,他也能理解,但是这玩意儿就像汽车的安全气囊,你可以不用,但是用的时候你不能没有,你要是说不会,那面试官不得不怀疑你的学习能力和实际开发经验是否作假;