1. 进程与线程的区别
进程是所有线程的集合,每一个线程是进程中的一条执行路径,线程只是一条执行路径。
使用多线程提高了应用程序的响应,提高了CPU的利用率,花销小,切换快。
2. 创建线程的三种方式
2.1. 继承Thread类创建线程类
- 优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程。
缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类。
2.2. 实现Runnable接口
优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。
2.3. 通过Callable创建线程 -AQS
Callable规定的方法是call(),Runnable规定的方法是run().
- Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
- call方法可以抛出异常,run方法不可以,因为run方法本身没有抛出异常,所以自定义的线程类在重写run的时候也无法抛出异常
- 实现Callable()接口的call方法,然后使用Future来包装call()方法的返回值,创建并启动线程,可以调用future对象的get方法来获取返回值。
- main线程会一直等到执行完
call()
方法中的所有代码才会继续执行main线程中接下来的代码(等待发生在方法:future.get()
) 所以调用这个方法会导致程序阻塞 - 阻塞是因为底层用到了AQS锁
AQS为抽象队列同步器,通过一个同步状态和队列来实现的,当一道线程过来获取同步状态失败后, 同步器会将当前线程以及等待状态等信息构造成一个节点Node ,并将其加入同步队列,同时会阻塞当前线程 ,当同步状态被更新时,队首的节点会被激活,来获取同步状态。
2.4. 线程的创建方式源码
3. 创建线程池的四种方式
3.1. newCachedThreadPool
创建一个可缓存的线程池,如果线程池长度超过处理需求,可灵活回收空闲线程,若无可回收,则新建线程
缺点:大家一般不用是因为newCachedThreadPool 可以无线的新建线程,容易造成堆外内存溢出,因为它的最大值是在初始化的时候设置为 Integer.MAX_VALUE,一般来说机器都没那么大内存给它不断使用。当然知道可能出问题的点,就可以去重写一个方法限制一下这个最大值。
3.2. newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()。可参考PreloadDataCache。其实newFixedThreadPool()在严格上说并不会复用线程,每运行一个Runnable都会通过ThreadFactory创建一个线程3.3. newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。
Executors.newScheduledThreadPool(5);与Timer 对比:Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务(比如:一个任务出错,以后的任务都无法继续)。
ScheduledThreadPoolExecutor的设计思想是,每一个被调度的任务都会由线程池中一个线程去执行,因此任务是并发执行的,相互之间不会受到干扰。需要注意的是,只有当任务的执行时间到来时,ScheduedExecutor 才会真正启动一个线程,其余时间 ScheduledExecutor 都是在轮询任务的状态。
通过对比可以发现ScheduledExecutorService比Timer更安全,功能更强大,在以后的开发中尽可能使用ScheduledExecutorService(JDK1.5以后)替代Timer3.4. newSingleThreadExecutor
创建一个单线程化的线程池,它只会唯一的工作线程来执行任务,保证所有任务按照指定
顺序(FIFO,LIFO,优先级)执行。
现行大多数GUI程序都是单线程的。Android中单线程可用于数据库操作,文件操作,应用批量安装,应用批量删除等不适合并发但可能IO阻塞性及影响UI线程响应的操作。3.5. 为什么要使用线程池
它的主要特点是,线程复用,控制最大并发数,管理线程
- new Thread 的弊端
- 每次new Thread新建对象性能差。
- 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
- 缺乏更多功能,如定时执行、定期执行、线程中断。
- 相比new Thread,Java提供的四种线程池的好处在于:
- 重用存在的线程,减少对象创建、消亡的开销,性能佳。
- 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
- 提供定时执行、定期执行、单线程、并发数控制等功能。
-
3.6. 线程数的规划
CPU密集型任务(批量审核属于该类型任务)
一般配置线程数=CPU总核心数+1 (+1是为了利用等待空闲)。
要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。- IO密集型任务
一般配置线程数=CPU总核心数 x 2 +1。
这类任务的CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。常见的大部分任务都是IO密集型任务,比如Web应用。对于IO密集型任务,任务越多,CPU效率越高(但也有限度)。4. 线程安全及解决
当多个线程同时共享,同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题。做读操作是不会发生数据冲突问题。
使用线程同步或使用锁能解决线程安全问题,只能让当前一个线程进行执行。被包裹的代码执行完成后释放锁,然后才能让其他线程进行执行。这样的话就可以解决线程不安全问题。
- 解决多线程同步问题:
(1) 同步代码块(对象锁)
(2) 同步函数(对象锁) ,修饰在方法上,多个线程调用同一个对象的同步方法会阻塞,调用不同对象的同步方法不会阻塞。
(3) 静态同步函数(类锁)
(4) 使用lock 同步代码块与同步函数区别?
同步代码块使用自定锁(明锁)
同步函数使用this锁
如果多个线程使用同一个锁的话,那么两者均可以使用,如果存在多个锁的话,只能使用同步代码块
同步代码块可以选择以什么来加锁,比同步方法更精确,我们可以选择只有会在同步发生同步问题的代码加锁,而并不是整个方法。
```java public class SynObj{ public synchronized void showA(){//只能使用当前对象进行加锁
System.out.println("showA..");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void showB(){
synchronized (this) {
System.out.println("showB..");
}
}
public void showC(){
//使用自定义的字符串来加锁
String s="1";
synchronized (s) {
System.out.println("showC..");
}
} }
public class Test {
public static void main(String[] args) {
final SynObj sy=new SynObj();
new Thread(new Runnable() {
@Override
public void run() {
sy.showA();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
sy.showB();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
sy.showC();
}
}).start();
}
}
代码的打印结果是,showA…..showC…..会很快打印出来,showB…..会隔一段时间才打印出来
- **同步函数与静态同步函数区别?**<br />**注意:**<br />面试会这样问:例如现在一个静态方法和一个非静态方法怎么实现同步?<br />同步函数使用this锁(实例对象本身)<br />静态同步函数使用字节码文件,也就是类.class(类对象本身)
- 使用lock<br />synchronized是在JVM层面实现的,因此系统可以监控锁的释放与否,而ReentrantLock使用代码实现的,系统无法自动释放锁,需要在代码中finally子句中显式释放锁lock.unlock();<br />在并发量比较小的情况下,使用synchronized是个不错的选择,但是在并发量比较高的情况下,其性能下降很严重,此时ReentrantLock是个不错的方案。
- lock与synchronized的区别
- 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
- synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
- synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
- **synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)**
- Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
- **最重要的是Lock是一个接口,而synchronized是一个关键字,synchronized放弃锁只有两种情况:①线程执行完了同步代码块的内容②发生异常;而lock不同,它可以设定超时时间,也就是说他可以在获取锁时便设定超时时间,如果在你设定的时间内它还没有获取到锁,那么它会放弃获取锁然后响应放弃操作。**
**Lock锁,可以得到和 synchronized一样的效果,即实现原子性、有序性和可见性。<br />相较于synchronized,Lock锁可手动获取锁和释放锁、可中断的获取锁、超时获取锁。<br />Lock 是一个接口,两个直接实现类:ReentrantLock(重入锁), ReentrantReadWriteLock(读写锁)。**
<a name="6c62d520"></a>
### 4.1. 加锁原理
在Java中,ReentrantLock和Synchronized都是可重入锁。
<a name="fJE2K"></a>
#### 4.1.1 synchronized加锁原理
Java虚拟机中,synchronized支持的同步方法和同步语句都是使用monitor来实现的。每个对象都与一个monitor相关联,当一个线程执行到一个monitor监视下的代码块中的第一个指令时,该线程必须在引用的对象上获得一个锁,这个锁是monitor实现的。在HotSpot虚拟机中,monitor是由ObjectMonitor实现,使用C++编写实现,具体代码在HotSpot虚拟机源码ObjectMonitor.hpp文件中。
查看源码会发现,主要的属性有_count(记录该线程获取锁的次数)、_recursions(锁的重入次数)、_owner(指向持有ObjectMonitor对象的线程)、_WaitSet(处于wait状态的线程集合)、_EntryList(处于等待锁block状态的线程队列)。
**当并发线程执行synchronized修饰的方法或语句块时,先进入_EntryList中,当某个线程获取到对象的monitor后,把monitor对象中的_owner(拥有着)变量设置为当前线程,同时monitor对象中的计数器_count加1,当前线程获取同步锁成功。当synchronized修饰的方法或语句块中的线程调用wait()方法时,当前线程将释放持有的monitor对象,monitor对象中的_owner变量赋值为null,同时,monitor对象中的_count值减1,然后当前线程进入_WaitSet集合中等待被唤醒。**
```java
public class Lock{
boolean isLocked = false;
Thread lockedBy = null;
int lockedCount = 0;
public synchronized void lock() throws InterruptedException{
Thread callingThread = Thread.currentThread();
while(isLocked && lockedBy != callingThread){
wait();
}
isLocked = true;
lockedCount++;
lockedBy = callingThread;
}
public synchronized void unlock(){
if(Thread.curentThread() == this.lockedBy){
lockedCount--;
if(lockedCount == 0){
isLocked = false;
notify();
}
}
}
}
4.1.2 ReentrantLock加锁原理
ReentrantLock 依靠内部的Sync变量 实现锁的功能,Sync抽象类继承自AQS。
AQS实现同步框架(构建同步队列,控制同步状态) 预留出了获取和释放共享资源的方法供子类实现。
4.2. 读写锁
读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。
何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。
无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。
对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。
但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。
自旋就是在循环判断条件是否满足,那么会有什么问题吗?如果锁被占用很长时间的话,自旋的线程等待的时间也会变长,白白浪费掉处理器资源。因此在JDK中,自旋操作默认10次,我们可以通过参数“-XX:PreBlockSpin”来设置,当超过来此参数的值,则会使用传统的线程挂起方式来等待锁释放。
4.3. 重入锁与不可重入锁(自旋锁)
在Java中,ReentrantLock和Synchronized都是可重入锁。
重入锁(可一定程度避免死锁)
就如同在饭堂打饭,你在窗口排着队。排到你的时候,突然路人A让你顺带着打个饭吧,然后你就打了两份饭,这时候你还没离开窗口,又有路人B让你打一份汤,于是你又额外打了一份汤。
即:可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。(一人得道,鸡犬升天。)
不可重入锁(自旋锁)
在另一个菜式比较好吃且热门的窗口,可不是这样的,在这里你在窗口,只能点一个菜(进入一次临界区),点完后,你想要再点别的菜,只能重新排一次队(虽然可以插队,当然我们可以引入服务员队伍管理机制:private Lock windows = new ReentrantLock(true);,指定该锁是公平的。)
即:自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分。
4.4. 悲观锁与乐观锁
乐观锁:乐观锁认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是再更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败就重复,读-比较-写的操作。Java中的乐观锁基本都是通过CAS操作实现的,CAS是一种原子性的更新操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
悲观锁:悲观所就是悲观思想,即认为写多于读,遇到并发写的可能性很高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人向读写这个数据久会block直到拿到锁。而synchronized就是其中的代表。
4.5. 偏向锁
Synchronized是重量级锁,但是随着JAVA1.6对synchronized的优化,其变得不再那么重了。1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。
Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。
如果在运行过程中,遇到了其他线程抢占锁,会判断偏向锁的偏向状态,如果处在偏向状态,但是线程不是当前线程,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
4.6. 轻量级锁
如果成功使用CAS将对象头重的Mark Word替换为指向锁记录的指针,则获得锁,失败则当前线程尝试使用自旋(循环等待)来获取锁。
当有另一个线程与该线程同时竞争时,锁会升级为重量级锁。为了防止继续自旋,一旦升级,将无法降级。
4.7. 重量级锁
其他线程试图获取锁时,都会被阻塞,只有持有锁的线程释放锁之后才会唤醒这些线程,进行竞争。
5. 多线程锁
多线程锁包括死锁、活锁、饥饿、无锁。
死锁、活锁、饥饿是关于多线程是否活跃出现的运行阻塞障碍问题,如果线程出现 了这三种情况,即线程不再活跃,不能再正常地执行下去了。
死锁
死锁是多线程中最差的一种情况,多个线程相互占用对方的资源的锁,而又相互等 对方释放锁,此时若无外力干预,这些线程则一直处理阻塞的假死状态,形成死锁。 举个例子,A 同学抢了 B 同学的钢笔,B 同学抢了 A 同学的书,两个人都相互占 用对方的东西,都在让对方先还给自己自己再还,这样一直争执下去等待对方还而 又得不到解决,老师知道此事后就让他们相互还给对方,这样在外力的干预下他们 才解决,当然这只是个例子没有老师他们也能很好解决,计算机不像人如果发现这 种情况没有外力干预还是会一直阻塞下去的。
public class Demo {
public static Demo a = new Demo();
public static Demo b = new Demo();
public void fun1(){
synchronized(a) {
System.out.println("fun1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
b.fun2();
}
}
public void fun2(){
synchronized(b) {
System.out.println("fun2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
a.fun1();
}
}
}
fun1方法中,锁住了a对象,使用b对象。
fun2方法中,锁住了b对象,使用a对象。
这就是互相等待。
活锁
活锁这个概念大家应该很少有人听说或理解它的概念,而在多线程中这确实存在。 活锁恰恰与死锁相反,死锁是大家都拿不到资源都占用着对方的资源,而活锁是拿 到资源却又相互释放不执行。当多线程中出现了相互谦让,都主动将资源释放给别 的线程使用,这样这个资源在多个线程之间跳动而又得不到执行,这就是活锁。
饥饿
我们知道多线程执行中有线程优先级这个东西,优先级高的线程能够插队并优先执 行,这样如果优先级高的线程一直抢占优先级低线程的资源,导致低优先级线程无 法得到执行,这就是饥饿。当然还有一种饥饿的情况,一个线程一直占着一个资源 不放而导致其他线程得不到执行,与死锁不同的是饥饿在以后一段时间内还是能够 得到执行的,如那个占用资源的线程结束了并释放了资源。
无锁
无锁,即没有对资源进行锁定,即所有的线程都能访问并修改同一个资源,但同时 只有一个线程能修改成功。无锁典型的特点就是一个修改操作在一个循环内进行, 线程会不断的尝试修改共享资源,如果没有冲突就修改成功并退出否则就会继续下 一次循环尝试。所以,如果有多个线程修改同一个值必定会有一个线程能修改成功, 而其他修改失败的线程会不断重试直到修改成功。 可以看出,无锁是一种非常良好的设计,它不会出现线程出现的跳跃性问题,锁使 用不当肯定会出现系统性能问题,虽然无锁无法全面代替有锁,但无锁在某些场合 下是非常高效的。
6. Wait()与Notify ()区别
Wait让当前线程有运行状态变为等待状态,和同步一起使用
Notify 唤醒现在正在等待的状态,和同步一起使用
7. Wait()与sleep()区别
sleep()方法,该方法是属于Thread类中的。wait()方法,则是属于Object类中的。
sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用了b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。
在调用sleep()方法的过程中,线程不会释放对象锁。sleep()方法导致了程序暂停执行指定的时间,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。 而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
sleep不出让系统资源,sleep不会释放锁,不占用CPU,当睡眠的时间到了之后,线程会自动进入可执行状态,等待cpu的执行;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。
使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
synchronized(x){
x.notify()
//或者wait()
}
sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常 。
理解:
sleep由线程自动唤醒,wait必须显示用代码唤醒。
如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
8. start()和run()的区别
- start()方法用来,开启线程,但是线程开启后并没有立即执行,他需要获取cpu的执行权才可以执行
- run()方法是由jvm创建完本地操作系统级线程后回调的方法,不可以手动调用(否则就是普通方法)
- start()方法让一个线程进入就绪队列等待分配cpu,分到cpu后才调用实现的run()方法。
9. join 方法
可以将并行改为串行,在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行 ```java public class JoinTest { public static void main(String [] args) throws InterruptedException {
}ThreadJoinTest t1 = new ThreadJoinTest("线程A");
ThreadJoinTest t2 = new ThreadJoinTest("线程B");
t1.start();
/**join的意思是使得放弃当前线程的执行,并返回对应的线程,例如下面代码的意思就是:
程序在main线程中调用t1线程的join方法,则main线程放弃cpu控制权,
并返回t1线程继续执行直到线程t1执行完毕
所以结果是t1线程执行完后,才到主线程执行,
相当于在main线程中同步t1线程,t1执行完了,main线程才有执行的机会
*/
t1.join();
t2.start();
} class ThreadJoinTest extends Thread{ public ThreadJoinTest(String name){ super(name); } @Override public void run(){ for(int i=0;i<1000;i++){ System.out.println(this.getName() + “:” + i); } } }
》》程序结果是先打印完线程A线程,在打印线程B线程; ```
10. sleep()与yield()的区别
sleep: 会强制让当前线程进入等待,即当前线程的状态为:等待、阻塞
yield: 会先去判断是否有和当前线程相同优先级的线程,如果没有,则自己继续执行,如果有,则将CPU资源让给它,然后进入到就绪状态。
11. 说一下ThreadLocal
- ThreadLocal是Java提供的本地存储机制,利用这个机制可以将数据缓存在线程内部,在任何时候都可以获取使用。
- ThreadLocal底层是通过ThreadLocalMap来实现的,每一个Thread对象中都存在一个ThreadLocalMap,Map的Key是ThreadLocal对象,Map的Value是缓存的值。
如果在线程池中使用ThreadLocal会造成内存泄露的问题,需要手动进行remove。因为线程池中的线程不会回收,而是去执行下一个任务,线程不被回收,Entry对象就不会被回收,造成了内存泄露。
12. Java中的原子类型-CAS
原子操作是指不会被线程调度机制打断的操作(表面不会打断),这种操作一旦开始,就一直运行到结束,中间不会有任何线程上下文切换。使用了CAS (Compare and Swap 【比较并交换】)
原理:( 1、2是在CPU和内存的层面来说的,3是在Java层面说的 )需要读写的内存址(V)、原值(A)和新值(B)。如果V的值与原值A相匹配,那么把B设置给V,否则处理器不做任何操作。
- 无论哪种情况,都返回V当前值。
- 原子类里,当失败时,就一直循环,直到成功。
- 原子类并不是提供真正的原子操作,其方法在执行时,还是可能被其它线程打断的,只是它在被打断后(共享变量改变),会获取新的值重新操作,一直重复到成功。所以在外部看来,它就是一个原子操作。
- CAS是非阻塞的。
- CAS的ABA问题:当然CAS也并不完美,它存在”ABA”问题,假若一个变量初次读取是A,在compare阶段依然是A,但其实可能在此过程中,它先被改为B,再被改回A,而CAS是无法意识到这个问题的。CAS只关注了比较前后的值是否改变,而无法清楚在此过程中变量的变更明细,这就是所谓的ABA漏洞。
Java从JDK 1.5开始提供了java.util.concurrent.atomic
包,这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。Atomic包大致可以属于4种类型的原子更新方式,分别是原子更新基本类型、原子更新数组、原子更新引用、原子更新属性。比如AtomicInteger、AtomicLong、AtomicBoolean等。
13. volatlie 关键字
volatlie关键字有可见性、有序性,没有原子性,那么value+=1这行代码实则会被分为4步执行
- 获取value的值
- 将获取到的值+1
- 将最新值赋值给value
- 将value的值刷入内存
假设当时value值为1,当线程1执行完+=操作的第1步时,cpu执行权被线程2抢走,然后线程2执行+=操作,直至输入内存,并输出2,这时cpu执行被线程1抢走,继续执行没有完成的+=操作,那么这时线程1会根据第一步拿到的1进行+1操作,那么返回输出的同样是2。
volatlie关键字保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值(只有在修改的情况下,只读取了不生效),这新值对其他线程来说是立即可见的。
volatlie关键字禁止进行指令重排序。
参考资料
- java 创建线程的三种方式、创建线程池的四种方式
- 多线程并发之读写锁(ReentranReadWriteLock&ReadWriteLock)使用详解
- 线程创建的三种方式及区别
- 死锁的条件、原因以及场景分析
- Java中Atomic原子类型的详细讲解
- Java并发编程:volatile关键字解析
- Java16个原子类介绍-基于JDK8
- synchronized与Lock的区别
- Java 中的Lock锁
- 简单说说重入锁与读写锁
- synchronized是可重入锁吗?
- java中几种锁,分别是什么?
- java中的各种锁详细介绍
- 深入理解数据库行锁与表锁
- MySQL/InnoDB中,乐观锁、悲观锁、共享锁、排它锁、行锁、表锁、死锁概念的理解
- Java多线程—-Runnable与Callable的区别
- Java中的AQS(一)AQS简介
- Java中的原子类-并发编程—CAS原理
- Synchronized 和AQS实现的要点
- synchronized原理分析及自旋锁、偏向锁、轻量级锁和重量级锁的概念和优化
- 一文彻底理解ReentrantLock可重入锁的使用
- java 并发编程