Java 并发编程

目录

回顾线程 

并发编程

并发编程

Java 内存模型(JMM)

编程核心问题--可见性,原子性,有序性

可见性

有序性

原子性

valatile 关键字

CAS(Compare-And-Swap,比较并交换)

原子类

java中的锁

乐观锁/悲观锁

可重用锁(递归锁)

读写锁

分段锁

自旋锁

独占锁/共享锁

公平锁/非公平锁

偏向锁/轻量级锁/重量级锁

Synchronized锁实现

AQS(AbstractQueuedSynchronizer)抽象同步队列

AQS的锁模式:独占和共享 

ReentrantLock实现

JUC常用类

ConcurrentHashMap(并发安全的map)

CopyOnWriterArrayList

CopyOnWriterArraySet

辅助类 CountDownLatch(递减计数器)

​编辑 CyclicBarrier

线程池

线程池参数

线程池的执行

4种拒绝策略

关闭线程池

对象引用

ThreadLocal 线程变量

回顾线程 

线程与进程之间的关系

进程:是操作系统分配系统资源的基本单位。

线程:一个进程里面有多个线程,线程是进程内的执行单位,是进行系统调度的最小单位。

如何创建线程?

继承Thread类

实现Runnable接口:

实现Callable接口:可以抛出异常,需要借助FutureTask类,获取返回结果,支持泛型的返回值

常用方法

run();sleep();wait();start();stop();wait();join();notify();notifyAll();yield();

wait()和sleep()方法的区别

wait()是用于线程间通信的,而sleep()是用于短时间暂停当前线程。更加明显的一个区别在于,当一个线程调用wait()方法的时候,会释放它锁持有的对象的管程和锁,但是调用sleep()方法的时候,不会释放他所持有的管程。

线程状态

创建状态、就绪状态、运行状态、阻塞状态、销毁/死亡状态

Java 并发编程_第1张图片

线程由运行状态如何转换为阻塞状态?

IO阻塞、wait()、join()、等待同步锁、sleep();

多线程

在同一个进程中,有多个线程可以访问同一共享资源,java语言支持多线程

优点:提高程序的处理速度,提高CPU的利用率。

缺点:多个线程对同一共享资源数据进行访问

如何解决多线程访问同一共享资源?

加锁:

1. 使用synchronized关键字

静态方法:锁对象是类的Class对象

非静态方法:锁对象是this

修饰代码块:对某一段代码进行加锁控制,需要自己传入锁对象,并且要求锁对象是唯一的。

  • 隐式的自动的加锁和释放锁
  • 底层通过java虚拟机来进行控制实现

2. 使用Lock接口下的实现类

是类,只能通过修饰代码块,显式的加锁和释放锁,底层通过java代码赖进行控制实现,Lock(),unLock();只能手动的添加,手动的释放。

守护线程和用户线程

守护线程:任何一个守护线程都是整个JVM中所有非守护线程的保姆: 只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作; 只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。例如:垃圾回收器

用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。

注意:设置线程为守护线程必须在启动线程之前,否则会跑出一个 IllegalThreadStateException异常。

线程死锁

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃 自己需要的同步资源,就形成了线程的死锁。

线程间的通信

指多个线程通过相互牵制,相互调度,即线程间的相互作用。

wait()、notify()和notifyAll() 必须在同步代码块中执行

  • wait();   线程阻塞,并同时释放同步锁
  • notify(); 唤醒被wait() 的一个线程,如果有多个线程被wait(); 会唤醒一个优先级高的线程。
  • notifyAll(); 唤醒所有被wait()的线程

生产者、消费者模式(消息队列组件 MQ)

并发编程

多线程访问共享数据,会出现安全问题

并发(concurrent)与并行 (Parallel)

并行:微观上同时执行,在同一时间点上,同时做多件事情。

并发:宏观上同时执行,多件事情在同一时间段内,交替执行。

例如:大家排队在一个咖啡机上接咖啡,交替执行,是并发;两台咖啡机上面接咖啡,
是并行。

并发编程

在很多线程对共享资源进行访问,需要通过控制,让多个线程并发的对共享数据访问。

多线程执行本质问题:

由于CPU、内存、硬盘三者之间的读写速度不一样。

速度排序:CPU > 内存 > I/O 设备
  • 多核CPU,每个内核中都会有一个高速缓存,每一个高速缓存数据不可见。(可见性
  • 线程中有IO操作,耗时比较长,操作系统需要切换线程执行。(原子性
  • 操作系统对指令进行优化,对指令进行重排序。(有序性

Java 内存模型(JMM)

先复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回主内存中。但是,这样存在 内存缓存不一致性的问题 !比如我执行一个 i++ 操作的话,如果两个线程同时执行的话,假设两个线程从 CPU Cache 中读取的 i=1,两个线程做了 1++ 运算完之后再写回 Main Memory 之后 i=2,而正确结果应该是 i=3。

        Java 内存模型(Java Memory Model,JMM)规范了 Java 虚拟机与计算机内存是如何协同工作的。Java 虚拟机是一个完整的计算机的一个模型,因此这个模型自然也包含一个内存模型——又称为 Java 内存模型。
因为Java语言是跨平台的,它需要自己提供一套内存模型以屏蔽系统差异。因为不同的操作系统内存模型不同,直接使用的话会导致内存模型不可用,我们使用JMM内存模型就是这个原因,目的是为了简化多线程编程,增强程序可移植性的(从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范)。

Java 并发编程_第2张图片

编程核心问题--可见性,原子性,有序性

可见性

        一个线程对共享变量的修改,另一个变量也会立即会看到,这种情况我们称为可见性。如今的多核处理器,每个 CPU 内核都有自己的缓存,而缓存仅仅对它所在的处理器内核可见,CPU 缓存与内存的数据不容易保证一致。

有序性

        有序性指的是程序按照代码的先后顺序执行。 为了优化性能,有时候会改变程序中语句的先后顺序。 cpu 的读等待同时指令执行是 cpu 乱序执行的根源。 读指令的同时可以同时执行不影响的其他指令。

原子性

        一个或多个操作在 CPU 执行的过程中不被中断的特性,我们称为原子性。 原子性是拒绝多线程交叉操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。 CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符。线程切换导致了原子性问题。

Java 并发编程_第3张图片

如 count++,至少需要三条 CPU指令
  • 指令 1:首先,需要把变量 count 从内存加载到工作内存;
  • 指令 2:之后,在工作内存执行 +1 操作;
  • 指令 3:最后,将结果写入内存;

  两个线程 A 和 B 同时执行 count++, 即便 count 使用 volatile 修辞,我们预期的结果值是 2,但实际可能是 1。

Java 并发编程_第4张图片

  缓存(CPU中)导致的可见性问题,编译优化带来的有序化问题,线程切换带来的原子性问题。

volatile 关键字

修饰的变量  可以保证可见性(一个内存更改的数据)

                   禁止指令重排序

不能保证原子性?

解决:原子性(CAS),加锁

volatile 底层是如何实现的?

      在对volatile修饰的变量读写操作前,可以添加内存屏障指令,禁止在该条指令执行前插入其他指令,在工作内存修改后,结合缓存一致性协议,将工作内存数据更新到主内存,其他内存读取更新。

CAS(Compare-And-Swap,比较并交换)

CAS是乐观锁(没有采用加锁的方式)的一种实现方式,使用自旋锁(一遍一遍的获取)思想去比较。

内部三个值:
内存值 V:操作前先将内存值读到工作内存。

预期值 A:在工作内存修改了变量后,将要将修改后的值向主内存写入的时候,再次读取的内存数据。

更新值 B:内部操作后的变量值。当向主内存写入数据时,必须满足V==A,就将V=B,否则就再次读入内存值。

优点:没有加锁,效率高于锁

缺点:CAS是无锁的,采用自旋的方法,线程不会阻塞,如果有大量的线程进行尝试,那么CPU就消耗较大(适合低并发量较小的情况)

ABA问题

Java 并发编程_第5张图片

就是内存中某个线程将内存值由 A 改为了 B,再由 B 改为了 A。当另外一个线程使用预期值去判断时,预期值与内存值相同,当前线程的 CAS 操作无法分辨,当前 V 值是否发生过变化。

解决:加入版本号,每次修改都要更新版本号。

解决 ABA 问题的主要方式,通过使用类添加版本号,来避免 ABA 问题。

如原先的内存值为(A,1),线程将(A,1)修改为了(B,2),再由(B,2)修改为(A,3)。此时另一个线程使用预期值(A,1)与内存值(A,3)进行比较, 只需要比较版本号 1 和 3,即可发现该内存中的数据被更新过了。

原子类

原子类原理(AtomicInteger 为例)
原子类的原子性是通过 volatile + CAS 实现原子操作的。 AtomicInteger 类中的 value 是有 volatile 关键字修饰的,这就保证了 value 的内存可见性,这为后续的 CAS 实现提供了基础。
低并发情况下:使用 AtomicInteger。

java中的锁

不全是指锁,有的指的是锁的特性,有的是锁的状态,有的是锁的设计

乐观锁/悲观锁

乐观锁:采用CAS机制,乐观认为不加锁是没有问题的。(适用于读操作,例:原子类)

悲观锁:采用加锁的方式实现,悲观的认为不加锁是会有问题的。(适用于写操作)

可重用锁(递归锁)

       指当一个线程进入外层方式获取锁后,如果内存调用另一个需要获取该锁的方法时,会自动的获取该锁修饰的方法,那么线程是可以进入的。

优点:可以一定程度上避免死锁。

读写锁

里面维护两个锁的实现,一个是读锁,一个是写锁如果使用的写锁,一次只能有一个线程进入;如果使用的是读锁,可以允许多个线程同时存在;写锁的优先级高于读锁

分段锁

  不是具体的所,将锁的粒度分的更小,以提高并发效率

自旋锁

  不断尝试去尝试获得锁,不会让线程进入阻塞状态,提高效率,但是耗CPU

独占锁/共享锁

独占锁

      ReentrantLock、synchronized,读写锁中的写锁或者互斥锁,一个只允许一个线程获取锁。

共享锁

 读写锁中读锁是共享的,可以有多个线程同时获取锁

公平锁/非公平锁

公平锁

可以按照请求顺序分配锁。ReentrantLock中有公平锁实现,里面维护一个队列,按顺序排队获取锁。

非公平锁

不按照请求顺序分配锁。synchronized非公平锁和ReentrantLock默认为非公平锁(可变为公平锁)

偏向锁/轻量级锁/重量级锁

偏向锁:只有一个线程访问一段同步代码,此时会将线程的id存入到对象头中,下次该线程来获取锁的时候,直接分配即可。

轻量级锁:当锁的状态为偏向锁时,又有线程访问,那么锁状态升级为轻量级锁,不会让线程进入阻塞状态,而是自旋尝试获得锁,以提高效率。

重量级锁:当锁的状态为轻量级锁,如果线程数量的太多,线程的自旋次数达到一定数量,锁的状态变为重量级锁,线程进入阻塞状态,等待操作系统调度分配。

在synchronized中的锁有三种状态,为了优化synchronized,在jdk 6之后提出的,针对不同的状态进行不同处理。

Synchronized锁实现

synchronized是关键字,可以修饰方法、代码块、一次只允许一个线程进入

synchronized如果修饰方法

     在编译后的指令中添加ACC_SYNCHRNIZED表示次方法是同步方法,有线程进入后其他线程不能进入,在对象头中锁标志+1,方法运行结束或出现异常,锁标志 -1。

Java 并发编程_第6张图片

synchronized如果修饰代码块

    在进入代码之前加入monitorenter指令,对象锁标志+1,同步代码块运行结束或出现异常,执行monitorexit指令,锁标志-1。

Java 并发编程_第7张图片

AQS(AbstractQueuedSynchronizer)抽象同步队列

JUC java.util.concurrent  java并发包     FIFO 先进先出队列

实现原理

       AQS是JUC中实现线程安全的核心组件,是从java代码级别实现内部维护锁的状态,用volatile关键字修饰state,内部维护一个FIFO队列,保存未获取到锁的线程。多个线程来访问,如果有一个线程访问到了state,就将其改为1,其他线程获取失败后,就会添加到队列中,Node(Thread)。

        就是说,多个线程来访问,如果有一个线程访问到了volatile关键字修饰state(标志位),就将其改为1,这时,其他的线程获取失败后,就会添加到FIFO队列中,等待该线程结束后,将state改为0,然后等待下一个线程访问state队列由 Node 对象组成,Node 是 AQS 中的内部类。

Java 并发编程_第8张图片

 它还维护一些获取锁,添加线程到队列,释放锁的一些方法。

AbstractQueuedSynchronizer 成员
private transient volatile Node head ;
private transient volatile Node tail ;
使用变量 state 表示锁状态,0-锁未被使用,大于 0 锁已被使用
共享变量 state,使用 volatile 修饰保证线程可见性
private volatile int state ;

 状态信息通过 getState , setState , compareAndSetState 进行操作.

获得锁状态
protected final int getState () {
        return state ;
}
设置锁状态
protected final void setState ( int newState) {
        state = newState;
}
使用 CAS 机制设置状态
protected final boolean compareAndSetState ( int expect, int update) {
        return unsafe .compareAndSwapInt( this , stateOffset , expect, update);
}

获取锁的方式有两种 

Java 并发编程_第9张图片

AQS 操作重点方法

acquire: 表示一定能获取锁

public final void acquire ( int arg) {
        if (!tryAcquire(arg) &&
                acquireQueued(addWaiter( Node . EXCLUSIVE ), arg))
                selfInterrupt();
}

 tryAcquire:尝试获取锁,如果tryAcquire获取锁成功,那么!tryAcquire(arg)false,说明已经获取了锁,不用向后执行,直接返回。

addWaiter:尝试获取锁失败后,将当前线程封装到一个Node对象中,添加到队尾,并返回Node节点。

acquireQueued:将线程添加到队列中,以自旋的方式去获取锁。

release 释放锁

tryRelease:释放锁,将state值进行修改为0

unparkSuccessor:唤醒节点的后继者(如果存在)

AQS的锁模式:独占和共享 

独占锁:每次只能有一个线程持有锁,比如 ReentrantLock 就是以独占方式实现的互斥锁

共享锁:允许多个线程同时获取锁,并发访问共享资源,比如ReentrantReadWriteLock。

ReentrantLock实现

ReentrantLock 是向外界提供的所实现的类,内部包含三个内部类(Sync,FairSync,NonfairSync)。

Java 并发编程_第10张图片

  • Sync   extends  AQS
  • FairSync   extends  Sync   公平实现
  • NonFairSync   extends Sync   非公平实现
构造方法
无参方法
默认为非公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}
有参方法
fair == true 为公平锁,fair == false 为非公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
NonFairSync   extends Sync   非公平实现
static final class NonfairSync extends Sync {
加锁
        final void lock () {
        //若通过 CAS 设置变量 state 成功,就是获取锁成功,则将当前线程设置为独占线程。
        //若通过 CAS 设置变量 state 失败,就是获取锁失败,则进入 acquire 方法进行后续处理。
                if (compareAndSetState( 0 , 1 )) // 没有排队,直接尝试去获取锁
                        setExclusiveOwnerThread( Thread . currentThread ());
                else
                        acquire( 1 );// 获取锁,表示一定能获取锁,获取不到就继续
        }
         //尝试获取锁,无论是否获得都立即返回
        protected final boolean tryAcquire ( int acquires) {
                return nonfairTryAcquire(acquires);
        }
}

Lock():尝试获取锁,肯定是能获取到锁的,直到有线程获取到锁,lock()方法就运行结束了。

unlock():释放锁

JUC常用类

JUC java.util.concurrent  java并发包 

ConcurrentHashMap(并发安全的map)

HashMap:线程不安全的,使用于单线程情况,键值可为空。

HashTable:是线程安全的,支持多线程并发访问(),但是锁是加在put()方法上的,效率低,一次只能有一个线程进入到put()方法中进行操作,键值都不能为空。

ConcurrentHashMap

       多线程并发访问安全的,将锁不在put()方法上添加,而是将每个位置看作是独立的空间,添加元素,通过Hash值计算它的位置,如果位置上还没有任何元素,采用CAS机制判断,添加元素到第一个位置,如果有元素,则使用头结点作为锁标记对象

优点:实现了锁粒度变小,提高了并发效率

为什么HashMap可以有null值,HashTable不能有空值?

 Hashtable:

Java 并发编程_第11张图片

Hashtable不支持存储 key == null 或 value == null的值,则抛出空指针异常。

 HashMap:


Java 并发编程_第12张图片

 当key == null ,存储在哈希值为0的位置;

ConcurrentHashMap 不支持存储 null 键和 null 值

Java 并发编程_第13张图片

从底层源码我们可以看出,当键或值为null值的时候,我们会抛出空指针异常。 

原因:ConcurrentHashMap用于多线程,为了消除歧义,无法分辨key是没有找到的null还是key值为null,当get(key)获取对应的value时,如果获取到的是null时你无法判断它是put(K,V)的时候value是空的,还是这个key没有对应的值(无映射)。

CopyOnWriterArrayList

Vector

 Java 并发编程_第14张图片

Java 并发编程_第15张图片

     由源码我们可以看出来,Vector线程安全的 但是对读(get())和写(add())操作也加了锁, 读和写用的是同一把锁。

       添加、查询的方法都添加了同步锁,在写操作时,其他线程是不能读的,效率很低。操作时经常读操作是比较多的,但是读不会改变数据。一次只允许一个线程读数据

CopyOnWriterArrayList

Java 并发编程_第16张图片

Java 并发编程_第17张图片

       CopyOnWriterArrayList读(get())和写(add())进行分离,读操作完全不用加锁,读不影响数据,写操作加锁,写操作时不影响读操作,只有两个线程同时添加时会互斥。

给add(),set()会影响数据的操作方法添加锁,保证修改数据是安全的,写入数据时,会先创建新的数组,将新添加的元素写入到新的数组中,写完之后在将新数组赋给底层原来数组的引用。而读没有做任何的控制。

CopyOnWriterArraySet

CopyOnWriteArraySet 的实现基于 CopyOnWriteArrayList,不能存储重复数据。添加元素时会判
断是否有重复元素。

辅助类 CountDownLatch(递减计数器

        使一个线程,等待其他线程执行结束后再执行,相当于一个线程计数器,是一个递减的计数器。先指定一个数量,当有一个线程执行结束后就减一 直到为0,关闭计数器,这样线程就可以执行了。


public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {

        CountDownLatch downLatch = new CountDownLatch(6);//计数
        for (int i = 0; i <6 ; i++) {
            new Thread(
                ()->{
                    System.out.println(Thread.currentThread().getName());
                    downLatch.countDown();//计数器减一操作
                }
            ).start();
        }
        downLatch.await();//关闭计数
        System.out.println("main线程执行");
    }
}

执行结果:

Java 并发编程_第18张图片 CyclicBarrier

     让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,是一个加法计数器,当线程数量到达指定数量时,开门放行。

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier c = new CyclicBarrier(5, ()->{
            System.out.println("大家都到齐了 该我执行了");
        });

        for (int i = 0; i < 5; i++) {
            new Thread(
                    ()->{
                        System.out.println(Thread.currentThread().getName());
                        try {
                            c.await();//加一计数器
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } catch (BrokenBarrierException e) {
                            e.printStackTrace();
                        }
                    }
            ).start();
        }
    }
}

执行结果:

Java 并发编程_第19张图片

线程池

为什么要用池?

    每次连接数据库创建连接对象,用完销毁,比较费时,增大了时空开销。而使用池,可以事先创建出一些连接对象放入池中,每次使用都从池子获取,用完还回到池子里面。

线程池:jdk5之后提供java内置版本,提供ThreadPoolExecutor类实现创建线程(推荐使用)

优点:

  • 可以重复使用线程,降低线程创建和销毁的资源消耗,即降低时空开销
  • 统一管理,线程的创建和销毁都由线程池统一管理
  • 提高响应速度,线程已存在,直接调用空闲的线程

线程池参数

线程中也有一些参数对线程池进行设置

  • coreOPoolSize:核心池的大小 。默认为0,当有任务到来后,创建线程去执行,执行完后,线程不销毁,直到创建出与核心池子大小相等的数量线程,当到达corePoolSize大小时,会将其放入缓存队列,除非调用了prestartAllCoreThreads() 或prestartCoreThread();
  • maximumPoolSize:线程池最大的线程数量。 它表示线程池最多能创建多少个线程
  • keepAliveTime:非核心线程中,表示线程没有任务执行时最多保持多久时间会终止
  • unit:参数keepAliveTime的时间单位,在TimeUnit类中一共有7个。Java 并发编程_第20张图片
  • workQueue:一个阻塞队列,用来存储等待队列的任务,当核心线程池到达corePoolSize时,用来存储等待执行的任务。
  • threadFactory:线程工厂,主要用来创建线程;
  • handler:表示当拒绝处理任务时的策略

线程池的执行

Java 并发编程_第21张图片

线程队列:

  • SynchronousQueue:同步队列是一个容量只有 1 的队列,这个队列比较特殊, 它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务,每个 put 必须等待一个 take.
  • ArrayBlockingQueue:有界队列,是一个用数组实现的有界阻塞队列,按 FIFO 排序量。
  • LinkedBlockingQueue:可设置容量队列,基于链表结构的阻塞队列,按 FIFO 排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度Integer.MAX_VALUE;

4种拒绝策略

  • AbortPolicy:报错(会抛出异常,阻止程序运行)
  • CallerRunsPolicy:只要线程池未关闭,由当前调用的线程执行(例如:任务是在main线程中,就由main线程执行)
  • DiscardOleddestPolicy丢弃等待时间最长的(等待队列)
  • DiscardPolicy线程池满后,直接丢弃。

execute 和 submit 区别

相同点:都用来执行任务

不同点:excecute是没有返回值的,而submit是有返回值的

关闭线程池

关闭线程池可以调用 shutdownNow 和 shutdown 两个方法来实现。
  • shutdownNow:对正在执行的任务全部发出 interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表。
  • shutdown:当我们调用 shutdown 后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务。

对象引用

概述

除了垃圾回收标记垃圾对象之外,还需要对垃圾对象进行撞他分类管理。

强引用:有引用指向的对象,不是垃圾对象。

例:Object object = new Object();

软引用(内存不足即回收):使用SoftReference来管理对象。已经是垃圾了,如果内存够用,不回收,否则,将软引用管理的对象进行回收。

弱引用(发现即回收):使用WeakReference来管理对象,只能存活到下一次垃圾回收

虚引用(对象回收跟踪机制):使用PhantomReference来管理对象,需要提供一个队列维护,随时可以被回收,主要就是记录跟踪对象是否被回收。

ThreadLocal 线程变量

创建一个ThreadLocal对象,复制用来为每个线程会存一份变量,实现线程封闭

底层实现原理

Java 并发编程_第22张图片

ThreadLocal内部维护了一个Map,ThreadLocal实现了一个叫做 ThreadLocalMap 的静态内部类。

      当有一个线程进入,在每个线程中,都会创建一个ThreadLocalMap对象,将ThreadLocalMap放入线程中,第一次创建的时候,在set()方法中,会将当前线程ThreadLocal作为键(this),将value做为值,存入到ThreadLocalMap中。

Java 并发编程_第23张图片

 在调用get方法时,会调用当前线程对象的来在ThreadLocalMap里面进行查找对应的值。Java 并发编程_第24张图片

有什么问题?
    存在内存泄露的问题

如何解决内存泄露问题?

Java 并发编程_第25张图片

原因:ThreadLocal作为key,被弱引用(WeakReference)进行管理(存活到下一次垃圾回收),而值value是强引用的,与ThreadLocalMap和Thread强关联。key回收而value去还存在,造成大量无用的对象存在,但却又回收不掉,造成内存泄露。

解决:用完变量之后,调用THreadLocal中的remove()删除键值对象(用完即删)。

Java 并发编程_第26张图片

多线程最主要的额外开销:锁竞争

在Java中多线程额外开销最主要的原因就是锁竞争,因为获取锁会导致串行操作同时获取锁失败会导致线程挂起出现上下文竞争。所以减少锁竞争是降低多线程开销最主要的手段

而在并发程序中,对可伸缩性的最主要威胁就是独占方式的资源锁。而影响锁竞争的因素:锁请求频率,每次持有锁的时间,如果两个的乘积很小,说明获取锁和持有锁的总时间就很少,那么大多数获取锁的操作都不会发生竞争。

所以减少锁竞争的主要方法是:减少锁的只有时间降低锁的请求频次、或者使用带有协调机制的独占锁替代,因为这些机制允许更高的并发性。

减少锁竞争手段

第一个是缩小锁的范围:将与锁无关代码移除同步代码块,尤其是那些可能发生阻塞的操作比如I/O;

第二个是减少锁的粒度:使用多个相互独立锁管理独立的状态变量,改变某个变量只用获取对应变量锁,而不用获取整体锁,其他线程仍然能使用其他变量。但是使用锁越多,那么发生死锁的风险也就越高。

第三个是锁分段:比如ConcurrentHashMap底层的链表数组,对数组中每一个数组元素进行加锁,数组长度是多少就有多少个锁,也就最大支持多少并发。不过在对数组扩张的时候就会更加复杂;

你可能感兴趣的:(java,开发语言)