分布式、多线程、锁

分布式、多线程、锁

是什么 - 为什么 - 怎么用

什么是分布式 为了解决单个物理服务器容量和性能瓶颈问题而采用的优化手段。 水平扩展,垂直拆分。

什么是线程?线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位

线程和进程有什么区别? 线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。每个线程都拥有单独的栈内存用来存储本地数据。

如何在Java中实现线程 继承java.lang.Thread 类或者直接调用Runnable接口来重写run()方法实现线程,Callable和Future创建线程 util包下。

什么是线程池? 为什么要使用它?

​ 创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。

​ 为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。

​ 从JDK1.5开始,Java API提供了Executor框架让你可以创建不同的线程池。比如单线程池,每次处理一个任务;数目固定的线程池或者是缓存线程池(一个适合很多生存期短的任务的程序的可扩展线程池)。

join方法 把指定的线程加入到当前线程中

主线程启动子线程,如果子线程中要进行大量的耗时运算,主线程会早于子线程结束,这时候主线程如果想等待子线程完成之后再运行,就需要join()方法

多线程 与 JUC

AMQP模型

JAVA 线程实现/创建方式

继承 Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。

1
2
3
4
5
6
7
public class MyThread extends Thread {
public void run() {
System.out.println("MyThread.run()");
}
}
MyThread myThread1 = new MyThread();
myThread1.start();

实现 Runnable 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyThread extends OtherClass implements Runnable {
public void run() {
System.out.println("MyThread.run()");
}
}
//启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
//事实上,当传入一个 Runnable target 参数给 Thread 后, Thread 的 run()方法就会调用
target.run()
public void run() {
if (target != null) {
target.run();
}
}

ExecutorService、 Callable、 Future 有返回值线程

有返回值的任务必须实现 Callable 接口,类似的,无返回值的任务必须 Runnable 接口。执行
Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务
返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 创建多个有返回值的任务
List<Future> list = new ArrayList<Future>();
for (int i = 0; i < taskSize; i++) {
Callable c = new MyCallable(i + " ");
// 执行任务并获取 Future 对象
Future f = pool.submit(c);
list.add(f);
}
// 关闭线程池
pool.shutdown();
// 获取所有并发任务的运行结果
for (Future f : list) {
// 从 Future 对象上获取任务的返回值,并输出到控制台
System.out.println("res: " + f.get().toString());
}

juc汇总

  1. Executors:具有Runnable任务的执行者。
  2. ExecutorService:一个线程池管理者,其实现类有多种,我会介绍一部分,我们能把Runnable,Callable提交到池中让其调度。
  3. Semaphore:一个计数信号量。
  4. ReentrantLock:一个可重入的互斥锁定Lock,功能类似synchronized,但要强大的多。
  5. Future:是与Runnable,Callable进行交互的接口,比如一个线程执行结束后取返回的结果等,还提供了cancel终止线程。
  6. BlockingQueue:阻塞队列。
  7. CompletionService:ExecutorService的扩展,可以获得线程执行结果的。
  8. CountDownLatch:一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
  9. CyclicBarrier:一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点。
  10. Future:表示异步计算的结果。
  11. ScheduldExecutorService:一个ExecutorService,可安排在给定的延迟后运行或定期执行的命令。

多线程操作(比如异步通知,或者一些操作的后续处理,为了不影响相应时间),我们是启动的时候new了队列,BlockingQueue LinkedBlockingQueue

1. 线程与进程

进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。

一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。

区别
a,地址空间:进程内的一个执行单元;进程至少有一个线程;它们共享进程的地址空间;而进程有自己独立的地址空间;
b,资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资
c,线程是处理器调度的基本单位,但进程不是.
d,二者均可并发执行.

2.守护线程

在Java中有两类线程:用户线程 (User Thread)、守护线程 (Daemon Thread)。
守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。

3.java thread状态

  1. NEW 状态是指线程刚创建, 尚未启动
  2. RUNNABLE 状态是线程正在正常运行中, 当然可能会有某种耗时计算/IO等待的操作/CPU时间片切换等, 这个状态下发生的等待一般是其他系统资源, 而不是锁, Sleep等
  3. BLOCKED 这个状态下, 是在多个线程有同步操作的场景, 比如正在等待另一个线程的synchronized 块的执行释放, 也就是这里是线程在等待进入临界区
  4. WAITING 这个状态下是指线程拥有了某个锁之后, 调用了他的wait方法, 等待其他线程/锁拥有者调用 notify / notifyAll 一遍该线程可以继续下一步操作, 这里要区分 BLOCKED 和 WATING 的区别, 一个是在临界点外面等待进入, 一个是在理解点里面wait等待别人notify, 线程调用了join方法 join了另外的线程的时候, 也会进入WAITING状态, 等待被他join的线程执行结束
  5. TIMED_WAITING 这个状态就是有限的(时间限制)的WAITING, 一般出现在调用wait(long), join(long)等情况下, 另外一个线程sleep后, 也会进入TIMED_WAITING状态
  6. TERMINATED 这个状态下表示 该线程的run方法已经执行完毕了, 基本上就等于死亡了(当时如果线程被持久持有, 可能不会被回收)

AMQP模型

4.请说出与线程同步以及线程调度相关的方法

  1. wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
  2. sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
  3. notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
  4. notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

5.进程调度算法

实时系统:FIFO(First Input First Output,先进先出算法),SJF(Shortest Job First,最短作业优先算法),SRTF(Shortest Remaining Time First,最短剩余时间优先算法)。
交互式系统:RR(Round Robin,时间片轮转算法),HPF(Highest Priority First,最高优先级算法),多级队列,最短进程优先,保证调度,彩票调度,公平分享调度。

时间片轮转法

6.wait()和sleep()的区别

  1. sleep来自Thread类,和wait来自Object类
  2. 调用sleep()方法的过程中,线程不会释放对象锁。而 调用 wait 方法线程会释放对象锁
  3. sleep睡眠后不出让系统资源,wait让出系统资源其他线程可以占用CPU
  4. sleep(milliseconds)需要指定一个睡眠时间,时间一到会自动唤醒

8.Synchronized 与Lock

ReentrantLock 拥有Synchronized相同的并发性和内存语义,此外还多了 锁投票,定时锁等候和中断锁等候
线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定,
如果使用 synchronized ,如果A不释放,B将一直等下去,不能被中断
如果 使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情

ReentrantLock获取锁定与三种方式:
a) lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁
b) tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;
c)tryLock(long timeout,TimeUnit unit), 如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;
d) lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或者锁定,或者当前线程被别的线程中断

总体的结论先摆出来:

synchronized:
在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronized,另外可读性非常好,不管用没用过5.0多线程包的程序员都能理解。
ReentrantLock:
ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。

9.线程池

线程池的作用:
在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。
常用线程池:ExecutorService 是主要的实现类,其中常用的有
Executors.newSingleThreadPool(),newFixedThreadPool(),newcachedTheadPool(),newScheduledThreadPool()。

线程池的4个组成部分

线程池管理器:用于创建并管理线程池;

工作线程:线程池中的线程;

任务接口:每个任务必须实现的接口,用于工作线程调度其运行;

任务队列:用于存放待处理的任务,提供一种缓冲机制。

ThreadPoolExecutor
1
2
3
4
5
6
7
8
9
10
11
12
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize, long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
corePoolSize:指定了线程池中的线程数量。
maximumPoolSize:指定了线程池中的最大线程数量。
keepAliveTime:当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多
次时间内会被销毁。
unit: keepAliveTime 的单位。
workQueue:任务队列,被提交但尚未被执行的任务。
threadFactory:线程工厂,用于创建线程,一般用默认的即可。
handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。

10.Executor拒绝策略

  1. AbortPolicy:为java线程池默认的阻塞策略,不执行此任务,而且直接抛出一个运行时异常,切记ThreadPoolExecutor.execute需要try
    catch,否则程序会直接退出.
  2. DiscardPolicy:直接抛弃,任务不执行,空方法
  3. DiscardOldestPolicy:从队列里面抛弃head的一个任务,并再次execute 此task。
  4. CallerRunsPolicy:不停重复执行提交,直到插入(好凶残)
  5. 用户自定义拒绝策略:实现RejectedExecutionHandler,并自己定义策略模式

11.4种线程池 - CachedThreadPool 、 FixedThreadPool、SingleThreadPool(固定长度、缺少创建多余删除、延时执行、单例)

  1. newSingleThreadExecutor :创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务, 保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
    适用场景:任务少 ,并且不需要并发执行
  2. newCachedThreadPool :创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程.
    线程没有任务要执行时,便处于空闲状态,处于空闲状态的线程并不会被立即销毁(会被缓存住),只有当空闲时间超出一段时间(默认为60s)后,线程池才会销毁该线程(相当于清除过时的缓存)。新任务到达后,线程池首先会让被缓存住的线程(空闲状态)去执行任务,如果没有可用线程(无空闲线程),便会创建新的线程。
    适用场景:处理任务速度 > 提交任务速度,耗时少的任务(避免无限新增线程)
  3. newFixedThreadPool :创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  4. newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行

12.Java里的阻塞队列

7个阻塞队列

记:数组有界阻塞队列、支持优先级排序无界阻塞、支持优先级队列无界阻塞、不存数据阻塞、链表(有界、无界、双向)阻塞

ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。

按照先进先出(FIFO)原则排序默认情况下不保证访问者公平的访问队列,因为公平访问降低吞吐量。

LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列(两个独立锁提高并发)。

基于链表的阻塞队列,按照先进先出(FIFO)原则排序,其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,能够处理并发数据。

PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列(compareTo 排序实现优先)。

采取自然顺序升序排列,可自定义实现 compareTo()。

DelayQueue:一个使用优先级队列实现的无界阻塞队列。(缓存失效、定时任务)

支持延时获取元素的无界阻塞队列。队列使用 PriorityQueue实现

缓存系统的设计:可以用 DelayQueue 保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从 DelayQueue 中获取元素时,表示缓存有效期到了。

定时任务调度: 使 用 DelayQueue 保 存 当 天 将 会 执 行 的 任 务 和 执 行 时 间 , 一 旦 从DelayQueue 中获取到任务就开始执行,从比如TimerQueue 就是使用 DelayQueue 实现的。

SynchronousQueue:一个不存储元素的阻塞队列。(不存储数据、可用于传递数据)

是一个不存储元素的阻塞队列。每一个 put 操作必须等待一个 take 操作,否则不能继续添加元素。(传递器)

LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。

多了 tryTransfer(试探下有没传入数据,没有就返回false,有没有数据都立即返回结果)、transfer 方法(去取数据,没数据就等,直到有数据才返回)

LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

双向队列指的你可以从队列的两端插入和移出元素。

添加元素

Java中的阻塞队列接口BlockingQueue继承自Queue接口。BlockingQueue接口提供了3个添加元素方法。
add:添加元素到队列里,添加成功返回true,由于容量满了添加失败会抛出IllegalStateException异常
offer:添加元素到队列里,添加成功返回true,添加失败返回false
put:添加元素到队列里,如果容量满了会阻塞直到容量不满

删除方法

3个删除方法
poll:删除队列头部元素,如果队列为空,返回null。否则返回元素。
remove:基于对象找到对应的元素,并删除。删除成功返回true,否则返回false
take:删除队列头部元素,如果队列为空,一直阻塞到队列有元素并删除

13.DelayQueue

队列中每个元素都有个过期时间,并且队列是个优先级队列,当从队列获取元素时候,只有过期元素才会出队列。

14.原子操作类

在java.util.concurrent.atomic包下,可以分为四种类型的原子更新类:原子更新基本类型、原子更新数组类型、原子更新引用和原子更新属性。

原子更新基本类型
使用原子方式更新基本类型,共包括3个类:
AtomicBoolean:原子更新布尔变量
AtomicInteger:原子更新整型变量
AtomicLong:原子更新长整型变量

atomic实现原理:通过CAS(比较并替换 - 乐观锁)的方式实现的,代价是可能循环时间会太长

原子更新数组
通过原子更新数组里的某个元素,共有3个类:
AtomicIntegerArray:原子更新整型数组的某个元素
AtomicLongArray:原子更新长整型数组的某个元素
AtomicReferenceArray:原子更新引用类型数组的某个元素
AtomicIntegerArray常用的方法有:
int addAndSet(int i, int delta):以原子方式将输入值与数组中索引为i的元素相加
boolean compareAndSet(int i, int expect, int update):如果当前值等于预期值,则以原子方式更新数组中索引为i的值为update值

原子更新引用类型
AtomicReference:原子更新引用类型
AtomicReferenceFieldUpdater:原子更新引用类型里的字段
AtomicMarkableReference:原子更新带有标记位的引用类型。

原子更新字段类
如果需要原子更新某个类的某个字段,就需要用到原子更新字段类,可以使用以下几个类:
AtomicIntegerFieldUpdater:原子更新整型字段
AtomicLongFieldUpdater:原子更新长整型字段
AtomicStampedReference:原子更新带有版本号的引用类型。
要想原子更新字段,需要两个步骤:
每次必须使用newUpdater创建一个更新器,并且需要设置想要更新的类的字段
更新类的字段(属性)必须为public volatile

15.死锁,以及解决死锁

死锁产生的四个必要条件

互斥条件:资源是独占的且排他使用,进程互斥使用资源,即任意时刻一个资源只能给一个进程使用,其他进程若申请一个资源,而该资源被另一进程占有时,则申请者等待直到资源被占有者释放。
不可剥夺条件:进程所获得的资源在未使用完毕之前,不被其他进程强行剥夺,而只能由获得该资源的进程资源释放。
请求和保持条件:进程每次申请它所需要的一部分资源,在申请新的资源的同时,继续占用已分配到的资源。
循环等待条件:在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路,环路中每一个进程所占有的资源同时被另一个申请,也就是前一个进程占有后一个进程所深情地资源。

解决死锁

一是死锁预防,就是不让上面的四个条件同时成立。
二是,合理分配资源。
三是使用银行家算法,如果该进程请求的资源操作系统剩余量可以满足,那么就分配。

16.进程间的通信方式

  1. 管道( pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
  2. 有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
  3. 信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  4. 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  5. 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
  6. 共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
  7. 套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。

17.怎么解决 hash冲突

1.开放地址法 2.再哈希法 3.链地址法(hashmap) 4.建公共溢出区

juc - 详细名称 concurrent 用过那个,知道为什么这么用 LinkedBlockingQueue (几个子类的区别) 需要记得有哪些子类 - 各有什么区别 。

18.ThreadLocal

hreadLocal为每个线程维护一个本地变量。

采用空间换时间,它用于线程间的数据隔离,为每一个使用该变量的线程提供一个副本,每个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。
ThreadLocal类中维护一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值为对应线程的变量副本。

4个方法:get/set/remove/ initialValue

可以用于解决SimpleDateFormat的线程安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class SimpleDateFormatDemo {

private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";

private static ThreadLocal threadLocal = new ThreadLocal<>();

/**
* 获取线程的变量副本,如果不覆盖initialValue方法,第一次get将返回null,故需要创建一个DateFormat,放入threadLocal中
* @return
*/
public DateFormat getDateFormat() {
DateFormat df = threadLocal.get();
if (df == null) {
df = new SimpleDateFormat(DATE_FORMAT);
threadLocal.set(df);
}
return df;
}

public static void main(String [] args) {
SimpleDateFormatDemo formatDemo = new SimpleDateFormatDemo();

MyRunnable myRunnable1 = new MyRunnable(formatDemo);
MyRunnable myRunnable2 = new MyRunnable(formatDemo);
MyRunnable myRunnable3 = new MyRunnable(formatDemo);

Thread thread1= new Thread(myRunnable1);
Thread thread2= new Thread(myRunnable2);
Thread thread3= new Thread(myRunnable3);
thread1.start();
thread2.start();
thread3.start();
}


public static class MyRunnable implements Runnable {

private SimpleDateFormatDemo dateFormatDemo;

public MyRunnable(SimpleDateFormatDemo dateFormatDemo) {
this.dateFormatDemo = dateFormatDemo;
}

@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 当前时间:"+dateFormatDemo.getDateFormat().format(new Date()));
}
}
}
源码分析
ThreadLocalMap

ThreadLocalMap内部是利用Entry来进行key-value的存储的。

1
2
3
4
5
6
7
8
static class Entry extends WeakReference<ThreadLocal> {            
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}

上面源码中key就是ThreadLocal,value就是值,Entry继承WeakReference,所以Entry对应key的引用(ThreadLocal实例)是一个弱引用。

set(ThreadLocal key, Object value)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal key, Object value) {
Entry[] tab = table;
int len = tab.length;
//根据ThreadLocal的散列值,查找对应元素在数组中的位置
int i = key.threadLocalHashCode & (len-1);
//采用线性探测法寻找合适位置
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal k = e.get();
//key存在,直接覆盖
if (k == key) {
e.value = value;
return;
}
// key == null,但是存在值(因为此处的e != null),说明之前的ThreadLocal对象已经被回收了
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//ThreadLocal对应的key实例不存在,new一个
tab[i] = new Entry(key, value);
int sz = ++size;
//清楚陈旧的Entry(key == null的)
// 如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值,则进行 rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

这个set操作和集合Map解决散列冲突的方法不同,集合Map采用的是链地址法,这里采用的是开放定址法(线性探测)。set()方法中的replaceStaleEntry()和cleanSomeSlots(),这两个方法可以清除掉key ==null的实例,防止内存泄漏。

getEntry()
1
2
3
4
5
6
7
8
private Entry getEntry(ThreadLocal key) {            
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}

由于采用了开放定址法,当前key的散列值和元素在数组中的索引并不是一一对应的,首先取一个猜测数(key的散列值),如果所对应的key是我们要找的元素,那么直接返回,否则调用getEntryAfterMiss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {            
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}

这里一直在探测寻找下一个元素,知道找的元素的key是我们要找的。这里当key==null时,调用expungeStaleEntry有利于GC的回收,用于防止内存泄漏。

ThreadLocal为什么会内存泄漏

ThreadLocalMap的key为ThreadLocal实例,他是一个弱引用,我们知道弱引用有利于GC的回收,当key == null时,GC就会回收这部分空间,但value不一定能被回收,因为他和Current Thread之间还存在一个强引用的关系。由于这个强引用的关系,会导致value无法回收,如果线程对象不消除这个强引用的关系,就可能会出现OOM。有些时候,我们调用ThreadLocalMap的remove()方法进行显式处理。

ThreadLocal总结

ThreadLocal不是用来解决共享变量的问题,也不是协调线程同步,他是为了方便各线程管理自己的状态而引用的一个机制。

每个ThreadLocal内部都有一个ThreadLocalMap,他保存的key是ThreadLocal的实例,他的值是当前线程的局部变量的副本的值。

19.Atomic - Integer

一个提供原子操作的 Integer 的类,同理的还有几个都在atomic包下。在多线程程序中,诸如++i 或 i++等运算不具有原子性,是不安全的线程操作之一。通常我们会使用 synchronized 将该操作变成一个原子操作,但 JVM 为此类操作特意提供了一些同步类,使得使用更方便,且使程序运行效率变得更高。

锁-单机锁如何实现 - sys 的好像就是同步和互斥锁.

同步锁和互斥锁的区别 同步就是一起用,互斥就是谁拿到了就是谁的,其他人睡着.

锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。 锁可以升级,但不可降级。

偏向锁:目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护

阻塞锁:让线程到阻塞状态等待,得到通知醒来开始竞争.不占cpu资源,但是恢复慢.竞争激烈使用阻塞锁,不激烈使用自旋锁.

乐观锁与悲观锁(为什么? 优势? )

悲观锁 - 假设最差情况- 每次拿别人都在改- 所以每次拿数据都上锁,比如行锁,表锁,读锁,写锁.等 synchronizedReentrantLock等独占锁就是悲观锁思想的实现

乐观锁(CAS)假设都是最好情况,别人不会改,所以不上锁,就判断这期间有没有人改数据,所以通过version判定,适用于多读情况 . 可以提高吞吐量.

乐观锁适用于写比较少的情况下(多读场景)但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

**乐观锁实现 : **1 version. 2 CAS算法 - 比较需要读写的值.与原先相同就修改,不同就不操作.一般情况下是自旋操作,即不断重试.

共享锁与独占锁:共享锁是一种乐观锁 ,独占锁是一种悲观锁

什么是可重入锁(ReentrantLock)?(递归锁)

可重入锁指的是在同一个线程中外层函数获取锁后,内层函数仍然有获取该锁的代码,却不受影响。在java中,ReentrantLock与 synchronized 都是可重入锁。(避免死锁)

https://blog.csdn.net/qq_36299025/article/details/89883989

公平锁与非公平锁

公平锁(Fair):加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得

非公平锁(Nonfair):加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待。

非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列;Java 中的 synchronized 是非公平锁, ReentrantLock 默认的 lock()方法采用的是非公平锁。

分拆锁(lock splitting)与分离锁(lock striping)

如果一个锁守护多个相互独立的状态变量,你可能能够通过拆分锁,使每个锁守护不同的变量,从而改进伸缩性.这样可以使锁的请求频率都变小.分拆锁对于中等竞争强度锁,能够转化为非竞争锁. 分拆锁的扩展,分为若干加锁块集合,并且他们属于相互独立对象,这个情况就是分离锁.如:ConcurrentHashMap就实现了16个锁数组,各自守护1/16. - (原文在深入分布式缓存中)

读写锁

1)多个读者可以同时进行读

2)写者必须互斥(只允许一个写者写,也不能读者、写者同时进行)

3)写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)

Java中读写锁有个接口java.util.concurrent.locks.ReadWriteLock也有具体的实现ReentrantReadWriteLock。

锁优化

**减少锁持有时间 **:只用在有线程安全要求的程序上加锁

减小锁粒度:将大对象拆分为小对象,降低锁竞争,可以提升偏向锁、轻量锁的成功率。典型案例还是ConcurrentHashMap

锁分离:读写锁就是锁分离。 LinkedBlockingQueue 从头部取出,从尾部放数据。

锁粗化:在某些情况下,不停的请求、同步、释放,本身会消耗系统资源,不利于性能优化(与减小粒度相反)。

锁消除:编译器层面优化。详情

使用双锁分离就得注意一点,那就是防止线程夯死。生产线程要唤醒生产线程,消费线程也要唤醒生产线程,消费线程唤醒消费线程,消费线程也要唤醒生产线程。

分布式锁 保证分布式领域中共享数据安全问题

1、数据库实现(效率低,不推荐)

2、redis实现(使用redission实现,但是需要考虑思索,释放问题。繁琐一些)

3、Zookeeper实现 (使用临时节点,效率高,失效时间可以控制)

4、Spring Cloud 实现全局锁(内置的)

https://www.cnblogs.com/wangjintao-0623/p/9727272.html

线程同步方式

synchronized 同步代码块时,一个时间内只能有一个线程得到执行.其他线程需要等待(加法计数器用到 - 计数器+机器码 生成订单号)

同步锁(lock)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Lock是控制多个线程对共享资源进行访问的工具.通常,锁提供了对共享资源的独占访问,每次只能有一个,线程对Lock对象加锁,线程开始访问共享资源之前,应该先获得Lock对象。
 //定义锁对象
private final ReentrantLock lock = new ReentrantLock();
//定义需要保证线程安全的方法
public void m(){
lock.lock();
try{
//需要保证线程安全的代码
//...method body
}
//使用finally块来保证释放锁
finally{
lock.unlock();
}
}

ConcurrentHashMap并发

Lost Wake-Up 问题

多线程情况下,一个消费者线程,一个生产者线程。

生产者

1
2
count+1;
notify();

消费者

1
2
3
while(count<=0)
wait()
count--

如果两个步骤同时执行

lost wake-up

如何解决 - 通过同步的方式:只能有一个执行,就可以解决这个问题。(java有强制要求)