一、基础认识
1.什么是线程和进程
一个进程就是一次程序的执行,需要注意的是进程不仅包括程序实体,还包括运行时占用的资源。因此一个程序的多次执行算多个不同的进程。
多个线程组成一个进程。进程是资源分配的基本单位,线程是调度的最小单位。
2.为什么要有线程?
在进行进程上下文切换时,由于一个进程拥有的资源较多,会增加并发的开销。相比之下进程更容易进行切换,开销更少。
3.什么是并行和并发
并行是多核cpu的环境中同时运行多个进程
并发指一种现象,就是从用户的角度看多个进程同时运行。实际上是多个进程根据时间片进行轮转调度(交替运行),但是时间片太短了,用户感知不到。
4.什么是上下文切换
进程之间是共享cpu资源的,不同的进程需要切换。一个进程让出cpu资源给另一个进程,叫做进程的上下文切换。
6.为什么要使用多线程,解决什么问题
多线程可以充分利用现代计算机多核cpu的特性,同时执行多个线程。这样可以提升响应处理效率。
二、创建线程的三种方式
- 自定义类继承Thread,重写run方法。在main函数中,创建线程对象,调用start方法。
- 自定义类实现Runnable接口,创建对象。将当前对象作为thread类构造器的参数,创建线程。
MyThread t1=new MyThread(); MyThread t2=new MyThread(); Thread thread1 = new Thread(t1); Thread thread2 = new Thread(t2); thread2.start();
3.Callable接口
步骤:
1.创建一个实现Callable的实现类
2.实现call方法,将此线程需要执行的操作声明在call()中
3.创建Callable接口实现类的对象
4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
6.获取Callable中call方法的返回值
三、线程的常用方法
run | 线程执行的代码 |
start | 启动线程。调用run方法 |
Thread.currentThread() | 返回当前执行的线程(静态方法) |
getName() | 当前线程的名字 |
setName() | 设置当前线程名字 |
yield() | 释放当前线程,去执行其它的线程,过后还是会继续执行该线程 |
join() | 如果在线程a中调用了线程b的join()方法,此时线程a就会进入阻塞状态,直到线程b完全执行完以后,线程a才会结束阻塞状态 |
sleep() | 将当前线程阻塞,指定毫秒数,阻塞时间。 |
interrupt() | 中断线程 |
四、java线程六种状态
- 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
- 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
- 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
- 阻塞(BLOCKED):表示线程阻塞于锁。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
- 终止(TERMINATED):表示该线程已经执行完毕。
五、Happens-before 原则
只要不改变程序的执行结果,无论是单线程程序还是多线程程序,编译器和处理机怎么优化都可以。
如果 A Happens-before B,那么 JMM 将向程序员保证 — A 操作的结果将对 B 可见,且 A 的执行顺序排在 B 之前。
六、JMM
1.什么是JMM
JMM就是java内存模型,Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。线程不能直接读写主内存中的变量。
2.java内存模型三大特征
原子性:操作不可分割
JMM规定了8中原子操作,对静态变量自增和自减操作不是原子操作,i=j这样的赋值也不是原子操作。i=1是原子操作。
在单线程环境下正常运行的一段代码,在多线程环境中可能发生各种意外情况,导致无法得到正确的结果。多线程下执行结果不正确,就叫做线程不安全。
1.为什么自增不是线程安全的原子操作?i++
实际上自增分成了三个步骤:读取原来的i,i+1;赋值给i(也就是读、改、写操作)
·对应下面的字节码指令:
getstatic i:获取静态变量 i 的值
iconst_1:准备常量 1
iadd:自增(自减操作对应 isub)
putstatic i:将修改后的值存入静态变量 i当正在执行某个指令时可能线程的时间片到了,要进行上下文切换。没有完全执行完成完整的周期,所以就会出错。
加synchronized
关键字,可以保证线程安全。
可见性:一个线程修改了变量,其它线程立刻就知道了。
由于高速缓存的存在,当一个线程修改了某个变量时。其它线程的高速缓存内这个变量的值不能及时更改。
例子:
// 线程 1 执行的代码
int i = 0;
i = 1;
// 线程 2 执行的代码
j = i;
当线程1执行时,从内存中读取i=0到cpu的高速缓存,高速缓存改变i=1(还没将高速缓存的i写回内存);这时如果刚好进行上下文切换,线程2执行。从内存中读取i,读到的还是0,于是j被赋值成0。这时就会线程不安全。
保证可见性:volatile
关键字修饰共享变量,sunchronized
和 final
这俩关键字也能保证可见性。
有序性
CPU 可能会对输入代码进行乱序执行优化,CPU 会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致
在重排序的时候,CPU 和编译器都需要遵守一个规矩,这个规矩就是 as-if-serial 语义:不管怎么重排序,单线程环境下程序的执行结果不能被改变。CPU 和编译器不会对存在数据依赖关系的操作做重排序
数据依赖性分为三种类型:写后读、写后写、读后写(在看能不能重排序的时候可以画图。)
volatile
和 synchronized
两个关键字来保证线程之间操作的有序性。