1.线程:
要解释线程,就必须明白什么是进程
进程时系统资源分配的最小单位,线程是系统调度的最小单位。一个进程内的线程之间是可以共享资源的。
每个进程至少有一个线程存在,即主线程。
1.1 多线程的优势 - 增加运行速度
- 多线程在一些场合下是可以提高程序的整体运行效率的
1.2 Java语言中,创建线程的基本方法 (本质核心都需要有一个Thread对象出来)
- 创建一个Thread对象 + 关联任务
-
- 创建一个
Thread
类,并且复写run
方法 (覆写了线程的 任务)new
该类的对象
- 创建一个
-
- 实现
Runnable
的一个类,并覆写run
方法 (覆写了任务)
- 实现
-
- 继承一个
Thread
类,覆写run
方法 把该类的对象当作Runnable
对象处理
- 继承一个
-
- 把
Thread
对象对应的线程加入到就绪队列中(随时可以被调度开始执行)线程对象.start();
这里是start
不是run
这里结束之后,才真正意义上出现一个调度单位!
Thread 概念 ———– 一个线程 Runnable 概念 ——– 一个任务
public class HowToInstanceThread {
static class A extends Thread {
@Override
public void run() {
System.out.println("我是A类");
}
}
static class B implements Runnable {
@Override
public void run() {
System.out.println("我是B类");
}
}
public static void main(String[] args) {
Thread thread;
{
// 1. 直接 new A 类的对象,本身就是一个 Thread 对象
A a = new A();
thread = a;
thread.start();
}
{
// 2. new B 类的对象,是一个 Runnable,作为任务传递给线程对象
B b = new B();
// thread = b; 这个不能指向
thread = new Thread(b);
thread.start();
}
{
// 3. new A 类的对象,但把该对象,当作 Runnable 使用
// 因为 Thread 本来就实现了 Runnable
A a = new A();
thread = new Thread(a);
thread.start();
}
}
}
1.3 Who First 问题
我们加入就绪队列的时机是确定的,但什么时候被调度到CPU不确定(随机),什么时候被从CPU上调度下来不确定(随机) 但为什么现象上看起来固定? 主线程现在能创建A/B两个线程,代表现在主线程占据着CPU呢! —- 我们的任何代码执行的前提,都是该线程拥有CPU 主线程创建两个线程+打印10次main需要的时间,远远小于时间片,所以,可以认为时间片耗尽之前,主线程可以把所有事情都干完!只有 main执行结束放弃CPU,A和B才有资格抢CPU! 大部分OS实现的时候会考虑公平性 —- 先就绪的先被调度,所以,因为a. start()先被执行,所以大概率,A线程先被拥有CPU,然后也是因为数据很少,所以就把事情都干完了再轮到B
2.Thread 类及常见方法
2.1 关于线程的常见属性:
Thread t = Thread.currentThread();
long id = t.getId();
System.out.println("线程的id: " + id + ": " + t.getId());
System.out.println("线程的名字: " + id + ": " + t.getName());
System.out.println("线程的优先级: " + id + ": " + t.getPriority());
System.out.println("线程的状态: " + id + ": " + t.getState());
System.out.println("线程是否存活: " + id + ": " + t.isAlive());
System.out.println("线程是否是后台线程: " + id + ": " + t.isDaemon());
线程的本质是一个调度单位!
我i什么说线程被从CPU上调度下来是随机的:不以用户看到的任何方法为边界,甚至不以一条语句为边界。而是随时随地地都可能发生,用户代码中完全无法预测!
线程的随机性!!!
来源:线程调度 1.什么时候排到 CPU 是随机的 2.什么时候从 CPU 上切换下来是随机的 无法以我们看到的方法(甚至是语句)为间隔!肯定是以 CPU 指令为单位
一个线程内部的顺序是一定的! 但线程和线程之间的顺序不做任何保证。出现任何顺序都是合理的!
2.2 线程的操作:
2.3 启动一个线程
start()
之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。
- 覆写 run 方法是提供给线程要做的事情的指令清单
- 线程对象可以认为是把 李四、王五叫过来了
- 而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了。
2.4 中断一个线程
停止线程 – 通知 + 收取通知 + 停止
目前常见的有以下两种方式:
- 通过共享的标记来进行沟通
- 调用 interrupt() 方法来通知
示例一
public class ThreadDemo {
private static class MyRunnable implements Runnable {
public volatile boolean isQuit = false;
@Override
public void run() {
while (!isQuit) {
System.out.println(Thread.currentThread().getName()
+ ": 别管我,我忙着转账呢!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()
+ ": 啊!险些误了大事");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
System.out.println(Thread.currentThread().getName()
+ ": 让李四开始转账。");
thread.start();
Thread.sleep(10 * 1000);
System.out.println(Thread.currentThread().getName()
+ ": 老板来电话了,得赶紧通知李四对方是个骗子!");
target.isQuit = true;
}
}
示例二
public class Thread2 {
private static class MyRunnable implements Runnable {
@Override
public void run() {
// 两种方法均可以
while (!Thread.interrupted()) {
//while (!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName()
+ ": 别管我,我忙着转账呢!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName()
+ ": 有内鬼,终止交易!");
break;
}
}
System.out.println(Thread.currentThread().getName()
+ ": 啊!险些误了大事");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
System.out.println(Thread.currentThread().getName()
+ ": 让李四开始转账。");
thread.start();
Thread.sleep(10 * 1000);
System.out.println(Thread.currentThread().getName()
+ ": 老板来电话了,得赶紧通知李四对方是个骗子!");
thread.interrupt();
}
}
重点说明下第二种方法:
- 通过
thread
对象调用interrupt()
方法通知该线程停止运行 thread
收到通知的方式有两种:-
- 如果线程调用了
wait
/join
/sleep
等方法而阻塞挂起,则以InterruptedException
异常的形式通知,清除中断标志
- 如果线程调用了
-
- 否则,只是内部的一个中断标志被设置,thread 可以通过
①
Thread.interrupted()
判断当前线程的中断标志被设置,清除中断标志 ②Thread.currentThread().isInterrupted()
判断指定线程的中断标志被设置,不清除中断标志
- 否则,只是内部的一个中断标志被设置,thread 可以通过
①
-
第二种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到
附录
方法 | 说明 |
---|---|
public void interrupt() | 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位 |
public static boolean interrupted() | 判断当前线程的中断标志位是否设置,调用后清除标志位 |
public boolean isInterrupted | 判断对象关联的标志位是否设置,调用后不清除标志位 |
2.5 等待一个线程 join()
Thread
中,join()
方法的作用是调用线程等待该线程完成后,才能继续往下运行
方法 | 说明 |
---|---|
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等millils毫秒 |
public void join(long millis, int nanos) | 同理,但可以更高精度 |
2.6 获取当前线程引用
方法 | 说明 |
---|---|
public static Thread currentThread(); | 返回当前线程对象的引用 |
2.7 休眠当前线程
有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证休眠时间是大于
等于休眠时间的。
方法 | 说明 |
---|---|
public static void sleep(long millis) throws InterruptedException | 休眠当前线程millis毫秒 |
public static void sleep(long millis, int nanos) throws InterruptedException | 可以更高精度的休眠 |
3 线程的状态
3.1 线程的所有状态
NEW RUNNABLE BLOCKED WAITING TIMED_WAITING TERMINATED
3.2 线程状态和状态转移的意义
4.多线程带来的风险 线程安全(重点)
4.1 线程安全的概念
想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
即就是 100% 正确
4.2 线程不安全的原因
-
- 原子性
-
- 内存可见性
-
- 代码重排序
4.2.1 原子性
什么是原子性
- 1.Java中的一条语句,对应不一定是一条字节码,更不一定是一条CPU指令
- 2.线程的调度中有随机性存在。什么时候从CPU上被调度下来 以及 什么时候被调度回 CPU 上
我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。 那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样 就保证了这段代码的原子性了。 有时也把这个现象叫做同步互斥,表示操作是互相排斥的。 一条 java 语句不一定是原子的,也不一定只是一条指令 比如刚才我们看到的 n++,其实是由三步操作组成的:
- 从内存把数据读到 CPU
- 进行数据更新
- 把数据写回到 CPU
不保证原子性会给多线程带来什么问题
如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的
关于变量赋值的原子性知识
原子性 (atomic) – 认为原子不可以再分割
JVM设计时,是按照 32 bit 为最小操作单位设计的
long
/ double
这两个 64bit 的不具备原子性
double num = 2L;
不具备原子性,因为long
类型是 64bit 的,所以,num = 2L
可以被分割为 高32位赋值 + 低32位赋值
看这两种情况
4.2.2 内存可见性
-
主内存-工作内存 为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题。
由于高速缓存的存在,导致不一定看到最新的结果,进而导致内存可见性问题 如果线程被调度到另外的CPU上了,可能出现之前计算的结果仍在原来的CPU的高速缓存上,所以看到的计算结果就不对了
Java 内存模型 (Java Memory Model / JMM) 规定 所有线程都有自己的工作内存,线程要操作任何数据,首先需要把数据从主内存加载到工作内存中。 线程就在工作内存中进行操作,在合适的机会,把计算结果,同步回工作内存中
4.2.3 代码重排序
什么是代码重排序
一段代码是这样的:
- 去前台取下 U 盘
- 去教室写 10 分钟作业
- 去前台取下快递
如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序
为什么要代码重排序
很多时候,重排序后的执行效率更高
谁会去做代码重排序
代码重排序是有基本要求的
单线程情况下,重排序后的结果得和重排序之前效果一致
多线程情况下,重排序可能带来的问题
重排序之后,出现的结果和一开始的预期完全不一致了!
关于重排序,JMM 还有一个规定:happend-before 原则
规定一些哪些语句必须在那些语句之前
《如何写 出一个线程安全的代码》
1.首先分析,代码中是否需要特意考虑线程不安全的情况
- 1.考虑为什么必须要用多线程! 一- 不要滥用多线程
- 2.需要用到多线程了,是否可以让线程之间不进行数据的共享,各干各的
- 3.必须共享的情况下,能否让共享数据只读
2.必须有多线程而且有共享而且必须修改一-一定需要从原子性/内存可见性/代码重排序角度分析代码中哪里有隐患
5.通信-对象的等待集 wait set
- wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。“直到其他线程调用此 对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”)
- notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的 线程。
- wait(long timeout)让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的notify()方法或 notifyAll() 方法, 或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。
5.1 wait()方法 其实wait()方法就是使线程停止运行。
- 方法wait()的作用是使当前执行代码的线程进行等待,wait()方法是Object类的方法,该方法是用来将当前线程 置入“预执行队列”中,并且在wait()所在的代码处停止执行,直到接到通知或被中断为止。
- wait()方法只能在同步方法中或同步块中调用。如果调用wait()时,没有持有适当的锁,会抛出异常。
- wait()方法执行后,当前线程释放锁,线程与其它线程竞争重新获取锁。
5.2 notify()方法 notify方法就是使停止的线程继续运行。
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对 其发出通知notify,并使它们重新获取该对象的对象锁。如果有多个线程等待,则有线程规划器随机挑选出一个 呈wait状态的线程。
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出 同步代码块之后才会释放对象锁。
5.3 notifyAll()方法 以上讲解了notify方法只是唤醒某一个等待线程,那么如果有多个线程都在等待中怎么办呢,这个时候就可以使用 notifyAll方法可以一次唤醒所有的等待线程 注意:唤醒线程不能过早,如果在还没有线程在等待中时,过早的唤醒线程,这个时候就会出现先唤醒,在等待的效果了。这样就没有必要在去运行wait方法了。
7.4 wait 和 sleep 的对比(面试题) 其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间, 唯一的相同点就是都可以让线程放弃执行一段时间。用生活中的例子说的话就是婚礼时会吃糖,和家里自己吃糖之间 有差别。说白了放弃线程执行只是 wait 的一小段现象。 当然为了面试的目的,我们还是总结下:
- wait 之前需要请求锁,而wait执行时会先释放锁,等被唤醒时再重新请求锁。这个锁是 wait 对像上的 monitor lock
- sleep 是无视锁的存在的,即之前请求的锁不会释放,没有锁也不会请求。
- wait 是 Object 的方法
- sleep 是 Thread 的静态方法