多线程并发知识

多线程并发
多线程相关问题
进程是程序运行的实例。运行一个Java程序的实质就是启动一个java虚拟机进程。进程是程序向操作系统申请资源的基本单位。线程是进程中可独立执行的最小单位。一个进程可以包括多个线程,同一个进程中的所有线程共享该进程的资源。
竞态:多线程编程中一个问题,对于同样的输入,程序的输出有时是正确的,有时是错误的,这种计算结果的正确性与时间有关的现象被成为竞态。
线程安全和非线程安全: 一个类在单线程环境下能够正常运行,并且在多线程环境下,使用方不做特别处理也能运行正常,我们就称其实线程安全的。反之,一个类在单线程环境下运行正常,而在多线程环境下无法正常运行,这个类就是非线程安全的。

线程状态:新建(new之后),就绪(start之后),运行(run之后),阻塞(等待,wait/sleep,阻塞IO/申请锁,有限等待),死亡。
多线程实现方式:1.实现Runnable接口2、继承Thread类。因为java不支持多重继承,但可以实现多个接口。所有实现Runnable有更好的扩展性;还可以实现资源个共享,即多个线程基于一个Runnable对象,和共享Runnable的资源。

第三种是实现Callable接口;将实现类当做线程中运行的任务,然后用Thread来调用。callable的call方法有返回值,通过FutureTask进行封装。
A.start()是启动一个新线程A,进入就绪状态。start方法只能被调用一次。
Run()方法,是就绪状态的线程获取cpu之后调用的,线程进入运行状态。如果手动调用,相当于执行一个普通方法。
Java中,每个对象有且只有一个同步锁。调用synchronized方法就获取了对象的锁。
当一个线程访问对象的同步方法或者同步代码块,其他线程不能再访问该对象的同步方法或者同步代码块,但可以访问该对象的非同步方法或者非同步代码块。
t1.wait(),让当前线程进入阻塞状态,释放t1对象的锁,直到其他线程调用t1.notify()方法,当前运行线程才可能被唤醒。
wait()和notify()方法必须出现在同步代码块中,等待和唤醒是依赖于同步锁实现的,同步锁是对象持有,每个对象有且只有一个。所以wait要释放锁,因为之后他释放了该锁,其他对象才能获取该锁,然后进入同步代码中,notify该对象上等待的对象。所以这也就是wait和notify方法为什么出现在Object对象上。
yield():Thread静态方法,表示当前线程从运行状态转入就绪状态,给其他线程竞争的机会。不会释放任何锁。
sleep():Thread静态方法,当前线程从运行状态进入等待阻塞状态,不释放任何锁,休眠一段时间后,该线程进入就绪状态。单位毫秒
A.join():当前线程进入阻塞状态,等线程A执行完之后,当前线程从阻塞状态进入就绪状态。
线程优先级,1-10,默认5.用户线程,守护线程,isDaemon() t1.setDaemon(true);用户线程运行时,jvm不得关闭。gc线程是守护线程。
中断:调用线程的A.interrupt()方法,会设置线程A的中断标记为true。如果线程A处于wait(),join(),sleep()等阻塞状态时(不是io阻塞和锁阻塞),清除中断标记,抛出InterruptedException,线程结束。
A.interrupted()方法,查看线程是否处于中断状态。且清除中断标记,而isInterrupted则查看中断标记,不会清除。
Executor的中断,shutdown(),不再接受新任务,等待线程池中所有的任务完成。shutdownNow(),不接受新的,和未处理的。调用每个正在执行线程的interrupt();
中断线程池中的一个线程:使用submit方法提交线程,会返回一个future对象,调用该对象的cancel(true)方法中断线程。
锁机制
悲观锁和乐观锁
乐观锁:每次访问数据的时候都认为其他线程不会修改数据,所以直接访问数据,更新的时候再判断在此期间其他线程是否修改数据。CAS和版本号机制是乐观锁的实现。
乐观锁适合多读场景,悲观锁适合多写情况。
版本号机制:数据有个version字段,表示被修改的次数。
CAS:无琐算法,非阻塞同步,需要读写的内存值V和旧的期望值A相同时,更新为B.一般都是自旋CAS,不断的重试。乐观锁缺点:1、ABA问题(加入版本号机制)。2、自旋CAS如果一直不成功,开销大。3、只对单变量有效,当涉及多个共享变量时,无效。
乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compare and Swap。CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。CAS是通过硬件命令保证了原子性。

悲观锁:每次访问数据的时候都会认为其他线程会修改数据,所以先获取锁,再访问数据。synchronized和ReentrantLock都是悲观锁思想的实现。
Synchronized关键字三种实现方式:
修饰实例方法,对当前实例对象加锁,进入同步代码前要获取对象实例的锁。修饰静态方法,对当前类对象加锁,修饰代码块,指定加锁对象,给对象加锁。
具体实例,双重校验锁实现对单例模式;
Synchronized同步的实现,是基于进入退出监视器Monitor对象实现的,无论是同步代码块还是同步方法,都是如此;同步代码块,是根据monitorenter 和 monitorexit 指令实现的,同步方法,是通过设置方法的 ACC_SYNCHRONIZED 访问标志;监视器Monitor对象存在于每个对象的对象头中。

synchronized和ReenTrantLock的区别:
两者都是可重入锁(自己可以再次获取自己的内部锁),锁计数器加1;
synchronized只能是是非公平锁,而ReenTrantLock默认实现非公平锁,也支持公平锁(先等先得)
synchronized依赖于JVM实现,而ReenTrantLock是基于JDK实现的;
ReenTrantLock功能加多:1、等待可中断,2、支持公平锁,3、基于Condition实现选择性唤醒;
synchronized经过一系列的优化,性能已得到大大的提升,和ReenTrantLock相差无几。

ReentrantLock,可重入互斥锁,独占锁。互斥锁:同一时间只能被一个线程持有。可重入锁:可以被单个线程多次获取。ReentrantLock支持公平锁和非公平锁,Synchronized只支持非公平锁。公平锁:线程依次排队获取锁。非公平锁:不管是不是队头都能获取。公平锁和非公平锁,它们尝试获取锁的方式不同:公平锁在尝试获取锁时,即使“锁”没有被任何线程锁持有,它也会判断自己是不是CLH等待队列的表头;是的话,才获取锁。而非公平锁在尝试获取锁时,如果“锁”没有被任何线程持有,则不管它在CLH队列的何处,它都直接获取锁。
公平锁要维护一个队列,后来的线程要加锁,即使锁空闲,也要先检查有没有其他线程在 wait,如果有自己要挂起,加到队列后面,然后唤醒队列最前面的线程。这种情况下相比较非公平锁多了一次挂起和唤醒。
线程切换的开销,其实就是非公平锁效率高于公平锁的原因,因为非公平锁减少了线程挂起的几率,后来的线程有一定几率逃离被挂起的开销。

JDK1.6引入了大量的锁优化:偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术减少开销。
锁主要存在4种状态:无琐状 态,偏向锁状态,轻量级锁状态,重量级锁状态 。锁可升级不可降级,提供获取锁和释放锁的效率。
自旋锁:进程进入阻塞的开销很大,为防止进入阻塞状态,在线程请求共享数据锁的时候循环自旋一段时间,如果在这段时间内获取到锁,就避免进入阻塞状态了。
1.6引入自适应自旋锁,自旋次数不再固定:由锁拥有者状态和上次获取锁的自旋次数决定。
锁消除:对于被检测出不可能存在竞争的共享数据的锁进行消除。(逃逸分析)
锁粗化:虚拟机探测到一系列连续操作都对同一个对象加锁解锁,就将加锁的范围粗化到整个操作系列的外部。
偏向锁:当锁对象第一次被线程获取的时候,进入偏向状态,标记为101,同时CAS将线程ID进入到对象头的Mark Word中,如果成功,这个线程以后每次获取锁就不再需要进行同步操作,甚至CAS不都需要。当另一个线程尝试获取这个锁,偏向状态结束,恢复到未锁定状态或者轻量级状态。
轻量级锁:对象头的内存布局Mark Word,有个tag bits,记录了锁的四种状态:无琐状态,偏向锁状态,轻量级锁状态,重量级锁状态.轻量级锁相对重量级锁而言,使用CAS去避免重量级锁使用互斥量的开销。线程尝试获取锁时,如果锁处于无琐状态,先采用CAS去尝试获取锁,如果成功,锁状态更新为轻量级锁状态。如果有两条以上的线程争用一个锁,状态重为重量级锁。
Java内存模型:数据主要存放在主内存中,为了加快对内存的数据处理,在cpu和内存中间,加入了寄存器,存储器,高速缓存等处理器缓存;从多线程角度,可以把内存模型简化成:主内存和线程本地内存。线程可以把变量从主内存读取到本地缓存中,然后再本地缓存中进行读写,然后将改变结果写入到内存中。这就导致线程本地内存和主内存数据不一致的情况。即可见性。volatile可解决这个问题。
Volatile变量在汇编阶段,会多出一条lock前缀指令,这会导致当前处理器缓存的数据写回到系统内存中,且让其他改数据的处理器缓存失效。这就保障了可见性
Volatile的修饰的变量,虚拟机会使用内存屏障禁止指令重排序保障其有序性。但volatile变量不能保证其原子性。所以volatile是线程同步的轻量级实现,性能好,多线程访问volatile变量不会发生阻塞,volatile变量主要用于变量在多线程之间的可见性。但并不能保障原子性,不可替代synchronized.synchronized解决的是多线程之间访问共享资源的同步性。
ThreadLocal,线程本地存储,提供了一个线程局部变量,让访问某个变量的线程都拥有自己的线程局部变量,这样线程对变量的访问就不存在竞争问题,也不需要同步。每个线程都有一个ThreadLocal.ThreadLocalMap对象。map的键是ThreadLocal t = new ThreadLocal(),值是value。ThreadLocalMap的Entry节点的key指向ThreadLocal是弱引用,虚拟机只要发现就可以垃圾回收。
Thread -> ThreadLocalMap -> Entry<ThreadLocal,value> ---> ThreadLocal(key弱引用到ThreadLocal);
所以ThreadLocal设置为null,ThreadLocal只有弱引用,可以被回收,但value存在强引用,不能被回收。
如何避免?事实上,ThreadLocalMap的get,set方法中,会对key(ThreadLocal)进行null判断,如果为null,value也设置为null.也可以手动条调用ThreadLocal.remove()方法。

AQS与JUC
AQS(AbstactQueuedSynchronizer)同步队列器,是一个构建锁和同步器的框架,使用AQS能够简单有效的构造出应用广泛的大量同步器。如ReentrantLock, Semphore>
AQS原理:如果被请求的共享资源空闲,则将当前请求线程设为有效的工作线程,并且将共享资源设置为锁定状态。如果请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的。即将暂时获取不到的线程放入队列中。CLH,是虚拟的双向队列,即不存在队列实例,仅存在节点与节点之间的pre和next关系。AQS将每条请求共享资源的线程封装成一个CLH锁队列的一个节点来实现锁的分配。
AQS属性(Node head, Node tail, int state(这个是最重要的,代表当前锁的状态,0代表没有被占用,大于 0 代表有线程持有当前锁), Thread 持有独占锁的线程);
等待队列中每个线程被封装为一个Node实例(thread + waitStatus(-1: 当前node的后继节点对应的线程需要被唤醒,) + pre + next);
State:表示当前锁的状态,等于0时,表示没有被线程占用。当大于0时,表示被线程占用。
Node节点的属性 watiStatus:默认为0,当大于0时,表示放弃等待,ReentrantLock是可以指定timeouot的。等于-1,表示当前node的后继节点对应的线程需要被唤醒。当等于-2时,标志着线程在Condition条件上等待的线程唤醒。等于-3时,用于共享锁,标志着下一个acquireShared方法线程应该被允许。
公平锁,只有处于队头的线程才被允许去获取锁。非公平性锁模式下线程上下文切换的次数少,因此其性能开销更小。公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。

AQS组件:
AQS类别:独占锁和共享锁。
共享锁有ReentrantReadWriteLock、CountDownLatch、CyclicBarrier、Semaphore:
ReadWriteLock,读写锁。维护了一对相关联的读取锁和写入锁。读取锁用于只读操作,共享。写入锁用于写入操作, 是独占锁。不能同时存在读取锁和写入锁。
CountDownLatch(lætʃ)是通过共享锁实现的;CountDownLatch doneSignal = new CountDownLatch(LATCH_SIZE);
CountDownLatch对象维护一个count,执行一次doneSignal.countDown()时,count减一直到count为0时, doneSignal.await()的等待线程才能运行。所以countDownLatch可以让一个线程等待一组线程完成之后才执行。
CyclicBarrier(ˈsaɪklɪk ˈbæriər) cb = new CyclicBarrier(SIZE);调用线程创建N个齐头并进的CyclicBarrier对象。每个线程执行cb.await()时,参与者数量加一,当参与者数量达到SIZE,阻塞的参与线程继续运行。
(01) CountDownLatch的作用是允许1或N个线程等待其他线程完成执行;而CyclicBarrier则是允许N个线程相互等待,直到到达某个公共屏障点.
(02) CountDownLatch的计数器无法被重置;CyclicBarrier的计数器可以被重置后使用,因此它被称为是循环的barrier。
Semaphore(ˈseməfɔːr) sem = new Semaphore(SEM_MAX);建立N个信号量。 sem.acquire(count);获取count个信号量。如果有就给予,没有就阻塞。sem.release(count);释放信号量。

JUC原子类 基本类型:AtomicInteger,AtomicLong,AtomicBoolean;数组类型AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray;引用类型,对象属性修改类型。在32位操作系统中,64位的long 和 double 变量由于会被JVM当作两个分离的32位来进行操作,所以不具有原子性;
原子类基本通过自旋CAS来实现,期望的值和现在的值是否一致,如果一致就更新。
public final boolean compareAndSet(long expect, long update) {
return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}
主要利用CAS+volatile + native方法来保证操作的原子性,从而避免同步方法的高开销。CAS原理是那期望的值和现在的值进行比较,如果相同则更新成新的值。

集合框架的多线程实现类:
CopyOnWriteArrayList(volatile数组来保持数据、增删改会新建数组,然后copy到volatile数组。只有查找效率高)线程安全机制是通过volatile和互斥锁实现的。增删改时,先获取互斥锁,然后创建新数组,复制到volatile数组中,释放锁。
1、CopyOnWriteArrayList相当于线程安全的ArrayList,它实现了List接口。CopyOnWriteArrayList是支持高并发的。
2、CopyOnWriteArraySet相当于线程安全的HashSet,它继承于AbstractSet类。CopyOnWriteArraySet内部包含一个CopyOnWriteArrayList对象,它是通过CopyOnWriteArrayList实现的。

1、ConcurrentHashMap是线程安全的哈希表(相当于线程安全的HashMap)
2、ConcurrentSkipListMap是线程安全的有序的哈希表(相当于线程安全的TreeMap)
3、ConcurrentSkipListSet是线程安全的有序的集合(相当于线程安全的TreeSet)

1、 ArrayBlockingQueu(kjuː)是数组实现的线程安全的有界的阻塞队列。
2、 LinkedBlockingQueue是单向链表实现的(指定大小)阻塞队列,该队列按 FIFO(先进先出)排序元素。
3、 LinkedBlockingDeque是双向链表实现的(指定大小)双向并发阻塞队列,该阻塞队列同时支持FIFO和FILO两种操作方式。
4、 ConcurrentLinkedQueue是单向链表实现的无界队列,该队列按 FIFO(先进先出)排序元素。
5、 ConcurrentLinkedDeque是双向链表实现的无界队列,该队列同时支持FIFO和FILO两种操作方式。

线程池
线程池则是为了减少线程建立和销毁带来的性能消耗。线程池的使用可以帮助我们更合理的使用系统资源.
Executors是个静态工厂类。它通过静态工厂方法返回ExecutorService、ScheduledExecutorService、ThreadFactory 和 Callable 等类的对象。
Executor ɪɡˈzekjətər只提供了execute()ˈeksɪkjuːt接口来执行已提交的 Runnable 任务的对象。
ExecutorService接口,继承Executor,AbstractExecutorService抽象类实现了ExecutorService接口。 ThreadPoolExecutor,真正线程池实现类。
ThreadPoolExecutor是线程池类。对于线程池,可以通俗的将它理解为"存放一定数量线程的一个线程集合。线程池允许若个线程同时运行;
当添加的到线程池中的线程超过它的容量时,会有一部分线程阻塞等待。线程池会通过相应的调度策略和拒绝策略,对添加到线程池中的线程进行管理。
关闭线程池:调用线程池的shutdown()接口时,线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
调用线程池的shutdownNow()接口时,线程池切换为STOP状态,不接收新任务,不处理已添加的任务,并且通过调用每个线程的interrupt()方法尝试中断正在处理的任务。
一定要通过 ThreadPoolExecutor(xx,xx,xx…) 来明确线程池的运行规则,指定更合理的参数。
ThreadPoolExecutor线程池的7大参数:
kɔːr corePoolSize:核心池的大小:创建线程池之后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;调用了prestartAllCoreThreads()或者prestartCoreThread()方法,可以在任务来之前预创建线程。核心线程会一直存活,及时没有任务需要执行;
设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭;
maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
keepAliveTime:非核心线程的最大空闲时间。
TimeUnit:空闲时间的单位。
BlockingQueue workQueue :等待执行的任务队列,队列分为有界队列和无界队列。有界队列:队列的长度有上限,当核心线程满载的时候,新任务进来进入队列,当达到上限,有没有核心线程去即时取走处理,这个时候,就会创建临时线程。(警惕临时线程无限增加的风险)
无界队列:队列没有上限的,当没有核心线程空闲的时候,新来的任务可以无止境的向队列中添加,而永远也不会创建临时线程。(警惕任务队列无限堆积的风险)
ThreadFactory:线程工厂,用来创建线程
RejectedExecutionHandler:队列已满,而且任务量大于最大线程的异常处理策略

线程池属性属性:
largestPoolSize,记录了曾经出现的最大线程个数。因为setMaximumPoolSize()可以改变最大线程数。
poolSize:线程池中当前线程的数量。
当提交一个新任务时:
(1)如果poolSize<corePoolSize,新增加一个线程处理新的任务。无论是否有空闲的线程新增一个线程处理新提交的任务;
(2)如果poolSize=corePoolSize,新任务会被放入阻塞队列等待。
(3)如果阻塞队列的容量达到上限,且这时poolSize<maximumPoolSize,新增线程来处理任务。
(4)如果阻塞队列满了,且poolSize=maximumPoolSize,那么线程池已经达到极限,会根据饱和策略RejectedExecutionHandler拒绝新的任务。

  • 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
  • 如果allowCoreThreadTimeout=true,则会直到线程数量=0

workQueue:一个阻塞队列:ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous;
workQueue的类型为BlockingQueue,通常可以取下面三种类型:
  1)ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
  2)LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
  3)synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。

线程拒绝策略:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:
shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务

动态调整大小:
setCorePoolSize:设置核心池大小
setMaximumPoolSize:设置线程池最大能创建的线程数目大小

其他种类的:Executor框架的工具类Execuors来实现。
newFixedThreadPool:建立一个线程数量固定的线程池,规定的最大线程数量,超过这个数量之后进来的任务,会放到等待队列中,如果有空闲线程,则在等待队列中获取,遵循先进先出原则。corePoolSize 和 maximumPoolSize 要一致,Executors 默认使用的是 LinkedBlockingQueue 作为等待队列,这是一个无界队列。
newSingleThreadExecutor:建立一个只有一个线程的线程池,如果有超过一个任务进来,只有一个可以执行,其余的都会放到等待队列中,如果有空闲线程,则在等待队列中获取,遵循先进先出原则。使用 LinkedBlockingQueue 作为等待队列。等待队列无限长的问题,容易造成 OOM。
newCachedThreadPool:缓存型线程池,在核心线程达到最大值之前,有任务进来就会创建新的核心线程,并加入核心线程池,即时有空闲的线程,也不会复用。达到最大核心线程数后,新任务进来,如果有空闲线程,则直接拿来使用,如果没有空闲线程,则新建临时线程。并且线程的允许空闲时间都很短,如果超过空闲时间没有活动,则销毁临时线程。关键点就在于它使用 SynchronousQueue 作为等待队列,它不会保留任务,新任务进来后,直接创建临时线程处理。容易造成无限制的创建线程,造成 OOM。
newScheduledThreadPool:计划型线程池,可以设置固定时间的延时或者定期执行任务,同样是看线程池中有没有空闲线程,如果有,直接拿来使用,如果没有,则新建线程加入池。使用的是 DelayedWorkQueue 作为等待队列,这中类型的队列会保证只有到了指定的延时时间,才会执行任务。容易造成无限制的创建线程,造成 OOM。

线程池大小如何设置?
对于计算密集型的任务,一个有Ncpu个处理器的系统通常通过使用一个Ncpu + 1个线程的线程池来获得最优的利用率.
对于包含了 I/O和其他阻塞操作的任务,不是所有的线程都会在所有的时间被调度,因此你需要一个更大的池。2*Ncpu
  Nthreads = Ncpu x Ucpu x (1 + W/C),其中
  Ncpu = CPU核心数
  Ucpu = CPU使用率,0~1
W/C = 等待时间与计算时间的比率
线程数 = Ncpu /(1 - 阻塞系数)= Ncpu x (1 + W/C)
线程数设置过大有什么缺点:
如果maxsize过大会占用更多资源,cpu会频繁地进行上下文切换,会导致cpu缓存的数据失效和重新加载,结果就是上下文切换和reload的时间变多,也就是说cpu将更多的时间花费到对线程的管理上去了,这时候更多的线程反而更慢

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值