java面试第二篇----多线程

悲观锁与乐观锁:
     悲观锁:
        总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程        想拿到这个资源就会阻塞直到锁被上一个持有者释放。
        也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
        像 Java 中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
    乐观锁:
        乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验        证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。
        在 Java 中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。
        乐观锁通常多于写比较少的情况下(多读场景),避免频繁加锁影响性能,大大提升了系统的吞吐量。
版本号机制:
一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取    数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新    成功。

CAS 算法:
CAS 的全称是 Compare And Swap(比较与交换) ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。

CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。

原子操作 即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。

CAS 涉及到三个操作数:

V :要更新的变量值(Var)
E :预期值(Expected)
N :拟写入的新值(New)
当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了V,则当前线程放弃更新。

乐观锁存在的问题:
ABA 问题是乐观锁最常见的问题。

1.ABA 问题
        如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是        不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的             "ABA"问题。

    ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。JDK 1.5 以后的 AtomicStampedReference 类就是用来解决 ABA 问题的,其中的                 compareAndSet() 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设    置为给定的更新值。

2.循环时间长开销大
        CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。

        如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用:

            1.可以延迟流水线执行指令,使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。
            2.可以避免在退出循环的时候因内存顺序冲而引起 CPU 流水线被清空,从而提高 CPU 的执行效率。

synchronized和Lock的区别


区别如下:

1.来源:
lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现;

2.异常是否释放锁:
synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起    死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)

3.是否响应中断
lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;

4.是否知道获取锁
Lock可以通过trylock来知道有没有获取锁,而synchronized不能;

5.Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)

6.在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

7.synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度。

synchronized和lock的用法区别
synchronized:
在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。

lock:
一般使用ReentrantLock类做为锁。在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。

synchronized和lock性能区别
synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。

在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。

但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。

2种机制的具体区别:

synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。 独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。

而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。 我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。

现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。

synchronized和lock用途区别
synchronized原语和ReentrantLock在一般情况下没有什么区别,但是在非常复杂的同步应用中,请考虑使用ReentrantLock,特别是遇到下面2种需求的时候。

1.某个线程在等待一个锁的控制权的这段时间需要中断
2.需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程
3.具有公平锁功能,每个到来的线程都将排队等候

下面细细道来……

先说第一种情况,ReentrantLock的lock机制有2种,忽略中断锁和响应中断锁,这给我们带来了很大的灵活性。比如:如果A、B 2个线程去竞争锁,A线程得到了锁,B线程等待,但是A线程这个时候实在有太多事情要处理,就是一直不返回,B线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。这个时候ReentrantLock就提供了2种机制:可中断/可不中断
第一,B线程中断自己(或者别的线程中断它),但是ReentrantLock不去响应,继续让B线程等待,你再怎么中断,我全当耳边风(synchronized原语就是如此);
第二,B线程中断自己(或者别的线程中断它),ReentrantLock处理了这个中断,并且不再等待这个锁的到来,完全放弃。
————————————————
版权声明:本文为CSDN博主「DRRZH」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/drrzh/article/details/119519455

volatile和synchronized特点

首先需要理解线程安全的两个方面:执行控制和内存可见。

执行控制的目的是控制代码执行(顺序)及是否可以并发执行。

内存可见控制的是线程执行结果在内存中对其它线程的可见性。根据Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存。

synchronized关键字解决的是执行控制的问题,它会阻止其它线程获取当前对象的监控锁,这样就使得当前对象中被synchronized关键字保护的代码块无法被其它线程访问,也就无法并发执行。更重要的是,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作,都happens-before于随后获得这个锁的线程的操作。

volatile关键字解决的是内存可见性的问题,会使得所有对volatile变量的读写都会直接刷到主存,即保证了变量的可见性。这样就能满足一些对变量可见性有要求而对读取顺序没有要求的需求。

使用volatile关键字仅能实现对原始变量(如boolen、 short 、int 、long等)操作的原子性,但需要特别注意, volatile不能保证复合操作的原子性,即使只是i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++,volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况。

在Java 5提供了原子数据类型atomic wrapper classes,对它们的increase之类的操作都是原子操作,不需要使用sychronized关键字。

对于volatile关键字,当且仅当满足以下所有条件时可使用:

1. 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
2. 该变量没有包含在具有其他变量的不变式中。
1
2
volatile和synchronized的区别
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
————————————————
版权声明:本文为CSDN博主「Heaven-Wang」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/suifeng3051/article/details/52611233

应用范围:
volatile关键字是对变量进行上锁,锁住的是单个变量,而synchronized还能对方法以及代码块进行上锁。

是否保证原子性:
在多线程环境下,volatile可以保证可见性和有序性,不能保证原子性,而synchronized在保证可见性和有序性的基础上,还可以保证原子性。

volatile变量的原子性与synchronized的原子性是不同的。synchronized的原子性是指,只要声明为synchronized的方法或代码块,在执行上就是原子操作,synchronized能保证被锁住的整个代码块的原子性。而volatile是不修饰方法或代码块的,它只用来修饰变量,对于单个volatile变量的读和写操作都具有原子性,但类似于volatile++这种复合操作不具有原子性。所以volatile的原子性是受限制的。所以,在多线程环境中,volatile并不能保证原子性。

使用场景:
volatile主要用于解决共享变量的数据可见性问题,而synchronized主要用于保证访问数据的同步性(同时也能保证可见性)。

保证有序性的方式:
volatile的有序性是通过禁止指令重排序来实现的。synchronized无法禁止指令重排,但是可以通过单线程机制来保证有序性。由于synchronized修饰的代码,在同一时刻只能被一个线程访问,从根本上避免了多线程的情况。而单线程环境下,在本线程内观察到的所有操作都是天然有序的,所以synchronized可以通过单线程的方式来保证程序的有序性。

性能方面:
volatile是线程同步的轻量级实现,性能高于synchronized。多线程访问volatile修饰的变量时不会发生阻塞(主要是因为volatile采用CAS加锁),而访问synchronized修饰的资源时会发生阻塞。
————————————————
版权声明:本文为CSDN博主「渣娃-小晴晴」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/LYQ20010417/article/details/124138734


创建多线程的两种方式:
方式一:继承Thread类的方式:
* 1. 创建一个继承于Thread类的子类
* 2. 重写Thread类的run() --> 将此线程执行的操作声明在run()中
* 3. 创建Thread类的子类的对象
* 4. 通过此对象调用start():①启动当前线程 ② 调用当前线程的run()

说明两个问题:
问题一:我们启动一个线程,必须调用start(),不能调用run()的方式启动线程。
问题二:如果再启动一个线程,必须重新创建一个Thread子类的对象,调用此对象的start().

方式二:实现Runnable接口的方式:
* 1. 创建一个实现了Runnable接口的类
* 2. 实现类去实现Runnable中的抽象方法:run()
* 3. 创建实现类的对象
* 4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
* 5. 通过Thread类的对象调用start()

两种方式的对比:
* 开发中:优先选择:实现Runnable接口的方式
* 原因:1. 实现的方式没类的单继承性的局限性
*      2. 实现的方式更适合来处理多个线程共享数据的情况。
*
* 联系:public class Thread implements Runnable
* 相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
          目前两种方式,要想启动线程,都是调用的Thread类中的start()。

线程的生命周期:
图示:

说明:
1.生命周期关注两个概念:状态、相应的方法
2.关注:状态a-->状态b:哪些方法执行了(回调方法)
        某个方法主动调用:状态a-->状态b
3.阻塞:临时状态,不可以作为最终状态
  死亡:最终状态。

线程通信:
1.线程通信涉及到的三个方法:
* wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
* notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
* notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。

2.说明:
* 1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
* 2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
*    否则,会出现IllegalMonitorStateException异常
* 3.wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
3.面试题:
面试题:sleep() 和 wait()的异同?
* 1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
* 2.不同点:1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
*          2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
*          3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。


4.
小结释放锁的操作:

小结不会释放锁的操作:


Callable和Runnable接口的区别:
* 如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
* 1. call()可以返回值的。
* 2. call()可以抛出异常,被外面的操作捕获,获取异常的信息
* 3. Callable是支持泛型的

两者的区别在于 Runnable 接口不会返回结果但是 Callable 接口可以返回结果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值