一、创建线程的方式及实现
地址:【https://www.jianshu.com/p/b13e09cce94b】
(1).继承Thread类(真正意义上的线程类)是Runnabel接口的实现
//创建已个线程类继承thread父类
public class SimpleThread extends Thread {
private int countDown = 5;
private static int threadCount = 0;
public SimpleThread() {
super(Integer.toString(++threadCount));
start();
}
public String toString() {
return "#" + getName() + "(" + countDown + ").";
}
public void run() {
while(true) {
System.out.println(this);
if(-- countDown == 0)
return;
}
}
public static void main(String[] args) {
for(int i = 0; i < 5; i++) {
new SimpleThread();
}
}
}
(2)、实现Runnable,并重写里面的run方法
public class LiftOff implements Runnable {
protected int countDown = 10;
private static int taskCount = 0;
private final int id = taskCount ++;
public LiftOff() {
}
public LiftOff(int countDown) {
this.countDown = countDown;
}
public String status() {
return "#" + id + "(" + (countDown >0 ? countDown: "LiftOff") + ").";
}
@Override
public void run() {
// TODO Auto-generated method stub
while(countDown -- > 0) {
System.out.println(status());
Thread.yield();
}
}
}
//MoreBasicThreads.java
public class MoreBasicThreads {
public static void main(String[] args) {
for(int i = 0; i < 5; i++) {
new Thread(new LiftOff()).start();
}
System.out.println("Waiting for LiftOff");
}
}
(3)、实现Callable接口,重写call方法
import java.util.ArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CallableDemo {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
ArrayList<Future<String>> results = new ArrayList<Future<String>>();
for(int i = 0; i < 10; i++) {
results.add(exec.submit(new TaskWithResult(i)));
}
for(Future<String> fs : results) {
try {
System.out.println(fs.get());
}catch(InterruptedException e) {
System.out.println(e);
}catch(ExecutionException s) {
System.out.println(s);
}finally {
exec.shutdown();
}
}
}
}
(4)、使用Executor框架创建线程池
//CachedThreadPool将为每个任务都创建一个线程
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPool {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
for(int i = 0; i < 5; i++) {
exec.execute(new LiftOff());
}
exec.shutdown();
}
}
//FixedThreadPool使用了有限的线程集来执行所提交的任务
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPool {
public static void main(String[] args) {
ExecutorService exec = Executors.newFixedThreadPool(5);
for(int i = 0; i < 5; i++) {
exec.execute(new LiftOff());
}
exec.shutdown();
}
}
//SingleThreadExecutor就像是线程数量为1的FixedThreadPool
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Executors;
public class SingleThreadExecutor {
public static void main(String[] args) {
ExecutorService exec = Executors.newSingleThreadExecutor();
for(int i = 0; i < 5; i++) {
exec.execute(new LiftOff());
}
exec.shutdown();
}
}
二、sleep() 、join()、yield()有什么区别
(1)、sleep()
使当前线程(即调用该方法的线程)暂停执行一段时间,让其他线程有机会继续执行,但它并不释放对象锁。也就是说如果有synchronized同步快,其他线程仍然不能访问共享数据。注意该方法要捕捉异常。
(2)、join()方法使调用该方法的线程在此之前执行完毕,也就是等待该方法的线程执行完毕后再往下继续执行。注意该方法也需要捕捉异常。
(3)、yield() 该方法与sleep()类似,只是不能由用户指定暂停多长时间,并且yield()方法只能让同优先级的线程有执行的机会。
(4)wait()和notify()、notifyAll()
这三个方法用于协调多个线程对共享数据的存取,所以必须在synchronized语句块内使用。synchronized关键字用于保护共享数据,阻止其他线程对共享数据的存取,但是这样程序的流程就很不灵活了,如何才能在当前线程还没退出synchronized数据块时让其他线程也有机会访问共享数据呢?此时就用这三个方法来灵活控制。
wait()方法使当前线程暂停执行并释放对象锁标示,让其他线程可以进入synchronized数据块,当前线程被放入对象等待池中。当调用notify()方法后,将从对象的等待池中移走一个任意的线程并放到锁标志等待池中,只有锁标志等待池中线程能够获取锁标志;如果锁标志等待池中没有线程,则notify()不起作用。
notifyAll()则从对象等待池中移走所有等待那个对象的线程并放到锁标志等待池中。
wait()和notify(),notifyAll()是Object类的方法,sleep()和yield()是Thread类的方法。
三、说说 CountDownLatch 原理
来源【https://www.jianshu.com/p/7c7a5df5bda6?ref=myread】
1.定义:CountDownLatch是同步工具类之一,可以指定一个计数值,在并发环境下由线程进行减1操作,当计数值变为0之后,被await方法阻塞的线程将会唤醒,实现线程间的同步。
//主线程启动10个子线程后阻塞在await方法,需要等子线程都执行完毕,主线程才能唤醒继续执行
public void startTestCountDownLatch() {
int threadNum = 10;
final CountDownLatch countDownLatch = new CountDownLatch(threadNum);
for (int i = 0; i < threadNum; i++) {
final int finalI = i + 1;
new Thread(() -> {
System.out.println("thread " + finalI + " start");
Random random = new Random();
try {
Thread.sleep(random.nextInt(10000) + 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread " + finalI + " finish");
countDownLatch.countDown();
}).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadNum + " thread finish");
}
四、说说 CyclicBarrier 原理
来源:【http://ifeve.com/concurrency-cyclicbarrier/】
定义:CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
运用场景:CyclicBarrier可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个Excel保存了用户所有银行流水,每个Sheet保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日均银行流水,最后,再用barrierAction用这些线程的计算结果,计算出整个Excel的日均银行流水。
CyclicBarrier和CountDownLatch的区别
CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。
CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。比如以下代码执行完之后会返回true。
五、说说 Semaphore 原理
来源:【https://www.jianshu.com/p/0090341c6b80】
定义:Semaphore也叫信号量,在JDK1.5被引入,可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。
Semaphore内部维护了一组虚拟的许可,许可的数量可以通过构造函数的参数指定。
访问特定资源前,必须使用acquire方法获得许可,如果许可数量为0,该线程则一直阻塞,直到有可用许可。
访问资源后,使用release释放许可。
Semaphore和ReentrantLock类似,获取许可有公平策略和非公平许可策略,默认情况下使用非公平策略。
应用场景:Semaphore可以用来做流量分流,特别是对公共资源有限的场景,比如数据库连接。
假设有这个的需求,读取几万个文件的数据到数据库中,由于文件读取是IO密集型任务,可以启动几十个线程并发读取,但是数据库连接数只有10个,这时就必须控制最多只有10个线程能够拿到数据库连接进行操作。这个时候,就可以使用Semaphore做流量控制。
private static final int COUNT = 40;
private static Executor executor = Executors.newFixedThreadPool(COUNT);
private static Semaphore semaphore = new Semaphore(10);
public static void main(String[] args) {
for (int i=0; i< COUNT; i++) {
executor.execute(new ThreadTest.Task());
}
}
static class Task implements Runnable {
@Override
public void run() {
try {
//读取文件操作
semaphore.acquire();
// 存数据过程
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
}
}
}
}
实现原理:
本文代码源于JDK1.8
Semaphore实现主要基于java同步器AQS,不熟悉的可以移步这里 [深入浅出java同步器]
内部使用state表示许可数量。
非公平策略
(1)acquire实现,核心代码如下:
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
acquires值默认为1,表示尝试获取1个许可,remaining代表剩余的许可数。
如果remaining < 0,表示目前没有剩余的许可。
当前线程进入AQS中的doAcquireSharedInterruptibly方法等待可用许可并挂起,直到被唤醒。
(2)release实现,核心代码如下:
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
releases值默认为1,表示尝试释放1个许可,next 代表如果许可释放成功,可用许可的数量。
通过unsafe.compareAndSwapInt修改state的值,确保同一时刻只有一个线程可以释放成功。
许可释放成功,当前线程进入到AQS的doReleaseShared方法,唤醒队列中等待许可的线程。
也许有人会有疑问,非公平性体现在哪里?
当一个线程A执行acquire方法时,会直接尝试获取许可,而不管同一时刻阻塞队列中是否有线程也在等待许可,如果恰好有线程C执行release释放许可,并唤醒阻塞队列中第一个等待的线程B,这个时候,线程A和线程B是共同竞争可用许可,不公平性就是这么体现出来的,线程A一点时间都没等待就和线程B同等对待。
公平策略
(1)acquire实现,核心代码如下:
protected int tryAcquireShared(int acquires) {
for (;;) {
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
acquires值默认为1,表示尝试获取1个许可,remaining代表剩余的许可数。
可以看到和非公平策略相比,就多了一个对阻塞队列的检查。
如果阻塞队列没有等待的线程,则参与许可的竞争。
否则直接插入到阻塞队列尾节点并挂起,等待被唤醒。
release实现,和非公平策略一样。
六、说说 Exchanger 原理
定义:
此类提供对外的操作是同步的;
用于成对出现的线程之间交换数据;
可以视作双向的同步队列;
可应用于基因算法、流水线设计等场景。
api:类提供对外的接口非常简洁,一个无参构造函数,两个重载的范型exchange方法
public V exchange(V x, long timeout, TimeUnit unit) throws InterruptedException, TimeoutException```
**应用场景:** Exchanger可以用于遗传算法,遗传算法里需要选出两个人作为交配对象,这时候会交换两人的数据,并使用交叉规则得出2个交配结果。
Exchanger也可以用于校对工作。比如我们需要将纸制银流通过人工的方式录入成电子银行流水,为了避免错误,采用AB岗两人进行录入,录入到Excel之后,系统需要加载这两个Excel,并对这两个Excel数据进行校对,看看是否录入的一致。
```public class ExchangerTest {
private static final Exchanger<String> exgr = new Exchanger<String>();
private static ExecutorService threadPool = Executors.newFixedThreadPool(2);
public static void main(String[] args) {
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
String A = "银行流水A";// A录入银行流水数据
exgr.exchange(A);
} catch (InterruptedException e) {
}
}
});
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
String B = "银行流水B";// B录入银行流水数据
String A = exgr.exchange("B");
System.out.println("A和B数据是否一致:" + A.equals(B) + ",A录入的是:"
+ A + ",B录入是:" + B);
} catch (InterruptedException e) {
}
}
});
threadPool.shutdown();
}
}
七、说说 CountDownLatch 与 CyclicBarrier 区别
地址:【https://www.cnblogs.com/xiaorenwu702/p/3977833.html】
CountDownLatch : 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。 CyclicBarrier : N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。
这样应该就清楚一点了,对于CountDownLatch来说,重点是那个“一个线程”, 是它在等待, 而另外那N的线程在把“某个事情”做完之后可以继续等待,可以终止。而对于CyclicBarrier来说,重点是那N个线程,他们之间任何一个没有完成,所有的线程都必须等待。
CountDownLatch 是计数器, 线程完成一个就记一个, 就像 报数一样, 只不过是递减的.
而CyclicBarrier更像一个水闸, 线程执行就想水流, 在水闸处都会堵住, 等到水满(线程到齐)了, 才开始泄流.
八、ThreadLocal 原理分析
地址:【https://www.cnblogs.com/dolphin0520/p/3920407.html】
定义:ThreadLocal,很多地方叫做线程本地变量,ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
应用场景:最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等。
九、讲讲线程池的实现原理
参考地址【https://www.jianshu.com/p/edab547f2710】 【https://www.jianshu.com/p/125ccf0046f3】
为什么要使用线程池
降低资源消耗。通过复用已存在的线程和降低线程关闭的次数来尽可能降低系统性能损耗;
提升系统响应速度。通过复用线程,省去创建线程的过程,因此整体上提升了系统的响应速度;
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,因此,需要使用线程池来管理线
* 将该Runnable任务加入线程池并在未来某个时刻执行
* 该任务可能执行在一个新的线程 或 一个已存在的线程池中的线程
* 如果该任务提交失败,可能是因为线程池已关闭,或者已达到线程池队列和线程数已满.
* 该Runnable将交给RejectedExecutionHandler处理,抛出RejectedExecutionException
*/
public void execute(Runnable command) {
if (command == null){
//如果没传入Runnable任务,则抛出空指针异常
throw new NullPointerException();
}
int c = ctl.get();
//当前线程数 小于 核心线程数
if (workerCountOf(c) < corePoolSize) {
//直接开启新的线程,并将Runnable传入作为第一个要执行的任务,成功返回true,否则返回false
if (addWorker(command, true)){
return;
}
c = ctl.get();
}
//c < SHUTDOWN代表线程池处于RUNNING状态 + 将Runnable添加到任务队列,如果添加成功返回true失败返回false
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//成功加入队列后,再次检查是否需要添加新线程(因为已存在的线程可能在上次检查后销毁了,或者线程池在进入本方法后关闭了)
if (! isRunning(recheck) && remove(command)){
//如果线程池处于非RUNNING状态 并且 将该Runnable从任务队列中移除成功,则拒绝执行此任务
//交给RejectedExecutionHandler调用rejectedExecution方法,拒绝执行此任务
reject(command);
}else if (workerCountOf(recheck) == 0){
//如果线程池线程数量为0,则创建一条新线程,去执行
addWorker(null, false);
}
}else if (!addWorker(command, false))
//如果线程池处于非RUNNING状态 或 将Runnable添加到队列失败(队列已满导致),则执行默认的拒绝策略
reject(command);
}
先判断线程池中核心线程池所有的线程是否都在执行任务。如果不是,则新创建一个线程执行刚提交的任务,否则,核心线程池中所有的线程都在执行任务,则进入第2步;
判断当前阻塞队列是否已满,如果未满,则将提交的任务放置在阻塞队列中;否则,则进入第3步;
判断线程池中所有的线程是否都在执行任务,如果没有,则创建一个新的线程来执行任务,否则,则交给饱和策略进行处理
十、线程池的几种方式与使用场景
线程池的种类,区别和使用场景 newCachedThreadPool:•底层:返回ThreadPoolExecutor实例,corePoolSize为0;maximumPoolSize为Integer.MAX_VALUE;keepAliveTime为60L;unit为TimeUnit.SECONDS;workQueue为SynchronousQueue(同步队列)•通俗:当有新任务到来,则插入到SynchronousQueue中,由于SynchronousQueue是同步队列,因此会在池中寻找可用线程来执行,若有可以线程则执行,若没有可用线程则创建一个线程来执行该任务;若池中线程空闲时间超过指定大小,则该线程会被销毁。•适用:执行很多短期异步的小程序或者负载较轻的服务器newFixedThreadPool:•底层:返回ThreadPoolExecutor实例,接收参数为所设定线程数量nThread,corePoolSize为nThread,maximumPoolSize为nThread;keepAliveTime为0L(不限时);unit为:TimeUnit.MILLISECONDS;WorkQueue为:new LinkedBlockingQueue() 无解阻塞队列•通俗:创建可容纳固定数量线程的池子,每隔线程的存活时间是无限的,当池子满了就不在添加线程了;如果池中的所有线程均在繁忙状态,对于新任务会进入阻塞队列中(无界的阻塞队列)•适用:执行长期的任务,性能好很多newSingleThreadExecutor:•底层:FinalizableDelegatedExecutorService包装的ThreadPoolExecutor实例,corePoolSize为1;maximumPoolSize为1;keepAliveTime为0L;unit为:TimeUnit.MILLISECONDS;workQueue为:new LinkedBlockingQueue() 无解阻塞队列 •通俗:创建只有一个线程的线程池,且线程的存活时间是无限的;当该线程正繁忙时,对于新任务会进入阻塞队列中(无界的阻塞队列) •适用:一个任务一个任务执行的场景NewScheduledThreadPool:•底层:创建ScheduledThreadPoolExecutor实例,corePoolSize为传递来的参数,maximumPoolSize为Integer.MAX_VALUE;keepAliveTime为0;unit为:TimeUnit.NANOSECONDS;workQueue为:new DelayedWorkQueue() 一个按超时时间升序排序的队列 •通俗:创建一个固定大小的线程池,线程池内线程存活时间无限制,线程池可以支持定时及周期性任务执行,如果所有线程均处于繁忙状态,对于新任务会进入DelayedWorkQueue队列中,这是一种按照超时时间排序的队列结构•适用:周期性执行任务的场景线程池任务执行流程: 1.当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。 2.当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行3.当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务4.当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理5.当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程 6.当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭
十一、线程的生命周期
参考地址:【https://www.jianshu.com/p/d41c9e17ad2b】
Java线程的状态可以使用监控工具查看,也可以通过Thread.getState()调用来获取。 Thread.getState()的返回值类型 Thread.State 是一个枚举类型(Enum)。Thread.State所定义的线程状态包括以下几种。NEW:一个已创建而未启动的线程处于该状态。由于一个线程实例只能够被启动一次,因此一个线程只可能有一次处于该状态。RUNNABLE:该状态可以被看成一个复合状态 。它包括两个子状态:READY 和 RUNNING。前者表示处于该状态的线程可以被线程调度器( Scheduler ) 进行调度而使之处于 RUNNING 状态 。后者表示处于该状态的线程正在运行,即相应线程对象的 run 方法 所对应的指令正在由处理器执行。执行Thread.yield()的线程,其状态可能会由RUNNING 转换为 READY。处于 READY 子状态的线程也被称为活跃线程。BLOCKED:一个线程发起一个阻塞式I/O(Blocking I/O )操作后,或者申请一个由其他线程持有的独占资源(比如锁)时,相应的线程会处于该状态。处于BLOCKED 状态的线程并不会占用处理器资源。当阻塞式I/O操作完成后,或者线程获得了其申请的资源,该线程的状态又可以转换为RUNNABLE。WAITING:一个线程执行了某些特定方法之后就会处于这种等待其他线程执行另外一些特定操作的状态。能够使其执行线程变更为 WAITING 状态的方法包括:Object.wait () 、 Thread.join()和 LockSupport.park(Object)。能够使相应线程从 WAITING 变更为 RUNNABLE 的相应方法包括 :Object.notify()/notify All()和 LockSupport. Unpark(Object)。TIMED WAITING:该状态和WAITING 类似,差别在于处于该状态的线程并非无限制地等待其他线程执行特定操作,而是处于带有时间限制的等待状态。当其他线程没有在指定时间内执行该线程所期望的特定操作时,该线程的状态自动转换为RUNNABLE。TERMINATED:已经执行结束的线程处于该状态。由于一个线程实例只能够被启动一次,因此一个线程也只可能有一次处于该状态。Thread.run()正常返回或者由于抛出异常而提前终止都会导致相应线程处于该状态。一个线程在其整个生命周期中,只可能有一次处于NEW状态和 TERMINATED 状态。
十二、锁机制
参考地址:【https://www.jianshu.com/p/4f2cf5c61db1】内容太多不再此赘述!
重入锁(ReentrantLock)是一种递归无阻塞的同步机制。重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。在JAVA环境下 ReentrantLock 和synchronized都是 可重入锁。
自旋锁,由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。如何旋转呢?何为自旋锁,就是如果发现锁定了,不是睡眠等待,而是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时 才能进入临界区。
偏向锁(Biased Locking)是Java6引入的一项多线程优化,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。
重入锁(ReentrantLock)是一种递归无阻塞的同步机制,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。 在JAVA环境下 ReentrantLock 和synchronized都是 可重入锁。
公平锁,就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己
非公平锁比较粗鲁,上来就直接尝试占有锁。在公平的锁上,线程按照他们发出请求的顺序获取锁,但在非公平锁上,则允许‘插队’:当一个线程请求非公平锁时,如果在发出请求的同时该锁变成可用状态,那么这个线程会跳过队列中所有的等待线程而获得锁。 非公平的ReentrantLock 并不提倡 插队行为,但是无法防止某个线程在合适的时候进行插队。
十三、说说线程安全问题
线程安全是在多线程编程中,有可能会出现同时访问同一个 共享、可变资源 的情况,始终都不会导致数据破坏以及其他不该出现的结果。这种资源可以是一个变量、一个对象、一个文件等。
十四、volatile 实现原理
地址【https://www.jianshu.com/p/6efe8d5bd567】
十五、synchronize 实现原理
地址【https://www.jianshu.com/p/2ba154f275ea】
十六、synchronized 与 lock 的区别
地址【https://www.jianshu.com/p/b343a9637f95】
Synchronized是关键字,内置语言实现,Lock是接口。
Synchronized在线程发生异常时会自动释放锁,因此不会发生异常死锁。Lock异常时不会自动释放锁,所以需要在finally中实现释放锁。
Lock是可以中断锁,Synchronized是非中断锁,必须等待线程执行完成释放锁。
Lock可以使用读锁提高多线程读效率。
十七、CAS 乐观锁
地址【https://www.jianshu.com/p/ae25eb3cfb5d】
什么是CAS机制
CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。
CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
Synchronized虽然确保了线程的安全,但是在性能上却不是最优的,Synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。
尽管Java1.6为Synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。
CAS的缺点:
1.CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
2.不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了
十八、ABA 问题
地址:【https://www.jianshu.com/p/72d02353dc7e】
【https://www.jb51.net/article/132977.htm】
在CAS算法中,需要取出内存中某时刻的数据(由用户完成),在下一时刻比较并替换(由CPU完成,该操作是原子的)。这个时间差中,会导致数据的变化。引用原书的话:如果在算法中的节点可以被循环使用,那么在使用“比较并交换”指令就可能出现这种问题,在CAS操作中将判断“V的值是否仍然为A?”,并且如果是的话就继续执行更新操作,在某些算法中,如果V的值首先由A变为B,再由B变为A,那么CAS将会操作成功
如何避免ABA问题:
Java中提供了AtomicStampedReference和AtomicMarkableReference来解决ABA问题
十九、悲观锁和乐观锁的业务场景及实现方式
地址:【https://www.jianshu.com/p/232a86cbd4b0?utm_campaign】
悲观锁和乐观锁的区别
什么时候使用乐观锁?
资源提交冲突,其他使用方需要重新读取资源,会增加读的次数,但是可以面对高并发场景,前提是如果出现提交失败,用户是可以接受的。因此一般乐观锁只用在高并发、多读少写的场景。
其中:GIT,SVN,CVS等代码版本控制管理器,就是一个乐观锁使用很好的场景,例如:A、B程序员,同时从SVN服务器上下载了code.html文件,当A完成提交后,此时B再提交,那么会报版本冲突,此时需要B进行版本处理合并后,再提交到服务器。这其实就是乐观锁的实现全过程。如果此时使用的是悲观锁,那么意味者所有程序员都必须一个一个等待操作提交完,才能访问文件,这是难以接受的。
什么时候使用悲观锁?
一旦通过悲观锁锁定一个资源,那么其他需要操作该资源的使用方,只能等待直到锁被释放,好处在于可以减少并发,但是当并发量非常大的时候,由于锁消耗资源,并且可能锁定时间过长,容易导致系统性能下降,资源消耗严重。因此一般我们可以在并发量不是很大,并且出现并发情况导致的异常用户和系统都很难以接受的情况下,会选择悲观锁进行。
悲观锁的实现一般都是通过锁机制来实现的,锁可以简单理解为资源的访问的入口。如果要对一个具有锁属性的资源执行访问时,在更新操作时,需要持锁权才能进行操作,但是往往这种操作可以保证数据的一致性和完整性。例如数据库表的行锁。