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