谈到多线程我们就应该知道,在多线程的并发情况下,安全问题和性能问题是我们额外关注的焦点,需要我们不断的选型,技术没有好与不好,只有适不适合,同样在各大公司面试题库中也是必问的,即将走向工作岗位的技术人员应该好好复习和深入理解这方面知识。
1.多线程的基础
1.何为线程安全?
线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
2.创建线程的四种常见方式
2.1 重写 Thread 类的 run() 方法。
2.2 实现 Runnable 接口,重写 run() 方法。
2.3 实现 Callable 接口,使用 FutureTask 类创建线程
2.4 使用线程池创建、启动线程
3.Runnable接口和Callable接口的区别。
相同点:
1.都是接口,都能够实现多线程编程,都需要Thread.start()来启动线程。
不同点:
1.Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实 现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!
4.wait方法和sleep方法的区别
首先wait方法是Object类中的,而sleep是Thread中的方法,其次调用wait方法会释放掉锁,而调用sleep只是进入睡眠状态,并不会释放掉锁。
2.线程安全和性能问题
实现线程安全主要有以下几种
1.使用synchronized、Lock
一、 synchronized和lock的区别?
1. synchronized是关键字,而Lock是一个接口。
2. synchronized会自动释放锁,而Lock必须手动释放锁。
3. synchronized是不可中断的,Lock可以中断也可以不中断。
4. 通过Lock可以知道线程有没有拿到锁,而synchronized不能。
5. synchronized能锁住方法和代码块,而Lock只能锁住代码块。
6. Lock可以使用读锁提高多线程读效率。
7. synchronized是非公平锁,ReentrantLock可以控制是否是公平锁
二、简述synchronized的底层实现原理?
首先它是一个重量级锁,是需要依赖底层的操作系统的。
当我们使用这个关键字的时候,我们知道无论它作用在哪,都是以对象为锁,java中对象组成部分为:
1.对象头(Mark Word、指向类的指针、数组长度)
2.实例数据
3.对齐填充字节
其中实现的主要都是和对象头中Mark Word有关,它记录了锁的状态,每个对象有一个监视器锁(monitor),当monitor被占用时该对象就会处于锁定状态。线程执行monitorenter指令时尝试获取monitor的所有权,如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者,如果线程已经占有该monitor,只是重新进入,则将monitor的进入数加1.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit:
执行monitorexit的线程必须是monitor对应的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
2.使用ThreadLocal进行绑定
ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别。synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
ThreadLocal为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,但大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。
如果想深入理解threadLocal的底层结构和实现原理,推荐
深入理解ThreadLocal
3.并发编程的艺术
通过上图我们可以知道java内存模型,
主内存
主内存是所有线程都共享的,都能访问的。所有的共享变量都存储于主内存。
工作内存
每一个线程有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量
这里既然讲到了java内存模型,补充一种实现线程安全的方式,使用CAS(compare and swap)算法,它就是基于java内存模型实现的一种乐观锁并发安全算法。
什么是乐观锁与悲观锁?
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样当第二个线程想拿这个数据的时候,第二个线程会一直堵塞,直到第一个释放锁,他拿到锁后才可以访问。传统的数据库里面就用到了这种锁机制,例如:行锁,表锁,读锁,写锁,都是在操作前先上锁。java中的synchronized的实现也是一种悲观锁。
乐观锁:乐观锁概念为,每次拿数据的时候都认为别的线程不会修改这个数据,所以不会上锁,但是在更新的时候会判断一下在此期间别的线程有没有修改过数据,乐观锁适用于读操作多的场景,这样可以提高程序的吞吐量。在Java中java.util.concurrent.atomic包下面的原子变量就是使用了乐观锁的一种实现方式CAS实现。
乐观锁的实现方式-CAS(Compare and Swap):
jdk1.5之前锁存在的问题:
java在1.5之前都是靠synchronized关键字保证同步,synchronized保证了无论哪个线程持有共享变量的锁,都会采用独占的方式来访问这些变量。这种情况下:
1.在多线程竞争下,加锁、释放锁会导致较多的上下文切换和调度延时,引起性能问题
2.如果一个线程持有锁,其他的线程就都会挂起,等待持有锁的线程释放锁。
3.如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能风险。
对比于悲观锁的这些问题,另一个更加有效的锁就是乐观锁。 乐观锁就是:每次不加锁而是假设没有并发冲突去操作同一变量,如果有并发冲突导致失败,则重试直至成功。
乐观锁的一种典型实现机制(CAS):
乐观锁主要就是两个步骤:冲突检测和数据更新。当多个线程尝试使用CAS同时更新同一个变量时,只有一个线程可以更新变量的值,其他的线程都会失败,失败的线程并不会挂起,而是告知这次竞争中失败了,并可以再次尝试。
CAS操作包括三个操作数:需要读写的内存位置(V)、预期原值(A)、新值(B)。如果内存位置与预期原值的A相匹配,那么将内存位置的值更新为新值B。如果内存位置与预期原值的值不匹配,那么处理器不会做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS其实就是一个:我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。这其实和乐观锁的冲突检测+数据更新的原理是一样的。
乐观锁是一种思想,CAS只是这种思想的一种实现方式。
并发编程的特性
1.原子性
只能同时有一个线程在进行操作。
2.可见性
工作内存的值改变后会刷新到主内存中
3.有序性
想知道它的含义,我们必须知道重排序,重排序就是原本按着我们编写代码的顺序自上向下执行,但是为了提高程序的执行效率,编译器和CPU会对程序中代码进行重排序。就是可能不按照我们编写程序的顺序执行,这个也可能会对线程造成安全问题,所以我们也必须保证有序性。
volatile
这个关键字的作用是变量在多个线程之间可见,并且能够保证所修饰的变量的有序性。
1.保证变量的可见性:当一个被volatile关键字修饰的变量被一个线程改变的时候,其他线程可以立即得到改变后的结果,虚拟机会强制刷新到主内存中,当另外一个线程需要用到被volatile关键字修饰的时候,虚拟机会强制要求它从主内存中读取。
2.屏蔽指令的重排序。
并发编程之线程之间的通信
什么是多线程之间的通信?
多个线程在处理同一个资源,并且任务不同时,需要线程通信来来帮助线程之间对同一个变量的使用或操作。
于是我们引出等待唤醒机制:wait(),notify(),notifyAll()是定义在object类里的方法,可以用来控制线程的状态,这三个方法最终调用都是jvm级别的native方法,随着jvm运行平台的不同可能有些许差异。
这里有一道经常考的线程通信面试题:
交替打印1-100。A线程负责打印奇数,B线程负责打印偶数。
public class ThreadDemo {
public static void main(String[] args) {
NumNode numNode = new NumNode();
new Thread(new JiNum(numNode)).start();
new Thread(new OuNum(numNode)).start();
}
// 打印奇数
static class JiNum implements Runnable{
private NumNode numNode;
public JiNum(NumNode numNode){
this.numNode = numNode;
}
public void run() {
while(true){
synchronized (numNode){
if(numNode.num<100){
if(numNode.num%2!=0){
System.out.println("奇数==>"+numNode.num);
numNode.num++;
numNode.notify();
}else{
try {
numNode.wait();
}catch(Exception e){
e.printStackTrace();
}
}
}else{
break;
}
}
}
}
}
//打印偶数
static class OuNum implements Runnable{
private NumNode numNode;
public OuNum(NumNode numNode){
this.numNode = numNode;
}
public void run(){
while(true){
synchronized (numNode){
if(numNode.num<100){
if(numNode.num%2==0){
System.out.println("偶数==>"+numNode.num);
numNode.num++;
numNode.notify();
}else{
try {
numNode.wait();
}catch(Exception e){
e.printStackTrace();
}
}
}else{
break;
}
}
}
}
}
//共享资源
static class NumNode{
int num = 1;
}
}