线程、线程池、CAS、synchronized和ReentrantLock的区别?相关面试题
1.线程的创建方式
继承Thread类
实现Runnable接口
实现Callable接口
使用线程池创建线程
--------------------------------------------------------------------------------
四种创建线程方法对比:
实现Runnable接口和实现Callable接口的方式基本相同,只不过Callable方式有方法的返回值,可以看成一种实现方式,与继承Thread相比:
多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
继承Thread类的实现方式不能继承其他的父类。
前三种方式如果创建关闭线程频繁,会消耗系统资源影响性能。在实际开发中主要使用线程池创建线程,因为线程池的线程可以回收利用,减少由线程创建和销毁所消耗的系统资源,可以提高性能。
创建线程本质只有1种,即创建Thread类,以上的所谓创建方式其实是实现run方法的方式的封装:
1、实现runnable接口的run方法,并把runnable实例作为target对象,传给thread类,最终调用target.run
2、继承Thread类,重写Thread的run方法,Thread.start会执行run方法
2.线程池的创建方式
1.使用Excetors中的静态方法进行创建六中不同的线程池
Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】
2.创建ThreadPoolExcetor对象创建一个线程池
任务拒绝策略:
也可以自定义任务拒绝策略
3.对比
使用Executors的弊端是可能会创建大量的线程或者堆积大量的请求,导致oom,即java.lang.OutOfMemoryError,意思就是说,当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error(注:非exception,因为这个问题已经严重到不足以被应用处理)。所以更推荐ThreadPoolExcetor方式来创建线程池,因为这种创建方式更可控,并且更加明确了线程池的运行规则,可以规避一些未知的风险。
3.CAS
CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。
CAS算法: 有三个操作数(内存值V, 旧的预期值 A, 要修改的值B)
当旧的预期值A= = 内存值 此时修改成功 将V改为B
当旧的预期值A != 内存值 此时修改失败 不做任何操作 并重新获取现在的最新值(自旋)
CAS(乐观锁)从乐观的角度出发 假设每次获取数据别人都不会修改 所以不会上锁 只在修改共享数据的时候会检查一下
如果修改过 重新获取数据
如果没有修改过 就直接修改
缺点:
1.循环时间长开销很大
自循环CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。很多时候,CAS思想体现,是有个自旋次数的,就是为了避开这个耗时问题。
2.只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
自适应自旋解决的是“锁竞争时间不确定”的问题,目标是降低线程切换的成本。
3.ABA问题
如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?
如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。
利用版本号机制解决ABA问题,一般是在数据表中加上一个数据库版本号version字段,表述数据被修改的次数当数据被修改时version值会加1。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
优点:
可以避免优先级倒置和死锁等危险,竞争比较便宜,协调发生在更细的粒度级别,允许更高程度的并行机制等等。
CAS 是非阻塞的轻量级乐观锁,通过 CPU 指令实现。在资源竞争不激烈的情况下,synchronized 重量锁会进行比较复杂的加锁、解锁和唤醒操作,而 CAS 不会加锁,性能高。
4.synchronized和ReentrantLock的区别?
相似点:这两个同步方式有很多相似之处,他们都是加锁方式同步,而且都是阻塞式同步,也就是说当一个线程获取对象锁之后,进入同步块,其他访问该同步块的线程都必须阻塞在该同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态和内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善)。
区别:这两种方式最大的区别就是对于synchronized来说,它是Java语言关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock他是jdk1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句来完成。相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下2项:
① 等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。
② Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。(公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁。)
便利性:很明显Synchronized的使用方便简洁,并且由编译器去保证锁的加锁和释放锁,而ReentrantLock则需要手动声明加锁和释放锁的方法,为了避免忘记手动释放锁,最好是在finally中声明释放锁。
锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized
性能区别:在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。
5.有三个线程T1,T2,T3,如何保证顺序执行?
在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。
public class JoinTest {
public static void main(String[] args) {
// 创建线程对象
Thread t1 = new Thread(() -> {
System.out.println("t1");
}) ;
Thread t2 = new Thread(() -> {
try {
t1.join(); // 加入线程t1,只有t1线程执行完毕以后,再次执行该线程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
}) ;
Thread t3 = new Thread(() -> {
try {
t2.join(); // 加入线程t2,只有t2线程执行完毕以后,再次执行该线程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3");
}) ;
// 启动线程 //这里三个线程的启动顺序可以任意
t1.start();
t2.start();
t3.start();
}
}
阿里云成长空间 794人发布