java并发编程核心技术点
1. CUP性能优化的博弈之路
cpu是宝贵的资源,为了保证他的极致性能所以工程师们提供了很多优化的方案,但是这些方案的落地会引起一系列的问题,进而工程师们又根据问题提出一系列的解决方案。
cpu性能的优化路线如上图
- 引入高速缓存解决cpu和io设备交互的速度问题;但是这会产生cpu缓存一致性问题。
- 进而 引入了总线锁/缓存锁(缓存一致性协议) 解决了缓存一致性问题;但是这又会导致因为cpu缓存同步引起的CPU阻塞问题。
- 进而引入了相关的异步优化StoreBuffer/StoreForwarding(可以理解为指令重排序);这又会引起指令执行顺序带来的多线程的可见性、有序性问题问题。
到目前为止,第3个优化引起的问题,cpu硬件层面已无法解决,因为cpu自己无法判断多线程场景引起的问题,所以CPU提供了内存屏障指令,让开发者自己调用指令解决问题。
例如java就提供了volatile关键字,该关键字就是通过调用内存屏障指令解决多线程的可见性和有序性问题。
2. JAVA内存模型(JMM)
JVM中定义了Java内存模型(Java Memory Model,JMM),他是的一个抽象概念,并不真实存在,用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果。
java内存模型定义了各个线程有自己的工作内存,各个线程的工作内存是互不相通的,线程对变量的所有操作都必须在工作内存中进行,不能直接读写主存。
3. ThreadLocal内存泄漏原因以及解决办法
原因:
每个thread中都存在一个map(ThreadLocalMap), map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收。
也就是说如果thread一直没结束(比如线程池中的线程),就可能会造成内存泄漏。
解决办法:
每次使用完ThreadLocal都调用它的remove()方法清除数据。(set(),get()方法其实也会有清除脏数据的效果)
结论:
https://www.jianshu.com/p/9a49ed06e936通过这篇文章可以得到如下结论:
其实内存泄漏应该只会存在于线程池数量较大且存储在ThreadLocal中的数据量较大时,但是手动调用 remove() 可以加快内存的释放,所以还是推荐手动调用的。
4. sleep、wait/notify 、join、yiled方法的区别
参考文章:
https://blog.csdn.net/ywlmsm1224811/article/details/94022647
sleep
sleep可以让线程睡眠指定的时间,会释放cpu时间片,但是不会释放锁(比如synchronized)。
wait/notify/notifyAll
wait 方法是属于 Object 类中的,wait 过程中线程会释放对象锁,只有当其他线程调用 notify 才能唤醒此线程。wait 使用时必须先获取对象锁,即必须在 synchronized 修饰的代码块中使用,那么相应的 notify 方法同样必须在 synchronized 修饰的代码块中使用,如果没有在synchronized 修饰的代码块中使用时运行时会抛出IllegalMonitorStateException的异常。
notify只会唤醒一个等待的线程,而notifyAll调用后可以唤醒所有等待的线程,让这些线程重新竞争锁。
https://www.zhihu.com/question/37601861/answer/145545371
join
join()方法是等待这个线程结束,完成其执行。它主要起同步作用,使线程之间的执行从“并行”变成“串行”。
也就是说,当我们在线程A中调用了线程B的join()方法时,在线程A中join()方法后面的代码必须等待线程B执行完毕后,才可以继续执行下去。
yiled
yield()方法是一个和sleep()方法有点相似的方法,它也是Thread类提供的一个静态方法。可以让当前正在执行的线程暂停,但它不会阻塞该线程,只是将该线程转入就绪状态。yeild()只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield()线程暂停之后,线程调度器又将其调度出来重新执行。
当某个线程调用了yield()方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行机会。
5. 线程的中止
interrupt()、isInterrupted()、、interrupted()
- interrupt(),在一个线程中调用另一个线程的interrupt()方法,即会向那个线程发出信号——线程中断状态已被设置。至于那个线程何去何从,由具体的代码实现决定。
- isInterrupted(),用来判断当前线程的中断状态(true or false)。
- interrupted()是个Thread的static方法,用来恢复中断状态
interrupt()其实会有两个步骤:
1.设置共享变量的值为true,即isInterrupted()获取的值为true,代表当前线程为中断状态
2.如果线程处于阻塞状态则唤醒它。阻塞中的线程被唤醒时会把中断状态再次设置为flase,即线程复位;同时会抛出InterruptedException异常。
可能大家没太理解,我举个例子:
public class InterruptDemo implements Runnable {
private int i = 1;
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {//触发线程复位--》设置成flase
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread a = new Thread(new InterruptDemo());
a.start();
//为了等待run方法运行
Thread.sleep(100);
//中断线程
a.interrupt();
}
}
执行代码会发现打印了异常,但是线程并没有结束,因为中断线程会触发我们上面说的那两个步骤,导致
Thread.currentThread().isInterrupted()获取的值为flase,所以线程依然可以一直循环。
正常写代码我们要让线程自己去控制自己是否中断,所以在catch中可以写相应的逻辑:
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {//触发线程复位--》设置成flase
//e.printStackTrace();
return;
}
}
}
6. Java内存模型
java内存模型是一种抽象的内存模型,它定义了多线程对共享内存的读写操作的规范,可以通过一些规则对内存的读写操作做一些约束(比如Synchronized 、volatile),保证指令执行的正确性。解决了cpu里的多级缓存、处理器优化、指令重排序导致的可见性问题。
总的来说Java内存模型就是为了保证并发场景的可见性。
目前我看到的比较好的文章:
https://www.jianshu.com/p/a2a7c64da7da
7. ReentrantLock与synchronized、ReentrantReadWriteLock的区别
ReentrantLock、synchronized
JUC就是java.util.concurrent工具包的简称,这是一个处理线程的工具包。ReentrantLock和ReentrantReadWriteLock都属于该工具包。
1.如果用汽车来类比,synchronize相当于自动挡,Lock相当于手动挡。即:synchronize是内置锁,只要加上synchronize的代码的地方开始,代码结束的地方自动释放资源。lock必须手动加锁,手动释放资源。
2.synchronize优点是代码量少,自动化。缺点是扩展性低,不够灵活。
3.Lock优点是扩展性好,灵活。缺点是代码量相对稍多。
4.synchronized是不公平锁,而ReentrantLock可以指定锁是公平的还是非公平的
5.释放锁的情况:
synchronize:1)线程执行完毕;2)线程发生异常;3)线程进入休眠状态。
Lock:通过unLock()方法。
Synchronized优化之前,synchronized的性能差于ReenTrantLock。
可是在jdk1.5后Synchronized引入了偏向锁,轻量级锁之后,synchronized和ReenTrantLock的性能就相差不一了,官方方面甚至建议使用synchronized。当然最终用那个需要具体场景具体分析。
ReentrantLock、ReentrantReadWriteLock
ReentrantLock虽然可以灵活地实现线程安全,但是他是一种完全互斥锁,即某一时刻永远只允许一个线程访问共享资源,不管是读数据的线程还是写数据的线程。这导致的结果就是,效率低下。
ReentrantReadWriteLock类的出现很好的解决了该问题。ReentrantReadWriteLock中维护了读锁和写锁。允许线程同时读取共享资源;但是如果有一个线程是写数据,那么其他线程就不能去读写该资源。即会出现三种情况:读读共享,写写互斥,读写互斥。
参考:https://www.cnblogs.com/yanfei1819/p/10314533.html
8. synchronized锁升级过程(偏向所锁,轻量级锁及重量级锁)
偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。
一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。
一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。