Java多线程

11 篇文章 0 订阅

实现线程的方法

准确地讲,创建线程只有一种方法那就是构造Thread类,而实现线程的执行单元有两种方式

1,继承Thread类:

run()整个都被重写

2,实现Runnable接口:

实现Runnable接口的run()方法,并把Runnable实例传给Thread类

最终调用的是target.run();

比较:

线程具体执行的任务(run()方法)应与线程的创建、运行机制(Thread类)解耦

继承Thread类,每次想新建任务,只能新建独立的线程,损耗大

Java不支持双继承

线程启动

start():

1,启动新线程检查线程状态

2,加入线程组

3,调用start0()

run():

普通方法

线程停止

错误:

stop():会导致线程运行一半突然停止,没办法完成一个基本单位的操作,会造成脏数据

suspend(),resume():线程挂起后在恢复之前不会释放锁

用volatile设置boolean标记位:陷入阻塞时,volatile是无法停止线程的(解决:使用interrupt()方法)

正确:

interrupt():

        在业务逻辑中加上判断线程是否被中断的方法

        interrupt()遇到sleep():会中断并抛出异常(sleep()函数响应中断,会清除中断(清除interrupt标记位)

判断是否被中断的相关方法:

Thread.interrupted():返回是否被中断,并且清除中断(中断状态设为false)。目标对象为当前运行的线程,执行它的线程,而不管这个方法是哪个实例调用的

Thread.isInterrupted():仅返回是否被中断

响应中断的方法:

Object.wait() / wait(long) / wait(long,int)

Thread.sleep(long) / sleep(long,int)

Thread.join() / join(long) / join(long,int)

java.util.concurrent.BlockingQueue.take() / put(E)

java.util.concurrent.locks.Lock.lockInterruptibly()

java.util.concurrent.CountDownLatch.await()

java.util.concurrent.CyclicBarrier.await()

java.util.concurrent.Exchanger.exchange(V)

java.nio.channels.InterruptibleChannel的相关方法

java.nio.channels.Selector的相关方法

线程的六种状态

初始态(NEW):

创建一个Thread对象,但还未调用start()启动线程时,线程处于初始态。

运行态(RUNNABLE):(包括 就绪态 和 运行态)

就绪态:

该状态下的线程已经获得执行所需的所有资源,只要CPU分配执行权就能运行。

所有就绪态的线程存放在就绪队列中。

运行态:

获得CPU执行权,正在执行的线程。

由于一个CPU同一时刻只能执行一条线程,因此每个CPU每个时刻只有一条运行态的线程。

阻塞态(BLOCKED):

当一条正在执行的线程请求某一资源失败时,就会进入阻塞态。

而在Java中,阻塞态专指请求锁失败时进入的状态。

由一个阻塞队列存放所有阻塞态的线程。

处于阻塞态的线程会不断请求资源,一旦请求成功,就会进入就绪队列,等待执行。

等待态(WAITING):

当前线程中调用wait、join、park函数时,当前线程就会进入等待态。

也有一个等待队列存放所有等待态的线程。

线程处于等待态表示它需要等待其他线程的指示才能继续运行。

进入等待态的线程会释放CPU执行权,并释放资源(如:锁)

超时等待态(TIMED WAITING):

当运行中的线程调用sleep(time)、wait、join、parkNanos、parkUntil时,就会进入该状态;

它和等待态一样,并不是因为请求不到资源,而是主动进入,并且进入后需要其他线程唤醒;

进入该状态后释放CPU执行权 和 占有的资源。

与等待态的区别:到了超时时间后自动进入阻塞队列,开始竞争锁。

终止态(TERMINATED):

线程执行结束后的状态。

Thread类和Object类中的重要方法

Thread:

sleep():进入超时等待态,不释放锁(包括Synchronized和Lock),可以响应中断(TimeUnit.HOURS.sleep(); TimeUnit.MINUTES.sleep(); TimeUnit.SECONDS.sleep();)

join():新的线程加入到我们,我们要进入等待态,等他执行完再出发(自动notifyAll)

yield():释放CPU时间片,进入运行态中的就绪态,不释放锁

currentThread():返回当前所执行的线程的引用

start():启动新线程,进入初始态

run():每个Thread类run()运行结束后,会自动执行类似notify()操作

interrupt():

Object:

wait():进入等待状态并释放锁,可以响应中断,必须在同步方法中

notify():唤醒由wait()方法进入等待状态的线程中的其中一个

notifyAll():唤醒所有由wait()方法进入等待状态的线程

wait()、notify()、notifyAll()被定义在Object类里面:

JAVA提供的锁是对象级的而不是线程级的,每个对象都有个锁,而线程是可以获得这个对象的。因此线程需要等待某些锁,那么只要调用对象中的wait()方法便可以了。而wait()方法如果定义在Thread类中的话,那么线程正在等待的是哪个锁就不明确了。这也就是说wait(),notify()和notifyAll()都是锁级别的操作

线程属性

编号(ID):标识不同线程,不允许修改

名称(Name):让程序员在开发、调试或运行中,更容易区分不同的线程、定位问题等

是否是守护线程(isDaemon):给用户线程提供服务,不影响JVM的停止

优先级(Priority):共10个等级,默认值是5,程序设计不应依赖于优先级

线程安全

线程不安全的情况:

1,运行结果错误

2,死锁等活跃性问题(死锁,活锁,饥饿)

3,对象发布和初始化的时候的安全问题

各种需要考虑线程安全的情况:

1,访问共享变量或资源,会有并发风险,比如对象的属性、静态变量、共享缓存、数据库等

2,所有依赖时序的操作,即使每一步操作都是线程安全的,还是存在并发问题:read-modify-write、check-then-act

3,不同的数据之间存在捆绑关系的时候

4,我们使用其它类的时候,如果对方没有声明自己是线程安全的

线程池

线程池的好处:

加快响应速度

合理利用CPU和内存

统一管理

线程池适合应用的场合

服务器接受到大量请求时,使用线城市技术是非常合适的,它可以大大减少线程的创建和销毁次数,提高服务器的工作效率

实际开发中,如果需要创建5个以上的线程,那么就可以使用线程池来管理

线程池的创建和停止

创建线程池的7个参数:

corePoolSize:核心线程数,线程池在完成初始化后,默认情况下,线程池中并没有任何线程,线程池会等待有任务来时,在创建新线程去执行任务

maximumPoolSize:最大线程数,线程池会在核心线程数的基础上,额外增加一些线程

keepAliveTime:空闲线程存活时间,如果线程池当前的线程数多于corePoolSize,那么如果多余的线程空闲时间超过keepAliveTime,它们就会被终止

unit:空闲线程存活时间单位,keepAliveTime的计量单位

workQueue:工作队列,有3种常见的队列类型:

ArrayBlockingQueue:有界阻塞队列

LinkedBlockingQuene:无界阻塞队列

SynchronousQuene:直接阻塞队列

DelayedWorkQueue:延时队列

PriorityBlockingQueue:优先级无界阻塞队列

threadFactory:线程工厂,新线程是由ThreadFactory创建的,创建出来的线程都在同一个线程组,拥有同样的优先级并且都不是守护线程,可自己指定ThreadFactory,改变线程名,线程组,优先级,是否是守护线程等

Handler:拒绝策略

添加线程的规则:

1,如果一个线程小于corePoolSize,即使其他工作线程处于空闲状态,也会创建一个新线程来运行新任务

2,如果线程数大于等于corePoolSize但少于maximumPoolSize,则将任务放入队列

3,如果队列已满,并且线程数小于maxPoolSize,则创建一个新线程来运行任务

4,如果队列已满,并且线程数大于等于maxPoolSize,则拒绝该任务

停止线程:

1,shutdown:拒绝新加的任务,将正在执行的任务和队列中等待的任务全部执行完之后,再关闭

2,isShutdown:返回线程池的状态是否是shutdown

3,isTerminated:返回整个线程池是否完全终止

4,awaitTermination:等待一段时间后检测线程是否完全终止,线程完全终止返回true,否则返回false

5,shutdownNow:立即终止线程池(使用interrupt()中断正在执行的线程,返回阻塞队列里的线程)

4种拒绝策略:

AbortPolicy:直接抛出异常并将任务丢弃

DiscardPolicy:将新任务丢弃

DiscardOldestPolicy:丢弃队列中存在时间最久的任务

CallerEunsPolicy:让提交任务的线程去运行该任务

钩子方法:

在每个任务执行前后做日志,统计时使用

常见的线程池

FixedThreadPool:

最大线程数=核心线程数=传入的参数;工作队列:LinkedBlockingQuene

特点:执行线程数不会超出范围

缺点:由于传进去的LinkedBlockingQuene没有容量上限,请求过多且无法及时处理时,会造成请求堆积,占用大量内存,可能导致OOM

SingleThreadExecutor:

最大线程数=核心线程数=1;工作队列:LinkedBlockingQuene

特点:执行线程数不会超出1

缺点:由于传进去的LinkedBlockingQuene没有容量上限,请求过多且无法及时处理时,会造成请求堆积,占用大量内存,可能导致OOM

CachedThreadPool:

核心线程数=0;最大线程数=Intege.MAX_VALUE;工作队列:SynchronousQuene

特点:可缓存,空闲线程可回收

缺点:最大线程数为Intege.MAX_VALUE,可能会创建非常多的线程,造成CPU100%

ScheduleThreadPool:

核心线程数=传入参数;最大线程数=Intege.MAX_VALUE;工作队列:DelayedWorkQueue

特点:支持定时及周期性任务执行的线程池

线程池的实现原理

线程池的组成部分:

线程池管理器:创建、停止线程池,主要用来管理线程池

工作线程:执行任务的线程

任务队列:存放未处理任务的队列(支持并发)

任务接口(Task):任务

线程池提交方法的区别:

execute()方法:无返回值

submit()方法:有返回值,方法里边调用executor()方法

Executor框架

Executor接口:顶层接口,只有一个用来执行任务的方法

ExecutorService接口:继承Executor接口,有管理线程池的方法

AbstractExecutorService类:抽象类,实现ExecutorService接口

ThreadPoolExecutor类:继承AbstractExecutorService抽象类

Executors类:工具类,用来快速创建线程池

线程池的状态:

RUNNING:接受新任务并处理排队任务

SHUTDOWN:不接受新任务但处理排队任务

STOP:不接受新任务,也不处理排队任务,并中断正在进行的任务

TIDYING:所有任务都已终止(workerCount为零时,线程会转到TIDYING状态并运行terminate()钩子方法)

TERMINATED:运行完成,线程池终止

ThreadLocal

使用场景:

场景1:每个线程需要一个独享的对象(通常时工具类,典型需要使用的类有SimpleDateFormat和Random)(initialValue:在ThreadLocal第一次get的时候把对象给初始化出来,对象的初始化时机可以由我们控制)

场景2:每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦(如果需要保存到ThreadLocal里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,用ThreadLocal.set直接放到我们的ThreadLocal中去,以便后续使用)

作用:

1,让两个需要用到的对象在线程间隔离(每个线程都有自己独立的对象)

2,在本线程任何方法中都可以轻松获取到该对象

好处:

达到线程安全

不需要加锁,提高执行效率

更高效地利用内存,节省开销

免去传参地繁琐

Thread、ThreadLocal、ThreadLocalMap的关系:

转载自:https://www.jianshu.com/p/80866ca6c424

ThreadLocal中的嵌套内部类ThreadLocalMap,这个类本质上是一个map,和HashMap之类的实现相似,依然是key-value的形式,其中有一个内部类Entry,其中key可以看做是ThreadLocal实例,但是其本质是持有ThreadLocal实例的弱引用

ThreadLocalMap的引用在Thread类中,每个线程在向ThreadLocal里塞值的时候,其实都是向自己所持有的ThreadLocalMap里塞入数据;读的时候同理,首先从自己线程中取出自己持有的ThreadLocalMap,然后再根据ThreadLocal引用作为key取出value

主要方法:

T initialValue():初始化

1,该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get的时候才会触发

2,当线程第一次使用get方法访问变量时,将调用此方法,除非线程先前调用了set方法,在这种情况下,不会为线程调用本initialValue方法

3,通常,每个线程最多调用一次此方法,但如果已经调用了remove()方法后,再调用get(),则可以再次调用此方法

4,如果不重写本方法,这个方法会返回null,一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象

void set(T t):

为这个线程设置一个新值

T get():

get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于ThreadLocal的value(这个map以及map中的key和value都是保存在线程中的,而不是保存在ThreadLocal中

void remove():

删除对应这个线程的值

内存泄漏:

内存泄漏:某个对象不再有用,但是占用的内存却不能回收

ThreadLocalMap中的Entry继承自WeakReference,是弱引用,ThreadLocalMap的每个Entry都是对key的弱引用,同时,每个Entry都包含一个对value的强引用,弱引用的特点是,如果这个对象只被弱引用关联(没有任何强引用关联),那么这个对象就可以被回收,因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,就可能出现OOM

正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了,但是,如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收。

JDK解决:在set,remove,rehash方法中会扫描key为null的Entry,并把对应的value设置为null,这样value对象就可以被回收,但是如果一个Thread Local不被使用,那么实际上set,remove,rehash方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致value的内存泄露

Synchronized

对象锁

方法锁:默认锁对象为this

同步代码块锁:自己指定锁对象

类锁

synchronized修饰静态的方法、指定锁对象为Class对象

类锁只能在同一时刻被一个对象拥有

性质

可重入:同一线程的外层函数获得锁之后,内层函数可以直接获得锁,好处:避免死锁、提升封装性

不可中断:一旦这个锁已经被别人获得了,如果我还想获得,只能选择等待或者阻塞,知道别的线程释放这个锁,如果别人永远不释放锁,那么我将只能永远地等待下去

缺陷

效率低:锁的释放情况少、试图获得锁时不能设定超时、不能中断一个正在试图获得锁的过程

不够灵活(读写锁更灵活):加锁和释放的实际单一,每个锁仅有单一的条件(某个对象)

无法知道是否成功获得锁

synchronized的实现原理

转载自:https://blog.csdn.net/jinjiniao1/article/details/91546512

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

(2)同步方法:方法级的同步是隐式的,无须通过字节码指令来控制,JVM可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。当方法调用的时,调用指令会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先持有monitor对象,然后才能执行方法,最后当方法执行完(无论是正常完成还是非正常完成)时释放monitor对象。在方法执行期间,执行线程持有了管程,其他线程都无法再次获取同一个管程。

synchronized的执行过程

转载自:https://blog.csdn.net/zqz_zqz/article/details/70233767

1. 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁

2. 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1

3. 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。

4. 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁

5. 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

6. 如果自旋成功则依然处于轻量级状态。

7. 如果自旋失败,则升级为重量级锁。

上面几种锁都是JVM自己内部实现,当我们执行synchronized同步块的时候jvm会根据启用的锁和当前线程的争用情况,决定如何执行同步操作;

在所有的锁都启用的情况下线程进入临界区时会先去获取偏向锁,如果已经存在偏向锁了,则会尝试获取轻量级锁,启用自旋锁,如果自旋也没有获取到锁,则使用重量级锁,没有获取到锁的线程阻塞挂起,直到持有锁的线程执行完同步块唤醒他们;

偏向锁是在无锁争用的情况下使用的,也就是同步开在当前线程没有执行完之前,没有其它线程会执行该同步块,一旦有了第二个线程的争用,偏向锁就会升级为轻量级锁,如果轻量级锁自旋到达阈值后,没有获取到锁,就会升级为重量级锁;

如果线程争用激烈,那么应该禁用偏向锁。

Lock

常用方法:

lock():最普通的获取锁,在异常时不会自动释放锁

tryLock():尝试获取锁,如果当前锁没有被其他线程所占用,则获取成功,返回true,否则获取失败,返回false,该方法会立即返回,不会等待

tryLock(long time,TimeUnit unit):在规定时间内尝试获取锁,获取成功,返回true,超时就放弃,返回false

lockInterruptibly():相当于tryLock(long time,TimeUnit unit)把超时时间设置为无限,在等待锁的过程中,线程可以被中断

Lock不会像synchronized那样在异常时自动释放锁,最佳是在finally中释放锁,以保证发生异常时锁一定被释放

死锁:

死锁的4个必要条件:

互斥条件,请求与保持条件,不可剥夺条件,循环等待条件

避免死锁:

1,设置超时时间

2,使用并发类(ConcurrentHashMap、ConcurrentLinkedQueue、AtomicBoolean等,实际应用中java.util.concurrent.atomic十分有用,简单方便且效率比使用Lock更高)

3,尽量降低锁的使用粒度:用不同的锁而不是一个锁

4,如果能使用同步代码块,就不使用同步方法:自己指定锁对象

5,给线程起个有意义的名字

6,避免锁的嵌套

7,分配资源前先看能不能收回来:银行家算法

8,尽量不要几个功能用同一把锁:专锁专用

活锁:

虽然线程没有阻塞,也始终在运行,但是程序却得不到进展,因为线程始终重复做同样的事

锁的分类:

悲观锁:

互斥同步锁,线程会锁住同步资源

典型例子:synchronized,Lock,数据库的select for update语句

适用场景:适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋的消耗

劣势:1,阻塞和唤醒带来性能的劣势;2,永久阻塞:如果持有锁的线程被永久阻塞,那么等待该线程释放锁的那几个线程永远得不到执行;3,优先级反转:拿到锁的线程优先级比较低,导致优先级高的线程先执行但却获得不到锁

乐观锁:

非互斥同步所,线程不会锁住同步资源,一般用CAS算法实现

典型例子:原子类,并发容器,Git的push操作

适用场景:适合并发写入少,大部分是读取的场景,不加锁能让读取性能大幅提高


可重入锁:

同一个线程可以重复获取同一把锁

好处:避免死锁,提高封装性,

典型例子:ReentrantLock,synchronized

不可重入锁:

同一个线程不能重复获取同一把锁

典型例子:ThreadPoolExecutor


公平锁:

多线程竞争时,需要排队

优势:各线程公平平等,每个线程在等待一段时间后,总有执行机会

劣势:速度慢,吞吐量小

典型例子:ReentrantLock(true)

非公平锁:

多线程竞争时,先尝试插队,插队失败再排队

优势:速度快,吞吐量大,避免唤醒带来的空档期

劣势:有可能产生线程饥饿

典型例子:ReentrantLock(false),tryLock()


共享锁:(读锁)

多个线程可以共享同一把锁,获得共享锁之后,可以查看但无法修改和删除数据,其他线程此时也可以获取到共享锁,也可以查看但无法修改和删除数据

典型例子:reentrantReadWriteLock.readLock()

排他锁(独占锁、独享锁):(写锁)

多个线程不能共享同一把锁

典型例子:reentrantReadWriteLock.writeLock()

读写锁插队策略:

公平锁:不允许插队

非公平锁:写锁可随时插队,读锁仅在等待队列头结点不是想获取写锁的线程的时候可以插队

锁的升降级:

降级:在拥有写锁的同时再获得读锁,然后将写锁释放

升级:在拥有读锁的同时,等待其他线程将读锁释放,然后获得写锁(易发生死锁:如果有两个线程都需要升级为读锁,所以都在等待对方释放读锁,造成死锁)

ReentrantReadWriteLock支持锁的降级,不支持升级


自旋锁:

在两个线程共享同步资源时,锁已经被前面线程调用,后面请求锁的那个线程不放弃CPU的执行时间,进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放锁,那么当前线程就可以不必阻塞,而是直接获取同步资源,从而避免切换线程的开销

缺点:如果锁被占用时间很长,自旋的线程只会白白浪费CPU资源,因为在自旋过程中,会一直消耗CPU

实现原理:在while循坏中进行CAS,直至成功

适用场景:一般用于多核的服务器,在并发度不是特别高的情况下,比阻塞锁的效率高,另外,自旋锁适用于临界区比较短小的情况

阻塞锁:

如果遇到没拿到锁的情况,会直接把线程阻塞,直到被唤醒


可中断锁:

典型例子:tryLock(time),lockInterruptibly()

不可中断锁:

典型例子:synchroniezd()

Java虚拟机对锁的优化:

自旋锁和自适应:

自旋锁旋转一定程度,会转为阻塞锁

锁消除:

在一些不需要加锁的场景下会自动消除锁

锁粗化:

将相邻的一系列锁对象相同的加锁解锁操作合为一个锁

原子类

原子类的作用和锁类似,是为了保证并发情况下线程安全。不过原子类相比于锁,有一定的优势:1,粒度更细:原子变量把竞争范围缩小到变量级别;2,效率更高:使用原子类的效率会比使用锁的效率高,除了高度竞争的情况

Atomic*基本类型原子类:

AtomicInteger:整形原子类,AtomicLong:长整型原子类,AtomicBoolean:布尔型原子类

常用方法:

public final int get()        //获取当前的值

public final int getAndSet(int newValue)        //获取当前值,并设置新的值

public final int getAndIncrement()        //获取当前的值,并自增

public final int getAndDecrement()        //获取当前的值,并自减

public final int getAndAdd(int delta)        //获取当前的值,并加上预期的值

boolean compareAndSet(int expect,int update)        //如果当前的数值等于预期值,则以原子方式将该值设置为输入值

Atomic*Array数组类型原子类:

Atomic*Reference引用类型原子类:

AtomicReference可以让一个对象保证原子性,用法和AtomicInteger类似

Atomic*FieldUpdater升级类型原子类:

将普通类型升级为原子类型

注意:1,可见范围(public);2,不支持static

Adder累加器:

高并发下LongAdder比AtomicLong效率高,不过本质是空间换时间,竞争激烈的时候,LongAdder把不同线程对应到不同的Cell上进行修改,降低了冲突的概率,是多段锁的理念,提高了并发性

AtomicLong在进行每一次加法时,都要flush和refresh(更新到主内存),导致很耗费资源,在高并发的时候会导致冲突比较多,降低了效率

LongAdder的每个线程都有自己的一个计数器,仅用来在自己线程内计数,不会和其他线程的计数器干扰,也不需要flush和refresh操作

LondAdder引入了分段累加的概念,内部有一个base变量和一个Cell[]数组共同参与计数:

base变量:竞争不激烈,直接累加到该变量上

Cell[]数组:竞争激烈,各个线程分散累加到自己的槽Cell[i]中

对比AtomicLong和LongAdder:

在低征用下,AtomicLong和LongAdder这两个类具有相似地特征,但是在竞争激烈地情况下,longAdder地预期吞吐量要高得多,但消耗更多的空间

适用场景:LongAdder适合统计求和计数地场景,基本只提供add方法

Accumulator累加器:

和Adder非常相似,Accumulator就是一个更通用版本地Adder

CAS

CompareAndSwap:比较并且交换

应用场景:

乐观锁,并发容器,原子类

利用CAS实现原子操作:

AtomicInteger加载Unsafe工具,用来直接操作内存数据

用Unsafe来实现底层操作,Unsafe是CAS地核心类,Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。但是Java中有一个类Unsafe,它提供了硬件级别的原子操作

用volatile修饰value字段,保证可见性

缺点:

1,ABA问题:A->B->A;解决:添加版本号

2,自旋时间过长

并发容器

ConcurrentHashMap:

古老和过时的同步容器:

Hashtable:方法直接加上synchronized,并发性能差

Collections.synchronizedMap(new HashMap<K,V>()):使用同步代码块保证线程安全

HashMap线程不安全:

同时put碰撞导致数据丢失

同时put扩容导致数据丢失

HashMap使用头插法操作节点上的链表,当多线程同时扩容,造成死循环,造成CPU100%

JDK1.7的ConcurrentHashMap实现和分析:

最外层有默认16个Segment[],最多同时支持16个线程并发写,默认值可在初始化时设置其他值,一旦设置不可更改,每个Segment里有一个类似HashMap的结构(数组+链表),发生冲突时使用链表,每个segment独立上ReentrantLock锁,每个segment之间互不影响

JDK1.8的ConcurrentHashMap实现和分析:

1.8中的ConcurrentHashMap结构类似1.8中的HashMap结构(数组+链表/红黑树),使用CAS操作和Synchronized来保证线程安全

ConcurrentHashMap从1.7的结构改为1.8的结构:

1,由原来的16个线程并发改为每个Node节点都独立,并发度提高了

2,数组+链表/红黑树结构比数组+链表结构效率高

3,1.7中使用分段锁segment(继承ReentrantLock)来保证并发安全,1.8中使用CAS+Synchronized来保证并发安全

CopyOnWriteArrayList:

古老和过时的同步容器:

vector:方法直接加上synchronized,并发性能差

Collections.synchronizedList(new ArrayList<E>()):使用同步代码块保证线程安全

CopyOnWriteArrayList实现原理和分析:

读取时完全不用加锁,写入也不会阻塞读取操作,只有写入与写入之间需要进行同步

在写入之前先copy一份新副本出来,在新副本的里面进行增删改,完成后再将容器的引用指向新副本的

优点:读取时不受限制,读写分离,迭代时可能出现数据过期的问题

缺点:数据一致性问题:CopyOnWrite只能保证数据的最终一致性,不能保证数据的实时一致性;内存占用问题:CopyOnWrite的写是复制机制,所以在进行写操作时,内存里会同时驻扎两个对象的内存

适用场景:读多写少,读操作尽可能快,写操作可以慢一点

BlockingQueue(阻塞并发队列):

阻塞队列是具有阻塞功能的队列,一端给生产者放数据,一端给消费者拿数据,是线程安全的;take()方法:获取并移除队列的头结点;put()方法:插入元素

主要方法:

put(),take():队列为空满时,阻塞

add(),remove(),element():队列为空或满时抛异常

offer(),poll(),peek():队列为空或满是返回false或null

ArrayBlockingQueue:

有界,可指定容量,可指定是否公平

基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。

ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue;按照实现原理来分析,ArrayBlockingQueue完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。Doug Lea之所以没这样去做,也许是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。 ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。而在创建ArrayBlockingQueue时,我们还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁。

LinkedBlockingQueue:

有界,默认容量为:Integer.MAX_VALUE,可指定是否公平,内部结构:Node,两把锁:takeLock和putLock

基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。

PriorityBlockingQueue:

无界,容量不够自动扩容,支持优先级,默认自然升序排序,可以自定义实现comparaTo()方法指定元素的排序规则或者初始化 PriorityBlockingQueue时,指定构造参数 Comparator 来对元素进行排序

基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。

SynchronousQueue:

容量为0:不需要去持有元素,直接传递,效率高

一种无缓冲的等待队列,类似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去集市找到所要商品的直接生产者,如果一方没有找到合适的目标,那么对不起,大家都在集市等待。相对于有缓冲的BlockingQueue来说,少了一个中间经销商的环节(缓冲区),如果有经销商,生产者直接把产品批发给经销商,而无需在意经销商最终会将这些产品卖给那些消费者,由于经销商可以库存一部分商品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖);但另一方面,又因为经销商的引入,使得产品从生产者到消费者中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。

声明一个SynchronousQueue有两种不同的方式,它们之间有着不太一样的行为。公平模式和非公平模式的区别:

如果采用公平模式:SynchronousQueue会采用公平锁,并配合一个FIFO队列来阻塞多余的生产者和消费者,从而体系整体的公平策略;

但如果是非公平模式(SynchronousQueue默认):SynchronousQueue采用非公平锁,同时配合一个LIFO队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。

DelayQueue:

无界,延迟队列,根据延迟时间排序,元素需要实现Delayed接口,规定排序规则,在延迟期满时才能从队列中提取元素

DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。

使用场景:DelayQueue使用场景较少,但都相当巧妙,常见的例子比如使用一个DelayQueue来管理一个超时未响应的连接队列。

ConcurrentLinkedQueue(非阻塞并发队列):

使用CAS非阻塞算法来实现线程安全

ConcurrentSkipListMap:

控制并发流程

CountDownLatch

流程:

在倒数结束之前,一直处于等待状态,直到倒计时结束,此线程才继续工作

CountDownLatch类在创建实例的时候,需要传递倒数次数,倒数到0时,之前等待的线程会继续运行

CountDownLatch不能回滚重置

主要方法:

CountDownLatch(int count):仅有一个构造函数,参数count为需要倒数的数值

await():调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行

countDown():将count值减1,直到为0时,等待的线程会别唤起

Semaphore信号量

Semaphore可以用来限制或管理数量有限的资源使用情况

流程:

1,初始化Semaphore并指定许可证数量

2,在需要被执行的Semaphore信号量代码前加acquire()或者acquireUninterruptibly()方法

3,在任务执行结束后,调用release()来释放许可证

主要方法:

new Semaphore(int permits,boolean fair):可以设置是否使用公平策略

tryAcquire():有空闲许可证就获取,没有不必阻塞,可以进行别的操作

tryAcquire(timeout):在timeout秒内获取不到许可证,就进行别的操作

acquire():获取许可证,可被中断

acquireUninterruptibly():获取许可证,不可被中断

release():释放许可证

扩展:

可获取多个许可证,释放多个许可证,获取和释放许可证数量需一致

获取和释放许可证对线程并无要求,并不是必须由获取许可证的那个线程释放那个许可证

可以实现等待条件,线程1在线程2完成后再开始,相当于轻量级的CountDownLatch

Condition接口

Condition类似synchronized中的Object.wait/notify的,一个线程需要等待某个条件的执行,就去执行condition.await()方法,进入阻塞状态,另外一个线程去执行相应的条件,直到条件达成的时候,就去执行condition.signal()方法,这时JVM就会从被阻塞的线程里找到那些等待该condition的线程,将其变成Runnable可执行状态

主要方法:

await():线程自动释放持有的Lock锁,进入阻塞状态,等待被唤起(必须持有锁,否则抛异常)

signal():唤起那个等待时间最长的线程

signalAll():唤起所有正在等待的线程

CyclicBarrier循环栅栏

CyclicBarrier循环栅栏和CountDownLatch很类似,都能阻塞一组线程

CyclicBarrier可以构造一个集结点,当某一线程执行完毕,就会到集结点等待,直到所有线程都到了集结点,那么栅栏就会被撤销,所有线程统一出发,继续执行剩下的任务

CyclicBarrier可重复使用

AQS

作用:

AQS是一个用于构建锁、同步器、协作工具类的工具类(框架),有了AQS以后,更多的协作工具类都可以很方便地被写出来

三大核心部分:

state:

这里的state具体含义,会根据具体实现类的不同而不同,比如在Semaphore里表示“剩余许可证数量”,在CountDownLatch里表示“还需要倒数的数量”,在ReentrantLock中,state用来表示“锁”的占有情况,包括可重入计数

state是volatile修饰的,会被并发地修改,所以所有修改state地方法都需要保证线程安全

控制线程枪锁和配合的FIFO队列

这个队列用来存放“等待的线程”,AQS就是“排队管理器”,当多个线程争用同一把锁时,必须有排队机制将没有拿到锁的线程串在一起,当锁释放时,锁管理器就会挑选一个合适的线程来占有这个刚刚释放的锁

AQS会维护一个等待的线程队列,把线程都放到这个队列里

这是一个双向形式的队列

期望协作工具类去实现的获取/释放等重要方法

获取方法:获取操作会依赖state变量,经常会阻塞(比如获取不到锁)

释放方法:释放操作不会阻塞

用法:

1,写一个类,想好写作的逻辑,实现获取/释放方法

2,内部写一个Sync类继承AbstractQueueSynchronizer

3,根据是否独占来重写tryAcquire/tryRelease或tryAcquireShared(int acquires)和tryReleaseShared(int releases)等方法,在之前写的获取/释放方法中调用AQS的acquire/release或者Shared方法

Future和Callable

Callable:

类似于Runnable,被其他线程执行的任务

call()方法类似于run()方法,但是call()方法有返回值,继承Exception,可抛出异常

Callable和Future的关系:

Future.get方法来获取Callable接口返回的执行结果

Future.isDone()方法来判断任务是否执行完

在call()未执行完毕之前,调用get()的线程会被阻塞,直到call()方法返回了结果

Future是一个存储器,它存储了call()这个任务的结果

Future的主要方法:

get()方法:获取结果

1,任务正常完成:get方法立刻返回结果

2,任务尚未完成:get将阻塞并直到任务完成

3,任务执行过程中抛出异常:get方法会抛出ExecutionException

4,任务被取消:get方法会抛出CancellationException

5,任务超时:get方法会抛出TimeoutException

get(long timeout,TimeUnit unit):有超时的获取结果

cancel()方法:取消任务的执行

1,如果这个任务还没有开始执行,会被正常取消,未来也不会执行,方法返回true

2,如果任务已完成,或者已取消,方法执行失败,返回false

3,如果任务已经开始执行,会根据我们填的参数mayInterruptIfRunning来决定是否直接取消

isDone()方法:判断线程是否执行完毕

isCancelled()方法:判断是否被取消

异步

创建异步对象

CompletableFuture 提供了四个静态方法来创建一个异步操作:

public static CompletableFuture<void> runAsync(Runnable runnable)

public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)

public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)

public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

runXxxx 都是没有返回结果的,supplyXxx 都是可以获取返回结果的

可以传入自定义的线程池,否则就用默认的线程池;

计算完成时回调方法

public CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)

public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action)

public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action,Executor executor)

public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)

whenComplete 可以处理正常和异常的计算结果,exceptionally 处理异常情况。

whenComplete 和 whenCompleteAsync 的区别:whenComplete:是执行当前任务的线程执行继续执行 whenComplete 的任务。 whenCompleteAsync:是执行把 whenCompleteAsync 这个任务继续提交给线程池 来进行执行。 方法不以 Async 结尾,意味着 Action 使用相同的线程执行,而 Async 可能会使用其他线程 执行(如果是使用相同的线程池,也可能会被同一个线程选中执行)

handle 方法

public <U> CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn)

public <U> CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn)

public <U> CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn,Executor executor)

和 complete 一样,可对结果做最后的处理(可处理异常),可改变返回值。

线程串行化方法

public <U> CompletableFuture<U> thenApply(Function<? super T, ? extends U> fn)

public <U> CompletableFuture<U> thenApplyAsync(Function<? super T, ? extends U> fn)

public <U> CompletableFuture<U> thenApplyAsync(Function<? super T, ? extends U> fn, Executor executor)

public CompletableFuture<Void> thenAccept(Consumer<? super T> action)

public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action)

public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action, Executor executor)

public CompletableFuture<Void> thenRun(Runnable action)

public CompletableFuture<Void> thenRunAsync(Runnable action)

public CompletableFuture<Void> thenRunAsync(Runnable action, Executor executor)

thenApply 方法:当一个线程依赖另一个线程时,获取上一个任务返回的结果,并返回当前 任务的返回值。

thenAccept 方法:消费处理结果。接收任务的处理结果,并消费处理,无返回结果。

thenRun 方法:只要上面的任务执行完成,就开始执行 thenRun,只是处理完任务后,执行 thenRun 的后续操作

带有 Async 默认是异步执行的。同之前。 以上都要前置任务成功完成。

Function<? super T,? extends U> T:上一个任务返回结果的类型

两任务组合 - 都要完成

public <U,V> CompletableFuture<V> thenCombine(CompletionStage<? extends U> other,BiFunction<? super T, ? super U, ? extends V> fn)

public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T, ? super U, ? extends V> fn)

public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T, ? super U, ? extends V> fn,Executor executor)

public <U> CompletableFuture<Void> thenAcceptBoth(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action)

public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action)

public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action,Executor executor)

public CompletableFuture<Void> runAfterBoth(CompletionStage<?> other, Runnable action)

public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action)

public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action, Executor executor)

两个任务必须都完成,触发该任务。

thenCombine:组合两个 future,获取两个 future 的返回结果,并返回当前任务的返回值

thenAcceptBoth:组合两个 future,获取两个 future 任务的返回结果,然后处理任务,没有 返回值。

runAfterBoth:组合两个 future,不需要获取 future 的结果,只需两个 future 处理完任务后, 处理该任务。

两任务组合 - 一个完成

public <U> CompletableFuture<U> applyToEither(CompletionStage<? extends T> other, Function<? super T, U> fn)

public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn)

public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn, Executor executor)

public CompletableFuture<Void> acceptEither(CompletionStage<? extends T> other, Consumer<? super T> action)

public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action)

public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action, Executor executor)

public CompletableFuture<Void> runAfterEither(CompletionStage<?> other, Runnable action)

public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other, Runnable action)

public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other, Runnable action, Executor executor)

当两个任务中,任意一个 future 任务完成的时候,执行任务。

applyToEither:两个任务有一个执行完成,获取它的返回值,处理任务并有新的返回值。

acceptEither:两个任务有一个执行完成,获取它的返回值,处理任务,没有新的返回值。

runAfterEither:两个任务有一个执行完成,不需要获取 future 的结果,处理任务,也没有返 回值。

多任务组合

public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)

public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)

allOf:等待所有任务完成

anyOf:只要有一个任务完成

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值