如果文章有错误的话,欢迎指正
文章目录
- 1. Util.concurrent包下有哪些类
- 2 什么是协程?
- 3 协程多与线程进行比较?
- 4 什么叫线程安全?
- 5 保证线程安全有哪些策略
- 6 线程常用方法解释
- 7 ThreadLocal的实现原理
- 8 java中两种创建线程的方式
- 9 callable 和 runnable 的区别
- 10 submit 和 execute 区别
- 11 标题线程生存周期
- 12 sleep 和 wait的区别
- 13.为什么调用start执行run(),而不是直接调用run()
- 14 线程状态
- 15 线程状态切换
- 16 线程中断
- 17 线程独占哪些资源
- 18 进程和线程的区别
- 19 线程池的作用及优点
- 20 常用线程池种类
- 21 线程池处理多任务流程图
- 22 如何创建线程池
- 23 进程的内存分配
- 24 进程间通信方式
- 25 线程同步机制?
- 26. 线程同步方法
- 27. 线程之间怎么共享数据
- 28.sychronized 使用方式
- 29 JDK1.6后 Synchronized的底层优化
- 30 synchronized 底层
- 31 重入锁ReetrantLock
- 32 ReentrantLock 和 Sychronized 区别
- 33 volatile关键字(jdk1.5以后才生效)
- 34. Synchronized 和 volatile 的区别
- 35. Synchronized 和 clock的区别
- 36 AQS
- 37 .乐观锁和悲观锁
- 38 CAS ,compare and swap
- 39 什么是死锁?
- 40. 产生死锁的四个条件
- 41.死锁检测
- 42. 死锁的处理方法
- 43 什么是活锁
- 44 并发、并行、异步的区别?
- 45 什么是IO多路复用?怎么实现?
- 46 有哪些常见的IO模型?
- 47 java内存模型-JMM
- 48 说一说阻塞队列
- 49 僵尸进程和孤儿进程
- 50 读写锁
- 51 原子类型
1. Util.concurrent包下有哪些类
Executors
FutureTask
LinkedBlockingQueue
ThreadPoolExecutors
2 什么是协程?
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
3 协程多与线程进行比较?
- 一个线程可以拥有多个协程,一个进程也可以单独拥有多个协程,这样python中则能使用多核CPU。
- 线程进程都是同步机制,而协程则是异步
- 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态
4 什么叫线程安全?
一个进程可以有多个线程组成,当线程任务中存在共享数据,并且多线程操作共享数据,则可能存在线程隐患问题,即多线程之间存在竞争则称为线程不安全,不存在竞争则称为线程安全。
5 保证线程安全有哪些策略
1.保证变量不可变
2.使用同步容器比如hashTable和concrurrentHashMap,vector,stack,或者sychronized修饰的容器
3加锁,使用synchronized关键字或者是lock方法还有乐观锁
4 线程封闭,就是把一个对象锁到一个线程,只有该线程可见,比如threadLocal就是一种常见的线程封闭方法
6 线程常用方法解释
1 join()方法
假设调用t.join(),表示让主线程进入等待池,等待t线程直线完毕之后才会被唤醒
2Thread.yield()方法
作用是:暂停当前正在执行的线程对象(及放弃当前拥有的cup资源)进行就绪状态,并执行其他线程。
3 yield和sleep的区别
使用sleep之后线程进入阻塞状态,在此期间不会获得cpu调度,而yield不一样他只是暂时的让出cpu调度,如果没有其他线程可以运行还是要继续运行当前的线程
7 ThreadLocal的实现原理
线程本地存储
调用set方法时,先获取了当前的线程,然后获取线程中ThreadLocalmap类型的对象threadlocals,然后将Entry<threadLocal,value>存入到当前线程中的对象threadlocals中,其中entry中的threadlocal,使用的是弱引用,目的是为了避免内存泄漏,如果使用强引用,那么当我们定义的变量threadLocal销毁时,对应的threadlocal对象并不会被回收,因为entry中的key仍然指向它
最后切记调用threadlocal中的remove方法,他会将所有的map清空
使用场景,Spring中的transanction会应用到,将connection对象放到threadlocal中
首先使用如下方式获得数组对应位置
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {//如果没有找到符合以下条件的位置,就遍历i以后的所有值,到尾部之后就从0开始
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;//如果key相等就将value进行替换
}
if (k == null) {
replaceStaleEntry(key, value, i);//如果key为空就替换该位置的键值对
return;
}
thread怎么解决的hash冲突
主要使用
开放寻址法
如果出现哈希冲突,就会重新探测一个空线位置,从当前位置依次往后查找,看是否有空闲位置
8 java中两种创建线程的方式
3使用callable接口和futureTask类相结合
myThread m=new myThread();//首先创建callable接口的实例对象
FutureTask<Integer> f=new FutureTask(m);//使用futuretask进行包装
//其中futureTask实现了 runnable接口
Thread tt=new Thread(f);
tt.start();
4 通过线程池也算一种吧
9 callable 和 runnable 的区别
callable 可返回值||抛出异常,runnable不行
callable可以取消执行
10 submit 和 execute 区别
execute提交【无返回值】的任务,无法根据返回值判断任务是否成功执行
submit提交【有返回值】的任务,根据返回值判断任务是否成功执行
11 标题线程生存周期
12 sleep 和 wait的区别
- 【是否释放锁】:sleep 没有释放锁,wait释放锁。
因此 wait多用线程交互,sleep只是暂停线程执行 - 【是否自动苏醒】:
线程调用sleep()后,会自动苏醒。
线程调用wait() ,
若wait()无参,需要等待其他线程调 【同一对象】的notify() notifyAll()唤醒。
若wait()方法有timeout参数,则会超时后苏醒。
13.为什么调用start执行run(),而不是直接调用run()
直接调用run()方法: 只是在main()线程中将run方法作为普通方法调用,还是在main线程中执行。这并不是多线程
调用start()方法 : 启动一个线程 & 使该线程进入就绪状态,当该线程分配到时间片后开始运行。
14 线程状态
- 初始(new):使用new关键字创建线程
2. 运行状态(runnable):正在运行+运行就绪状态; - 等待(waiting) 如调用join方法时或调用wait()方法时,调用park()方法时,或等待用户输入
4.timed-waiting:调用sleep进入等待状态,此时不会释放锁
5.阻塞状态(blocked),比如获取不到锁的时候,线程会进入阻塞状态
5. 终止(Terminated) : 当run函数或者main函数运行结束,线程终止
15 线程状态切换
情况一:NEW->Runnable
调用start()方法
情况二:Runnable<-> Waitting
t线程用Sychronized(obj)获取了对象锁之后
调用obj.wait()方法,t线程由runnable状态变为waitting
调用obj.notify()等方法时
竞争锁成功,t线程从waiting到runnable
竞争锁失败,t线程从waitting到blocked
情况三: runnable<->waitting
当前线程(不是t线程)调用t.join()方法时
注意是在当前线程在t线程对象的监视器上等待时
t线程运行结束,或调用当前线程的interrupt()时,从waitting到runnable
情况四:runnable<->waitting
当前线程调用LockSupport.park()方法时,从runnable到waitting
调用LockSupport.unpark()或调用线程的interrupt()时,从waitting到runnable
情况五:runnable<->timed_waitting
t线程调用synchronized(obj)获取了对象锁之后
调用obj.wait(long n)时,t线程从runnable到timed_waitting
t线程等待时间超过毫秒,或调用obj.notify等时
竞争锁成功,t线程从timed到runnable
竞争锁失败,t线程从timed到blocked
可用以下代码更加直观的理解
public class TestState {
public static void main(String[] args) throws IOException, IOException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running...");
}
};
Thread t2 = new Thread("t2") {
@Override
public void run() {
while(true) { // runnable
}
}
};
t2.start();
Thread t3 = new Thread("t3") {
@Override
public void run() {
log.debug("running...");
}
};
t3.start();
Thread t4 = new Thread("t4") {
@Override
public void run() {
synchronized (TestState.class) {
try {
Thread.sleep(1000000); // timed_waiting
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t4.start();
Thread t5 = new Thread("t5") {
@Override
public void run() {
try {
t2.join(); // waiting
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t5.start();
Thread t6 = new Thread("t6") {
@Override
public void run() {
synchronized (TestState.class) { // blocked
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t6.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1 state {}", t1.getState());
log.debug("t2 state {}", t2.getState());
log.debug("t3 state {}", t3.getState());
log.debug("t4 state {}", t4.getState());
log.debug("t5 state {}", t5.getState());
log.debug("t6 state {}", t6.getState());
System.in.read();
}
}
代码二:帮助理解wait和blocked状态
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
final static Object obj=new Object();
public static void main(String[] args) {
Thread t1=new Thread("t1"){
@Override
public void run() {
synchronized (obj) {
log.debug("run");
try {
log.debug("waiting");
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("other");
}
}
};
t1.start();
Thread t2=new Thread("t1"){
@Override
public void run() {
synchronized (obj) {
log.debug("run");
try {
log.debug("waiting");
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("other");
log.debug(Thread.currentThread().getState().toString());
log.debug(t1.getState().toString());
}
}
};
t2.start();
Sleeper.sleep(0.5);
synchronized (obj){
log.debug("notify others");
obj.notifyAll();
}
}
}
16 线程中断
可以调用线程的中断方法 Thread.interrupt()方法
注意调用线程的中断方法并不会立即中断线程,而是修改线程的中断标志位,
然后调用线程的isInterrupt方法
17 线程独占哪些资源
1 线程ID
2.寄存器组的值
由于线程间是并发运行的,每个线程有自己不同的运行线索,当从一个线程切换到另一个线程上时,必须将原有的线程的寄存器集合的状态保存,以便将来该线程在被重新切换到时能得以恢复。
2 虚拟机栈
3 本地方发栈
4 错误返回码:线程可能会产生不同的错误返回码,一个线程的错误返回码不应该被其它线程修改;
5 信号掩码/信号屏蔽字(Signal mask):表示是否屏蔽/阻塞相应的信号,由于每个线程所感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自己管理。但所有的线程都共享同样的信号处理器。
6 线程的优先级。由于线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参数,这个参数就是线程的优先级。
18 进程和线程的区别
首先解释一下,程序,进程,线程
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须加载。在指令运行过程中还需要用到磁盘、网络设备。
进程是用来加载指令、管理内存、管理IO的
当一个程序被运行,从磁盘加载这个程序的代码至内存,就会开启一个进程
进程就可以视为程序的一个实例。大部分程序可以运行多个实例(比如记事本,画图,浏览器),有的只能启动一个实例(网易云音乐,360等);
线程
一个进程之内可以分为一到多个线程
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行
java中,线程作为最小调度单位,进程作为资源分配的最小单位。在windows中进程是不活动的,只是作为线程的容器
1 进程是系统进行资源分配的最小单位,一个进程可以有多个线程;线程是CPU调度的最小单位
2 每个独立的进程程有一个程序运行的入口、顺序执行序列;线程依赖于进程而存在,一个进程至少有一个线程
3 进程有自己独立的地址空间,每启动一个进程,系统都会为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段;线程没有独立的地址空间,同一进程的线程共享本进程的地址空间。
4 进程是拥有系统资源的一个独立单位,而线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源
5 线程之间的通信更方便,同一进程下的线程共享全局变量等数据,而进程之间的通信需要以进程间通信(IPC)的方式进行;
6 多线程程序只要有一个线程崩溃,整个程序就崩溃了,但多进程程序中一个进程崩溃并不会对其它进程造成影响,因为进程有自己的独立地址空间,因此多进程更加健壮
19 线程池的作用及优点
1、线程池的重用
线程的创建和销毁的开销是巨大的,而通过线程池的重用大大减少了这些不必要的开销,当然既然少了这么多消费内存的开销,其线程执行速度也是突飞猛进的提升。
2、控制线程池的并发数
并发:在某个时间段内,多个程序都处在执行和执行完毕之间;但在一个时间点上只有一个程序在运行。
回到线程池,控制线程池的并发数可以有效的避免大量的线程池争夺CPU资源而造成堵塞。
3、线程池可以对线程进行管理
线程池可以提供定时、定期、单线程、并发数控制等功能。比如通过ScheduledThreadPool线程池来执行S秒后,每隔N秒执行一次的任务。
20 常用线程池种类
定长连接池newFixedThreadPool
定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
定长线程池的大小最好根据系统资源进行设置
Runtime.getRuntime().availableProcessors()。
单任务线程池 newSingleThreadExecutor
可缓存线程池 newCachedThreadPool
创建对象时不用设置线程池数量,直接创建线程就可以
可缓存线程池,若线程池长度超过处理需要,则回收空线程,否则创建新线程,线程规模可无限大。
当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
定长连接池,newScheduledThreadPool
支持定时及周期性任务执行
21 线程池处理多任务流程图
当线程池中的线程数等于最大线程数量是,就会采取饱和策略来处理新的任务
- abort : 默认使用的拒绝策略, 拒绝执行任务,抛出异常
- CallerRunPolicy : 调用 当前线程池所在线程执行任务
- discard : 直接抛弃任务,不做任何处理
- discardOldest : 抛弃 请求队列中最早的任务
22 如何创建线程池
1,通过Executors工厂方法创建
Executorservice es=new Executors.SingledeThreadPool();
2,通过newThreadPoolExcutors创建
ThreadPoolExecutor t=new ThreadPoolExecutor();
1. int corePoolSize:
2. int maximunPoolSize:
最大线程数量
3. long keepAliveTime:线程限制时间
4. BlockingQueue workQueue,
5. ThreadFactory
负责创建线程
6. handler 制定拒绝策略
7. allowCoreThreadTimeOut 这个属性设置为true表示核心线程限制时间超过也要销毁
3,继承Thread类创建线程
4, 实现Runnable接口
5,实现Callable接口
4,Executor创建线程池的【弊端】
FixedThreadPool 、SingleThreadExecutor 中等待队列长度设置为Integer.MAX_VALUE
CachedThreadPool 和 ScheduledThreadPool 允许最大线程数设置为 Integer.MAX_VALUE
这两者都可能造成 OOM的问题
23 进程的内存分配
从低地址到高地址分别内存区分别为:
代码段
数据段(初始化)
数据段(未初始化)(BSS)
堆
栈
命令行参数和环境变量
其中,堆向高内存地址生长,栈向低内存地址生长。
24 进程间通信方式
24.1 管道
管道就是操作系统在内核中开辟的一段缓冲区
以父子进程为例:创建一个子进程,并且他们指向的是同一个管道,由于父子进程都能访问这个管道,就可以通信。进程1可以将需要交互的数据拷贝到这段缓冲区,进程2就可以读取了,只能承载无格式字节流
无名管道 pipe
1. 半双工(单向,读写端固定)
2. 仅使用【父子、兄弟进程】通信
3. pipe文件仅存于【内存】中
4. 无名管道只能用于具有亲缘关系的进程之间
有名管道(命名管道)FIFO
1. 半双工(单向,读写端固定)
1. 【无关】进程可以通信
24.2 信号(Signal):用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身; 承载量较小
24.3 消息队列(Message):消息队列是消息的链表,存放在内核中并由消息队列标识符标识
,有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。同一主机的每一个进程都可以使用
**24.4共享内存:**映射一段能被其他进程访问的内存(将物理内存映射到虚拟地址空间中,操作虚拟地址),这段共享内存由一个进程创建,但是多个进程可以访问。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
1. 共享内存是【最快】的IPC,进程直接对 【内存】读取
2. 需要【信号量】实现【进程同步||互斥】
24.5信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
1. 信号量实现进程间【互斥和同步】,而不是实现【通信数据】。通常结合【共享内存】使用。
2. 是一个具有等待队列的计数器(表示现在是否有资源可以使用),用来控制多个进程或线程对资源的访问,通常作为一种锁机制
24.6 套接字(Socket):上面的都是本地进程通信方式,通过进程的ID标识进程,而他可用于不同机器之间的进程间通信。
使用socket进行进程通信的进程采用的客户/服务器系统是如何工作的呢?
1、服务器端
首先服务器应用程序用系统调用socket来创建一个套接字,它是系统分配给该服务器进程的类似文件描述符的资源,它不能与其他的进程共享。
接下来,服务器进程会给套接字起个名字,我们使用系统调用bind来给套接字命名。然后服务器进程就开始等待客户连接到这个套接字。
然后,系统调用listen来创建一个队列并将其用于存放来自客户的进入连接。
最后,服务器通过系统调用accept来接受客户的连接。它会创建一个与原有的命名套接不同的新套接字,这个套接字只用于与这个特定客户端进行通信,而命名套接字(即原先的套接字)则被保留下来继续处理来自其他客户的连接。
2、客户端
基于socket的客户端比服务器端简单,同样,客户应用程序首先调用socket来创建一个未命名的套接字,然后将服务器的命名套接字作为一个地址来调用connect与服务器建立连接。
一旦连接建立,我们就可以像使用底层的文件描述符那样用套接字来实现双向数据的通信。
而TCP套接字也是通过单个文件描述符进行读写套接字的,为了保证读和写的位置不错乱,操作系统在内核空间为每个TCP套接字维护了两个buffer空间,一个buffer用于写、一个buffer用于读。提供读的buffer空间称为recv buffer,提供写的buffer空间称为send buffer,它们统称为socket buffer。
所以,服务端和客户端通过两个套接字通信就简单了,一端向send buffer写数据,该buffer的数据会通过已经建立好的TCP连接发送到另一端的recv buffer,于是另一端只需从recv buffer中读数据即可实现不同计算机上的进程间通信。过程如图。
25 线程同步机制?
为什么需要线程同步:线程有时候会和其他线程共享一些资源,比如内存、数据库等。当多个线程同时读写同一份共享资源的时候,可能会发生冲突。因此需要线程的同步,多个线程按顺序访问资源。
互斥量Mutex:采用互斥对象机制,只有拥有互斥对象的线程才有访问互斥资源的权限。因为互斥对象只有一个,所以可以保证互斥资源不会被多个线程同时访问;当前拥有互斥对象的线程处理完任务后必须将互斥对象交出,以便其他线程访问该资源;
信号量 Semaphore:它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。信号量对象保存了最大资源计数和当前可用资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就减1,只要当前可用资源计数大于0,就可以发出信号量信号,如果为0,则将线程放入一个队列中等待。线程处理完共享资源后,应在离开的同时通过ReleaseSemaphore函数将当前可用资源数加1。如果信号量的取值只能为0或1,那么信号量就成为了互斥量;
事件 Event:允许一个线程在处理完一个任务后,主动唤醒另外一个线程执行任务。事件分为手动重置事件和自动重置事件。手动重置事件被设置为激发状态后,会唤醒所有等待的线程,而且一直保持为激发状态,直到程序重新把它设置为未激发状态。自动重置事件被设置为激发状态后,会唤醒一个等待中的线程,然后自动恢复为未激发状态。
临界区 Critical Section:当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。
临界区线程同步适用范围:它只能同步一个进程中的线程,不能跨进程同步。一般用它来做单个进程内的代码快同步,效率比较高。Lock是以临界区的方式来实现线程同步的。
26. 线程同步方法
- 使用synchronized关键字修饰方法或者代码块
- volatile关键字修饰变量
- 重入锁
- 局部变量threadlocal
- 使用阻塞队列
27. 线程之间怎么共享数据
如果多线程运行的代码一样,可以直接将对象包装到runnable类中
如果代码不一致,可以使用内部包装类,即在声明runnable对象时,将共同的操作对象作为参数传入
28.sychronized 使用方式
Synchronized 修饰对象 = {1. 代码块 2. 方法}
- Synchronized 修饰 静态方法 || Sychronized(Class)修饰代码块 = {
Class 类上锁
}
- Synchronized 修饰 实例方法 {
该类的某个对象实例上锁,即this对象
}
29 JDK1.6后 Synchronized的底层优化
四种锁状态 : 无锁 --> 偏向锁 --> 轻量级锁 —>重量级锁
锁随着【竞争程度】上升,逐渐升级。但不可降级。
-
【初次执行Synchronized代码块】,锁对象变为【偏向锁】
(通过CAS操作修改对象头中【锁标志位】和【持锁线程ID】)。(偏向锁意为偏向于第一个获取它的线程)。执行完同步块后,线程不主动释放偏向锁。
当执行下一个同步块时,检测该当前想要获取锁的线程是否就是持有锁的线程。
如果是,则正常执行。【线程没有释放锁,因此也不用重新加锁】
-
【一旦出现锁竞争】,偏向锁升级为【轻量级锁】。
如果锁标志位=释放,则线程通过【CAS操作】修改锁标志位,并获取锁。如果锁标志位=锁定,则线程通过【自旋】等待锁的释放。
自旋:一个线程获取锁,其他线程通过忙循环等待线程释放锁。
轻量级锁本质 = 【忙等开销 换取 用户态切换到核心态的开销】
-
【忙等是有限度】,当某个线程自旋次数达到最大自选次数。
该线程通过CAS操作修改对象头的锁标志位,表明轻量级锁升级为【重量级锁】
一个线程持有锁时,其他请求线程只能阻塞等待。
30 synchronized 底层
Sychronized 修饰 代码块 || 方法
- 修饰代码块时
每一个对象头都会有monitor
通过 【monitorenter 和 monitorExit 两条指令】,分别指定同步代码块的 开始位置和结束位置
线程获取锁 = 执行monitorenter指令时尝试获取位于对象头的monitor的持有权
获取到锁,则计数器++。 执行到monitorExit,则计数器–
2.修饰方法
JVM通过 ACC_SYNCHRONIZED 辨别方法为同步方法
31 重入锁ReetrantLock
重入锁特点:
1 可重入,即持有锁的线程可以再次使用锁并正常运行,代码如下所示
@Slf4j(topic = "c.Test22")
public class Test2 {
static ReentrantLock lock=new ReentrantLock();
public static void main(String[] args) {
//验证ReentrantLock的可重入性
lock.lock();
log.debug("m");
m1();
}
public static void m1(){
lock.lock();
try {
log.debug("m1");
m2();
}finally {
lock.unlock();
}
}
public static void m2(){
lock.lock();
try {
log.debug("m2");
}finally {
lock.unlock();
}
}
}
2,可打断性,指线程因为没有获取到锁而阻塞时,可以调用interrupt()方法将其打断,代码如下所示
@Slf4j(topic = "c.Test22")
public class Test2 {
static ReentrantLock lock=new ReentrantLock();
public static void main(String[] args) {
//锁可打断性,指在等待获取锁的时候是可以打断的
Thread t1=new Thread("t1"){
@Override
public void run() {
try {
log.debug("try");
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("interrupt");
return;
}
try{
log.debug("try");
lock.lock();
log.debug("lock");
Sleeper.sleep(2);
}finally {
lock.unlock();
log.debug("unlock");
}
}
};
log.debug("lock");
lock.lock();
t1.start();
Sleeper.sleep(1);
t1.interrupt();
log.debug("unlock");
lock.unlock();
}
}
32 ReentrantLock 和 Sychronized 区别
-
两者都是【可重入锁】 :
某一线程获得某一对象锁时,若自己未释放,也可重复再次获得该对象的锁 -
Sychronized 依赖JVM实现,而ReentrantLock 依赖API实现(JDK层面)
ReentrantLock 调用 lock() unlock()
Sychronized 在JVM层面,通过字节码指令 monitorEnter monitorExit指定同步块的开始和结束位置 -
ReentrantLock 实现高级功能
(1) ReentrantLock实现等待可中断 :
通过调用 lockInterruptibly() 中断等待锁的线程
(2) ReentrantLock可实现公平锁,而Sychronized仅实现非公平锁:
公平锁 = 先等待的线程,先获得锁
(3) 等待/通知机制 不同:
Sychronized 通过 notiy() notifyAll() wait() 实现等待/通知机制
ReentrantLock 通过 Condition对象实现。
Condition 对象调用signal ||signalAll()
唤醒线程所在范围 = 注册的线程,
而Sychronized 调用 notify() || notifyAll()
唤醒线程 = JVM选择的
因此 ReentrantLock的等待通知机制更加灵活
33 volatile关键字(jdk1.5以后才生效)
最适用于一个线程写其他线程读的情况,因为volatile只能保证可见性已经防止指令重排,并不能保证原子性
1 volatile写的内存语义:当写线程写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
2 volatile读的内存语义:当读线程读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存读取共享变量。
3 它可以用来修饰成员变量以及静态成员变量,但是不可以修饰局部变量
4 防止指令重排
添加volatile关键字的变量,在涉及到读操作的时候,会在读操作之前添加屏障,防止读屏障之后的代码先执行;在涉及到写操作的时候会在写操作之后添加屏障,防止写屏障之前的代码后执行
5 volatile 作用
1. 防止指令重排
2. 保证变量在多个线程间可见
当前Java内存模型,不是从主存读取变量,而是将变量保存在本地内存(寄存器)。
可能存在 一个线程修改【主存】中的变量值,而另一个线程仍使用【本地内存】中的变量拷贝值,
造成【数据】不一致。
变量声明为volatile,即告知JVM该变量不稳定。每次要在主存中读取值。
34. Synchronized 和 volatile 的区别
- 【是否阻塞】
volatile 不会造成阻塞,Synchronized会造成阻塞 - 【作用范围不同】
volatile 只能修饰变量, Synchronized 修饰 代码块和 方法 - 【作用不同】
volatile 主要用于保证变量在多个线程之间的可见性,而Synchronized则是保证临界资源在多个线程之间的同步性
volatile 仅保证变量的可见性,而不保证原子性。Synchronized两者都能保证
volatile可以阻止重排序,但是synchronized不能阻止重排序,如果某个变量所有的作用域都被synchronized锁住,那么是不会产生与有序性,可见性,原子性等问题的,但是如果该变量的作用域在synchronized作用之外,就不能保证会产生上述三种问题 - 【效率】
volatile 是 线程同步的轻量级实现,效率高于Sychronized - 两者都能实现变量的可见性,但是原理不同
volatile原理,就是其读写原理的特性
sunchoronized原理:
解决第一个因素:在加锁前会将工作内存的值全部重新加载一遍,保证最新;释放锁前将工作内存的值全部更新到主存;由于在带锁期间,没有其他线程能访问本线程正在使用的共享变量,这样就保证了可见性。
解决第二个因素: 由于Synchronized修饰的代码块都是原子性执行的,即一旦开始做,就会一直执行完毕,期间有其他线程不可以访问本线程所使用的共享变量,这样,即便指令重排了也不会出现问题。
35. Synchronized 和 clock的区别
Lock是API层面的,synchronized是JVM层面的
采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
lock的实现类有哪些
reetrantlock
ReentrantReadWriteLock.ReadLock , ReentrantReadWriteLock.WriteLock
36 AQS
1. AQS 是构建同步器的【框架】
【核心思想】 : 线程请求资源
情况1 : 资源空闲
则 请求线程设置为工作线程,资源上锁
情况2 : 资源被占用
则 请求线程阻塞,加入CLH队列。等待资源空闲时竞争资源
2. AQS 定义两种 资源共享模式
独占锁 Exclusive : 锁只能被一个线程占有
例如 : ReentrantLock 又分为 公平锁和非公平锁
共享锁 shared : 多个线程共享锁
例如 : CountDownLatch 、Semaphore
3. AQS框架 自定义模块
尝试 获取/释放 独占资源
tryAcquire()
tryRelease()
尝试 获取/释放共享资源
tryAcquireShared()
tryReleaseShared()
4. AQS 常见组件
- ReentrantLock
A 线程调用 lock()方法
若 state=0 ,
则资源空闲 ,state++,且 A线程可重复获取锁
若 state!=0 ,
则资源被占有,当state=0时其他线程才能竞争
-
CountDownLatch
(1) 构造器初始化 【state = N】当【子线程】调用countDown(),通过 CAS操作state自减1
当state=0 时,调用await的线程 恢复正常继续执行
-
CyclicBarrier
构造方法 state=n每当一个线程调用 await()方法,则CAS操作state自减1
当state=0 时 ,所有调用await()的线程恢复
5.AQS源码
1. aquire()
public void aquire(){
if(!tryAcquire() // 尝试获取一次
&& acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
// acquireQueued 【作用】: 自旋检测 (tryAcquire()&& node==head.next)
// addWaiter【作用】: 添加当前线程node至 队列尾部
selfInterrupt();
}
【问题】: 为何不仅调用 acuqireQueued(addWaiter())
优先尝试最可能成功的代码, 可减少执行的字节码指令
37 .乐观锁和悲观锁
悲观锁和乐观锁 是锁的两种分类,并非特指某一个锁。
悲观锁【通过阻塞机制】实现,乐观锁通过【回滚重试】实现。
- 悲观锁 :
【描述】:线程独占临界资源,在线程执行完毕前,其他【请求】线程只能【阻塞】等待
【适用场景】: 写操作较多,资源竞争激烈。 - 乐观锁 :
【描述】:乐观锁并【未上锁】,【更新前】检测该数据在【读取到更新】这一段时间内是否被其他线程修改。如果被修改,则循环操作直到成功为止。
【使用场景】: 读操作较多,资源竞争情况较少 - CAS 是原子性操作,是乐观锁实现的基础:
(1) 读取值A,在更新为B之前,检测【原值】是否为A
(2) 如果是,则更新为B
如果不是,则更新失败。
38 CAS ,compare and swap
使用CAS乐观锁可以实现无锁并发,但是需要配合volatile关键字,保证变量的可见性,这种组合适用于竞争不激烈、多核CPU的场景
使用CAS和Synchronized比较的代码实例,代码中我们用比较好理解的方式展示了CAS基本原理
public class Test33 {
public static void main(String[] args) {
UnsafeAccount unsafeAccount=new UnsafeAccount(50000);
Account.demo(unsafeAccount);
CasAccount casAccount=new CasAccount(50000);
Account.demo(casAccount);
}
}
class CasAccount implements Account{
private AtomicInteger account;
public CasAccount(Integer account) {
this.account = new AtomicInteger(account);
}
@Override
public void withdraw() {
while (true){
int pre=account.get();
int next=pre-10;
if(account.compareAndSet(pre,next)){
break;
}
}
}
@Override
public Integer getaccount() {
return account.get();
}
}
class UnsafeAccount implements Account{
private Integer account;
public UnsafeAccount(Integer account) {
this.account = account;
}
@Override
public synchronized void withdraw() {
this.account-=10;
}
@Override
public Integer getaccount() {
return this.account;
}
}
interface Account{
void withdraw();
Integer getaccount();
static void demo(Account account ){
List<Thread> ts=new ArrayList<>();
for (int i = 0; i < 5000; i++) {
ts.add(new Thread(()->{
account.withdraw();
}));
}
long start=System.nanoTime();
ts.forEach(Thread::start);
ts.forEach(t->{
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end=System.nanoTime();
System.out.println(account.getaccount()+"cost:"+(end-start)/1000_1000+"ms");
}
}
缺点:
1. ABA如果操作值由A变成B再变成A,用SAC检查的时候不会发现
2. 循环时间开销大,如果CAS检验一直不成功就会一直尝试
3. 只能保证一个共享变量的原子操作
39 什么是死锁?
在两个或者多个并发进程中,每个进程持有某种资源而又等待其它进程释放它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁(deadlock)。
如下代码所示,将会产生死锁
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {
public static void main(String[] args) {
Object A=new Object();
Object B=new Object();
Thread t1=new Thread("t1"){
@Override
public void run() {
synchronized (A){
log.debug("get A");
Sleeper.sleep(1);
synchronized (B){
log.debug("get B");
}
}
}
};
t1.start();
Thread t2=new Thread("t2"){
@Override
public void run() {
synchronized (B){
log.debug("get B");
Sleeper.sleep(0.5);
synchronized (A){
log.debug("get A");
}
}
}
};
t2.start();
}
}
40. 产生死锁的四个条件
41.死锁检测
1.通过idea中的命令行窗口,先输入jps查看要检查的进程号
jstack 进程号,输出进程运行信息
下图显示死锁情况
下图显示了死锁代码所在位置
2. 通过工具jconsole来查看进程运行情况,其中有一个检测思索的功能,点击后会看到如下信息
42. 死锁的处理方法
如何检测死锁:检测有向图是否存在环;或者使用类似死锁避免的检测算法。
鸵鸟策略
直接忽略死锁。因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。
死锁解除的方法:
- 利用抢占:挂起某些进程,并抢占它的资源。但应防止某些进程被长时间挂起而处于饥饿状态;
- 利用回滚:让某些进程回退到足以解除死锁的地步,进程回退时自愿释放资源。要求系统保持进程的历史信息,设置还原点;
- 利用杀死进程:强制杀死某些进程直到死锁解除为止,可以按照优先级进行。
死锁预防
基本思想是破坏形成死锁的四个必要条件:
- 破坏互斥条件:允许某些资源同时被多个进程访问。但是有些资源本身并不具有这种属性,因此这种方案实用性有限;
- 破坏占有并等待条件:
- 实行资源预先分配策略(当一个进程开始运行之前,必须一次性向系统申请它所需要的全部资源,否则不运行);
-或者只允许进程在没有占用资源的时候才能申请资源(申请资源前先释放占有的资源); - 缺点:很多时候无法预知一个进程所需的全部资源;同时,会降低资源利用率,降低系统的并发性;
- 破坏非抢占条件:允许进程强行抢占被其它进程占有的资源。会降低系统性能; - 破坏循环等待条件:对所有资源统一编号,所有进程对资源的请求必须按照序号递增的顺序提出,即只有占有了编号较小的资源才能申请编号较大的资源。这样避免了占有大号资源的进程去申请小号资源。
死锁避免
动态地检测资源分配状态,以确保系统处于安全状态,只有处于安全状态时才会进行资源的分配。所谓安全状态是指:即使所有进程突然请求需要的所有资源,也能系统对进程的资源分配顺序,可以使得每一个进程运行完毕。
43 什么是活锁
活锁出现在两个线程互相改变对方的结束条件最后导致谁也无法结束,如下代码所示
44 并发、并行、异步的区别?
并发:同一时刻线程轮流使用CPU,在一个时间段中同时有多个程序在运行,但其实任一时刻,只有一个程序在CPU上运行,宏观上的并发是通过不断的切换实现的;
多线程:并发运行的一段代码。是实现异步的手段
并行(和串行相比):在多CPU系统中,多个程序无论宏观还是微观上都是同时执行的
异步(和同步相比):同步是顺序执行,异步是在等待某个资源的时候继续做自己的事.
阻塞和非阻塞
阻塞与非阻塞的重点在于进/线程等待消息时候的行为,也就是在等待消息的时候,当前进/线程是挂起状态,还是非挂起状态。
阻塞调用在发出去后,在消息返回之前,当前进/线程会被挂起,直到有消息返回,当前进/线程才会被激活.
非阻塞调用在发出去后,不会阻塞当前进/线程,而会立即返回。
45 什么是IO多路复用?怎么实现?
IO多路复用(IO Multiplexing)是指单个进程/线程就可以同时处理多个IO请求。
实现原理:用户将想要监视的文件描述符(File Descriptor)添加到select/poll/epoll函数中,由内核监视,函数阻塞。一旦有文件描述符就绪(读就绪或写就绪),或者超时(设置timeout),函数就会返回,然后该进程可以进行相应的读/写操作。
1 它的形成原因
如果一个I/O流进来,我们就开启一个进程处理这个I/O流。那么假设现在有一百万个I/O流进来,那我们就需要开启一百万个进程一一对应处理这些I/O流(——这就是传统意义下的多进程并发处理)。思考一下,一百万个进程,你的CPU占有率会多高,这个实现方式及其的不合理。所以人们提出了I/O多路复用这个模型,一个线程,通过记录I/O流的状态来同时管理多个I/O,可以提高服务器的吞吐能力
2 select/poll/epoll三者的区别?
select:将文件描述符放入一个集合中,调用select时,将这个集合从用户空间拷贝到内核空间),由内核监控,根据就绪状态修改该集合的内容,采用水平触发机制。一旦有文件描述符就绪,select函数返回后,需要通过遍历这个集合,找到就绪的文件描述符,当文件描述符的数量增加时,效率会线性下降;
2.2 select函数优缺点
缺点1:每次都要复制,开销大
缺点2集合大小有限制
缺点3:轮询的方式效率较低
优点:跨平台支持
poll:和select几乎没有区别,区别在于文件描述符的存储方式不同,poll采用链表的方式存储,没有最大存储数量的限制;
2.3 poll函数优缺点
优点:连接数(也就是文件描述符)没有限制(链表存储)
缺点:大量拷贝
epoll:通过内核和用户空间共享内存,避免了不断复制的问题;支持的同时连接数上限很高(1G左右的内存支持10W左右的连接数);文件描述符就绪时,采用回调机制,避免了轮询(回调函数将就绪的描述符添加到一个链表中,执行epoll_wait时,返回这个链表);支持水平触发和边缘触发,采用边缘触发机制时,只有活跃的描述符才会触发回调函数。
2.4 epoll的优点
- 没有最大并发连接的限制
- 只有活跃可用的fd才会调用callback函数
- 内存拷贝是利用mmap()文件映射内存的方式加速与内核空间的消息传递,减少复制开销。(内核与用户空间共享一块内存)
总结,区别主要在于: - 一个线程/进程所能打开的最大连接数 - 文件描述符传递方式(是否复制) - 水平触发 or 边缘触发 - 查询就绪的描述符时的效率(是否轮询)
2.5 什么时候使用select/poll,什么时候使用epoll?
当连接数较多并且有很多的空闲连接或不活跃连接时,epoll的效率比其它两者高很多;但是当连接数较少并且都十分活跃的情况下,由于epoll需要很多回调,因此性能可能低于其它两者。
2.6 什么是文件描述符?
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。
内核通过文件描述符来访问文件。文件描述符指向一个文件
46 有哪些常见的IO模型?
阻塞IO(Blocking IO):用户线程发起IO读/写操作之后,其他线程阻塞,直到可以开始处理数据;对CPU资源的利用率不够;
非阻塞IO(Non-blocking IO):发起IO请求之后可以立即返回,如果没有就绪的数据,需要不断地发起IO请求直到数据就绪;不断重复请求消耗了大量的CPU资源;
IO复用模型
信号驱动IO
异步IO(Asynchronous IO):用户线程发出IO请求之后,继续执行,由内核进行数据的读取并放在用户指定的缓冲区内,在IO完成之后通知用户线程直接使用。
47 java内存模型-JMM
即Java Merory Model,简单地说,JMM定义了一套在多线程读写共享数据(成员变量、数组)时,对数据的可见性、有序性、和原子性的规则和保障
54.1原子性
因为不能保证数据的原子性,所以多线程操作同一个对象时,会出现各种问题,使用锁来解决问题,比如使用synchronized
54.2 可见性
跳不出的循环,当一个线程频繁的从主内存中读取一个变量值时JIT编译器会将变量值缓存到自己的高速缓存中,减少对主内存中该变量的访问,因此会造成以下循环无法跳出
解决方法
1 使用关键字volatile
保证了可见性,即多个线程对volatile变量修改时,彼此之间的操作都是可见的,她不能保证原子性,仅用在一个写线程,多个读线程的情况
2 在循环中加入 System.out.println语句也会导致程序停止,因为该语句的底层使用了synchorized关键字,可以同时保证原子性和可见性
54.3 有序性
同样是jvm的优化,他会对指令进重排,使用volatile关键字可以避免指令重排
54.4 happens before 规则
A happens B,意思是A的操作对B可见
1单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
2 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。线程解锁之前对变量的写,对于接下来对加锁的其他线程对该变量的读可见
3 volatile的happen-before原则: 对一个volatile变量的写操作happen-before对此变量的任意操作。线程对volatile变量的写,对接下来其他线程对该变量的读可见
4 happen-before的传递性原则: 如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
5 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。线程开始之前对变量的写,对该线程开始后对该变量的读可见
6 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。线程t1打断t2前对变量的写,对于其他线程得知t2被打断后对变量的读可见(通过t2.interrupted或t2.isInterrupted)
7 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。线程结束之前对变量的写,对其他线程得知他结束后的读可见(比如执行了t1.alive(),t1.join())
8 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。
48 说一说阻塞队列
BlockingQueueshi一个接口具体的阻塞队列继承了这个接口
* Q:好的,问题来了,我们看到下面三个方法都是添加元素,那么他们有什么不同嘛?
* A:有的,我们看上面,BlockingQueue 实现了 Queue 接口,Queue 接口又实现了 Collection 接口。其实这三个方法分别属于不同接口中的定义。
* add 方法来源于 Collection 接口,并且在 BlockingQueue 中定义当大小不足时,会抛出 IllegalStateException;
* offer 方法来源于 Queue 接口,并且在 BlockingQueue 中定义当大小不足时,会返回 false 而不是抛错;
* put 方法是 BlockingQueue 接口自己定义的,并且在 BlockingQueue 中定义当大小不足时会挂起,直到有足够的大小。允许被打断,打断会抛出InterruptedException。
*/
// 添加元素
boolean add(E e);
// 添加元素
boolean offer(E e);
// 添加元素
void put(E e) throws InterruptedException;
// 添加元素,并且有一个时间。超过时间就放弃
boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException;
// 把第一个元素取走并删除,如果没有的话就挂起直到有,打断会抛出 InterruptedException
E take() throws InterruptedException;
// 也是拿元素,超时就放弃,打断会抛出 InterruptedException
E poll(long timeout, TimeUnit unit)
throws InterruptedException;
// 查询剩余容量
int remainingCapacity();
// 删除元素。这里为什么不用 E 而用 Object 呢?我觉得有两点吧:1、限定了元素为非基础类型;2、在元素比较时需要用到 equals 方法,在注释中有用到
boolean remove(Object o);
// 查询元素,也是用 Object
public boolean contains(Object o);
// 将当前所有可用元素放到 C 中,需要子类自己实现。原注释提出,drainTo 方法比迭代调用 poll 元素要好,因为迭代过程出现异常可能会导致元素的丢失
int drainTo(Collection<? super E> c);
// 将指定大小个元素放到 C 中
int drainTo(Collection<? super E> c, int maxElements);
}
实现类1 ArrayBlockingQueue
ArrayBlockingQueue是一个有界的BlockingQueue,内部存储使用数组。ArrayBlockingQueue提供先进先出的机制,提供公平锁和非公平锁的获取
final Object[] items 创建之后大小不可改变
使用头尾指针来构建队列
使用了可重入锁来保证并发安全
内部使用了condition,notempty 和notfull来唤醒或者等待
三中获取方法:
1 poll() 队列中没有元素的话就执行null
2 take() 如果队列为空就等待,直到被入队方法唤醒
3 poll(long timeout),如果队列为空就等待,超时就中断
实现类2 LinkedBlockingQueue
LinkedBlockingQueue是一个无界的阻塞队列,内部采用Node链表来实现队列,实现了FIFO的特性。不同于ArrayBlockingQueue,LinkedBlockingQueue只提供了非公平的抢锁机制,因此入队先后是不公平的
默认是无边界队列,最好自己设初始值,否则可能会造成OOM
注意这里,LinkedBlockingQueue 将入队锁出队锁分离,提高了队列的操作速度,因此能够提高并发量
• LinkedBlockingQueue是一个默认无界(推荐调用有界的构造方法进行构造,避免出现 OOM 的情况)的阻塞队列,一旦创建大小不能修改;
• LinkedBlockingQueue内部使用Node存储元素实现队列,单链表,有头尾指针(头指针专注出队、尾指针专注出队);
• LinkedBlockingQueue将入队锁和出队锁分离,极大地提高了并发的效率。但是由于这两个锁都是 final 修饰的并且使用饿汉加载,因此默认是使用ReentrantLock的非公平策略,因此虽然保证了阻塞队列的先进先出,但是入队的过程是非公平的;
• 内部使用 Condition 机制是进程进行休眠和唤醒;
• 使用AtomicInteger 类中的private volatile int value 来表示count ,修改的时候使用CAS进行修改
49 僵尸进程和孤儿进程
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
僵尸进程危害
如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。
已经产生的僵尸进程,解决方法:kill掉父进程,它产生的僵尸进程就变成了孤儿进程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源。
僵尸进程解决办法
(1)通过信号机制
子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait或waitpid进行处理僵尸进程。
(2)fork两次
原理是将僵尸进程成为孤儿进程,从而使其的父进程变为init进程,通过init进程可以处理僵尸进程。
50 读写锁
ReadWriteLock
读写锁就是分了两种情况,一种是读时的锁,一种是写时的锁,它允许多个线程同时读共享变量,但是只允许一个线程写共享变量,当写共享变量的时候也会阻塞读的操作。这样在读的时候就不会互斥,提高读的效率。
51 原子类型
原子类型是一种线程安全的类型
51.1 原子整数
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class TestAccount {
public static void main(String[] args) {
Account account=new CasAccount(10000);
Account.demo(account);
}
}
class CasAccount implements Account{
private AtomicInteger balance;
public CasAccount(int balance) {
this.balance = new AtomicInteger(balance);
}
@Override
public Integer getBalance() {
return balance.get();
}
@Override
public void withDraw(Integer amount) {
while(true){
int prev=balance.get();
int next=prev-amount;
if(balance.compareAndSet(prev,next)){
break;
}
}
}
}
interface Account{
//获取余额
Integer getBalance();
//取款
void withDraw(Integer amount);
/**
* 方法内会启动10000个线程,每个线程做一次-10操作,初始余额为10000
* 如果线程安全,最后的余额应该为0
* */
static void demo(Account account){
List<Thread> ts=new ArrayList<>();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(()->{
account.withDraw(10);
}));
}
long start=System.nanoTime();
ts.forEach(Thread::start);
ts.forEach(t->{
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end=System.nanoTime();
System.out.println(account.getBalance()+"cost"+(end-start)/1000_1000+"ms");
}
}
51.2 原子引用
还可以通过引用达到特殊类型数值的线程安全
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
public class TestBigDecimal {
public static void main(String[] args) {
Account1 account=new SafeAccount(new BigDecimal(10000));
Account1.demo(account);
}
}
class SafeAccount implements Account1{
AtomicReference<BigDecimal> balance;
@Override
public void withDraw(BigDecimal amount) {
while(true){
BigDecimal prev=balance.get();
BigDecimal next=prev.subtract(amount);
if(balance.compareAndSet(prev,next)){
break;
}
}
}
public SafeAccount(BigDecimal balance) {
this.balance = new AtomicReference<BigDecimal>(balance);
}
}
interface Account1{
BigDecimal balance=new BigDecimal(0);
void withDraw(BigDecimal amount);
static void demo(Account1 account){
List<Thread> list=new ArrayList<Thread>();
for (int i = 0; i < 1000; i++) {
list.add(new Thread(()->{
account.withDraw(BigDecimal.TEN);
}));
}
for (Thread thread : list) {
thread.start();
}
for (Thread thread : list) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(balance);
}
}
但是很明显,原子引用并不能解决ABA的问题,所以就有了如下的引用,这个引用加上了版本号,所以可以解决ABA的问题,但是者的注意的是,CAS本身不适合线程太多的情况,而这种引用需要比较两个数值,更加不适合。
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicStampedReference;
@Slf4j
public class Test36 {
static AtomicStampedReference<String> str=new AtomicStampedReference<>("A",0);
public static void main(String[] args) {
log.debug("main start...");
String prev=str.getReference();
int stamp=str.getStamp();
log.debug("stamp:{}",stamp);
other();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("stamp:{}",stamp);
log.debug("change A->B{}",str.compareAndSet(prev,"B",stamp,stamp+1));
log.debug("stamp:{}",stamp);
}
static void other(){
new Thread(()->{
int stamp=str.getStamp();
log.debug("stamp:{}",stamp);
log.debug("change A->B{}",str.compareAndSet(str.getReference(),"B",stamp,stamp+1));
}).start();
new Thread(()->{
int stamp=str.getStamp();
log.debug("stamp:{}",stamp);
log.debug("change B->A{}",str.compareAndSet(str.getReference(),"A",stamp,stamp+1));
}).start();
}
}
51. 3原子更新器
原子更新器即使用CAS原理以及volatile关键字来保护类中的变量
51.4 原子累加器
jdk8种同样提供了很经典的累加器,如下代码所示
public class Test41 {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
demo(
()->new AtomicLong(0),
(adder)->adder.getAndIncrement()
);
}
for (int i = 0; i < 5; i++) {
demo(
()->new LongAdder(),
(adder)->adder.increment()
);
}
}
public static <T>void demo(Supplier<T> addSupplier, Consumer<T> consumer){
T adder=addSupplier.get();
List<Thread> ts=new ArrayList<>();
for (int i = 0; i < 4; i++) {
ts.add(new Thread(()->{
for (int j = 0; j < 500000; j++) {
consumer.accept(adder);
}
}));
}
long start=System.nanoTime();
ts.forEach(t->t.start());
ts.forEach(t-> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end=System.nanoTime();
System.out.println(adder+"cost:"+(end-start)/1000_000);
}
}
其中LongAdder的效率明显好于AtomicLong的效率,原因是
51.5 LongAdder
其中cellsBusy,使用了CAS用于扩容时锁住cells防止多个线程同时扩容,如下代码,就简单地实现CAS锁
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j(topic = "c.LockCAS")
public class LockCAS {
private AtomicInteger station=new AtomicInteger(0);//该变量表示一个状态,如果取值为1表示上锁,如果取值为0表示没有上锁。
public void lock(){//这个方法保证当一个线程上锁时,即将state的值置为0时,其他线程将会一直在死循环中等待,知道state的值被置为0
while(true){
if(station.compareAndSet(0,1)){
log.debug("lock");
break;
}
log.debug("trying...");
}
}
public void unlock(){
log.debug("unlock");
station.set(0);
}
public static void main(String[] args) {
LockCAS l=new LockCAS();
Thread t1=new Thread(()->{
l.lock();
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
l.unlock();
});
Thread t2=new Thread(()->{
l.lock();
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
l.unlock();
});
log.debug("begin...");
t1.start();
t2.start();
}
}
上述代码简单的实现了两个线程竞争时的CAS锁,但是值得注意的是,改代码不适用于实际应用,因为没有获取到锁的线程会一直在死循环等待,知道开锁,这样会耗费cpu,影响整个运行速度