1 线程的结束方式
1.1 stop方法(不用)
强制让线程结束,无论你再干嘛,不推荐。
1.2 使用共享变量(很少)
这种方式用的也不多,有的线程可能会通过死循环来保证一直运行。咱们可以通过修改共享变量在破坏死循环,让线程退出循环,结束run方法
1.3 interrupt方式
共享变量方式
public static void main(String[] args) throws InterruptedException {
// 线程默认情况下, interrupt标记位:false
System.out.println(Thread.currentThread().isInterrupted());
// 执行interrupt之后,再次查看打断信息
Thread.currentThread().interrupt();
// interrupt标记位:ture
System.out.println(Thread.currentThread().isInterrupted());
// 返回当前线程,并归位为false interrupt标记位:ture
System.out.println(Thread.interrupted());
// 已经归位了
System.out.println(Thread.interrupted());
// =====================================================
Thread t1 = new Thread(() -> {
while(!Thread.currentThread().isInterrupted()){
// 处理业务
}
System.out.println("t1结束");
});
t1.start();
Thread.sleep(500);
t1.interrupt();
}
通过打断WAITING或者TIMED_WAITNG状态的线程,从而抛出异常自行处理,这种停止线程是最常用的一种。
wait和sleep的区别?
- sleep 属于Thread类中的static方法,wait属于Object方法
- sleep属于TIMED_WAITING,自动被唤醒,wait属于WAITING,需要手动唤醒
- sleep方法在持有锁时执行,不会释放锁。wait执行后,会释放锁资源
- sleep可以在持有锁或者不持有锁时,执行。 wait方法必须在只有锁时才可以执行。
2 并发编程的三大特性
2.1 原子性
2.1.1 什么是原子性
原子性指一个操作是不可分割的,不可中断的,一个线程在执行时,另一个线程不会影响到他。
导致的问题:多线程操作临界资源时,预期的结果与最终结果不一致
2.1.2 保证并发编程的原子性
2.1.2.1 synchronized
2.1.2.2 CAS
比较和交换。
**CAS缺点:**CAS只能保证一个变量操作是原子性的,无法实现对多行代码实现原子性
CAS问题:
- ABA问题:问题如下,可以引入版本号的方式来解决ABA问题,Java中提供了一个类在CAS时,针对各个版本追加版本号的操作。 AtomicStampeReference
-
自选时间过长问题:
可以在指定CAS一共循环多少次,如果超过这个次数,直接失败或者挂起线程
可以在CAS一次失败后,将这个操作暂存起来,后面需要获取结果时,将暂存的操作全部执行,在返回最后的结果
2.1.2.2 Lock锁
ReentrantLock底层是基于AQS实现的,有一个基于CAS维护的state变量来实现锁的操作。
2.12.3 ThreadLoal
Java中的四种引用类型:强,软,弱,虚
强引用:把一个对象赋给一个引用变量,这个引用变量就是强引用。这个引用变量就是强引用。当一个对象被强引用变量引用时,他始终处于可达状态,他是不可能被垃圾回收机制回收的。即使该对象永远都不会被用到jvm也不会回收,因此强引用是造成Java内存泄漏的主要原因之一。例如:User user =new User;
**软引用:**对于只有软引用的对象来说,当系统内存足够时他不会被回收,当系统内存空间足够时他会被回收。
软引用通常用在对内存敏感的程序中,作为缓存使用。
**弱引用:**他比软引用的生存周期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管jvm一运行,该对象就会被回收,可以解决内存泄漏问题,ThreadLocal就是基于弱引用解决内存泄漏的问题
**虚引用:**他不能单独使用,必须和引用队列联合使用,虚引用的主要作用跟踪对象被垃圾回收的状态
开发中更多使用强引用
ThreadLocal保证原子性的方式,是不让多线程去操作临界资源,让每个线程去操作属于自己的数据
static ThreadLocal tl1 = new ThreadLocal();
static ThreadLocal tl2 = new ThreadLocal();
public static void main(String[] args) {
tl1.set("123");
tl2.set("456");
Thread t1 = new Thread(() -> {
System.out.println("t1:" + tl1.get());
System.out.println("t1:" + tl2.get());
});
t1.start();
System.out.println("main:" + tl1.get());
System.out.println("main:" + tl2.get());
}
ThreadLocal实现原理:
- 每个ThreadLocal中存储着一个成员变量,ThreadLocalMap
- ThreadLocal本身不存储数据,像是一个工具类,基于ThreadLocal操作ThreadLocalMap
- ThreadLocalMap本身基于Entry[]实现的,因此一个线程可以绑定多个ThreadLocal,这样依赖,可能需要存储多个数据,所以采用Entry[]的形式实现
- 每一个线程都有自己独立的ThreadLocalMap,在基于ThreadLocal对象本身作为key,对value进行存储
- ThreadLocalMap的key是一个弱引用,弱引用的特点是,即便有弱引用,在GC时,也必须被回收。这里是为了在ThreadLocal对象失去引用后,如果key的引用是强引用,会导致ThreadLocal对象无法被回
ThreadLocal内存泄漏问题:
- 如果ThreadLocal引用丢失,key因为弱引用会被GC回收掉,如果同时线程还没有被回收,就会导致内存泄漏,内存中的value无法被回收,同时也无法获取到
- 只需要使用完毕ThreadLocal对象之后,及时调用remove方法移除Entry即可
2.2 可见性
可见性问题是基于CPU位置出现的,CPU处理速度非常快,相对CPU来说,去主内存获取数据这个事情太慢了,CPU就提供了L1,L2,L3的三级缓存,每次去主内存拿完数据后,就会存储到CPU的三级缓存,每次去三级缓存拿数据,效率肯定会提升。
这就带来了问题,现在CPU都是多核,每个线程的工作内存(CPU三级缓存)都是独立的,会告知每个线程中做修改时,只改自己的工作内存,没有及时的同步到主内存,导致数据不一致问题。
2.2.1 解决可见性办法
2.2.1.1 volatile
volatile是一个关键字,用了修饰成员变量。
如果属性被volatile修饰,相当于告诉CPU,对当前属性的操作,不允许使用CPU的缓存,必须去和主存内操作
volatile的内存语义:
- volatile属性被写:当写一个volatile变量,JMM(内存模型)会将当前线程对应CPU缓存及时的刷新到主存。
- volatile属性被读:当读一个volatile变量,JMM会将对应的CPU缓存中的内存设置为无效,必须去主存中重新读取共享变量
volatile修饰的属性转化为汇编之后,追加了一个lock前缀
2.2.1.2 synchronizaed
如果涉及到了synchronized的同步代码块或者是同步方法,获取锁资源之后,将内部涉及到的变量从CPU缓存中移除,必须去主内存中重新拿数据,而且在释放锁之后,会立即将CPU缓存中的数据同步到主内存
2.2.1.3 Lock
Lock锁是基于volatile实现的,Lock锁内部在进行加锁和释放锁,会对一个由volatile修饰的state属性进行加减操作
2.2.1.4 final
final修饰的属性,在运行期间是不允许修改的,这样一来,就间接的保证了可见性,所有多线程读取final属性,值肯定是一样。
final并不是说每次取数据从主内存读取,他没有这个必要,而且final和volatile是不允许同时修饰一个属性的
final修饰的内容已经不允许再次被写了,而volatile是保证每次读写数据去主内存读取,并且volatile会影响一定的性能,就不需要同时修饰。
2.3 有序性
2.3.1 什么是有序性
.java文件中的内容会被编译,在执行前需要再次转为CPU可以识别的指令,CPU在执行这些指令时,为了提升执行效率,在不影响最终结果的前提下(满足一些要求),会对指令进行重排。
2.3.2 as-if-serial
b不论指令如何重排序,需要保证单线程的程序执行的结果不变。而且如果存在依赖关系,那么也不可以做指令重排
2.3.3 volatile
volatile如何实现的禁止指令重排?
内存屏障概念。将内存屏障看成一条指令。
会在两个操作之间,添加上一道指令,这个指令就可以避免上下执行的其他指令进行重排序。