JAVA 多线程

什么是线程?

线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对 运算密集型任务提速。比如,如果一个线程完成一个任务要100毫秒,那么用十个线程完成改任务只需10毫秒。Java在语言层面对多线程提供了卓越的支 持,它也是一个很好的卖点。

java.util.concurrent.CountDownLatch

CountDownLatch这个类能够使一个线程等待其他线程完成各自的工作后再执行。例如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有的框架服务之后再执行。
function:
CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
在这里插入图片描述

多个线程更改同一个内存出现数据一致性问题的原因

每个线程有着自己独有的工作内存,工作内存中保存了被该线程使用到的变量,这些变量来自主内存变量的副本拷贝。线程对变量的所有读写操作都必须在工作内存中进行,不能直接读写主内存中的变量。而不同线程间的工作内存也是独立的,一个线程无法访问其他线程的工作内存中的变量。

线程工作时,把需要的变量从主内存中拷贝到自己的工作内存,线程运行结束之后再将自己工作内存中的变量写回到主内存中,而多个线程间对变量的交互只能通过主内存来间接实现。具体的线程、工作内存、主内存的交互关系图如下:

在这里插入图片描述

通过上面的图和前面的介绍,我们就很容易明白我们平常所说的多线程编程时遇到数据状态不一致的问题是怎么产生的。例如:线程1和线程2都需要操作主内存中的共享变量A,当线程1已经在工作内存中修改了共享变量A副本的值但是还没有写回主内存,这时线程2拷贝了主内存中共享变量A到自己的工作内存中,紧接着线程1将自己工作内存中修改过的共享变量A的副本写回到了主内存,很明显线程2加载的共享变量A是之前的旧状态的数据,这样就产生了数据状态不一致的问题。

java内存模型的三个特征(jmm)

Java内存模型其实一直是围绕着并发过程中的如何处理原子性、可见性和有序性这三个特征建立的。

原子性(Atomicity)
什么是原子性呢,原子性是指一个操作不可中断,不可分割,在多线程中就是指一旦一个线程开始执行某个操作,就不能被其他线程干扰。

Java内存模型直接用来保证原子性变量的操作包括use、read、load、assign、store、write,我们大致可以认为Java基本数据类型的访问都是原子性的(long,double除外,前面已经介绍过了),如果用户要操作一个更大的范围保证原子性,Java内存模型还提供了lock和unlock来满足这种需求,但是这两种操作没有直接开放给用户,而是提供了两个更高层次的字节码指令:monitorenter 和 moniterexit,这两个指令对应到Java代码中就是synchronized关键字,所以synchronized代码块之间的操作具有原子性。

可见性(Visibility)
可见性是指当一个线程修改了变量之后,其他线程能立刻得知这个修改。

Java内存模型通过将变量修改后将新值同步写回主内存,在读取前从主内存刷新变量值,所以JVM内存模型是通过主内存作为传递介质来实现可见性的。无论是普通变量还是volatile修饰的变量都是这样的,唯一的区别就是volatile变量在被修改之后会立刻写回主内存,而在读取时都会重新去主内存读取最新的值,而普通变量则在被修改后会先存储在工作内存,之后再从工作内存写回主内存,而读的时候则是从工作内存中读取该变量的副本拷贝。

除了volatile可以实现可见性之外,synchronized和final关键字也能实现可见性。synchronized同步块的可见性是因为对一个变量执行unlock操作之前,必须将变量的改动写回主内存来(store、write两个操作)实现的。而final字段则是因为一旦final字段初始化完成,其他线程就可以访问final字段的值,而且final字段初始化完成之后就不再可变。

有序性(Ordering)
前面说过处理器在执行运算的时候,会对程序代码进行乱序执行优化,也叫做重排序优化。同样的,在JVM中也存在指令重排序优化,这种优化在单线程中是不会存在问题的,但如果这种优化出现在多线程环境中,就可能会出现多线程安全的问题,因为线程1的指令优化可能影响线程2中某个状态。

Java提供了volatile和synchronized关键字来保证线程间操作的有序性。volatile是因为其本身的禁止指令重排序语义来实现的,而synchronized则是由“同一个变量在同一时刻只能有一个线程对其进行lock操作”这条规则来实现的,这也就是synchronized代码块对同一个锁只能串行进入的原因。

上面介绍了Java内存模型的3中特性,我们可以发现synchronized可以说是万能的,它能实现Java多线程中的这3大特性,所以这也早就了很多人在遇到多线程并发操作事都是直接使用synchronized完成,但使用synchronized内置锁会阻塞需要而又没有获取该内置锁的线程,而Java中的线程与操作系统中的原生线程是一一对应的,所以当synchronized内置锁导致某个线程阻塞后,会导致系统从用户态切换到内核态执行阻塞操作,这个操作是非常耗时的。

实现runnable接口与继承Thread类相比的优势

继承Thread类的,我们相当于拿出三件事即三个卖票10张的任务分别分给三个窗口,他们各做各的事各卖各的票各完成各的任务,因为MyThread继承Thread类,所以在new MyThread的时候在创建三个对象的同时创建了三个线程;

实现Runnable的, 相当于是拿出一个卖票10张得任务给三个人去共同完成,new MyThread相当于创建一个任务,然后实例化三个Thread,创建三个线程即安排三个窗口去执行。

在我们刚接触的时候可能会迷糊继承Thread类和实现Runnable接口实现多线程,其实在接触后我们会发现这完全是两个不同的实现多线程,一个是多个线程分别完成自己的任务,一个是多个线程共同完成一个任务。

其实在实现一个任务用多个线程来做也可以用继承Thread类来实现只是比较麻烦,一般我们用实现Runnable接口来实现,简洁明了。

大多数情况下,如果只想重写 run() 方法,而不重写其他 Thread 方法,那么应使用 Runnable 接口。这很重要,因为除非程序员打算修改或增强类的基本行为,否则不应为该类(Thread)创建子类。

start和run的区别

1,start()方法来启动线程,真正实现了多线程运行,这时无需等待。run方法体代码执行完毕而直接继续执行下面的代码: 通过调用Thread类的start()方法来启动一个线程,这时此线程是处于就绪状态,并没有运行。

然后通过此Thread类调用方法run()来完成其运行操作的,这里方法run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程终止,而CPU再运行其它线程。

2,run()方法当作普通方法的方式调用,程序还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码: 而如果直接用run方法,这只是调用一个方法而已,程序中依然只有主线程–这一个线程,其程序执行路径还是只有一条,这样就没有达到写线程的目的。

3,调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。

这两个方法应该都比较熟悉,把需要并行处理的代码放在run()方法中,start()方法启动线程将自动调用 run()方法,这是由jvm的内存机制规定的。并且run()方法必须是public访问权限,返回值类型为void。

4,还有就是尽管线程的调度顺序是不固定的,但是如果有很多线程被阻塞等待运行,调度程序将会让优先级高的线程先执行,而优先级低的线程执行的频率会低一些。

多线程的优势

1、进程之间不能共享内存,而线程之间可以共享内存。

2、系统创建进程需要为该进程重新分配系统资源,创建线程的代价则小的多,因此多任务并发时,多线程效率高。

3、Java 语言本身内置多线程功能的支持,而不是单纯作为底层系统的调度方式,从而简化了多线程编程。

注意:多线程是为了同步完成多个任务,不是为了提高程序运行效率,而是通过提高资源使用效率来提高系统的效率。

多线程的同步问题

在这里插入图片描述
解决办法分析:即我们不能同时让超过两个以上的线程进入到 if(num>0)的代码块中,不然就会出现上述的错误。我们可以通过以下三个办法来解决:

1、使用 同步代码块
2、使用 同步方法
3、使用 锁机制

①、使用同步代码块
语法:
synchronized (同步锁) {
//需要同步操作的代码
}

同步锁:为了保证每个线程都能正常的执行原子操作,Java 线程引进了同步机制;同步锁也叫同步监听对象、同步监听器、互斥锁;
Java程序运行使用的任何对象都可以作为同步监听对象,但是一般我们把当前并发访问的共同资源作为同步监听对象

注意:同步锁一定要保证是确定的,不能相对于线程是变化的对象;任何时候,最多允许一个线程拿到同步锁,谁拿到锁谁进入代码块,而其他的线程只能在外面等着

实例

下面展示一些 内联代码片

public void run() {
        //票分 50 次卖完
        for(int i = 0 ; i < 50 ;i ++){
            //这里我们使用当前对象的字节码对象作为同步锁
            synchronized (this.getClass()) { //this.getClass()是为了获得当前的对象
                if(num > 0){
                    try {
                        //模拟卖一次票所需时间
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"卖出一张票,剩余"+(--num)+"张");
                }
            }
             
        }
    }

②、使用 同步方法
语法:即用 synchronized 关键字修饰方法

下面展示一些 内联代码片

@Override
    public void run() {
        //票分 50 次卖完
        for(int i = 0 ; i < 50 ;i ++){
            sell();
             
        }
    }
    private synchronized void sell(){
        if(num > 0){
            try {
                //模拟卖一次票所需时间
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"卖出一张票,剩余"+(--num)+"张");
        }
    }

注意:不能直接用 synchronized 来修饰 run() 方法,因为如果这样做,那么就会总是第一个线程进入其中,而这个线程执行完所有操作,即卖完所有票了才会出来。(这样的话锁的粒度就太粗了)

③、使用 锁机制
1
public interface Lock

主要方法:

在这里插入图片描述
常用实现类:

下面展示一些 内联代码片

public class ReentrantLock
extends Object
implements Lock, Serializable<br>//一个可重入互斥Lock具有与使用synchronized方法和语句访问的隐式监视锁相同的基本行为和语义,但具有扩展功能。


package com.ys.thread;
 
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class TicketSellRunnable implements Runnable{
 
    //定义一共有 50 张票,继承机制开启线程,资源是共享的,所以不用加 static
    private int num = 50;
    //创建一个锁对象
    Lock l = new ReentrantLock();
     
    @Override
    public void run() {
        //票分 50 次卖完
        for(int i = 0 ; i < 50 ;i ++){
            //获取锁
            l.lock();
            try {
                if(num > 0){
                //模拟卖一次票所需时间
                Thread.sleep(10);
                System.out.println(Thread.currentThread().getName()+"卖出一张票,剩余"+(--num)+"张");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }finally{
                //释放锁
                l.unlock();
            }
             
             
        }
    }
    private void sell(){
         
    }
 
}

同步锁池

同步锁池:同步锁必须选择多个线程共同的资源对象,而一个线程获得锁的时候,别的线程都在同步锁池等待获取锁;当那个线程释放同步锁了,其他线程便开始由CPU调度分配锁

锁池

锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

等待池

假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.

锁池是一个对象中多个被激活的线程准备竞争某个锁
等待池是调用了wait()方法的线程所处的位置,当某个线程被notify之后,会进入对象的锁池之中。

关于让线程等待和唤醒线程的方法,如下:(这是 Object 类中的方法)

在这里插入图片描述

在这里插入图片描述
  
wait():执行该方法的线程对象,释放同步锁,JVM会把该线程放到等待池中,等待其他线程唤醒该线程

notify():执行该方法的线程唤醒在等待池中等待的任意一个线程,把线程转到锁池中等待(注意锁池和等待池的区别)

notifyAll():执行该方法的线程唤醒在等待池中等待的所有线程,把线程转到锁池中等待。

注意:上述方法只能被同步监听锁对象来调用,这也是为啥wait() 和 notify()方法都在 Object 对象中,因为同步监听锁可以是任意对象,只不过必须是需要同步线程的共同对象即可,否则别的对象调用会报错:        java.lang.IllegalMonitorStateException

线程的生命周期(各个状态)

在这里插入图片描述
阻塞和等待的区别:阻塞是在锁池中,等待是在等待池中

1、新建状态(new):使用 new 创建一个线程,仅仅只是在堆中分配了内存空间

新建状态下,线程还没有调用 start()方法启动,只是存在一个线程对象而已

Thread t = new Thread();//这就是t线程的新建状态

2、可运行状态(runnable):新建状态调用 start() 方法,进入可运行状态。而这个又分成两种状态,ready 和 running,分别表示就绪状态和运行状态

就绪状态:线程对象调用了 start() 方法,等待 JVM 的调度,(此时该线程并没有运行)

运行状态:线程对象获得 JVM 调度,如果存在多个 CPU,那么运行多个线程并行运行

注意:线程对象只能调用一次 start() 方法,否则报错:illegaThreadStateExecptiong

3、阻塞状态(blocked):正在运行的线程因为某种原因放弃 CPU,暂时停止运行,就会进入阻塞状态。此时 JVM 不会给线程分配 CPU,知道线程重新进入就绪状态,才有机会转到 运行状态。

注意:阻塞状态只能先进入就绪状态,不能直接进入运行状态

阻塞状态分为两种情况:

①、当线程 A 处于可运行状态中,试图获取同步锁时,却被 B 线程获取,此时 JVM 把当前 A 线程放入锁池中,A线程进入阻塞状态

②、当线程处于运行状态时,发出了 IO 请求,此时进入阻塞状态

4、等待状态(waiting):等待状态只能被其他线程唤醒,此时使用的是无参数的 wait() 方法

①、当线程处于运行状态时,调用了 wait() 方法,此时 JVM 把该线程放入等待池中

5、计时等待(timed waiting):调用了带参数的 wait(long time)或 sleep(long time) 方法

①、当线程处于运行状态时,调用了带参数 wait 方法,此时 JVM 把该线程放入等待池中

②、当前线程调用了 sleep(long time) 方法

6、终止状态(terminated):通常称为死亡状态,表示线程终止

①、正常终止,执行完 run() 方法,正常结束

②、强制终止,如调用 stop() 方法或 destory() 方法

③、异常终止,执行过程中发生异常

线程五种状态(新建、就绪、运行、阻塞、死亡)

在这里插入图片描述
1、新生状态

在程序中用构造方法(new操作符)创建一个新线程时,如new Thread®,该线程就是创建状态

此时它已经有了相应的内存空间和其它资源,但是还没有开始执行。

2、就绪状态
新建线程对象后,调用该线程的 start()方法就可以启动线程。当线程启动时,线程进入就绪状态(runnable)。

由于还没有分配CPU,线程将进入线程队列排队,等待 CPU 服务,这表明它已经具备了运行条件。当系统挑选一个

等待执行的Thread对象后,它就会从等待执行状态进入执行状态。系统挑选的动作称之为“CPU调度"。一旦获得CPU

线程就进入运行状态并自动调用自己的run方法。

3、运行状态

当就绪状态的线程被调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的 run()方法。

run()方法定义了该线程的操作和功能。运行状态中的线程执行自己的run方法中代码。直到调用其他方法或者发生阻塞

而终止。

4、阻塞状态

一个正在执行的线程在某些特殊情况下,如被人为挂起或需要执行耗时的输入输出操作时,将让出 CPU 并暂时中止

自己的执行,进入堵塞状态。在可执行状态下,如果调用 sleep()、 suspend()、 wait()等方法,线程都将进入堵塞状态。

堵塞时,线程不能进入排队队列,只有当引起堵塞的原因被消除后,线程转入就绪状态。重新到就绪队列中排队等待,

这时被CPU调度选中后会从原来停止的位置开始继续执行。

记住:阻塞被消除后是回到就绪状态,不是运行状态。

5、死亡状态

线程调用 stop()方法、destory()方法或 run()方法执行结束后,线程即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。

11、常用的并发工具类有哪些?

CountDownLatch

CyclicBarrier

Semaphore

Exchanger

12、CyclicBarrier和CountDownLatch的区别

1)CountDownLatch简单的说就是一个线程等待,直到他所等待的其他线程都执行完成并且调用countDown()方法发出通知后,当前线程才可以继续执行。

2)cyclicBarrier是所有线程都进行等待,直到所有线程都准备好进入await()方法之后,所有线程同时开始执行!

3)CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。

4)CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。如果被中断返回true,否则返回false。

在CyclicBarrier类的内部有一个计数器,每个线程在到达屏障点的时候都会调用await方法将自己阻塞,此时计数器会减1,当计数器减为0的时候所有因调用await方法而被阻塞的线程将被唤醒。这就是实现一组线程相互等待的原理,下面我们先看看CyclicBarrier有哪些成员变量。

volatile变量的特性

保证可见性,不保证原子性,禁止指令重排

可见性:当某个线程修改volatile变量时,JMM会强制将这个修改更新到主内存中,并且让其他线程工作内存中存储的副本失效。

指令重排是指JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行重新排序

指令重排的目的是为了在不改变程序执行结果的前提下,优化程序的运行效率。需要注意的是,这里所说的不改变执行结果,指的是不改变单线程下的程序执行结果。

**重排序操作不会对存在数据依赖关系的操作进行重排序。**比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运时这两个操作不会被重排序。

**重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。**比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系, 所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。

使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,volatile禁止指令重排序也有一些规则:

当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
通俗的说就是执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见。

内存屏障

内存屏障分为两种

Load Barrier 读屏障
Store Barrier 写屏障

内存屏障的两个作用

阻止屏障两侧的指令重排序
写的时候,强制把缓冲区/高速缓存中的数据写回主内存,并让缓冲中的数据失效;读的时候直接从主内存中读取
对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据

对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见

java的内存屏障通常所谓的四种即LoadLoad,StoreStore,LoadStore,StoreLoad实际上也是上述两种的组合,完成一系列的屏障和数据同步功能

LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

volatile与内存屏障

在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;
在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;
所以volatile防止了指令的重排序(保证有序性)和内存的一致性(保证可见性)

当然是用synchronized或者Lock给代码加锁,也是可以保证有序性与可见性的

join方法

在很多情况下,我们都是通过主线程创建并启动子线程的,如果子线程中需要耗费大量的时间计算的话,主线程往往会比子线程先结束,这个时候就会导致有时候主线程想获取子线程计算之后的结果,但是却获取不到。这个时候,我们就可以通过join方法来解决这个问题。

join方法的作用:

join方法的作用是使所属的线程对象x正常执行run()方法中的任务,而使当前线程z进行无限期的阻塞,等待线程x销毁后再继续执行线程z后面的代码。方法join具有使线程排队运行的作用,有些类似同步的运行效果。

代码实例:

public class MyThread extends Thread{
    @Override
    public void run() {
        int sencondValue = (int)(Math.random()*1000);
        System.out.println(sencondValue);
        try {
            Thread.sleep(sencondValue);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

下面展示一些 内联代码片

public class Run {
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        myThread.start();
        myThread.join();
        System.out.println("当对象myThread执行完毕后再执行");
    }
}

用户态线程和内核态线程的区别

在这里插入图片描述
在这里插入图片描述
核心是由app还是操作系统管理

JVM虚拟机使用的是KLT内核线程

JAVA线程的创建依赖于系统内核,通过JVM调用系统库创建内核线程,内核线程与java线程是1对1的关系。

线程池

意义:线程的创建与销毁需要占用的资源较多,需要操作系统状态的切换
线程池就是一个线程缓存,负责对线程进行统一分配,调优与监控

优点:
在这里插入图片描述
在这里插入图片描述
线程池阻塞队列
在这里插入图片描述
在这里插入图片描述
先放入核心线程,再放到阻塞队列中,如果阻塞队列也满了,再用临时线程,最后如果都满了就拒绝
不能做到先来的线程先执行
在这里插入图片描述
在这里插入图片描述
线程池的五种状态

在这里插入图片描述

hashmap为什么是线程不安全的

hashmap在多线程插入并需要扩容的情况下会出现死循环的问题
HashMap不是线程安全的,多个线程同时使用put方法添加元素时,有可能导致Node链表形成环形数据结构,引起死循环,导致 CPU 利用率接近100%。但不需要spring框架解决,用JDK提供的ConcurrentHashMap替代HashMap就可以了。(不要使用Hashtable,因为Hashtable是用synchronized加锁的,而ConcurrentHashMap使用了分段锁技术,效率更高)HashMap的效率更高,能使用就尽量使用。多线程环境下,也可以运用栈封闭、ThreadLocal类、UnmodifiableMap类等方式使用HashMap

当多个线程进行put操作时有可能会一起调用resize函数进行扩容操作,扩容操作是transfer函数完成的,是采用头插法
transfer逻辑其实也简单,遍历旧数组,将旧数组元素通过头插法的方式,迁移到新数组的对应位置问题出就出在头插法。

头插法结束之后,链表的顺序会发送逆转,在另一个线程进行头插后,线程再次逆转,尾节点指向头节点,线程会形成一个环
而尾插法因为不会使线程逆转,所以会解决这个问题

作者:万猫学社
链接:https://www.zhihu.com/question/353373612/answer/876957281
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

ConcurrentHashMap

在这里插入图片描述

ConcurrentHashMap锁的方式是稍微细粒度的。 ConcurrentHashMap将hash表分为16个桶(默认值),诸如get,put,remove等常用操作只锁当前需要用到的桶。

试想,原来 只能一个线程进入,现在却能同时16个写线程进入(写线程才需要锁定,而读线程几乎不受限制,之后会提到),并发性的提升是显而易见的。

更令人惊讶的是ConcurrentHashMap的读取并发,因为在读取的大多数时候都没有用到锁定,所以读取操作几乎是完全的并发操作,而写操作锁定的粒度又非常细,比起之前又更加快速(这一点在桶更多时表现得更明显些)。只有在求size等操作时才需要锁定整个表。
使用了锁分离的技术
有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
put操作已开始就锁住了整个segment。这是因为修改操作时不能并发的。
用lock和unlock实现

threadlocal

threadlocal是给每个线程建立一个单独的自己的变量副本,每个线程都可以独立的去改变自己的变量副本
其中有一个threadlocalmap类,这个类是实现线程隔离机制的关键
threadlocalmap内部是由键值对key value组成的Entry数组,key是threadlocal本身的一个弱引用,value是对应的线程变量的副本
threadlocal本身是不存储值的,只是提供一个能查到这个值的key
threadlocal包含在thread中,而不是thread包含在threadlocal中
在这里插入图片描述

synchronized用法

synchronized用法
synchronized修饰的对象有几种:

修饰一个类:其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象;

修饰一个方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;

修饰一个静态的方法:其作用的范围是整个方法,作用的对象是这个类的所有对象;

修饰一个代码块:被修饰的代码块称为同步语句块,其作用范围是大括号{}括起来的代码块,作用的对象是调用这个代码块的对象;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值