一、线程基础概念
1. 基础概念
1.1 进程和线程
进程: 进程是指运行中的程序。 比如我们使用钉钉、浏览器、等程序。操作系统会给程序分配内存资源。
线程: 线程是cpu调度的基本单位, 每个线程执行的都是某个进程的代码的某个片段。
区别;
- 进程像是线程的容器。一个进程至少包含一个线程
- 一个进程下的线程可以共享进程中的资源, 线程也有自己独立的存储空间。 而进程间的资源通常是独立的。而进程之间的通信是很麻烦的,需要借助内核才能实现,而线程之间的通信就方便很多
1.2 多线程
多线程是指: 进程中同时运行了多个线程。多线程的目的是为了提高cpu的利用率。避免一些网络io或者磁盘io的等待时间,让cpu去操作其他线程,这样可以提高程序的效率, 不用一直等待。
如: tomcat可以做并行处理, 提升处理的效率,而不是一个个排队等待请求完成才能处理下个请求。
线程的局限性: 并不是线程越多越好, 因为cpu在切换线程的时候会有上下文切换开销,如果是cpu密集型的程序设置了很多线程,那么执行效率可能还不如单线程。如果是io密集型,cpu大部分都在等待io操作的时间, 那么cpu 切换到到其他线程去执行,可以提到利用率
如: redis就是单线程的, 但是他的执行效率非常高
线程安全问题; 虽然多线程带来了一定的性能提升,但是做一些操作时,多线程如果操作临界资源时候会出现线程安全问题,如果使用锁不当, 还会出现死锁问题。
1.3 串行、并发、并行
- 串行: 串行就是一个个排队,第一个完成, 第二个才能上
- 并行: 并行就是同时一起执行。开两个窗口同时排两个队伍一起工作
- 并发: 这里的并发并不是大量请求的的意思, 是指cpu在极短的时间类反复切换执行不同的线程, 在我们眼里感觉好像是一起执行, 但是在cpu的层级他是串行去执行的。并行囊括并发。
1.4 同步异步、 阻塞非阻塞
同步与异步: 执行某个功能后, 被调用者是否**主动反馈**信息。
阻塞和非阻塞: 执行某个功能后,调用者是否需要==一直等待结果==的反馈
2.线程的创建
2.1继承Thread类,重写润方法
public class Test {
public static void main(String[] args) {
Job job = new Job();
job.start();
}
}
class Job extends Thread {
private int a;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("Job:" + a++);
}
}
}
2.2实现Runnable接口, 重写run方法
public class Test {
public static void main(String[] args) {
Thread thread = new Thread(new Job());
thread.start();
}
}
class Job implements Runnable {
private int a;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("Job:" + a++);
}
}
}
2.3实现Callable重写call方法, 配合FutureTask
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("开始...."+ new Date());
FutureTask<String> futureTask = new FutureTask<String>(new Job());
Thread thread = new Thread(futureTask);
thread.start();
System.out.println("等待任务执行完成"+ new Date());
String s = futureTask.get();
System.out.println("任务执行完成"+ new Date());
System.out.println(s);
}
}
class Job implements Callable<String> {
private int a;
@Override
public String call() throws InterruptedException {
Thread.sleep(5000);
a = 1;
System.out.println("Callable: ");
return "a";
}
}
3.线程的基本使用
3.1 线程的状态
传统的线程状态。
- 新建状态(new)>刚刚创建出来
- 就绪状态(ready)>调用start方法,就会变成就绪状态, 但是cpu还未调度
- 运行状态(run) > cpu 划到了时间片正在运行的状态
- 等待状态(waiting) > cpu时间片结束了但是任务还未结束的时候(cpu不会划分到时间片)。还有wait、sleep、join方法也会让cpu当前线程处于阻塞状态
- 结束状态(Terminated)> 线程生命周期到头了
在java中线程提供了6种状态
- 新建状态(new)>刚刚创建出来
- 运行/就绪状态(runnable)>调用start方法,就会变成就绪状态, 但是cpu还未调度
- 阻塞状态(blocked) > synchronized 没有拿到资源, 被放在EntryList中阻塞
- 等待状态(wanting) > 调用wait方法,让线程处于等待状态,需要手动唤醒
- 时间等待状态(timed_wating)> 调用sleep、join方法让线程处于时间等待状态, 会被自动唤醒无需手动唤醒
- 结束状态(Terminated)> 线程生命周期到头了
3.2 线程的常用方法
- 获取当前线程的方法(Thread.currentThread())
Thread thread = Thread.currentThread();
System.out.println(thread);
- 获取当前线程的名字( thread1.setName())
Thread thread = new Thread(()->{
System.out.println(Thread.currentThread().getName());
},"业务模块 - 功能");
thread.start();
- 线程优先级(thread1.setPriority())
//线程优先级就是cpu调度优先级
Thread thread1 = new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName());
}
},"业务模块 - 功能1");
Thread thread2 = new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName());
}
},"业务模块 - 功能2");
thread1.setPriority(1);
thread1.start();
thread2.setPriority(10);
thread2.start();
- 线程的让步(Thread.yield())
Thread thread1 = new Thread(()->{
for (int i = 0; i < 10; i++) {
if (i == 5) {
Thread.yield();
}
System.out.println("t1: " + i);
}
},"业务模块 - 功能1");
Thread thread2 = new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println("t2: " + i);
}
},"业务模块 - 功能2");
thread1.start();
thread2.start();
- 线程的休眠(Thread.sleep())
// 让线程从运行状态变为等待状态
System.out.println(System.currentTimeMillis());
Thread.sleep(1000);
System.out.println(System.currentTimeMillis());
- 线程的强占, 那个线程中调用这个方法,那个线程就会等待该线程执行完成(t1.join())
也饿可以传递等待时间参数(t1.join(1000))需要注意的是如果t1 线程提前执行完成那么调用该方法的对象就继续 执行了不会等待1s才开始
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("t1: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("t2: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.start();
t2.start();
for (int i = 0; i < 10; i++) {
System.out.println("main: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (i ==1){
// 这里会等待t1线程执行完成后继续执行
t1.join();
}
}
- 守护线程( t1.setDaemon(true);)如果设置了这个线程为守护线程, 那么在程序中非守护线程执行完成后守护线程也会停止执行
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("t1: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.setDaemon(true);
t1.start();
}
- 线程的等待和唤醒(wait() ,notify(),notifyAll() )
可以让获取synchronized锁资源的线程通过wait方法进入到锁的等待池,并且会释放锁资源
可以让获取synchronized锁资源的线程,通过notify或者通过notifyAll方法,将等待池中的线程唤醒,添加到锁池中
notify随机唤醒等待池中的一个线程到锁池中
notifyAll 将等待池中的所有线程都唤醒,并且添加到锁池中
notify 和 wait 方法要写在获取锁的方法中或者同步块内部
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread t1 = new Thread(() -> {
aaa();
}, "t1");
Thread t2 = new Thread(() -> {
aaa();
},"t2");
t1.start();
t2.start();
Thread.sleep(1000);
synchronized (Test.class){
// Test.class.notify();
Test.class.notifyAll();
}
}
public static synchronized void aaa(){
try {
for (int i = 0; i < 10; i++) {
if (i == 5) {
Test.class.wait();
}
System.out.println(Thread.currentThread().getName());
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
3.2 线程的结束方式
- 常用方式是return 或者 throw 抛出异常结束方法
- stop 方法, 这个方法现在已经被抛弃, 不推荐
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread t1 = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
System.out.println(i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "t1");
t1.start();
Thread.sleep(3000);
t1.stop();
}
- 使用共享变量, 有些线程是使用死循环保证线程一直运转, 使用共享变量结束死循环
- interrupt 标记位的方式, 其实和共享变量的方式差不多
public static void main(String[] args) throws ExecutionException, InterruptedException {
//线程默认情况下, interrupt 标记位: false
System.out.println(Thread.currentThread().isInterrupted());
// 执行interrupt后, 标记位改为true
Thread.currentThread().interrupt();
// 查询一下标记位变为: true
System.out.println(Thread.currentThread().isInterrupted());
// 先返回当前线程interrupt标记位后, 并归位, 改为false
System.out.println(Thread.interrupted());
// 再次查询一下标记位, 已经归位变为: false
System.out.println(Thread.currentThread().isInterrupted());
}
- 通过打断WAITING或者TIMED_WAITING状态的线程, 从而抛出异常自行处理。这中停止打断线程的方式是最常用的,在框架和juc中也是最常见的。
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("结束线程!");
return;
}
}
});
thread.start();
Thread.sleep(500);
thread.interrupt();
Thread.sleep(5000);
}
二、并发编程的三大特性
1. 原子性
1.1 什么是并发编程的原子性
JMM(Java Memory Model)。不同的硬件和不同的操作系统在内存上的操作有一定差异的。 Java为了解决相
同代码在不同操作系统上出现的各种问题,用 JMM屏蔽掉各种硬件和操作系统带来的差异。
让Java的并发编程可以做到跨平台,
JMM规定所以的变量都会存储在主内存中,在操作的时候, 需要从主内存中复制一份到线程内存(CPU内存),在线程内存做计算。 然后再写回主内存(不一定及时操作)
原子性的定义:原子性指一个操作是不可分割的, 不可中断的,一个线程在执行时,另一个线程不会影响到他。
原子性:多线程操作临界资源,预期结果与实际结果一致
1.2 如何保证并发编程的原子性
- synchhronized
- CAS
- Lock锁
- ThreadLocal
1. synchronized
正常的a++ 操作在字节码层面分为3步
public static void main(String[] args) {
a++;
}
![在这里插入图片描述](https://img-blog.csdnimg.cn/c9e244d1c3114e9a82e8dfa6124e7d54.png)
添加了synchronized代码块后, 在字节码添加了 monitorenter 和monitorexit, 在线程执行到了monitorenter 后,会加锁, 后续其他线程执行到这个指令就会阻塞, 等待拿到锁的线程执行了monitorexit后锁释放, 其他线程去竞争锁
public static void main(String[] args) {
synchronized (test3.class) {
a++;
}
}
2. CAS
什么是cas
compare and swap : 比较并交换, 他是一条cpu的并发原语
他在替换内存的某个位置的值时,首先查看内存中的值与预期的是否相同, 如果一致,执行替换操作。而整个操作是原子性操作。
java 中提供了Unsafe的类提供了对CAS的操作方法, jvm会帮助我们将方法实现CAS汇编指令
但是cas只是比较和交换, 但是在获取原值要自己来实现
static AtomicInteger atomicInteger = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
atomicInteger.incrementAndGet();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
atomicInteger.incrementAndGet();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(atomicInteger);
}
**CAS的缺点: **CAS只能保证对一个变量的操作是原子性的, 无法实现对多行代码实现原子性。
CAS的问题:
- ABA的问题:可以使用版本号的方式来解决ABA的问题。java 中提供了一个类在CAS时,针对各个版本追加版本号的操作。AtomicStampeReference
AtomicIntegerAtomicInteger
static AtomicInteger atomicInteger = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
atomicInteger.incrementAndGet();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
atomicInteger.incrementAndGet();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(atomicInteger);
}
Doug Lea 在CAS的基础上实现了一些原子类,其中就包括实例上用的AtomicInteger, 还有其他很多原子类。。。。
- 自旋时间过长问题:
- 可以指定CAS一共循环多少次, 如果超过这个次数,直接失败即可。(自旋锁,自适应锁)。
- 在CAS一次失败后, 将这个操作暂存起来,后面需要获取结果时, 将暂存的操作全部执行, 再返回最后的结果。
3. Lock锁
Lock锁是在DK1.5由Doug Leat研发的,他的性能相比synchronized在JDK1.S的时期,性能好了很多,但是
在JDK1.6对synchronized优化之后,性能相差不大,但是如果涉及并发比较多时,推荐ReentrantLock锁,性能
会更好。
static ReentrantLock lock = new ReentrantLock();
private static int aInt;
public static void increment() {
lock.lock();
try {
aInt++;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(aInt);
}
ReentrantLock 可以直接对比synchronized ,在功能上来说, 都是锁。 但是ReentrantLock 的功能性相比于synchronized 更加丰富。
ReentrantLock 底层是基于AQS实现的, 有一个基于CAS维护的state变量来实现锁的操作
4. ThreadLocal
ThreadLocal保证原子性的方式,是不让多线程去操作临界资源,让每个线程去操作属于自己的数据
static ThreadLocal tl1 = new ThreadLocal();
static ThreadLocal tl2 = new ThreadLocal();
public static void main(String[] args) throws InterruptedException {
tl1.set("111");
tl2.set("222");
Thread t1 = new Thread(() -> {
System.out.println(tl1.get());
System.out.println(tl2.get());
});
System.out.println(tl1.get());
System.out.println(tl2.get());
t1.start();
t1.join();
}
ThreadLocal实现原理:
- 每个Thread中都存储着一个成员变量,ThreadLocalMap
- ThreadLocalz本身不存储数据,像是一个工具类,基于ThreadLocal去操作ThreadLocalMap
- ThreadLocalMap本身就是基于Entry[ ]实现的,因为一个线程可以绑定多个ThreadLocal,这样一来,可能需要存储多个数据,所以采用Entry[ ]的形式实现。
- 每个线程都有自己独立的ThreadLocalMap, 再基于ThreadLocal对象本身作为kay,对value进行存取。
- ThreadLocalMap的key是弱引用, 弱引的特点是,即便有弱引用,在GC时, 也必须被回收, 这里是为了在ThreadLocal对象在失去引用后, 如果key的引用是强引用, 会导致ThreadLocal对象无法被回收。
ThreadLocal内存泄漏问题:
- 如果ThreadLocal引用丢失,key因为弱引用会被GC回收掉,如果同时线程还没有被回收,就会导致内存泄漏,内存中的value无法被回收, 同时也无法被获取。
- 只需要在使用完毕ThreadLocal对象之后,及时的调用remove方法,移除Entry即可
2. 可见性
2.1 什么是可见性
可见性问题是基于CPU的位置出现的一个问题,CPU处理速度非常快,相对CPU来说,去主内存获取数据这个事情太慢了,CPU就提供了L1,L2,L3的三级缓存,每次去主内存拿完数据后,就会存储到CPU的三级缓存,每次去三级缓存拿数据,效率肯定会提升。
这就带来了问题,现在CPU都是多核,每个线程的工作内存(CPU三级缓存)都是独立的,会告知每个线程中做修改时,只改自己的工作内存,没有及时的同步到主内存,导致数据不一致问题。
可见性代码问题:
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) {
}
}).start();
Thread.sleep(1000);
flag = false;
System.out.println("主线程完成");
}
2.2 解决可见性的方式
2.2.1 volatile
volatile 是一个关键字, 用来修饰成员变量。
如果属性被volatile修饰, 相当于会告诉cpu,当前属性的操作,不允许使用CPU的缓存, 必须去和主内存操作
volatile的内存含义:
- volatile属性被写:当写一个volatile变量,JMM会将当前线程对应的CPU缓存及时的刷新到主内存中
- volatile属性被读:当读一个volatile变量,JMM会将对应的CPU缓存中的内存设置为无效,必须去主内存中重新读取共享变量
其实加了volatile就是告知CPU,对当前属性的读写操作,不允许使用CPU缓存,加了volatile修饰的属性,会在转为汇编之后后,追加一个lock的前缀,cpu在执行这个指令时,如果带有lock前缀会做两件事:
- 将当前处理器缓存行的数据写回到主内存。
- 这个写回的数据,在其他的CPU内核的缓存中,直接无效。
总结:volatile就是让CPU每次操作这个数据时,必须立即同步到庄内存,以及从主内存读取数据。
private volatile static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) {
}
System.out.println("线程结束");
}).start();
Thread.sleep(1000);
flag = false;
System.out.println("主线程完成");
}
2.2.2 synchronized
其实在上面synchronized 原子性的实例代码中也有可见性问题,但是最终加了synchronized后最终结果正确。
synchronized 能实现可见性是基于synchronized的内存语义,如果涉及到了synchronized的同步代码块或者是同步方法,获取锁资源之后,将内部涉及到的变量从CPU缓存中移除**(只有在获取锁的时候会释放缓存)**,
new Thread(() -> {
// 锁加载while外面不生效
synchronized (test2.class) {
while (flag) {
}
System.out.println("t1线程结束");
}
}, "t1").start();
new Thread(() -> {
while (flag) {
// 锁加载while内部, 在获取锁的时候清除cpu缓存,达到可见性的方式
synchronized (test3.class) {
}
}
System.out.println("t2线程结束");
}, "t2").start();
Thread.sleep(1000);
flag = false;
System.out.println("主线程完成");
2.2.3 Lock
Lock锁保证可见性的方式和synchronized完全不同,synchronized基于他的内存语义,在获取锁和释放锁时,对CPU缓存做一个同步到主内存的操作。
Lock锁是基于volatile实现的。Lock锁内部再进行加锁和释放锁时,会对一个由volatile修饰的state属性进行加减操作。
如果对volatile修饰的属性进行写操作,CPU会执行带有lock前缀的指令,CPU会将修改的数据,从CPU缓存立即同步到主内存,同时也会将其他的属性也立即同步到主内存中。还会将其他CPU缓存行中的这个数据设置为无效,必须重新从主内存中拉取。
private static boolean flag = true;
private static Lock lock = new ReentrantLock();
private volatile static boolean a = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) {
//使用lock操作保证可见性
// lock.lock();
// try {
// } finally {
// lock.unlock();
// }
// 在循环内部如果对修饰了volatile的变量进行写操作,就会结束循环
a = false;
}
System.out.println("t2线程结束");
}, "t2").start();
Thread.sleep(1000);
flag = false;
System.out.println("主线程完成");
}
3. 有序性
3.1 什么是有序性
在java 中, java 文件中的内容会被编译, 在执行前需要转化为CPU可以识别的指令, CPU在执行这些指令时, 为了提升执行效率, 在不影响最终结果的前提下(满足一些要求),会对指令进行重排。而指令乱序执行的原因是为了提高cpu的使用率
以下java 中的程序是乱序执行的。
static int a, b, x, y;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
a = 0;
b = 0;
x = 0;
y = 0;
Thread t1 = new Thread(() -> {
a = 1;
x = b;
}, "t1");
Thread t2 = new Thread(() -> {
b = 1;
y = a;
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
if (x == 0 && y == 0) {
System.out.println("发生了指令重排: " + i);
}
}
}
单例模式如果不加volatile 可能会发生的指令重排问题:
private static volatile Test3 test3;
private Test3() {
}
public static Test3 getTest3() {
if (test3 == null) {
synchronized (Test3.class) {
if (test3 == null) {
//java 中new 一个对象 会分为三步
// 开辟空间, 初始化, test指针地址,。这三步如果没有加 volatile是乱序执行的
// 如果没有加volatile 那指令可以是 开辟空间,test指针地址,初始化
//有可能 a线程拿到锁后已经执行完成 开辟空间和 test指针地址了那b 线程在判断test3 == null 时候就会判断为false
//那么后续线程会拿着为初始化对象。操作了
test3 = new Test3();
}
}
}
return test3;
}
3.2 as - if -serial( cpu 级别指令重排 )
as-if-seriali语义:
- 不论指定如何重排序,需要保证单线程的程序执行结果是不变的。
- 而且如果存在依赖的关系,那么也不可以做指令重排。
3.3 happen-before( JVM 级别指令重排 )
具体规则:
- 单线程happen-beforel原则:在同一个线程中,书写在前面的操作happen-before后面的操作,
- 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
- volatile的happen-before原则:对一个volatile?变量的写操作happen-before对此变量的任意操作。
- happen-before的传递性原则:如果A操作happen-before B操作,B操作happen-before C操作,那么A操作nappen-before C操作,
- 线程启动的happen-before)原则:同一个线程的start方法happen-beforel此线程的其它方法。
- 线程中断的happen-before原侧:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
- 线程终结的happen-before)原则:线程中的所有操作都nappen-before线程的终止检测。
- 对象创建的nappen-before原则:一个对象的初始化完成先于他的finalize方法调用。
JMM 只有在不出现上述8 中的情况时, 才不会触发指令重排效果
3.3 volatile
如果需要让程序对某一个属性的操作不出现指令重排,除了满足nappens-before原则之外,还可以基于volatile修饰属性,从而对这个属性的操作,就不会出现指令重排的问题了。
volatile如何实现的禁止指令重排?
volatile 是通过内存屏障的概念去实现的, 可以将内存屏障单做一条指令。
他会在可能重拍的操作之间,添加一条指令,这个指令就可以避免上下执行的其他指令进行重排序。