我是靠谱客的博主 无心路人,最近开发中收集的这篇文章主要介绍Multi-thread,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

Thread

Java线程一共有七个状态,分别是新建,可运行,运行中,睡眠,阻塞,等待,死亡。

线程间通信:
wait()/notify():必须在synchronized同步块中使用,wait()是在线程获得了对象的锁后主动释放锁同时线程进入wait状态,其他线程获得了释放的对象锁后,可调notify()唤醒wait的线程,notify()调用后并不立即生效,在同步块执行完后才生效。若有多个线程wait,notify只会唤醒一个线程,由jvm决定唤醒哪个。被notify唤醒的线程重新进入blocked状态,恢复对对象锁的竞争。使用notify可能造成死锁,建议使用notifyAll。没有该对象monitor的线程调用了该对象的notify()或者notifyAll()方法将会抛出java.lang.IllegalMonitorStateException。
sleep():sleep是Thread类的静态方法,只对当前线程生效,不释放锁。

 

ThreadPool

Executors是java线程池的工厂类,通过不同的参数初始化一个ThreadPoolExecutor对象。
Executors.newSingleThreadExecutor():
    new FinalizableDelegatedExecutorService(
        new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())
    );
    队列大小为默认的Integer.MAX_VALUE,可以无限的往里面添加任务,直到内存溢出
Executors.newFixedThreadPool(10):
    new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
    队列大小为默认的Integer.MAX_VALUE,可以无限的往里面添加任务,直到内存溢出
Executors.newCachedThreadPool():
    new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
    初始无线程,线程池的最大值了Integer.MAX_VALUE,线程存活60秒,会导致无限创建线程,导致性能问题和内存溢出
Executors.newScheduledThreadPool(10):
    new ThreadPoolExecutor(10, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());
    

corePoolSize:核心线程数,线程池满后继续提交的任务被保存到阻塞队列中
maximumPoolSize:线程池中允许的最大线程数,如果阻塞队列满了且继续提交任务,则创建新的线程执行任务
keepAliveTime:除核心线程外的线程存活的时间,默认0毫秒
unit:keepAliveTime的单位
workQueue:等待被执行的任务的阻塞队列
    ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务
    LinkedBlockingQueue:默认,基于链表结构的无界阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQueue
    SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue
    PriorityBlockingQueue:基于数组(默认长度11)的具有优先级的无界阻塞队列,优先级最高的先取出,元素都要实现Comparable接口
    DelayedWorkQueue:delayed 元素的一个无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部 是延迟期满后保存时间最长的 Delayed 元素。如果延迟都还没有期满,则队列没有头部,并且 poll 将返回 null。当一个元素的getDelay(TimeUnit.NANOSECONDS) 方法返回一个小于或等于零的值时,则出现期满。元素都要实现Comparable接口
handler:线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
    AbortPolicy:默认,直接抛出异常RejectedExecutionException
    CallerRunsPolicy:用调用者所在的线程来执行任务
    DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,当前任务进入队列
    DiscardPolicy:直接丢弃任务
    * 上述饱和策略类为ThreadPoolExecutor的静态内部类,实现RejectedExecutionHandler接口,实际使用时建议自定义饱和策略类

执行流程:
1. 如果线程池中线程数量 < core,新建一个线程执行任务 
2. 如果线程池中线程数量 >= core ,则将任务放入任务队列 
3. 如果线程池中线程数量 >= core 且 < maxPoolSize 且任务队列已满,则创建新的线程;队列如果是无界队列,那么设置线程池最大数量是无效的

 

volatile

有volatile变量修饰的共享变量进行写操作的时候,会多出一行以Lock为前缀的汇编代码,这个前缀指令会在多核处理器下引发两件事情:
1.将当前处理器缓存行的数据写回到系统内存。
2.这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

CPU为了提高处理性能,并不直接和内存进行通信,而是将内存的数据读取到内部缓存(L1,L2)再进行操作,但操作完并不能确定何时写回到内存,如果对volatile变量进行写操作,当CPU执行到Lock前缀指令时,会将这个变量所在缓存行的数据写回到内存,不过还是存在一个问题,就算内存的数据是最新的,其它CPU缓存的还是旧值,所以为了保证各个CPU的缓存一致性,每个CPU通过嗅探在总线上传播的数据来检查自己缓存的数据有效性,当发现自己缓存行对应的内存地址的数据被修改,就会将该缓存行设置成无效状态,当CPU读取该变量时,发现所在的缓存行被设置为无效,就会重新从内存中读取数据到缓存中。

JAVA内存分为主存和工作内存
    主存:存储类成员变量等
    工作内存:即java线程的本地内存,是单独给某个线程分配的,存储局部变量等,同时也会复制主存的共享变量作为本地的副本,目的是为了减少和主存通信的频率,提高效率。工作内存是cpu的寄存器和高速缓存的抽象描述:现在的计算机,cpu在计算的时候,并不总是从内存读取数据,它的数据读取顺序优先级 是:寄存器-高速缓存-内存。线程耗费的是CPU,线程计算的时候,原始的数据来自内存,在计算过程中,有些数据可能被频繁读取,这些数据被存储在寄存器和高速缓存中,当线程计算完后,这些缓存的数据在适当的时候应该写回内存。
局部变量不存在可见性问题,而共享内存就会有可见性问题,因为本地线程在创建的时候,会从主存中读取一个共享变量的副本,且修改也是修改副本,副本的修改不会立即刷新到主存中去,那么其他线程就不能马上知道变量的修改。因此,线程B修改共享变量后,线程A并不会马上知晓。


原子性:volatile不能保证原子性,不能用在getAndOperate场景,只能用在get或set场景
    1. 对一个volatile变量的写操作,只有所有步骤完成,才能被其它线程读取到。
    2. 多个线程对volatile变量的写操作本质上是有先后顺序的。也就是说并发写没有问题。
实例:
User user = new User()的语义:
1. 分配对象的内存空间
2. 初始化对象
3. 设置user指向刚分配的内存地址
步骤2和步骤3可能会被重排序,流程变为1->3->2
线程1在执行完第3步而还没来得及执行完第2步的时候,如果内存刷新到了主存,那么线程2将得到一个未初始化完成的对象。因此如果将user声明为volatile的,那么步骤2,3将不会被重排序。用volatile修饰的对象的写操作会变成一个原子操作,没有初始化完,不会刷新到主存中。
不安全实例:
i++不是原子操作,要分成3步:
1. 读取volatile变量值到local
2. 变量的值加1
3. 把local的值写回主存
volatile变量在执行完3步后才会把值写回主存,如果在上述步骤中别的线程修改了值,这些值会丢失。

 

synchronized

Synchronized经过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。

Java对象头和monitor是实现synchronized的基础。
对象头:主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。其中Klass Pointer是指向类的Class对象的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。
monitor:监视器,在JVM里,monitor就是实现lock的方式。entermonitor就是获得某个对象的lock(owner是当前线程),leavemonitor就是释放某个对象的lock。同步代码编译后,monitorenter指令插入到同步代码块的开始位置,monitorexit插入到方法结束处和异常处。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

synchronized块生成JVM指令是monitorenter, monitorexit,最后生成的汇编指令是
lock cmpxchg %r15, 0x16(%r10)  和 lock cmpxchg %r10, (%r11)
cmpxchg是CAS的汇编指令,lock cmpxchg指令前者保证了可见性和防止重排序,后者保证了操作的原子性。这里的含义是先用lock指令对总线和缓存上锁,然后用cmpxchg CAS操作设置对象头中的synchronized标志位。CAS完成后释放锁,把缓存刷新到主内存。

synchronized用的锁是存在Java对象头里的。JDK6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在JDK6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
偏向锁:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
轻量级锁:当有竞争且竞争不强烈时,JVM就会由偏向锁膨胀为轻量级锁,考虑到线程的阻塞和唤醒需要CPU从用户态转为核心态,而这种转换对CPU来说是一件负担很重的操作,因此没有获取到锁的线程不会进入阻塞状态,而是通过自旋的方式一直尝试获取锁,处于一种忙等状态,所以这种处理竞争的方式比较浪费CPU,但是响应速度很快。(JDK1.6以后默认开启了自旋锁) 自旋的次数默认是10次。JDK1.6中引入了自适应的自旋锁,自适应意味着自旋的时间不再是固定的,而是由前一次在同一个锁上的自旋时间以及锁拥有者的状态来决定。
重量级锁:当竞争竞争激烈时,线程直接进入阻塞状态。不过在高版本的JVM中不会立刻进入阻塞状态而是会自旋一小会儿看是否能获取锁如果不能则进入阻塞状态。

 

Atomic类

Atomic类基于无锁策略实现。无锁是一种乐观锁,假设对共享资源的访问没有冲突,线程可以不停执行,无需加锁,无需等待,一旦发现冲突,无锁策略采用CAS技术来保证线程执行的安全性。

CAS compare and swap 比较交换
执行函数 CAS(V,E,N)
V表示要更新的变量,E表示预期值,N表示新值
我们提供一个期望值,当期望值与当前线程的变量值相同时,说明还没线程修改该值,当前线程可以进行修改,也就是执行CAS操作,但如果期望值与当前线程不符,则说明该值已被其他线程修改,此时不执行更新操作,但可以选择重新读取该变量再尝试再次修改该变量,也可以放弃操作。

 

Reentrantlock

所谓重入锁,指的是一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。实现原理实现是通过为每个锁关联一个请求计数和一个占有它的线程。当计数为0时,认为锁是未被占有的。线程请求一个未被占有的锁时,jvm讲记录锁的占有者,并且讲请求计数器置为1。如果同一个线程再次请求这个锁,计数将递增;每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。可重入锁的意义在于防止死锁,synchronized和ReentrantLock都是可重入锁。

ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。

1. ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
2. ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
3. ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制,这相当于Synchronized来说可以避免出现死锁的情况。

 

悲观锁和乐观锁

悲观锁:先拿锁再操作;适用于经常冲突(多写少读)的情形。
乐观锁:先操作,更新数据的最后一步再拿锁;在数据上增加一个版本号或时间戳,操作进行获取需要锁的数据的版本号,实际更新数据时再次对比版本号确认与之前获取的相同,若不同需要回滚操作;适用于多读少写的情形。

synchronized:是一种悲观锁,默认每次都需要竞争锁。
CAS:compare and swap,操作的是乐观锁,假设不会冲突操作对象,发现冲突后重试。在java中,CAS通过Unsafe类的native方法实现。

 

转载于:https://my.oschina.net/u/3139896/blog/1648831

最后

以上就是无心路人为你收集整理的Multi-thread的全部内容,希望文章能够帮你解决Multi-thread所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(42)

评论列表共有 0 条评论

立即
投稿
返回
顶部