JAVA多线程
2021-03-05 13:03:56 5 举报
AI智能生成
JAVA多线程
作者其他创作
大纲/内容
线程
基本概念
什么是线程与锁
在做一件事时,可以采用多线程的形式,如果涉及到公用资源时,会面临资源安全的问题,锁可以解决资源只能同时被一个线程使用的问题,解决了线程安全<br>java默认有两个线程 mian GC
线程的实现
Thread
Thread有局限性,因为继承只能有一个,底层原理是静态代理了Runnable接口<br>类*继承extends*Thread类,重写run()方法,类的实例调用start开启线程<br>
Runnable
Runnable更灵活,接口可以继承多个,而Thread实质上也是继承Runnable<br>类*继承接口implements*Runnable接口,重写run()方法,需要使用Thread构造函数传入类的实例,用Thread的实例调用start开启线程<br>
Callable
Callable有线程池<br>首先类继承接口Callable<call方法返回值类型>,重写call()方法,调用时需要先创建服务ExecutorService ser = Executors.newFixedThreadPool(3);在服务实例中使用submit方法提交线程实例,会得到返回值类型Future<>。通过get()可得到call()方法的返回值。<br>使用完毕需要执行关闭服务:ser.shutdownNow();
常用方法
Thread.currentThead():获取当前线程对象<br>getPriority():获取当前线程的优先级<br>setPriority():设置当前线程的优先级 注意:线程优先级高,被CPU调度的概率大,但不代表一定会运行,还有小概率运行优先级低的线程。<br>isAlive():判断线程是否处于活动状态 (线程调用start后,即处于活动状态)<br>join():调用join方法的线程强制执行,其他线程处于阻塞状态,等该线程执行完后,其他线程再执行。有可能被外界中断产生InterruptedException 中断异常。<br>sleep():在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。休眠的线程进入阻塞状态。<br>yield():调用yield方法的线程,会礼让其他线程先运行。(大概率其他线程先运行,小概率自己还会运行)<br>interrupt():中断线程<br>wait():导致线程等待,进入堵塞状态。该方法要在同步方法或者同步代码块中才使用的<br>notify():唤醒当前线程,进入运行状态。该方法要在同步方法或者同步代码块中才使用的<br>notifyAll():唤醒所有等待的线程。该方法要在同步方法或者同步代码块中才使用的<br>setDaemon() : 守护线程,默认是false表示用户线程,true是守护线程,JVM不会等待守护线程执行完毕,守护线程一般为监控,记录,垃圾回收等组件。
优、 活、 强、 睡、 礼、 中、 等、 唤(优活强睡,礼中等唤)<br>优先级 活跃 强制执行 睡眠 礼让 中断 等待 唤醒<br>Priority Alive join sleep yield interrupt wait notity
线程状态
新建状态:当用new操作符创建一个线程时。此时程序还没有开始运行线程中的代码。<br>
就绪状态:当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序来调度的。<br>
运行状态(running):当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法。
阻塞状态(blocked):线程运行过程中,可能由于各种原因进入阻塞状态:<br>①线程通过调用sleep方法进入睡眠状态;<br>②线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;<br>③线程试图得到一个锁,而该锁正被其他线程持有;<br>④线程在等待某个触发条件;<br>所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。
(1) 调用sleep(毫秒数),使线程进入"睡眠"状态。在规定的时间内,这个线程是不会运行的。<br>(2) 用suspend()暂停了线程的执行。除非线程收到resume()消息,否则不会返回"可运行"状态。<br>(3) 用wait()暂停了线程的执行。除非线程收到nofify()或者notifyAll()消息,否则不会变成"可运行"(是的,这看起来同原因2非常相象,但有一个明显的区别是我们马上要揭示的)。<br>(4) 线程正在等候一些IO(输入输出)操作完成。<br>(5) 线程试图调用另一个对象的"同步"方法,但那个对象处于锁定状态,暂时无法使用。
死亡状态(dead)一旦进入死亡状态就不能再进行启动,有两个原因会导致线程死亡:<br>①run方法正常退出而自然死亡;<br>②一个未捕获的异常终止了run方法而使线程猝死;<br>为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法,如果是可运行或被阻塞,这个方法返回true;如果线程仍旧是new状态且不是可运行的,或者线程死亡了,则返回false。
异常与锁
死锁排查
1.使用JPS定位进程号<br>在命令行终端输入:<br>`jps -l`<br>2.使用`jstack 进程号`寻找死锁<br>
破解其中一个条件即可避免死锁,产生死锁的四个必要条件:<br>1.互斥条件:一个资源每次只能被一个进程使用<br>2.请求与保持条件:一个进程因请求资源而阻塞时,对以获得的资源保持不放<br>3.不剥夺条件:进程已获得资源,在未使用完之前,不能强行剥夺<br>4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
JMM模型
内存模型的8种操作:<br>主存-工作内存:read - X - load<br>工作内存 - 执行引擎:use - assign<br>工作内存 - 主存:write - X - store<br>加锁 - 解锁:lock - unlock<br>如果线程阻塞无法加载主存的内容,可见性怎么保证?
Volatile 关键字<br>保证多线程间的变量可见性
是JVM提供的轻量级同步机制<br>1.保证可见性<br>2.不保证原子性<br>3.禁止指令重排
AtomicXXX原子包装类(JUC) 大量使用CAS(Unsafe)<br>保证多线程间的变量原子性<br>i++不是原子性 因为很简单:<br>获取值 — 值+1 — 写回值<br>
Unsafe
指令重排:特别情况下会导致先后代码执行顺序颠倒,进而改变正确结果<br>volatile可以保证指令的执行顺序,实现原理是volatile写的上下会有-内存屏障-,禁止顺序交换<br>写的程序代码不会按照写的顺序去执行<br>源代码->编译器优化的重排序->指令并行也可能会重排->内存系统也会重排->执行<br>使用的最多:单例模式(懒汉式,饿汉式)
单例的防止反射安全操作:<br>最终防御.枚举类型:反射会判断是否为枚举类型,如果是则无法反射<br>其余可以用单例内设置信号变量等方式,或构造器种判断是否为null,但反射均可破坏<br>
饿汉式:程序加载自动创建单例模式,有可能造成内存浪费<br>私有化构造方法,私有化final静态创建实例,共享静态方法返回实例
懒汉式:<br>问题1:线程不安全<br>问题2:有可能发生指令重排现象,在执行构造函数的时候,分配内存,先执行对象指向空间,再执行构造函数,此时第二个线程进行判断,导致第一次循环有地址却没有实例<br>解决方法:返回单例使用双重检测锁模式 (DCL)懒汉式,且实例使用volatile关键词保证禁止指令重排<br>私有化构造方法,私有化volatile静态实例(未初始化),共享静态方法返回实例(双重检测锁判断实例是否为空)<br>
public class User {<br> private User(){}<br> private volatile static User USER;<br> public static User getUser(){<br> if (USER == null) {<br> synchronized (User.class) {<br> if (USER == null) {<br> USER = new User();<br> }}}<br> return USER;<br> }}
静态内部类实现:存在线程安全<br>私有构造函数,使用静态内部类创建实例,共享静态方法返回实例<br>
线程池
三大方法、七大参数、四大拒绝策略
三大创建线程池方法<br>Excecutors包装类<br>包装ThreadPoolExecutor
Executors.newSingleThreadExecutor();//单个线程池<br>Executors.newFixedThreadPool(5);//固定的线程池 (允许请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,导致OOM)<br>Executors.newCachedThreadPool();//可伸缩的,遇强则强,遇弱则弱 (允许创建线程数量为Integer.MAX_VALUE,可能会堆积大量的请求,导致OOM)<br>.execute(Runnable)运行Runnable线程<br>切记:.shutdown();//关闭线程池<br>是ThreadPoolExecutor的封装类。会产生OOM异常,不安全,建议用ThreadPoolExecutor<br>
七大参数<br>ThreadPoolExecutor<br>
核心大小<br>最大承受线程数量(CPU密集型:最大核心数 IO密集型:大于耗IO的线程可以设置成2倍)<br>超时时间<br>时间单位<br>BlockingQueue队列<br>创建线程的工厂Executors.defaultThreadFactory()<br>拒绝策略<br>
四大拒绝策略
new ThreadPoolExecutor<br>.AbortPolicy()抛出异常策略<br>.CallerRunsPolicy()哪条来的回哪条执行策略<br>.DiscardPolicy()队列满了丢掉任务,不抛出异常<br>.DiscardOldestPolicy()队列满了先尝试和最早的线程竞争,没竞争上就丢掉任务,不抛出异常<br>
异步调用Fature
CompletableFuture<返回值类型> 类似Ajax异步请求<br>常用API:<br>get() 等待这个未来完成的必要,然后返回结果。 <br>whenComplete((T,U)->{}) 方法调用后执行,T为返回值,U为异常信息<br>exceptionally((E)->{return X;}) 若方法调用发生异常则运行,E为异常信息,返回特定值<br>
没有返回值的异步任务<br>CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(Runnable)<br>
有返回值的异步任务<br>CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(Supplier(供给型接口))<br>
ThreadPoolExecutor<br>线程池接口<br>
常用API:<br>void execute(Runnable command): 执行任务/命令,没有返回值,一般用来执行Runnable<br><T> Future<T> submit(Callable<T> task) : 执行任务,有返回值,一般用来执行Callable<br>void shutdown(): 关闭线程池<br>
Runnable<br>
Runnable更灵活,接口可以继承多个,而Thread实质上也是继承Runnable<br>类*继承接口implements*Runnable接口,重写run()方法,需要使用Thread构造函数传入类的实例,用Thread的实例调用start开启线程<br>
Callable - 带返回值的Runnable<br>
特点:可以有返回值,可以抛出异常,方法不同,run()/call()<br>Callable实际上是使用了Runnable接口的一个实现类FutureTask,FutureTask中把Callable的call方法在run中进行了一个增强<br>FutureTask获得返回值get时如果线程未执行完毕会产生阻塞<br>FutureTask具有缓存功能,即一个FutureTask实例如果运行多次,可能不会执行第二次,提高效率。<br>
常用线程池<br>及创建方法Executors
fixed
cached
spngle
scheduled
workstealing<br>
Forkjoin 工作窃取线程池<br>大量工作时候适用:<br>先完成任务的线程会替未完成任务的线程分担任务<br>缺点:小量任务会发生任务抢夺,降低效率<br>
ForkJoinPool:ForkJoin线程池<br>new ForkJoinPool();新建线程池<br>.submit(new ForkJoinTask())提交线程 有返回值<br><span style="font-size: inherit;">.execute(new ForkJoinTask())提交线程 无返回值</span><br>
RecursiveTask是ForkJoinTask线程子类实现:<br>ForkJoinTask是在ForkJoinPool线程池内运行的任务的抽象基类<br>特点:工作窃取 里面维护的是双端队列<br>标准方法:<br>fork() 压入线程队列<br>join() 获取线程返回值<br>流程:<br>新建一个类继承RecursiveTask<继承方法需要迭代计算的数值类型><br>compute()方法中,使用新建本身类以用于迭代,传输数值后,使用fork()来提交支线任务<br>使用join()来获取支线任务的计算结果<br>return返回结果<br>
线程实现实例<br>class PX extends RecursiveTask<Integer>{<br> private Integer start=1;<br> private Integer end=10;<br> private Integer off = 5;<br> public PX(Integer start, Integer end) {this.start = start;this.end = end;}<br> @Override<br> protected Integer compute() {<br> if ((end - start) <= off){<br> System.out.println("计算了");<br> return 1;<br> }else {<br> Integer RT = (start + end)/2;<br> PX px1 = new PX(start, RT);<br> px1.fork();//拆分任务,把任务压入线程队列<br> PX px2 = new PX(RT + 1, end);<br> px2.fork();<br> int i = px1.join() + px2.join();<br> return i;}}}<br>
使用实例:<br>ForkJoinPool forkJoinPool = new ForkJoinPool();<br> ForkJoinTask<Integer> px = new PX(1,21);<br> ForkJoinTask<Integer> submit = forkJoinPool.submit(px);<br> System.out.println(px.join());<br>
线程安全
线程同步
同步方法
Synchronized:
字节码层级: monitorenter 加锁 moniterexit 解锁<br>分两种加锁方式: 同步代码块,可以锁住任意一个object。方法锁,锁是对象本身(this)静态方法就是 xx.class<br>加锁指的是锁定对象头(8byte)<br>jdk1.6之前,Synchronized是一个重量级锁,在1.6之后对其进行了优化。重量锁在多线程下会导致线程阻塞;但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。<br>
锁状态 <br>无锁态 对象的hashCode 分代年龄 0 (01)<br>轻量级锁 指向栈中锁记录的指针 (00)<br>重量级锁 指向互斥量(重量级锁)的指针 (10)<br>GC标记 空 (11)<br>偏向锁 线程ID Epoch 分代年龄 1 (01)
Synchronized锁升级
无锁—偏向锁—轻量级锁—重量级锁
第一次调用对象锁的时候,会修改是否为偏向锁为1并且记录线程的ID,如果多个线程访问这个锁,会立即升级为轻量级锁,把偏向锁中的内容改为指向抢锁成功的线程栈的LockRecord空间,再次发生多次抢夺锁且CAS自旋次数达到10次(默认)以上还没有成功,则升级为重量级锁,重量级锁是用操作系统层级来实现的, monitorenter 加锁 moniterexit 解锁
偏向锁
偏向锁是jdk1.6引入的一项锁优化,这个锁会偏向于第一个获得它的线程,在接下来的执行过程中,假如该锁没有被其他线程所获取,没有其他线程来竞争该锁,那么持有偏向锁的线程将永远不需要进行同步操作。在此线程之后的执行过程中,如果再次进入或者退出同一段同步块代码,并不再需要去进行加锁或者解锁操作。<br>优点:加锁和解锁不需要额外消耗,和执行非同步方法相比较为纳秒级差距。缺点:如果线程存在锁竞争存在一个锁撤销的过程revoke,会带来额外的锁撤销损耗。适用场景:只有一个线程访问同步块。<br>当使用Synchronized关键字的时候,检查锁标志位是否为01,检查是否为偏向锁是否为0,如果是0就修改为1,并且把线程的hashCode抹除改为线程ID与Epoch,hashCode备份在线程栈上。在markword上记录当前线程指针,下次加锁判断线程指针是否同一个,不需要争用。线程销毁,锁降级为无锁<br>如果一个偏向锁有两个线程争夺时,会自动升级为轻量级锁
过程:如果发生线程争夺。<br>threadID | epoch | age | 1 | 01<br>stack建立LockRecord<br>copy markword到LockRecord<br>CAS替换markword的LR指针(线程争用)<br>LockRecord Pointer | 00
轻量级锁<br>(自旋锁)<br>
轻量级锁也被称为非阻塞同步、乐观锁,因为这个过程并没有把线程阻塞挂起,而是让线程空循环等待,串行执行。<br>优点:线程不会阻塞,提高程序响应速度。缺点:始终得不到锁竞争的线程,使用自旋操作会损耗CPU。使用场景:追求响应时间,同步块执行速度非常快<br>自旋锁:<br>在轻量级锁等待锁的过程并不会阻塞而是进行空循环等待,在进行10还没有获得锁的话会进行锁膨胀,变为重量级锁。<br>用户可以通过-XX:PreBlockSpin来进行更改<br>自适应自旋锁:<br>适用于刚刚获得过一次锁的线程,系统会认为该线程得到锁的几率更大,会增加空循环的次数,以便于得到锁<br>另外,如果对于某一个锁,一个线程自旋之后,很少成功获得该锁,那么以后这个线程要获取该锁时,是有可能直接忽略掉自旋过程,直接升级为重量级锁的,以免空循环等待浪费资源。<br>
轻量级锁的对象头中有一个指针(LockRecord Pointer),他会指向已经成功争夺锁的栈中的一个栈帧的地址
重量级锁
优点:线程竞争不会使用自旋,不会消耗CPU。缺点:线程阻塞,响应时间缓慢。适用场景:追求吞吐量,同步块执行速度较长
Synchronized同步方法与非同步方法
Synchronized锁插入
Lock:
区别:<br>加锁或使用 synchronized 关键字带来的性能损耗较大,而用 CAS 可以实现乐观锁,它实际上是直接利用了 CPU 层面的指令,所以性能很高。<br>Synchronized 可重入锁,不可以中断,非公平。Lock 可重入锁,可以判断是否取到了锁,非公平(true设置为公平)<br>ReentrantLock 是CAS自旋的实现,高争用,高耗时时Synchronized效率高,低争用低耗时,CAS效率更高<br>synchorinuzed升级到重量级锁时是队列(不损耗CPU)CAS(等待期间损耗CPU)<br>Lock是显示锁(手动开启和关闭锁,必须手动释放,否则会死锁) Synchronized是隐式锁,出了作用域就自动释放<br>使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性<br>
ReentrantLock
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
线程通信
生产者/消费者模式<br>synchronized使用wait(),notify()<br>
缓冲数据区
定义一个定量的数据缓冲区,设置存入与取出两个方法并添加锁,如果数据达到上限或者下限,就触发wait(),释放锁并持续等待,直到另一个方法改变了上限或者下限,并执行notifyAll()方法,通知等待方法可以继续操作。
信号灯法
设置一个布尔类型的开关,利用这个开关来切换等待的线程,每次调用完线程方法,切换开关并执行notifyAll()
生产者/消费者模式<br>ReentrantLock加Condition方式<br>
Lock的生产消费模式比Synchronized相比更加灵活,且功能更多,比如Condition的精准唤醒<br>与Synchronized相比,需要根据RenntrantLock锁实例创建一个同步监视器 Condition condition = lock.newCondition();<br>等待wait() = condition.await();<br>唤醒全部notifyAll() = condition.signalAll();<br>精确唤醒:1.监视器Condition可以创建多个 A,B,C,D 当A.await() 等待时候, 可以使用A.signal()指定这个监视器唤醒。
join的使用
join在线程里面意味着“插队”,哪个线程调用join代表哪个线程插队先执行——但是插谁的队是有讲究了,不是说你可以插到队头去做第一个吃螃蟹的人,而是插到在当前运行线程的前面,比如系统目前运行线程A,在线程A里面调用了线程B.join方法,则接下来线程B会抢先在线程A面前执行,等到线程B全部执行完后才继续执行线程A。
yield的使用
yield()方法作用是放弃当前CPU资源,让其他任务去占用CPU执行时间。但放弃的时间不确定。
同步容器
同步容器类的演变
Map/set从无锁到同步
HashMap
HashMap存储结构1.7之前为数组+链表,1.8之后当链表超过8时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能<br>hash计算:通过字符串算出ASCII码,进行mod(取模),算出哈希表中的下标<br>扩容:为减缓哈希冲突,当Map元素>hash桶(默认16)*负载因子(默认0.75)时会扩容至2倍,等到满16个元素才扩容,某些桶里可能就有不止一个元素了。当发生哈希冲突并且size大于阈值的时候,需要进行数组扩容,扩容时,需要新建一个长度为之前数组2倍的新的数组,然后将当前的Entry数组中的元素全部传输过去,扩容后的新数组长度为之前的2倍,所以扩容相对来说是个耗资源的操作。<br>
线程安全:HashTable:<br>底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化<br>初始size为11,扩容:newsize = olesize*2+1 | map的默认为16,且扩容为2倍<br>计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length<br>
HashMap和Hashtable都是用hash算法来决定其元素的存储,因此HashMap和Hashtable的hash表包含如下属性:<br>容量(capacity):hash表中桶的数量<br>初始化容量(initial capacity):创建hash表时桶的数量,HashMap允许在构造器中指定初始化容量<br>尺寸(size):当前hash表中记录的数量<br>负载因子(load factor):负载因子等于“size/capacity”。负载因子为0,表示空的hash表,0.5表示半满的散列表,依此类推。轻负载的散列表具有冲突少、适宜插入与查询的特点(但是使用Iterator迭代元素时比较慢)<br>
线程安全:Collections.synchronizedMap(map):<br>hashtable锁级别是方法级别的,Collections.synchronizedMap()是代码块级别的锁方法,并且可以锁对象<br>两者性能相近,但是Collections.synchronizedMap()允许null作为key或value(锁对象),这个时候只允许同时存在一个操作,有点串行化的感觉<br>
线程安全:ConcurrentHashMap:<br>底层采用分段的数组+链表实现,线程安全<br>通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)<br>Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术<br>有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁<br>扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容<br>锁分段:首先将数据分成几段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。 <br>
LinkedHashMap:<br>HashMap存储时是无序的,LinkedHashMap存储时是有序的
HashSet
HashSet底层就是HashMap存储原理是HashMap的Key计算方式<br>仅存储对象,且不能重复,线程不安全
队列
ArrayList<br>默认10 扩容1.5倍<br>并发修改异常<br>ConcurrentModificationException<br>
new Vector<>();最古老的JDK1.0<br>
Collections.synchronizedList(new ArrayList<>());将ArrayList加上Synchronized
new CopyOnWriteArrayList<>();<br>与Vector相比,Vector所有方法均被Synchronized修饰,效率十分低,并且每次扩容为2倍,这也是vector被弃用的原因<br>COW核心思想是 每次添加操作会产生一个副本,给副本扩容1,添加数据并返还副本,使用Lock锁,这样即效率高并且集合容量不会有浪费
LinkedList
Queue<br>四组API:<br>方法 抛出异常 不抛异常 阻塞等待 超时等待<br>添加 add offer put offer(,,)<br>取出 remove poll take poll(,)<br>判断队列首 element peek - -<br>
ConcurrentLinked Queue
ConcurrentArrya Queue
Blocking Queue<br>阻塞队列<br>
LinkedBlockingQueue
ArrayBlockingQueue
SynchronousQueue<br>常用:put take<br>同步队列,一个进一个出<br>和其他BlockingQueue不一样,SynchronousQueue不存储元素<br>
Abstract Queue<br>非阻塞队列
Deque<br>双端队列
TransferQueue
DelayQueue
JUC同步工具
cas自旋锁
CAS(Compare-and-Swap):比较并替换<br>从Hotspot层面来讲,首先这个一个死循环,自旋操作会在每个循环开始,获取地址值,并赋值给期望值,执行一个CPU 的 CAS指令 来比较地址值是否与期望值相等,如果相等,则把目标值进行一个赋值,如果判断时不相等则继续循环直到成功为止。基于 CAS 的操作可认为是无阻塞的,一个线程的失败或挂起不会引起其它线程也失败或挂起。并且由于 CAS 操作是 CPU 原语,所以性能比较好,在CPU层级来说进行CAS操作也会添加一个Lock锁住这个CAS指令。利用的是基于冲突检测的乐观并发策略。 这种乐观在线程数目非常多的情况下,失败的概率会指数型增加。
ABA问题的解决:<br>原子引用(乐观锁)替换原子包装类<br>AtomicStampedReference<>(Object, 版本号);<br>CAS方法些许不同<br>compareAndSet(目标值,预期值,版本哈,版本号修改)<br>
Integer包装类的坑:<br>因为泛型Integer会遇到判断是否为目标值的情况,而Integer类里面默认自带缓存<br>[-128-127]之间的赋值,Integer对象是在IntegerCache.cache产生,会复用已有对象,这个区间内的Integer值可以直接使用==进行判断,但是这个区间外的所有数据都会在堆上产生,并不会复用已有对象,这是一个大坑。<br>推荐使用equals方法进行判断<br>
总线风暴问题:<br>由于volatile的mesi缓存一致性协议需要不断的从主内存嗅探和cas不断循环无效交互导致总线带宽达到峰值<br>解决办法:部分volatile和cas使用synchronize<br>
CAS JAVA层级:compareAndSet(目标值,预期值)比较并替换<br>JAVA 无法操作内存,JAVA可以调用c++ native,C++可以操作内存<br>JAVA中CAS操作会调用unsafe(JAVA的后门,可以通过通过这个类操作内存)<br>操作系统层级:<br>CAS是CPU原语,性能好<br>缺点:1.循环会耗时,2.一次性只要一个共享变量原子性,3.ABA问题
public final int getAndAddInt(Object var1, long var2, int var4) {<br> int var5;<br> do {<br> var5 = this.getIntVolatile(var1, var2);<br> } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));<br> return var5;<br> }<br>var1:当前对象 var2:内存地址偏移值 var4:预期值<br>compareAndSwapInt使用CAS原语在内存中进行修改,在CPU层级来说进行CAS操作也会添加一个Lock锁住这个CAS指令。<br>sun.misc.Unsafe可以通过一个对象实例和该属性的偏移量用原语获得该对象对应属性的值<br>
AQS:AbstractQueuedSynchronizer<br>抽象的同步队列 奠定了公平锁与非公平锁的基础,AQS 在内的很多并发相关类中,CAS 都扮演了很重要的角色。<br>API:<br>acquire(state) // 独占模式获取资源<br>release(state) // 独占模式释放资源<br>acquireShared(state) // 共享模式获取资源<br>releaseShared(state) // 共享模式释放资源<br>常用子类有ReentrantLock锁等<br>
ReentrantLock可重入锁
可重入锁:拿到了外面的锁,就可以拿到里面的锁,自动获得<br>如果锁具备可重入性,则称作为可重入锁。像synchronized(一把锁)和ReentrantLock(多把锁)都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。<br>公平锁:十分公平,先来后到<br>非公平锁:十分不公平,可以插队(默认)<br>
ReadWriteLock读写锁
实现类为: ReentrantReadWriteLock();<br>与ReentranLock相比具有更加细粒度的解决(指读锁和写锁)可以分别控制<br>独占锁(写锁) 一次只能被一个线程占有<br>共享锁(读锁) 多个线程可以同时占有<br>读-读 可以共存<br>读-写 不能共存<br>写-写 不能共存<br>lock.writeLock()/readLock.lock()/unlock();<br>
Condition条件等待与通知
等待wait() = condition.await();<br>唤醒全部notifyAll() = condition.signalAll();<br>精确唤醒:1.监视器Condition可以创建多个 A,B,C,D 当A.await() 等待时候, 可以使用A.signal()指定这个监视器唤醒。
Latch门阀
减法计数器(门阀):全部执行并释放等操作<br>如果有若干线程并发执行某个特定任务,需要等到所有的子任务都执行结束之后在统一汇总,就可以采用Latch设计模式。<br>Latch(门阀)设计模式:该模式指定了一个屏障,只有所有的条件都达到满足的时候,门阀才能打开。<br>
CountDownLatch
new CountDownLatch(6); 创建门阀个数<br>countDown(); 门阀-1<br>.await();每次有线程调用couuntDown()数量-1,假设计数器变为0,await()方法就会被唤醒,继续执行<br>
CyclicBarrier线程栅栏
加法计数器 :同步后再执行方法等<br>CyclicBarrier它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。比如将计数器设置为10,那么凑齐第一批10个线程后,计数器就会归0,然后接着凑齐下一批10个线程,这就是循环栅栏内在的含义。<br>Cyclibarriery源码:await()等待时,当全部等待的线程数量等于总数量时,则执行Runnable的.run()方法;<br>CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{System.out.println("召唤神龙");});<br>
int await():在所有参与线程都已经在此 barrier 上调用 await 方法之前,将一直等待。<br>int await(long timeout, TimeUnit unit):在所有参与线程都已经在此屏障上调用 await 方法之前将一直等待,或者超出了指定的等待时间。<br>int getNumberWaiting():返回当前在屏障处等待的参与者数目。<br>int getParties():返回要求启动此 barrier 的参与线程数目。<br>boolean isBroken():查询此屏障是否处于损坏状态。<br>void reset():将屏障重置为其初始状态。
Semphore信号量
线程坑位:停车位、限流<br>Semaphore用来控制访问某资源的线程数,比如数据库连接.假设有这个的需求,读取几万个文件的数据到数据库中,由于文件读取是IO密集型任务,可以启动几十个线程并发读取,但是数据库连接数只有20个,这时就必须控制最多只有20个线程能够拿到数据库连接进行操作。这个时候,就可以使用Semaphore做流量控制。特殊:如果信号量为0,先执行释放,则不会阻塞,会多出一个信号量,可获取<br>Semaphore必须要放在Try/Finally中 <br>new Semaphore(3);规定有多少许可证,<br>acquire();获得许可证<br>release();释放许可证<br>
Semaphore(int permits) 创建state等于permits的非公平Semaphore<br>Semaphore(int permits, boolean fair)创建可选的公平/非公平的state等于permits的Semaphore<br>acquire() 申请一个许可证,申请不到则阻塞,可以被中断<br>acquireUninterruptibly() 申请一个许可证,申请不到则阻塞,不可以被中断<br>tryAcquire() 尝试申请一个许可证,申请不到返回false,申请的返回true<br>tryAcquire(long timeout, TimeUnit unit)尝试在时间内申请一个许可证,申请不到返false,否则返回true <br>release() 归还一个许可证<br>availablePermits() 获取state的值<br>drainPermits() 设置state的值为0<br>isFair() 获取当前Semaphore是否公平 <br>hasQueuedThreads() 获取是否有线程等待在当前Semaphore上(不准改数量可变的,只具有参考价值)<br>getQueueLength() 获取等待在Semaphore上线程的数量(不准改数量可变的,只具有参考价值)
Semaphare与Lock的区别
Lock如果不先获取锁就释放锁会报错<br>Semaphore 可以不获取许可证就释放一个许可证。<br>
ThreadLocal线程本地变量
ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
0 条评论
下一页