JMM内存模型?
Java内存模型是一种规范,用来解决多线程通过共享内存进行通信时数据不一致,编译器和处理器对代码和指令进行重排序而带来的问题,从而保证多线程场景下的原子性,可见性和有序性。共享数据存放在主内存中,每个线程都有一个本地内存,线程读取共享数据时,需要从主内存中拷贝数据副本,更新数据后写回主内存。
JMM有八大原子操作(lock, unlock,read, write,load,store,use,assign)
Volatile关键字的原理和作用?
Volatile的底层原理是缓存一致性协议。即多个cpu从主内存读取某数据到缓冲区中,该数据在执行lock前缀指令的cpu的缓冲区中锁定,那么数据锁定期间,其他cpu无法读写该数据。只有当该数据更新完被写回主内存后,其他cpu通过总线嗅探机制检测到该数据被修改了,然后及时失效自己缓存区中的数据,等到下一次需要用到该数据的时候,重新从主内存中读取该数据到缓冲区中。
Synchronized的原理?
Synchronized修饰代码块的时候,会在代码块的前后生成一个monitorenter和monitorexit指令,当执行monitorenter命令时就会去获取对象的锁,如果对象的锁空闲或者已经有对象锁了,就把锁的计数器+1,当执行monitorexit指令后就把锁的计数器-1,当锁的计数器为0的时候对象锁就被释放了。如果获取对象锁不成功,则进去阻塞等待状态,等待锁被释放后再去竞争对象锁。
Synchronized锁升级?
- 首先是无锁状态
- 当有一个线程尝试获取对象锁的时候,就会在对象头的markword中记录线程的id,表明这个对象锁偏向该线程。
- 当有另一个线程来竞争对象锁的时候,首先会撤销偏向锁,然后给原持有偏向锁的线程的栈中分配锁记录,然后拷贝对象头的markword到锁记录中,通过cas修改对象的锁记录指针指向该线程,这样就将偏向锁升级为了轻量级锁。另一个线程栈中分配锁记录,然后尝试通过自旋的方式cas操作修改对象头的markword的所记录指针指向该线程,如果修改成功,则表明该线程获得了轻量级锁。自旋一定次数后,就撤销轻量级锁,使用操作系统底层的mutex来实现重量级锁。
cas原理以及可能出现的问题?
Cas是基于乐观锁的思想来实现的,主要有三个参数,要更新的值,期望的值和更新后的值
只有当要更新的值等于期望的值时,就把要更新的值更新为更新后的值。
可能出现ABA问题
如何创建线程(四种方式)?
- 继承Thread类
- 实现Runnable接口
- Callable接口和futuretask配合使用
- 使用线程池创建
线程的状态和生命周期?
线程的状态:1.new -> runnable -> terminated -> waiting -> time_waiting -> blocked
多线程都有哪些锁?
synchronized和ReentrantLock
Synchronized是jvm提供的
ReentrantLock是jdk提供的
ThreadLocal原理?
使用threadlocal存放对象后,对象是线程私有的,可以避免多线程竞争。
Threadlocalmap是threadlocal的静态内部类,entry就是threadlocalmap的静态内部类。
每个线程都有一个threadlocalmap类型的变量threadlocals,threadlocalmap又维护了一个Entry类型的数组。Entry继承WeakReference(ThreadLocal),Entry是一个键值对,键存放的是threadlocal的引用,值就是threadlocal对应的值。Threadlocal的set和get方法其实是对threadlocalmap的Entry进行操作,threadlocalmap相当于一个hash表,解决hash冲突的方法是线性探测。
内存泄漏问题:1.threadlocal推荐使用private static关键字修饰,那么它的生命周期跟线程一样,threadlocal不使用了长期得不到释放。2.threadlocal对象的强引用没了后,entry和threadlocal对象之间是弱引用,会被垃圾回收器回收,但是entry中的value不会被回收掉,所以threadlocal使用完毕需要调用remove方法。
锁和ThreadLocal分别适用什么场景?对于性能要求特别高的场景应该用哪个?
锁:以时间换空间:数据在线程间共享
ThreadLocal则是以空间换时间。:每个线程都有自己的副本。
Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
对于性能要求特别高的场景用threadlocal。
Reentrantlock原理底层实现?
Reentrantlock是基于AQS来实现的。
AQS?
AQS抽象队列同步器,当线程请求资源时,资源空闲,那么就把线程设置为工作状态,并把资源锁定。如果资源被占用,那么需要一套线程阻塞以及唤醒时锁分配机制。
Synchronized和ReentrantLock区别?
Synchronized和Reentrantlock都是加锁阻塞式同步。
- Synchronized是由jvm提供的,而reentrantlock则是由jdk提供的.
- Synchronized在老版本性能不高,优化升级后跟reentrantlock的性能差不多
- Synchronized不可中断(只有获取到锁之后才能中断,等待锁时不可中断。),而reentrantlock等待可以中断
- Reentrantlock可以用来实现公平锁。
- Reentrantlock配置Condition条件类,使用起来比synchronized更灵活。
Synchronized不可中断(只有获取到锁之后才能中断,等待锁时不可中断。):
- 若线程被中断前,如果该线程处于非阻塞状态(未调用过
wait
,sleep
,join
方法),那么该线程的中断状态将被设为true, 除此之外,不会发生任何事。 - 若线程被中断前,该线程处于阻塞状态(调用了
wait
,sleep
,join
方法),那么该线程将会立即从阻塞状态中退出,并抛出一个InterruptedException
异常,同时,该线程的中断状态被设为false, 除此之外,不会发生任何事。
ReentrantLock实现原理:
ReentrantLock是基于AQS来实现的,AQS是一个抽象队列同步器,使用了模板设计模式,已经制定好了acquire,acquiredshared,release,releasedShared四个主要的模板方法。子类只需要实现它的tryacquire,tryacquredshared,tryrelease,tryreleasedShared即可。
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
java线程同步?
- synchronized同步方法
- Synchronized同步代码块
- Volatile关键字修饰变量+cas操作
- Reentrantlock
- Threadlocal
- 阻塞队列
- 使用原子变量实现线程同步
CountDownlatch的用法?
再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
如何让一个线程终止?
- 使用退出标志。(推荐)
- thread.stop(可能会产生预料不到的结果)
- Thread.interrupt()1.线程处于阻塞状态,比如使用了sleep()则抛出异常。
- 使用while(!isInterrupted)来判断线程是否被中断。
BlockingQueue?
BlockingQueue是双缓冲队列。BlockingQueue内部使用两条队列,允许两个线程同时向队列一个存储,一个取出操作。在保证并发安全的同时,提高了队列的存取效率。
-
ArrayBlockingQueue(int i):规定大小的BlockingQueue,其构造必须指定大小。其所含的对象是FIFO顺序排序的。
-
LinkedBlockingQueue()或者(int i):大小不固定的BlockingQueue,若其构造时指定大小,生成的BlockingQueue有大小限制,不指定大小,其大小有Integer.MAX_VALUE来决定。其所含的对象是FIFO顺序排序的。
-
PriorityBlockingQueue()或者(int i):类似于LinkedBlockingQueue,但是其所含对象的排序不是FIFO,而是依据对象的自然顺序或者构造函数的Comparator决定。
-
SynchronizedQueue():特殊的BlockingQueue,对其的操作必须是放和取交替完成。
-
DelayedWorkQueue() 延时队列
ArrayBlockingQueue和LinkedBlockingQueue区别,使用的场景?
ArrayBlockingQueue和LinkedBlockingQueue最大的区别还是有界和无界的区别,所以ArrayBlockQueue适合任务量不多的场景比如人事系统人员变动后,其他依赖应用进行数据同步。(人员变动不是很频繁)
LinkedBlockQueue适合任务量大的场景比如邮件/短信提醒这种任务。
典型的线程池特点:
1.newFixedThreadPool(创建一个定长线程池)
corePoolSize = n,maximumPoolSize = n
keepAliveTime = 0,workQueue=LinkedBlockingQueue(无界阻塞队列)
适用:执行长期的任务,性能好很多
2.newSingleThreadExecutor(创建一个单线程的线程池)
corePoolSize = 1, maxinumPoolSize = 1,
keepAliveTime = 0, workQueue= LinkedBlockingQueue(无界阻塞队列)
适用:一个任务一个任务执行的场景
3.newCachedThreadPool(创建一个可缓存线程池)
corePoolSize = 0, maximumPoolSize = Integer.MAX_VALUE;
keepAliveTime=60L,workQueue=SynchronousQueue(同步队列)
适用:执行很多短期异步的小程序或者负载较轻的服务器
4.newScheduledThreadPool(创建一个定时或者延时执行任务的定长线程池)
corePoolSize = n, maximumPoolSize = Integer.MAX_VALUE,
keepAlivetime=0,workQueue = DelayedWorkQueue(延时队列)
适用:周期性执行任务的场景
线程在线程池中的状态?
- running 既接受新任务也处理已经添加的任务
- Shutdown 不接受新任务但处理已经添加的任务
- Stop 不接受新任务,也不处理已经添加的任务
- Tidying 所有任务终止
- Terminated 线程池终止
用户向线程池请求线程后线程池的处理过程?
线程池参数和作用,拒绝策略有哪些?
- 核心线程数
- 最大线程数
- 线程工厂
- 阻塞队列
- Keep alive time 空闲线程存活时间 (一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定)
- 时间单位unit
- 拒绝策略
- AbortPolicy 直接抛出拒绝异常
- CallerRunsPolicy 只要线程池未关闭,直接在调用者线程中运行当前被丢弃的任务。
- DiscardOldestPolicy,丢弃队列中最老的,然后再尝试提交新任务
- DiscardPolicy,丢弃无法加载的任务。
合理配置线程池中各个参数
1.corePoolSize:核心线程数(最重要)
对于Cpu密集型任务(大量需要cpu计算) corePoolSize = cpu核数 + 1
对于IO密集型任务(大量I/O操作,CPU利用率很低) 线程数可以尽可能的多
推荐:corePoolsize = cpu核数 + 1或者corePoolSize = CPU核数/(1-阻塞系数 ) 阻塞系数在(0.8-0.9)之间
2.其他参数根据实际场景来设置
多线程手撕代码
1.打印奇偶数
方法1:
import java.util.concurrent.atomic.AtomicInteger;
public class Main{
private static AtomicInteger num = new AtomicInteger(1);
public static void main(String[] args) {
new Thread(()->{
while (num.intValue() < 100) {
if (num.intValue() % 2 == 1) {
System.out.println("奇数线程:" + num.intValue());
num.incrementAndGet();
}
}
}).start();
new Thread(()->{
while (num.intValue() < 100) {
if (num.intValue() % 2 == 0) {
System.out.println("偶数线程:" + num.intValue());
num.incrementAndGet();
}
}
}).start();
}
}
方法2:
public class PrintABVolatile implements IPrintAB {
private static final int MAX_PRINT_NUM = 100;
private static volatile int count = 0;
@Override
public void printAB() {
new Thread(() -> {
while (count < MAX_PRINT_NUM) {
if (count % 2 == 0) {
log.info("num:" + count);
count++;
}
}
}).start();
new Thread(() -> {
while (count < MAX_PRINT_NUM) {
if (count % 2 == 1) {
log.info("num:" + count);
count++;
}
}
}).start();
}
}
2.一个线程打印1-26,另一个线程打印A-Z
public class Main{
private static BlockingDeque<Integer> bq1 = new LinkedBlockingDeque<>(1);
private static BlockingQueue<Character> bq2 = new LinkedBlockingDeque<>(1);
public static void main(String[] args) {
new Thread(()->{
for (int i = 1; i <= 26; i++) {
try {
bq1.put(i);
System.out.print(bq2.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(()->{
for (char i = 'A'; i <= 'Z'; i++) {
try {
System.out.print(bq1.take());
bq2.put(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
3.消费者-生产者模式
public class Main{
private static ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
public static void main(String[] args) {
Thread t1 = new Thread(()->{
while (true) {
try {
Integer o = queue.take();
if (o != null) {
System.out.println(Thread.currentThread().getName() + "消费了" + o);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 10; i++) {
if (queue.offer(i)) {
System.out.println(Thread.currentThread().getName() + "生产了" + i);
}
}
});
t1.start();
t2.start();
}
}