创建线程的几种方法?
这是最普通的方式,继承Thread类,重写run方法,如下:
public class ExtendsThread extends Thread {
@Override
public void run() {
System.out.println("1......");
}
public static void main(String[] args) {
new ExtendsThread().start();
}
}
这也是一种常见的方式,实现Runnable接口并重写run方法,如下:
public class ImplementsRunnable implements Runnable {
@Override
public void run() {
System.out.println("2......");
}
public static void main(String[] args) {
ImplementsRunnable runnable = new ImplementsRunnable();
new Thread(runnable).start();
}
}
实现Callable接口
public class ImplementsCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("3......");
return "zhuZi";
}
public static void main(String[] args) throws Exception {
ImplementsCallable callable = new ImplementsCallable();
FutureTask<String> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
System.out.println(futureTask.get());
}
}
线程池创建线程
线程池参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
核心线程数
最大线程数
存活时间
时间单位
阻塞队列
线程工厂
拒绝策略
1,任务在提交的时候,首先判断核心线程数是否已满,如果没有满则直接添加到工作线程执行
2,如果核心线程数满了,则判断阻塞队列是否已满,如果没有满,当前任务存入阻塞队列
3,如果阻塞队列也满了,则判断线程数是否小于最大线程数,如果满足条件,则使用临时线程执行任务
如果核心或临时线程执行完成任务后会检查阻塞队列中是否有需要执行的线程,如果有,则使用非核心线程执行任务
4,如果所有线程都在忙着(核心线程+临时线程),则走拒绝策略
拒绝策略
1.AbortPolicy:直接抛出异常,默认策略;
2.CallerRunsPolicy:用调用者所在的线程来执行任务;
3.DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
4.DiscardPolicy:直接丢弃任务;
如何确定核心线程数
在设置核心线程数之前,需要先熟悉一些执行线程池执行任务的类型
IO密集型任务
一般来说:文件读写、DB读写、网络请求等
推荐:核心线程数大小设置为2N+1 (N为计算机的CPU核数)
CPU密集型任务
一般来说:计算型代码、Bitmap转换、Gson转换等
推荐:核心线程数大小设置为N+1 (N为计算机的CPU核数)
常见的线程池
线程生命周期
第一创建
第二就绪
第三运行
第四阻塞
第五死亡
线程和进程之间通信方式
线程
信号量:信号量是一种计数器,用于控制多个线程对共享资源的访问。当一个线程需要访问共享资源时,它会尝试获取信号量,如果信号量的值大于0,则线程可以访问共享资源,然后信号量的值减1;否则,线程将等待信号量的值变为大于0。
互斥锁:互斥锁用于保护共享资源的访问,只有一个线程可以获取锁,其他线程必须等待锁的释放。当一个线程获取锁后,其他线程不能获取锁,直到该线程释放锁为止。
条件变量:条件变量是一种线程同步机制,用于等待某个条件的发生。当一个线程等待某个条件时,它可以通过条件变量进行阻塞,当条件满足时,其他线程可以通过条件变量来通知等待的线程。
管道:管道是一种IPC机制,用于在两个线程之间传递数据。一个线程将数据写入管道,另一个线程则可以从管道中读取数据。
进程
管道(Pipe):管道是一种半双工的通信方式,只能用于具有亲缘关系(父子进程或兄弟进程)的进程之间。
命名管道(Named Pipe):命名管道也是一种半双工的通信方式,但是可以用于任意进程之间进行通信。
信号(Signal):信号是一种异步的通信方式,进程之间可以通过发送信号来进行通信,例如,进程可以通过发送 SIGTERM 信号来请求另一个进程终止。
消息队列(Message Queue):消息队列是一种全双工的通信方式,进程可以通过消息队列发送和接收消息。
共享内存(Shared Memory):共享内存是一种高效的通信方式,进程可以通过共享同一块内存来进行通信。
套接字(Socket):套接字是一种可靠的、全双工的通信方式,进程可以通过套接字在网络中进行通信。
信号量(Semaphore)、互斥锁(Mutex)和条件变量(Condition Variable)等同步原语,也可以用于进程之间的通信和同步。信号量是一种计数器,用于控制多个进程对共享资源的访问。当一个进程需要访问共享资源时,它会尝试获取信号量,如果信号量的值大于0,则进程可以访问共享资源,然后信号量的值减1;否则,进程将等待信号量的值变为大于0。
sleep和wait方法对比
sleep() 方法没有释放锁,而 wait() 方法释放了锁
wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep()方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。
sleep() 是 Thread 类的静态本地方法,wait() 则是 Object 类的本地方法
为什么 wait() 方法不定义在 Thread 中?
因为wait是让获取对象锁的线程实现等待 每个object都有对象锁
而sleep是让当前线程暂停执行
可以直接调用 Thread 类的 run 方法吗?
调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。
volatile 关键字
volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
写内存语义:当写一个volatile变量时,JMM会把该线程本地内存中的共享变量的值刷新到主内存中。
读内存语义:当读一个volatile变量时,JMM会把该线程本地内存置为无效,使其从主内存中读取共享变量。
原理: 而是会把值刷新回主内存 当其它线程读取该共享变量 ,会从主内存重新获取最新值,而不是使用当前线程的本地内存中的值。
双重校验锁实现对象单例(线程安全)
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
说说 synchronized 和 ReentrantLock 的区别?
AQS了解多少?
AQS 是基于一个 FIFO 的双向队列,其内部定义了一个节点类 Node,Node 节点内部的 SHARED 用来标记该线程是获取共享资源时被阻挂起后放入 AQS 队列的, EXCLUSIVE 用来标记线程是 取独占资源时被挂起后放入 AQS 队列
AQS 使用一个 volatile 修饰的 int 类型的成员变量 state 来表示同步状态,修改同步状态成功即为获得锁,volatile 保证了变量在多线程之间的可见性,修改 State 值时通过 CAS 机制来保证修改的原子性
ReentrantLock实现原理?
ReentrantLock的流程
state初始化为0,表示未锁定状态
A线程lock()时,会调用tryAcquire()获取锁并将state+1
其他线程tryAcquire获取锁会失败,直到A线程unlock() 到state=0,其他线程才有机会获取该锁。
A释放锁之前,自己可以重复获取此锁(state累加),这就是可重入的概念。
Synchronized
底层原理
Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住
它的底层由monitor实现的,monitor是jvm级别的对象( C++实现),线程获得锁需要使用对象(锁)关联monitor
在monitor内部有三个属性,分别是owner、entrylist、waitset
其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线
重量级锁 锁升级
下面,我们再从实际场景出发,来具体说说锁升级的过程。
一开始,没有任何线程访问同步块,此时同步块处于无锁状态。
然后,线程1首先访问同步块,它以CAS的方式修改Mark Word,尝试加偏向锁。由于此时没有竞争,所以偏向锁加锁成功,此时Mark Word里存储的是线程1的ID。
然后,线程2开始访问同步块,它以CAS的方式修改Mark Word,尝试加偏向锁。由于此时存在竞争,所以偏向锁加锁失败,于是线程2会发起撤销偏向锁的流程(清空线程1的ID),于是同步块从偏向线程1的状态恢复到了可以公平竞争的状态。
然后,线程1和线程2共同竞争,它们同时以CAS方式修改Mark Word,尝试加轻量级锁。由于存在竞争,只有一个线程会成功,假设线程1成功了。但线程2不会轻易放弃,它认为线程1很快就能执行完毕,执行权很快会落到自己头上,于是线程2继续自旋加锁。
最后,如果线程1很快执行完,则线程2就会加轻量级锁成功,锁不会晋升到重量级状态。也可能是线程1执行时间较长,那么线程2自旋一定次数后就放弃自旋,并发起锁膨胀的流程。届时,锁被线程2修改为重量级锁,之后线程2进入阻塞状态。而线程1重复加锁或者解锁时,CAS操作都会失败,此时它就会释放锁并唤醒等待的线程。
总之,在锁升级的机制下,锁不会一步到位变为重量级锁,而是根
ThreadLocal
ThreadLocal,也就是线程本地变量。
ThreadLocal 主要功能有两个,第一个是可以实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用引发的线程安全问题,第二个是实现了线程内的资源共享
是应为ThreadLocalMap 中的 key 被设计为弱引用,它是被动的被GC调用释放key,不过关键的是只有key可以得到内存释放,而value不会,因为value是一个强引用。
在使用ThreadLocal 时都把它作为静态变量(即强引用),因此无法被动依靠 GC 回收,建议主动的remove 释放 key,这样就能避免内存溢出。
Thread 类有一个类型为 ThreadLocal.ThreadLocalMap 的实例变量 threadLocals,每个线程都有一个属于自己的 ThreadLocalMap。
ThreadLocalMap 内部维护着 Entry 数组,每个 Entry 代表一个完整的对象,key 是 ThreadLocal 的弱引用,value 是 ThreadLocal 的泛型值。
每个线程在往 ThreadLocal 里设置值的时候,都是往自己的 ThreadLocalMap 里存,读也是以某个 ThreadLocal 作为引用,在自己的 map 里找对应的 key,从而实现了线程隔离。
ThreadLocal 本身不存储值,它只是作为一个 key 来让线程往 ThreadLocalMap 里存取值
ConcurrentHashMap
先来看下JDK1.7
JDK1.7 中的 ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成,即 ConcurrentHashMap 把哈希桶数组切分成小数组(Segment ),每个小数组有 n 个 HashEntry 组成。
如下图所示,首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问,实现了真正的并发访问。
首先会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。
尝试自旋获取锁。
如果重试的次数达到了 MAX_SCAN_RETRIES 64 则改为阻塞锁获取,保证能获取成功
在数据结构上, JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的Node数组+链表+红黑树结构;在锁的实现上,抛弃了原有的 Segment 分段锁,采用CAS + synchronized实现更加细粒度的锁。
将锁的级别控制在了更细粒度的哈希桶数组元素级别,也就是说只需要锁住这个链表头节点(红黑树的根节点),就不会影响其他的哈希桶数组元素的读写,大大提高了并发度。
大致可以分为以下步骤:
根据 key 计算出 hash 值;
判断是否需要进行初始化;
定位到 Node,拿到首节点 f,判断首节点 f:
如果为 null ,则通过 CAS 的方式尝试添加;
如果为 f.hash = MOVED = -1 ,说明其他线程在扩容,参与一起扩容;
如果都不满足 ,synchronized 锁住 f 节点,判断是链表还是红黑树,遍历插入;
当在链表长度达到 8 的时候,数组扩容或者将链表转换为红黑树。
JDK1.8 中为什么使用内置锁 synchronized替换 可重入锁 ReentrantLock?★★★★★
在 JDK1.6 中,对 synchronized 锁的实现引入了大量的优化,并且 synchronized 有多种锁状态,会从无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁一步步转换。
减少内存开销 。假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承 AQS 来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费
ConcurrentHashMap 和 Hashtable 的效率哪个更高?为什么?★★★★★
ConcurrentHashMap 的效率要高于 Hashtable,因为 Hashtable 给整个哈希表加了一把大锁从而实现线程安全。而ConcurrentHashMap 的锁粒度更低,在 JDK1.7 中采用分段锁实现线程安全,在 JDK1.8 中采用CAS+synchronized实现线程安全。
死锁
互斥条件
指线程对己经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
请求并持有条件
指一个线程己经持有了至少一个资源,但又提出了新的资源请求,而新资源己被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己己经获取的资源。
不可剥夺条件
指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。
环路等待条件
指在发生死锁时,必然存在一个线程一资源的环形链,即线程集合{T0, Tl, T2,…,Tn}中的T0正在等待一个T1占用的资源,T1正在等待T2占用的资源,……Tn正在等待己被T0占用的资源。
评论区