一.概述
上一篇把集合的最后一部分学习完了。Java集合(二)
这一篇开始学习Java并发线程有关的基本使用,比如我们在电脑运行很多软件,就是一种多进程的体现,注意并发的进程数目并不是由CPU的数目制约的。一个进程又分为了很多线程,每一个任务被称为一个线程。那么多进程和多线程有什么区别呢?本质区别在于每个进程拥有自己的一整套变量,而线程则共享数据
。本篇的所有代码会以资源的形式发布。
二.线程
1.什么是线程
书中列举了一个小球的实例。点击start按钮开始小球开始移动,当小球碰触到墙壁时就会弹开。但是想要关闭或者重新开始一个小球就要等待小球静止后才可以进行中断,这样显然是不合理的。这时候就要使用多线程来让多个小球同时进行。
2.线程状态
- New(新创建)
- Runnable(可运行状态)
- Blocked(被阻塞状态)
- Waiting(等待)
- Time waiting(计时等待)
- Terminated(被终止)
(1)新创建
Thread t = new Thread(); 创建一个线程但还没有运行
(2)可运行状态
调用start()方法变成可运行状态,这个时候也有可能没有运行,因为此时CPU可能并没有进行调度。
(3)被阻塞和等待线程
- 当想要获取一个对象的内部锁时,没有获取就进行阻塞状态,直到获取进入非阻塞状态
- 当线程等待另一个线程调用时,自己进入等待状态。
- 当线程将进入休眠时间时,将会进入计时等待状态。
(4)终止
run方法执行完毕或者出现异常。
3.线程属性
(1)线程优先级
可以通过设置线程的优先级,来选择线程的优先级,但是最好不要设置优先级,因为如果总会进行高优先级的线程会导致低优先级的线程长时间无法得到响应。
(2)守护线程
守护线程的唯一用途是为其他线程提供服务。比如计时线程,定时为其他线程发送定时信号等。
三.同步
线程之间会共享同一个数据,如果同时对一个数据的修改和读取会是共享数据变得混乱,这个时候就需要进行处理。
(1)竞争条件
这里列举了一个银行账户间存钱的实例,由于没有实现同步的方法,在银行转账之间出现了总额的错误,因为同一个账户进行存取总额应当保持不变。
(2)竞争条件详解
account[to] += amount;
问题在于这个操作并不是原子性操作;
该语句将会被执行为
首先将account[to]加载到寄存器,增加amount,最后将结果写会account[to]。
假定线程1执行步骤1和2被剥夺了运行权。这个时候线程2开始执行修改了acoount数组中的同一个内容,在执行线程1的最后的步骤。这样就会擦数线程2对此数据所做的更新。需要解决的就是确保线程1运行完毕后线程2才可以运行,这样就解决了这个问题。
(3)锁对象
一共有两种方案来进行,第一种就是使用锁加条件来实现同步,另一种就是使用synchronized关键字来实现同步。
锁和条件的关键之处:
- 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。
- 锁可以管理试图进入被保护代码段的线程。
- 锁可以拥有一个或多个相关的条件对象。
- 每个条件对象管理那些已经进入被保护的代码段但不能运行的线程。
synchronized利用的也是锁和条件只不过使用的是对象内本身存在的一个锁以及条件。
内部锁和条件的一些局限:
- 不能中断一个正在试图获得锁的一个线程。
- 试图获得锁时不能设定超时。
- 每个锁仅有单一的条件,可能是不够的。
那么这两种同步的方法该使用哪一种呢?
- 最好既不使用Lock/Condition也不使用synchronized。可以使用阻塞队列来同步完成一个共同任务的线程。
- 如果synchronized适合你的程序,就先使用这种方案。
- 如果有特别需要时才使用锁加条件的形式。
(4)监视器
监视器具有如下特性:
- 监视器是只包含私有域的类。
- 每个监视器类的对象有一个相关的锁。
- 使用该锁对所有的方法进行加锁
- 该锁可以有任意多个相关条件。
(5)死锁
用银行存取的实例说明,当线程1进行转账,发现没有足够的余额,则线程进入等待状态,进行线程2,线程2也发现没有足够的余额,这样导致没有能够调用signalAll的方法,这些等待的状态无法被唤醒,最后进入死锁。
(6)线程局部变量
例如simpledateformat方法不是一个线程同步的方法,这样会导致格式化混乱,可以将此方法放进一个线程来运行
public static final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(()-> new SimpleDateFormat());
// 在一个给定线程中首次调用get()方法会调用initialValue方法。get方法会返回属于当前线程的那个实例
String date = dateFormat,get().format(new Date());
(7)锁测试和超时
(8)读/写锁
对所有的get方法加上读锁,所有的set方法加上写锁
四.阻塞队列
对于实际编程来说,应该尽可能远离底层结构。可以使用一个或多个队列来解决线程同步问题。使用队列可以安全的从一个线程向另一个线程传递数据。
五.线程安全的集合
六.Callable和Future
七.线程池
方法 | 描述 |
---|---|
newCachedThreadPool | 必要时创建新线程,空闲线程会被保留60秒 |
newFixedThreadPool | 该线程包含固定数量的线程,空闲线程会一直被保留 |
newSingleThreadExecutor | 只用一个线程的池,该线程顺序执行每一个提交的任务 |
newScheduledThreadPool | 用于预定执行而构建的固定线程池 |
newSingleThreadScheduledExecutor | 用于预定执行而构建的单线程池 |
八.总结
这一篇学习了线程的基本使用,如何达到线程间的同步等等。
下一篇学习Java8特性的一大改变流的基本使用
有些可能我理解的不够深刻,大家如果觉得我说的不够详细可以参考我的推荐书,详细的看一下。欢迎大家评论。第一时间我会回复大家。谢谢!