如何开启线程?怎么保证线程安全?
进程和线程的区别?
进程是操作系统进行资源分配的最小单元。
线程是操作系统进行任务分配的最小单元。
线程隶属于进程。
如何开启线程?
- 继承Thread类,重写run方法。(Thread实现了Runnable接口,常用)
- 实现Runnable接口,实现run方法。(常用)
- 实现Callable接口,实现call方法。(能获取线程执行返回值)
- 通过线程池
继承Thread类后调用start开启线程,内部会调用run方法,直接调用run方法不会开启线程
对线程安全的理解
不是线程安全,应该是内存安全,堆是共享内存,可以被所有线程访问
和单线程执行的结果一样,就是线程安全的
栈:每个线程相互独立,保证代码执行顺序
堆:每个进程会被分配独立的内存空间,里面有一块堆内存,进程内的所有线程都可以访问到这块堆内存,这就是造成线程安全问题的潜在原因
如何保证线程安全?
总结:加锁。
- 使用JVM提供的锁,也就是Synchronized关键字
- JDK提供的各种锁Lock
Volatile和Synchronized
Volatile和Synchronized有什么区别?
Synchronized关键字用来加锁,Volatile只是用来保证变量的线程可见性。通常适用于一个线程写,多个线程读的场景。
Volatile能不能保存线程安全?
不能,Volatile关键字只能保证线程可见性,不能保证原子性。
DCL(Double Check Lock)单例为什么要加Volatile?
DCL(双重检查锁定机制) 详细描述
Volatile能防止指令重排。在DCL中,防止高并发情况下,指令重排造成的线程安全问题
例如:Integer i = 1;分为三个步骤
- 分配内存,初始化0
- 对象建立
- 建立指针对应关系, i 指向1
指令重排就是这三个任务顺序被打乱,单线程没什么影响。多线程存在可能在指令未全部执行时来访问变量导致出错
// 单例-DCL
public class SingleDemo{
private static volatile SingleDemo singleDemo;
// 构造函数私有化
private SingleDemo(){};
public SingleDemo getInstance(){
if(null == singleDemo){
synchronized(SingleDemo.class){
if(null == singleDemo){
singleDemo = new SingleDemo();
}
}
// 其他业务逻辑
}
return singleDemo;
}
}
java锁
java的锁就是在对象的Markword中记录一个锁状态。无锁,偏向锁,轻量级锁,重量级锁对应不同的锁状态。
java线程的锁机制是什么样的?
java的锁机制就是根据资源竞争的激烈程度不断进行锁升级的过程。
偏向锁、轻量级锁、重量级锁有什么区别?
锁机制是如何升级的?
可重入锁
AQS
- AQS是一个java线程同步的框架。是JDK中很多锁工具的核心实现框架
- 在AQS中,维护了一个信号量state和一个线程组成的双向链表队列。其中,这个线程队列就是用来给线程排队的,而state就像是一个红绿灯,用来控制线程排队或者放行的,在不同的场景下有不同的意义。
- 在可重入锁这个场景下,state就用来表示加锁次数,0表示无锁,每加一次锁,state就加1,释放锁state就减1。
如何保证三个线程同时执?如何在并发情况下保证三个线程依次执行?如何保证三个线程有序交错进行?
CountDownLatch,CylicBarrier,Semaphore。
守护线程
守护线程:为所有非守护线程提供服务的线程,任何一个守护线程都是整个IVM中所有非守护线程的保姆;
守护线程类似于整个进程的一个默默无闻的小喽喽;它的生死无关重要,它却依赖整个进程而运行;哪天其他线程结束了,没有要执行的了,程序就结束了,理都没理守护线程,就把它中断了;
注意:由于守护线程的终止是自身无法控制的,因此千万不要把io、File等重要操作逻辑分配给它;因为它不靠谱;
守护线程的作用是什么?
举例,GC垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
应用场景:
(1)来为其它线程提供服务支持的情况,
(2) 或者在任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用,反之,如果一个正在执行某个操作的线程必须要正确地关闭掉否则就会出现不好的后果的话,那么这个线程就不能是守护线程,而是用户线程。通常都是些关键的事务,比方说,数据库录入或者更新,这些操作都是不能中断的。
thread.setDaemon(true)必须在thread.start0之前设置,否则会跑出一个llegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
在Daemon线程中产生的新线程也是Daemon的。
ThreadLocal
每一个Thread对象均含有一个ThreadLoca1Map类型的成员变量threadLoca1s,它存储本线程中所有ThreadLocal对象及其对应的值
ThreadLocalMap由一个个Entry对象构成
Entry继承自WeakReference<ThreadLocal<?>>,一个Entry由ThreadLoca1对象和object构成。由此可见,Entry的key是ThreadLocal对象,并且是一个弱引用。当没指向key的强用后,该key就会被垃圾收集器回收
当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,将值存储进ThreadLocalMap对象中
get方法执行过程类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value。
由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。
使用场景:
1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。
4、数据库连接,Session会话管理。
ThreadLocal内存泄漏
内存泄漏为程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏危害可以忽略,但内存泄漏堆积后过很严重,无论多少内存,迟早会被占光。
不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
强引用:
使用最普遍的引用(new),一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
如果想取消强用和某个对象之间的关联,可以显式地将引用赋值为nul,这样可以使JVM在合适的时间就会回收该对象。
弱引用:
JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。
ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例value为线程变量的副本
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null,而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉,但如果当前线程再迟迟不结束的话,这些key为nul的Entry的value就会一直存在一条强引用链 (红色链条)
key 使用强引用
当hreadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
key 使用弱引用
当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set0),get0),remove()方法的时候会被清除value值。
因此,ThreadLocal内存泄漏的根源是: 由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
ThreadLocal正确的使用方法
- 每次使用完ThreadLocal都调用它的remove()方法清除数据0
- 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。