Java 并发编程一文详解 Java 中有几种创建线程的方式
Posted 没对象的指针
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 并发编程一文详解 Java 中有几种创建线程的方式相关的知识,希望对你有一定的参考价值。
Java 中有几种创建线程的方式?
1. Java 程序天然就是多线程的
一个 Java 程序从 main()
方法开始执行,然后按照既定的代码逻辑执行,看似没有其他线程参与,但实际上 Java 程序天生就是多线程程序
,因为执行 main() 方法的是一个名称为 main 的线程
。
public static void main(String[] args)
// Java 虚拟机线程系统的管理接口
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 不需要获取同步的 monitor 和 synchronizer 信息,仅仅获取线程和线程堆栈信息
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
// 遍历线程
for (ThreadInfo threadInfo : threadInfos)
System.out.println("线程ID:" + threadInfo.getThreadId() + ",线程名:" + threadInfo.getThreadName());
而一个 Java 程序的运行就算是没有用户自己开启的线程,实际也有有很多 JVM 自行启动的线程,一般来说有:
/Library/Java/JavaVirtualMachines/jdk1.8.0_301.jdk/Contents/Home/bin/java ...
线程ID:5,线程名:Monitor Ctrl-Break
线程ID:4,线程名:Signal Dispatcher
线程ID:3,线程名:Finalizer
线程ID:2,线程名:Reference Handler
线程ID:1,线程名:main
Process finished with exit code 0
- main - main 线程,用户程序入口;
- Reference Handler - 清除 Reference 的线程;
- Finalizer - 调用对象 finalize 方法的线程;
- Signal Dispatcher - 分发处理发送给 JVM 信号的线程;
- Monitor Ctrl-Break - 监控 Ctrl-Break 中断信号。
不同的 JDK 版本,启动的线程数会有差异,但这依然证明了 Java 程序天生就是多线程的。
2. 线程的启动与终止
2.1 线程的启动
(1)继承 Thread 类,重写 run() 方法
/**
* @author pointer
* @date 2023-03-26 14:57:49
*/
public class NewThread
static class MyThread extends Thread
@Override
public void run()
// do something
System.out.println("继承 Thread 类,重写 run() 方法创建线程");
public static void main(String[] args)
MyThread myThread = new MyThread();
myThread.start();
(2)实现 Runnable 接口,重写 run() 方法
/**
* @author pointer
* @date 2023-03-26 14:57:49
*/
public class NewThread
static class MyRunnable implements Runnable
@Override
public void run()
// do something
System.out.println("实现 Runnable 接口,重写 run() 方法");
public static void main(String[] args)
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
(3)Thread 和 Runnable 的区别
Thread 才是 Java 里对线程的唯一抽象,Runnable 只是对任务(业务逻辑)的抽象。Thread 可以接受任意一个 Runnable 的实例并执行。
Runnable 是一个接口,在它里面只声明了一个 run()方法,由于 run( )方法返回值为 void 类型,所以在执行完任务之后无法返回任何结果。
(4)Callable、Future 和 FutureTask
Callable
位于 java.util.concurrent 包下,它也是一个接口,与 Runnable 接口类似,因为两者都是为其实例可能由另一个线程执行的类设计的。然而,Runnable 不返回结果,也不能抛出异常。
Future
就是对于具体的 Runnable 或者 Callable 任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过 get() 方法获取执行结果,该方法会阻塞直到任务返回结果。
因为 Future 只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的 FutureTask:
FutureTask 类实现了 RunnableFuture 接口,RunnableFuture 继承了 Runnable 接口和 Future 接口,而 FutureTask 实现了 RunnableFuture 接口。所以它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。
因此我们通过一个线程运行 Callable,但是 Thread 不支持构造方法中传递 Callable 的实例,所以我们需要通过 FutureTask 把一个 Callable 包装成 Runnable,然后再通过这个 FutureTask 拿到 Callable 运行后的返回值。
/**
* @author pointer
* @date 2023-03-26 14:57:49
*/
public class NewThread
static class MyCallable implements Callable<Integer>
@Override
public Integer call() throws Exception
int sum = 0;
for (int i = 0; i < 101; i++)
sum += i;
return sum;
public static void main(String[] args)
MyCallable myCallable = new MyCallable();
// 1. 用 FutureTask 接收结果
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
new Thread(futureTask).start();
// 2. 接收线程运算后的结果
// futureTask.get() - 这个是堵塞性的等待
try
Integer sum = futureTask.get();
System.out.println("sum = " + sum);
System.out.println("-------------------");
catch (InterruptedException e)
e.printStackTrace();
catch (ExecutionException e)
e.printStackTrace();
2.2 中止线程
(1)线程自然终止
要么是 run() 执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。
(2)stop
暂停、恢复和停止操作对应在线程 Thread 的 API 就是 suspend()、resume() 和 stop()。但是这些 API 是过期的,也就是不建议使用的。
不建议使用的原因主要有:
以 suspend() 方法为例:在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop() 方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。正因为 suspend()、resume() 和 stop() 方法带来的副作用,这些方法才被标注为不建议使用的过期方法。
(3)中断
安全的中止则是其他线程通过调用某个线程 A 的 interrupt()
方法对其进行中断操作, 中断好比其他线程对该线程打了个招呼,“A,你要中断了”,不代表线程 A 会立即停止自己的工作,同样的 A 线程完全可以不理会这种中断请求。
线程通过检查自身的中断标志位是否被置为 true 来进行响应,线程通过方法 isInterrupted()
来进行判断是否被中断,也可以调用静态方法 Thread.interrupted()
来进行判断当前线程是否被中断,不过 Thread.interrupted() 会同时将中断标识位改写为 false。
如果一个线程处于了阻塞状态(如线程调用了 thread.sleep()、thread.join()、thread.wait() 等),则在线程在检查中断标示时如果发现中断标示为 true,则会在这些阻塞方法调用处抛出 InterruptedException 异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为 false。
不建议自定义一个取消标志位来中止线程的运行
。因为 run() 方法里有阻塞调用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取消标志。这种情况下,使用中断会更好,因为:1. 一般的阻塞方法,如 sleep 等本身就支持中断的检查;2. 检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可以避免声明取消标志位,减少资源的消耗。
注意:处于死锁状态的线程无法被中断
2.3 深入理解 run() 和 start()
Thread 类是 Java 里对线程概念的抽象,可以这样理解:我们通过 new Thread() 其实只是 new 出一个 Thread 的实例,还没有操作系统中真正的线程挂起钩来。只有执行了 start() 方法后,才实现了真正意义上的启动线程。
从 Thread 的源码可以看到,Thread 的 start() 方法中调用了 start0() 方法,而 start0() 是个 native 方法,这就说明 Thread.start() 一定和操作系统是密切相关的。
start() 方法让一个线程进入就绪队列等待分配 cpu,分到 cpu 后才调用实现的 run() 方法,start() 方法不能重复调用,如果重复调用会抛出异常(此处可能有面试题:多次调用一个线程的 start() 方法会怎么样?
)。
而 run() 方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,也可以被单独调用。
3. Java 中有几种方式创建一个线程?
大家在学习 Java 线程或者面试的时候,肯定见过这个问题。这个问题的答案也是众说纷纭,有 2 种、3 种、4 种等等答案。那么一起来看一下 Java 源码是怎么说的:
官方说法是在 Java 中有两种方式创建一个线程用以执行
,一种是派生自 Thread 类
,另一种是实现 Runnable 接口
。
当然本质上 Java 中实现线程只有一种方式,都是通过 new Thread() 创建线程对象,调用 Thread.start() 启动线程。
至于基于 callable 接口的方式,因为最终是要把实现了 callable 接口的对象通过 FutureTask 包装成 Runnable,再交给 Thread 去执行,所以这个其实可以和实现 Runnable 接口看成同一类。
而线程池的方式,本质上是池化技术,是资源的复用,和新启线程没什么关系。
所以,比较赞同官方的说法,有两种方式创建一个线程用以执行。
Java并发面试题
题目参考自并发编程网:http://ifeve.com
多线程
java中有几种方法可以实现一个线程?
继承Thread类;
实现Runnable接口;
实现Callable接口通过FutureTask包装器来创建Thread线程;
使用ExecutorService、Callable、Future实现有返回结果的多线程(也就是使用了ExecutorService来管理前面的三种方式)。
详情参见:
https://radiancel.github.io/2018/08/02/Multithreading/
如何停止一个正在运行的线程?
1、使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。 2、使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。 3、使用interrupt方法中断线程。
参考:
https://www.cnblogs.com/greta/p/5624839.html
如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
参考:
sleep()和 wait()有什么区别?
对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。
当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。
参考:
https://www.cnblogs.com/hongten/p/hongtenjavasleep_wait.html
什么是Daemon线程?它有什么意义?
Java语言自己可以创建两种进程“用户线程”和“守护线程”
用户线程:就是我们平时创建的普通线程.
守护线程:主要是用来服务用户线程.
Daemon就是守护线程,他的意义是:
只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。
Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。
参考:
https://www.cnblogs.com/ChrisWang/archive/2009/11/28/1612815.html
java如何实现多线程之间的通讯和协作?
参考这篇:
http://www.cnblogs.com/hapjin/p/5492619.html
锁
什么是可重入锁(ReentrantLock)?
线程可以进入任何一个它已经拥有的锁所同步着的代码块。
代码设计如下:
参考链接:
https://www.cnblogs.com/dj3839/p/6580765.html
当一个线程进入某个对象的一个synchronized的实例方法后,其它线程是否可进入此对象的其它方法?
如果其他方法前加了synchronized关键字,就不能,如果没加synchronized,则能够进去。
如果这个方法内部调用了wait(),则可以进入其他加synchronized的方法。
如果其他方法加了synchronized关键字,并且没有调用wai方法,则不能。
ynchronized和java.util.concurrent.locks.Lock的异同?
主要相同点:Lock能完成Synchronized所实现的所有功能。
主要不同点:Lock有比Synchronized更精确的线程予以和更好的性能。Synchronized会自动释放锁,但是Lock一定要求程序员手工释放,并且必须在finally从句中释放。
乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
乐观锁是假设每次操作都不会冲突,若是遇到冲突失败就重试直到成功;悲观锁是让其他线程都等待,等锁释放完了再竞争锁。
乐观锁实现方式:cas,volatile
悲观锁实现方式:synchronized,Lock
并发框架
SynchronizedMap和ConcurrentHashMap有什么区别?
SynchronizedMap()和Hashtable一样,实现上在调用map所有方法时,都对整个map进行同步。而ConcurrentHashMap的实现却更加精细,它对map中的所有桶加了锁。所以,只要有一个线程访问map,其他线程就无法进入map,而如果一个线程在访问ConcurrentHashMap某个桶时,其他线程,仍然可以对map执行某些操作。
所以,ConcurrentHashMap在性能以及安全性方面,明显比Collections.synchronizedMap()更加有优势。同时,同步操作精确控制到桶,这样,即使在遍历map时,如果其他线程试图对map进行数据修改,也不会抛出ConcurrentModificationException。
参考:
https://www.cnblogs.com/shamo89/p/6700353.html
CopyOnWriteArrayList可以用于什么应用场景?
CopyOnWriteArrayList的特性是针对读操作,不做处理,和普通的ArrayList性能一样。而在写操作时,会先拷贝一份,实现新旧版本的分离,然后在拷贝的版本上进行修改操作,修改完后,将其更新至就版本中。
那么他的使用场景就是:一个需要在多线程中操作,并且频繁遍历。其解决了由于长时间锁定整个数组导致的性能问题,解决方案即写时拷贝。
另外需要注意的是CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
参考:
https://www.cnblogs.com/duchanggang/p/4627082.html https://www.cnblogs.com/yw-technology/p/7476106.html
线程安全
什么叫线程安全?servlet是线程安全吗?
线程安全就是说多线程访问同一代码,不会产生不确定的结果。
在多线程环境中,当各线程不共享数据的时候,即都是私有(private)成员,那么一定是线程安全的。但这种情况并不多见,在多数情况下需要共享数据,这时就需要进行适当的同步控制了。
线程安全一般都涉及到synchronized, 就是一段代码同时只能有一个线程来操作 不然中间过程可能会产生不可预制的结果。
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
Servlet不是线程安全的
同步有几种实现方法?
1.同步方法
即有synchronized关键字修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
2.同步代码块
即有synchronized关键字修饰的语句块。
被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
3.使用特殊域变量(volatile)实现线程同步
a.volatile关键字为域变量的访问提供了一种免锁机制,
b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,
c.因此每次使用该域就要重新计算,而不是使用寄存器中的值
d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
4.使用重入锁实现线程同步
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
5.使用局部变量实现线程同步。
参考:
https://www.cnblogs.com/jiansen/p/7351872.html
volatile有什么用?能否用一句话说明下volatile的应用场景?
作用是:作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值,即不是从寄存器里取备份值,而是去该地址内存存储的值。
一句话说明volatile的应用场景:
对变量的写操作不依赖于当前值且该变量没有包含在具有其他变量的不变式中。
请说明下java的内存模型。
Java内存模型的逻辑视图
为了保证并发编程中可以满足原子性、可见性及有序性。有一个重要的概念,那就是内存模型。
为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。
通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。
它解决了 CPU 多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。
内存模型解决并发问题主要采用两种方式:
限制处理器优化
使用内存屏障
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节。
Java内存模型定义了以下八种操作来完成:
lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
也就是read和load之间,store和write之间是可以插入其他指令的,如对主内存中的变量a、b进行访问时,可能的顺序是read a,read b,load b, load a。
Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:
不允许read和load、store和write操作之一单独出现
不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现
如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。
参考:
https://www.cnblogs.com/nexiyi/p/javamemorymodelandthread.html http://ifeve.com/java-memory-model-6/
为什么代码会重排序?
直接参考:
https://www.cnblogs.com/toov5/p/9831696.html https://blog.csdn.net/qq_32646795/article/details/78221064
并发容器和框架
如何让一段程序并发的执行,并最终汇总结果?
使用CyclicBarrier 在多个关口处将多个线程执行结果汇总, CountDownLatch 在各线程执行完毕后向总线程汇报结果。
CountDownLatch : 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。
CyclicBarrier : N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。
这样应该就清楚一点了,对于CountDownLatch来说,重点是那个“一个线程”, 是它在等待,而另外那N的线程在把“某个事情”做完之后可以继续等待,可以终止。而对于CyclicBarrier来说,重点是那N个线程,他们之间任何一个没有完成,所有的线程都必须等待。
从api上理解就是CountdownLatch有主要配合使用两个方法countDown()和await(),countDown()是做事的线程用的方法,await()是等待事情完成的线程用个方法,这两种线程是可以分开的(下面例子:CountdownLatchTest2),当然也可以是同一组线程;CyclicBarrier只有一个方法await(),指的是做事线程必须大家同时等待,必须是同一组线程的工作。
CyclicBarrier例子:
如何合理的配置java线程池?如CPU密集型的任务,基本线程池应该配置多大?IO密集型的任务,基本线程池应该配置多大?用有界队列好还是无界队列好?任务非常多的时候,使用什么阻塞队列能获取最好的吞吐量?
虽然Exectors可以生成一些很常用的线程池,但毕竟在什么情况下使用还是开发者最清楚的。在某些自己很清楚的使用场景下,java线程池还是推荐自己配置的。下面是java线程池的配置类的参数,我们逐一分析一下:
corePoolSize - 池中所保存的线程数,包括空闲线程。
maximumPoolSize - 池中允许的最大线程数。
keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
unit - keepAliveTime 参数的时间单位。
workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute 方法提交的 Runnable 任务。用BlocingQueue的实现类都可以。
threadFactory - 执行程序创建新线程时使用的工厂。自定义线程工厂可以做一些额外的操作,比如统计生产的线程数等。
handler - 饱和策略,即超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。策略有:Abort终止并抛出异常,Discard悄悄抛弃任务,Discard-Oldest抛弃最老的任务策略,Caller-Runs将任务退回给调用者策略。
至于线程池应当配置多大的问题,一般有如下的经验设置:
如果是CPU密集型应用,则线程池大小设置为N+1。
如果是IO密集型应用,则线程池大小设置为2N+1。
用有界队列好还是无界队列好?这种问题的答案肯定是视情况而定:
有界队列有助于避免资源耗尽的情况发生。但他带来了新的问题:当队列填满后,新的任务怎么办?所以有界队列适用于执行比较耗资源的任务,同时要设计好相应的饱和策略。
无界队列和有界队列刚好相反,在资源无限的情况下可以一直接收新任务。适用于小任务,请求和处理速度相对持平的状况。
其实还有一种同步移交的队列 SynchronousQueue ,这种队列不存储任务信息,直接将任务提交给线程池。可以理解为容量只有1的有界队列,在特殊场景下有特殊作用,同样得设计好相应的饱和策略。
参考:
https://blog.csdn.net/qq_34039315/article/details/78542498
如何使用阻塞队列实现一个生产者和消费者模型?请写代码。
下面这是一个完整的生产者消费者代码例子,对比传统的wait、nofity代码,它更易于理解。
ProducerConsumerPattern.java如下:
生产者,Producer.java如下:
消费者,Consumer.java如下所示:
参考:
https://www.cnblogs.com/expiator/p/9317929.html
多读少写的场景应该使用哪个并发容器,为什么使用它?比如你做了一个搜索引擎,搜索引擎每次搜索前需要判断搜索关键词是否在黑名单里,黑名单每天更新一次。
Java中的锁
如何实现乐观锁(CAS)?如何避免ABA问题?
CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”这其实和乐观锁的冲突检查+数据更新的原理是一样的。
这里再强调一下,乐观锁是一种思想。CAS是这种思想的一种实现方式。
ABA问题:
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
解决方法:通过版本号(version)的方式来解决,每次比较要比较数据的值和版本号两项内容即可。
读写锁可以用于什么应用场景?
在多线程的环境下,对同一份数据进行读写,会涉及到线程安全的问题。比如在一个线程读取数据的时候,另外一个线程在写数据,而导致前后数据的不一致性;一个线程在写数据的时候,另一个线程也在写,同样也会导致线程前后看到的数据的不一致性。
这时候可以在读写方法中加入互斥锁,任何时候只能允许一个线程的一个读或写操作,而不允许其他线程的读或写操作,这样是可以解决这样以上的问题,但是效率却大打折扣了。因为在真实的业务场景中,一份数据,读取数据的操作次数通常高于写入数据的操作,而线程与线程间的读读操作是不涉及到线程安全的问题,没有必要加入互斥锁,只要在读-写,写-写期间上锁就行了。
对于以上这种情况,读写锁是最好的解决方案!其中它的实现类:ReentrantReadWriteLock--顾名思义是可重入的读写锁,允许多个读线程获得ReadLock,但只允许一个写线程获得WriteLock
读写锁的机制:
"读-读" 不互斥
"读-写" 互斥
"写-写" 互斥
参考:
https://www.cnblogs.com/liang1101/p/6475555.html
什么时候应该使用可重入锁?
可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁。
参考:
http://ifeve.com/javalocksee4/
什么场景下可以使用volatile替换synchronized?
状态标志:把简单地volatile变量作为状态标志,来达成线程之间通讯的目的,省去了用synchronized还要wait,notify或者interrupt的编码麻烦。
替换重量级锁:如果某个变量仅是单次读或者单次写操作,没有复合操作(i++,先检查后判断之类的)就可以用volatile替换synchronized。
并发工具
如何实现一个流控程序,用于控制请求的调用次数?
扩展阅读
来源:Java知音
链接:https://mp.weixin.qq.com/s/pGUN8u_GNxYeXLJqBiS9dw
以上是关于Java 并发编程一文详解 Java 中有几种创建线程的方式的主要内容,如果未能解决你的问题,请参考以下文章