别人的多线程:
你的多线程:
这几天看了看多线程,虽然还是一脸懵,不过已经比之前有头绪多了,之前看的关于线程的知识都比较散,现在希望从自己的理解出发,将这些线程相关的东西连成一条线。文章以思路为主,整理我自己关于线程的逻辑思考,设计具体的实现不多,都给出了可参考的博文,如有不对请指正
使用多线程是为了提高效率,让多个线程并行地执行,而多线程主要要解决的问题就是要让并发的线程之间能有有效的共享资源以及能相互合作。
明确两个方向:线程之间的关系与共享资源与线程的关系,这两个方向都是以访问共享资源为核心的
- 线程之间存在两种关系:互斥与同步,互斥是一种特殊的同步。
1.互斥关系就是一个线程在访问一个共享资源的时候,其他线程都得等待这个线程。
2.同步关系就是多个线程之间存在合作的关系,即有一个先后的顺序。
- 从共享资源的角度出发,线程对资源的操作也有两种
1.多个线程行为一致地操作同一个共享数据
2.多个线程行为不一致地操作同一个共享数据
- 随着线程孕育而生的事物
1.线程的状态和基础使用
2.管理多个线程——线程池
3.互斥同步的实现——锁
4.对共享资源安全的访问——装载共享资源的并发队列(阻塞队列与非阻塞队列)
5.让线程之间相互合作——同步器,wait()方法等
现在具体说说第三点中的各个玩意
一.线程的状态和基础使用
1.线程的状态转换
注意一定是先到Runnable就绪状态才能变为Running运行态。
线程状态的参考博文:https://www.cnblogs.com/happy-coder/p/6587092.html 图也来自该文
2.线程的基础使用
- 继承Thread类,覆写run方法
- 实现Runnable接口,覆写run方法
- 实现Callable接口,覆写call方法
三者的对比:
由于java是单继承,所以如果已经继承了某个类,则不好再去继承Thread方法,因此实现Runnable接口的方法更为常用。
实现Callbale接口覆写call方法可以有返回值,返回值通过FutureTask类进行封装。
不管是实现Runnable接口还是实现Callable接口,都要通过新建Thread类对象,传入接口实现类对象作为构造函数参数的方式来新建线程:
Thread thread = new Thread(new Runnable(){ @Override public void run(){} });
Thread thread = new Thread(new Callable(){
@Override
public Integer call() { return 123; } });
启动线程:
thread.start();
如果是使用thread.run()或者thread.call(),那么就只是对run方法和call方法的普通调用,不是线程的启动
3.线程的其他方法
Thread.sleep(mill) ;当前线程休眠,参数单位毫秒
Thread.yield(); 声明该线程已经完成使命,建议线程调度器可以切换其他线程
interrupted();中断线程,也可以在循环体中使用该方法来判断线程是否处于中断状态
private static class MyThread extends Thread { @Override public void run() { while (!interrupted()) { // .. } } }
二.管理多个线程——线程池
使用线程池是为了减少创建和销毁线程的次数,每个线程都可以被重复使用
接口:ExecutorService
工具:Executors
常用的线程池:固定线程数量的线程池FixedThreadPool,缓存线程池CachedThreadPool,单一线程池SingleThreadPool,延时线程池ScheduleThreadPool
使用方法:新建线程池对象与线程后,将线程提交入线程池即可(会自动执行线程);使用shutdown和shutdownNow可以关闭线程池。
关于线程池的具体使用:https://www.cnblogs.com/aaron911/p/6213808.html
三.互斥同步的实现——锁
对共享资源的访问的互斥同步由锁来实现,一个线程得到锁后把资源锁起来访问,访问完之后释放锁,再由下一个资源来获取锁之后访问。
java的锁机制有两种,分别是JVM实现的synchronized和JDK实现的ReentrantLock(可重用锁)
- synchronized:对象锁,只能作用与同一个对象。弄清楚synchronized作用的对象是使用的核心,他可以作用于一个代码块,一个方法以及一个类
- ReentrantLock:可重用入锁,可重入的意思就是一个线程中有多个方法,其中一个方法获取了锁,后面的其他方法就可以使用到自己已经获取到的锁。这样避免了同一个线程由于多次获取锁而导致死锁的情况。使用ReentrantLock要先执行lock方法上锁,然后再在执行完后使用unlock方法释放锁。
两种锁的使用:https://www.jianshu.com/p/abe9104bb421
两种锁的比较:
1.synchronized是由JVM实现的,性能较好且会被优化;ReentrantLock中有许多高级功能,例如可以绑定多个条件等
2.等待可中断:持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待。ReentrantLock 可中断,而 synchronized 不可中断
3.synchronized 中的锁是非公平的,哪个线程抢到就是哪个线程的,不用排队。ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
一般情况下用synchronizrd,因为是JVM原生的。如果要用ReentrantLock 的高级功能才使用ReentrantLock
四.对共享资源安全的访问——装载共享资源的并发队列(阻塞队列与非阻塞队列)
这里就非常的热闹了。。。
并发队列包含阻塞队列和非阻塞队列
1.阻塞队列:阻塞队列插入数据时,如果队列已满,线程将会被阻塞直到队列不满;从阻塞队列取出数据时,如果队列已空,线程将会阻塞等待直到队列非空
- BlockingQueue:阻塞队列接口
- ArrayBlockingQueu:有界阻塞队列,可以定义大小,内部是数组
- DelayQueue:延迟队列,对插入的元素持有一定时间,到期才给插入。注入的元素必须实现java.util.concurrent.Delayed接口
- LinkedBlockingQueue:链式接口实现的阻塞队列,可以定义大小,不定义就是Integer.MAX_VALUE
- PriorityBlockingQueue:无界的阻塞队列,排序规则和PriorityQueue一样。不接受null。插入进去的元素必须要实现Comparable接口,排序需要自己实现的Comparable
- SynchronizedQueue:特殊的阻塞队列,内部只能有一个元素。
2.非阻塞队列:非阻塞队列的执行不会被阻塞,在底层使用的是CAS(compare and swap)来实现
关于CAS可以看我的另一篇:https://blog.youkuaiyun.com/weixin_40616523/article/details/86427962(里面还有一些无同步方案)
- ConcurrentLinkedQueue:非阻塞无界链表队列,基于链表实现,是无界的
- ConcurrentHashMap:基于分段锁实现的线程安全的HashMap
- ConcurrentSkipListMap:非阻塞Hash跳表集合
3.另外说一个java.util.concurrent.atomic包,里面都是原子类,能保证变量的原子性,也是无同步方案的一种实现。这里要先了解线程的内存模型和三个特性:
https://www.cnblogs.com/chihirotan/p/6486436.html
五.让线程之间相互合作——同步器,wait()方法等
juc包中的一些同步器可以实现线程的相互合作
- CountdownLatch:
内部维护一个计数器,当每次调用countDown方法的时候,计数器都会-1,减到0时,所有因为调用了await()而等待的线程都会被唤醒
-
CyclicBarrier:
内部维护一个计数器,每次有线程调用await()方法的时候,计数器都会-1,直到计数为0,然后所有等待的线程都会被唤醒
-
Semaphore:可以控制一个资源能同时被多少线程访问。每个对象覆写run方法都要使用semaphore对象的acquire()方法来获取访问,如果同时访问的已经达到上线,则等待。访问完毕之后要在finally语句块中调用semaphore对象的release()方法释放
线程类本身或者Object类的一些可以让线程写作的方法:
- join() :在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。A线程中调用B线程的join方法,会让B先完成,A再继续
- wait(),notify(),notifyAll():调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。这三个方法是Object类的方法