线程和进程

JUC

java.util.concurrent工具包的简称,专门用来处理线程,JDK1.5出现。

线程和进程的区别(并发,管程)

程序:是为完成特定任务、用某种语言编写的一组指令的集合,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。

进程:是程序的一次执行过程,是系统进行资源分配和调度的基本单位。

线程:是一个程序内部的一条执行路径。是操作系统能够进行运算调度的最小单位。程序执行的最小单位。进程可以细化为多个线程。

线程和进程最⼤的不同在于基本上各进程是独⽴的,⽽各线程则不⼀定,因为同⼀进程中的线程极有可能会相互影响。从另⼀⻆度来说,进程属于操作系统的范畴,主要是同⼀段时间内,可以同时执⾏⼀个以上的程序,⽽线程则是在同⼀程序内⼏乎 同时执⾏⼀个以上的程序段。线程执⾏开销⼩,但不利于资源的管理和保护;⽽进程正相反。

线程共享进程的内存空间。用户级线程与内核无关

并行:多项任务一起执行,之后再汇总。

两个线程互不抢占CPU资源,可以同时进行

并发:同一时刻多个线程访问同一资源,多个线程对一个点。

把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。

管程: Monitor监视器 就是锁

是一种同步机制,保证同一时间内,只有一个线程访问被保护的数据或者代码。

用户线程和守护线程

用户线程:自定义线程

守护线程:运行在后台,比如垃圾回收

主线程结束了,用户线程还在运行,jvm存活;没有用户线程了,都是守护线程,jvm结束。

集合的线程安全问题

ArrayList

线程不安全,add()上没加synchronized关键字

解决方案:

Vector;Collections类中synchronizedList(List <T> list);这两种比较古老,无论读还是写,他们两个都会给整个集合加锁,导致同一时间的其他操作阻塞。

JUC下的CopyOnWriteArrayList类;

涉及的底层原理为写时复制技术

  • 读的时候并发(多个线程操作)

  • 写的时候独立,先copy出一个副本,再往新的副本里添加这个新的数据,最后把新的副本的引用地址赋值给了之前那个旧的副本地址,但是在添加这个数据的期间,其他线程如果要去读取数据,仍然是读取到旧的容器里的数据。

  • 只能保证数据的最终一致性,不能保证数据的实时一致性

HashSet

synchronizedSet,JUC下的CopyOnWriteArraySet

HashMap

HashTable;Collections.synchronizedMap();性能差

JUC下的ConcurrentHashMap

进行写操作时,它会锁住一小部分,其他部分的读写不受影响,其他线程访问没上锁的地方不会被阻塞。

创建线程的四种方式对比

1.继承Thread类的方式创建多线程

好处:编写简单,访问当前线程无需使用Thread.currentThread()方法,直接使用this

缺点:线程类继承了Thread类,无法继承其他父类。

2.实现Runnable、Callable接口的方式

实现的方式没有类的单继承性的局限性;在这种方式下,多个线程可以共享同一个target对象,更适合来处理多个线程有共享数据的情况。

如果要访问当前线程,则必须使用Thread.currentThread()方法

Runnable和Callable:

Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()
call()可以有返回值,Runnable的任务是不能返回值的
call()可以抛出异常,被外面的操作捕获,获取异常的信息
运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。

使用线程池

java通过Executors提供四种线程池

Executors.newFixedThreadPool(n); 创建一个指定大小的线程池,底层用LinkedBlockingQueue。

newSingleThreadExecutor(): 创建单个线程数的线程池,它可以保证先进先出的执行顺序

newCachedThreadPool():创建一个可缓存的线程池,可扩容

newScheduledThreadPool():创建一个支持定时任务的线程池

底层都new了ThreadPoolExecutor

好处:

提高响应速度(减少了创建新线程的时间);
降低资源消耗(重复利用线程池中线程,不需要每次都创建);
便于线程管理

线程池的参数(ThreadPoolExecutor)

int corePoolSize:常驻线程数量(核心)

int maximumPoolSize:最大线程数量(当队列中存放的任务达到队列容量的时候,当前可以同时运⾏的线程数量变为最⼤线程数。)

long keepAliveTime:线程存活时间

TimeUnit unit:keepAliveTime 参数的时间单位

BlockingQueue<Runnable> workQueue 阻塞队列(常驻线程用完后,再来请求会放入阻塞队列进行等待)

ThreadFactory threadFactory:线程工厂

RejectedExecutionHandler handler:拒绝策略

内置的四种策略
ThreadPoolExecutor.AbortPolicy抛出 RejectedExecutionException 异常来拒绝新任务的处理。
CallerRunsPolicy:将某些任务退回到调用者,从而降低新任务的流量。
DiscardOldestPolicy:丢弃最早的未处理的任务请求(队列中等待最久的任务)
DiscardPolicy:不处理新任务,直接丢弃

线程池的调优(针对任务的不同特性 + 建议使用有界队列)

线程池的工作流程

在创建了线程池后,等待提交过来的任务请求.
当调用execute()方法添加一个请求任务时,线程池就会做如下判断:
​
如果正在运行的线程数量小于corePoolSize,那么马上创建新的线程运行这个任务(即使有空闲线程也要创建)
如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务
如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行.
当一个线程完成任务时,它会从队列中取下一个任务来执行.
​
当一个非核心线程空闲超过一定的时间(keepAliveTime)时,线程池会判断:  (线程从工作队列里取任务时,可以加上超时限制)
如果当前运行的线程数大于corePoolSize,那么这个线程就会被停掉。所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小。
​
keepAliveTime是指当线程池中线程数量大于corePollSize时,此时存在非核心线程,keepAliveTime指非核心线程空闲时间达到的阈值会被回收。
​
核心线程为什么不会销毁?如果队列中没有任务时,小于核心数的线程会一直阻塞在获取任务的方法,直到返回任务。
核心线程通常不会回收,java核心线程池的回收由allowCoreThreadTimeOut参数控制,默认为false,若开启为true,keepAliveTime同样会作用于核心线程,只要其空闲时间达到keepAliveTime都会被回收。但如果这样就违背了线程池的初衷(减少线程创建和开销),所以默认该参数为false。
​
​

如何实现一个线程池

实现一个完整版的线程池,它必须具备如下功能 1.能够在执行任务中创建新的线程,在多余线程空闲时可以销毁多余线程。 2.能够使用不同的阻塞队列实现类达到不同的效果,如有界、无界、同步、定时(使用以时间为维度的优先队列实现),这个比较容易,依托于Java的面向接口变成即可,只需在使用时传入不同实现类。 3.能够有不同的拒绝策略,也自己创建一个接口,该接口的方法在任务超出容量时调用,然后实现类就可以实现不同的策略。

实际开发中,为什么不允许Excutors.的方式手动创建线程池

线程池不允许使用Excutors去创建,而是通过new ThreadPoolExecutor()的方式,避免资源耗尽的风险。自定义线程池

Executors返回的线程池对象的弊端:

FixedThreadPool和SingleThreadPool允许的队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM

CachedThreadPool和ScheduledThreadPool允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM

execute() vs submit()

execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;

submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功

shutdown() VS shutdownNow()

shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。

shutdownNow():关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的线程列表List<Runnable>。

isTerminated() VS isShutdown()

isShutDown 当调用 shutdown() 方法后返回为 true。

isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true

为什么要使用多线程

从计算机底层来说,线程可以认为是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核CPU时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。

从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。

在单核时代多线程主要是为了提高CPU和IO设备的综合利用率。当只有一个线程的时候会导致CPU计算时,IO设备空闲;进行IO操作时,CPU空闲。当有两个线程的时候就不一样了,当一个线程执行CPU 计算时,另外一个线程可以进行IO操作
​
多核时代多线程主要是为了提高CPU利用率。只用一个线程的话,CPU只会有一个CPU核心被利用到,而创建多个线程就可以让多个CPU核心被利用到,这样就提高了CPU的利用率。

线程的生命周期

新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)

新建:当线程对象创建后,进入新建状态 就绪:调用线程对象的start()方法后进入,处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行。 运行:当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注意:就绪状态是进入到运行状态的唯一入口。 阻塞:处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态。阻塞是一个临时状态,不可以作为最终状态。

阻塞有三种原因:
等待阻塞:运行状态中的线程执行wait()方法;
同步阻塞:线程获取synchronized同步锁失败(锁被其他线程占用);
其他阻塞:通过调用线程的sleep()或join()或发出I/O请求时,线程会进入阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

死亡:最终状态,线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

不推荐使用stop方法来终止线程

停止线程的方法

正常运行结束:这个也是最常见的,指线程体执行完成,线程自动结束

1.Thread.stop()不推荐

thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用 thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。

2.使用退出标志退出

在一般情况下,在 run 方法执行完毕的时候,线程会正常结束。

可以在run()方法内部加上该线程的interrupted状态判断,若为true,则可以用return直接退出run()方法。

3.使用 Interrupt 方法终止线程

在调用线程的 interrupt 方法时,会抛出 InterruptException 异常,我们通过在代码中捕获异常,然后通过 break 跳出状态监测循环,结束这个线程的执行。

线程间如何进行通信

1.线程之间可以通过共享内存或基于网络来进行通讯。

基于 volatile关键字来实现线程间相互通信是使用共享内存的思想。大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。这也是最简单的一种实现方式.

2.如果是通过共享内存来进行通讯,则需要考虑并发问题,什么时候阻塞什么时候唤醒

像java中的wait()、notify()就是阻塞和唤醒

3.使用JUC工具类 CountDownLatch同步计数器

4.通过网络就比较简单了,通过网络连接将通信数据发送给对方,当然也要考虑并发问题,处理方式就是加锁等方式

java实现线程间通信的四种方式

synchronized同步

共享内存的方式,由于线程A和线程B持有同一个对象,尽管这两个线程需要调用不同的方法,但是它们是同步执行的,比如:线程B需要等待线程A执行完了methodA()方法之后,它才能执行methodB()方法。这样,线程A和线程B就实现了通信。

while轮询

其实就是多线程同时执行,会牺牲部分CPU性能。线程A不断地改变条件,线程ThreadB不停地通过while语句检测这个条件(list.size()==5)是否成立 ,从而实现了线程间的通信。JVM调度器将CPU交给线程B执行时,它没做啥“有用”的工作,只是在不断地测试 某个条件是否成立。

wait/notify机制

线程A调用wait() 放弃CPU,并进入阻塞状态。线程B调用 notify()通知,唤醒线程A,并让它进入就绪状态。

管道通信

管道流主要用来实现两个线程之间的二进制数据的传播

ThreadLocal是什么?

ThreadLocal是除了加锁这种同步方式之外的一种规避多线程访问出现线程不安全的方法)

ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部。

如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。

在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不同的变量值完成操作的场景。

简单说ThreadLocal就是一种以空间换时间的做法,在每个线程Thread里面维护了一个以开地址法实现的 ThreadLocal.ThreadLocalMap,(以当前线程的ThreadLocal的hash值作为key,变量作为value)把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。(Thread类中的threadLocals也是一个map)

经典应用场景:连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享同一个连接)

存在的问题

如果在线程池中使用ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该要把设置的key,value即Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏。

(内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果)

解决办法是:1.使用了ThreadLocal后,手动调用ThreadLocal的remove方法,手动清除Entry对象。2.key指向ThreadLocal的引用使用弱引用。

yield()方法

它是Thread类中的一个静态方法

Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。 但是只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。

sleep() 方法和 wait() 方法

都可以阻塞线程

sleep方法:是Thread类的静态方法,当前线程将睡眠n毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进入可运行状态,等待CPU的到来。睡眠不释放锁。

wait方法:是Object的方法,必须与synchronized关键字一起使用,线程进入阻塞状态,当notify或者notifyall被调用后,会解除阻塞。但是,只有重新占用互斥锁之后才会进入可运行状态。睡眠时,会释放互斥锁。

sleep 方法没有释放锁,而 wait 方法释放了锁

sleep 通常被用于暂停执行,Wait 通常被用于线程间交互/通信

sleep() 方法执行完成后,线程会自动苏醒;wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法

调用 start() 方法时会执行 run() 方法,为什么不能直接调用 run() 方法

因为类Thread中的start方法中,调用了Thread中的run方法

调用run()方法是为了实现多线程。

new 一个 Thread,线程进入了新建状态; 调用start() 会执行线程的相应准备工作,进入就绪状态,然后自动执行 run() 方法的内容。启动了一个子线程,这是真正的多线程工作

如果我们直接调用子线程的run()方法,其方法还是运行在main主线程中,代码在程序中是顺序执行的,并不会在某个线程中执行它,就不是多线程了。

start( )与run( )之间有什么区别?

run()方法:在本线程内调用该Runnable对象的run()方法,可以重复多次调用; start()方法:启动一个线程,调用该Runnable对象的run()方法,不能多次启动一个线程

什么是线程死锁?如何避免?

死锁:

死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

死锁发生的原因

死锁的发生是由于资源竞争导致的,导致死锁的原因如下:

  • 系统资源不足,如果系统资源充足,死锁出现的可能性就很低。

  • 进程(线程)运行推进的顺序不合适。

  • 资源分配不当等。

死锁必须具备以下四个条件

互斥条件:该资源任意一个时刻只由一个线程占用。 
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 
不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。 
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

如何避免线程死锁?

只要破坏产生死锁的四个条件中的其中一个就可以了

互斥条件没有办法破坏,因为我们用锁本来就是想让他们互斥的。

破坏请求与保持条件: 一次性申请所有的资源。

破坏不剥夺条件: 占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

破坏循环等待条件: 靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

锁排序法:(必须回答出来的点)

对所有锁按照一定规则进行排序,所有线程在申请锁之前均按照先后顺序进行申请,以此来消除“循环等待资源”这个条件,从而来规避死锁

使用显式锁中的ReentrantLock.try(long,TimeUnit)来申请锁

怎么定位死锁

1.通过jstack定位死锁信息

jstack用于生成JVM当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。

  • jps查看发生死锁的进程pid (jps是jdk提供的一个查看当前java进程的小工具)

  • 使用 jstack 【-l】 pid 查看死锁信息( jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息)

    a. 针对活着的进程做本地的或远程的线程dump; 
    b. 针对core文件做线程dump。
  • 通过打印信息我们可以找到发生死锁的代码是在哪个位置。具体哪个进程的哪个线程,哪个类,哪一行发生死锁

2.通过Arthas(阿尔萨斯)工具定位死锁

Alibaba开源的Java诊断工具

下载好Arthas的jar,然后运行

有一个 thread -b 就可以查看到死锁信息

  1. 通过 Jvisualvm 定位死锁

Jvisualvm ((Java VisualVM)是一种自带的可视化工具,往往在在本地执行。JVM性能监测工具

通过 Jvisualvm 命令打开软件,选中进程,进入线程视图,会给出死锁提示:

什么叫可重入锁?哪些是重入锁?

在一个线程中可以多次获取同一把锁

同一个线程再次进入同步代码的时候.可以使用自己已经获取到的锁,这就是可重入锁。

当某个线程想要请求一个被其他线程持有的锁时,就只能进入阻塞状态,等待着这个锁被释放;但是设想一下,如果这个锁是被这个线程自己持有,然后这个线程再次请求获取这个锁,就不会被阻塞。

可重入锁的简单实现逻辑:
为每一个锁关联一个持有者线程和计数值,当计数值为0时,意味着这个锁没有被任何线程持有,而如果有线程请求获取一个没有被持有的锁,JVM会把这个线程记录下来作为持有者线程,同时计数值加一,如果是相同的线程再次获取锁,那么这个计数值会继续加一,当线程退出同步代码块时,计数值就会减一,当减到0时,这个锁就会被释放。

有哪些可重入锁

隐式锁:同步代码块、同步方法

显式锁:ReentrantLock

synchronized和ReentrantLock 的区别

两者都是可重入锁

可重入锁:重入锁,也叫做递归锁,可重入锁指的是在一个线程中可以多次获取同一把锁

比如: 一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁。

两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

synchronized是关键字,ReetrantLock是一个类。

synchronized锁的是对象,锁信息保存在对象头中,ReetrantLock通过代码中int类型的state标识锁的状态。

1.synchronized机制在执行完相应的同步代码后,自动的释放同步监视器

Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock()),如果没有主动释放锁,有可能导致死锁

2.synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API(JDK)

ReentrantLock是Lock接口的一个实现类

ReentrantLock需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成

3.ReentrantLock 比 synchronized 功能更强大

①等待可中断;

正在等待的线程可以选择放弃等待,改为处理其他事情。

可实现公平锁

所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的 ReentrantLock(boolean fair)构造方法来制定是否是公平的。

③可实现选择性通知(锁可以绑定多个条件)

ReentrantLock类线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活

4.大量线程同时竞争时,Lock性能远远优于sychronized

4.使用选择除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。

并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放

公平锁和非公平锁

公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。

  • 优点:所有的线程都能得到资源,不会饿死在队列中。

  • 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。

非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

  • 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。

  • 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

自旋锁

自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋), 等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

线程自旋是需要消耗cpu 的,如果一直获取不到锁,那线程也不能一直占用 cpu自旋做无用功,所以需要设定一个自旋等待的最大时间。 如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

自旋锁的优缺点

自旋锁会尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能可以 大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换! 但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了。线程自旋的消耗大于线程阻塞挂起操作的消耗, 其它需要 cpu 的线程又不能获取到 cpu,造成 cpu 的浪费。

锁升级

synchronized锁本质是一个对象锁,锁信息保存在对象头中。在运行期间对象头里Mark Word(对象标记)的数据会随着锁标志位的变化而变化,有四种状态。

CAS(无锁操作):使用CAS叫做比较交换来判断是否出现冲突,出现冲突就重试当前操作直到不冲突为止。

锁可以升级不能降级,目的是为了提高获得锁和释放锁的效率。

无锁

当一个对象被创建之后,还没有线程进入,这个时候对象处于无锁状态。

偏向锁

当锁处于无锁状态时,有一个线程A访问同步块并获取锁时,会在对象头和栈帧中的锁记录记录线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来进行加锁和解锁,只需要简单的测试一下对象头中的线程ID和当前线程是否一致。

只会在第一次请求时采用CAS操作,省去了大量有关锁申请的操作。

轻量级锁

在偏向锁的基础上,又有另外一个线程B进来,这时判断对象头中存储的线程A的ID和线程B不一致,就会使用CAS竞争锁,并且升级为轻量级锁,会在线程栈中创建一个锁记录(lock Record),将Mark Word复制到锁记录中,然后线程尝试使用CAS将对象头的Mark Word替换成指向锁记录的指针,如果成功,则当前线程获得锁;失败,表示其他线程竞争锁,当前线程便尝试CAS来获取锁。

解锁时,会使用CAS将复制的mark word替换回对象头,如果成功,表示没有竞争发生,正常解锁。如果失败,表示当前锁存在竞争,进一步膨胀为重量级锁。

竞争不是很激烈,等待锁的线程会自旋,不释放CPU。

重量级锁

当线程没有获得轻量级锁时,线程会CAS自旋来获取锁,当一个线程自旋10次之后,仍然未获得锁,那么就会升级成为重量级锁。说明竞争比较激烈。

成为重量级锁之后,线程会进入阻塞队列,线程不再自旋获取锁,而是由CPU进行调度,线程串行执行。

static synchronized

synchronized与static synchronized 的区别

synchronized是对类的当前实例进行加锁,防止其他线程同时访问该类的该实例的所有synchronized块。

static synchronized是类锁(即Class本身,注意:不是实例), 作用范围是整个静态方法,作用的对象是这个类的所有对象。即一个类的所有静态方法公用一把锁。

同一个实例的synchronized域不能被同时访问,因为所的是对象。

不同实例的static synchronized方法不能被同时访问,因为一个类的所有静态方法公用一把锁。

static synchronized和实例的synchronized可以被同时访问,synchronzied的是实例方法与synchronzied的类方法由于锁定(lock)不同的原因。

synchronized 与static synchronized 相当于两帮派,各自管各自,相互之间就无约束了,可以被同时访问。

并发编程的三要素

原子性:不可分割的操作,多个步骤要保证同时成功或同时失败

有序性:程序执行的顺序和代码的顺序保持一致

可见性:一个线程对共享变量的修改,另一个线程能立马看到。

volatile的使用及其原理

volatile的两层语义:

1、volatile保证变量对所有线程的可见性:当volatile变量被修改,新值对所有线程会立即更新。 在使用了volatile修饰成员变量后,所有线程在任何时候所看到变量的值都是相同的。

Java内存模型规定,对于多个线程共享的变量,存储在主内存当中,每个线程都有自己独立的工作内存(比如CPU的寄存器),线程只能访问自己的工作内存,不可以访问其他线程的工作内存。
工作内存中保存了主内存共享变量的副本,线程要操作这些共享变量,只能通过操作工作内存中的副本来实现,操作完毕之后再同步回到主内存当中。

volatile修饰的变量,在每个读操作(load操作)之前都加上Load屏障,强制从主内存读取最新的数据。每次在assign赋值后面,加上Store屏障,强制将数据刷新到主内存。 通过一个屏障让volatile的变量每次读都读主存,每次修改后立即刷到主存里面。

2、jdk1.5以后volatile完全避免了指令重排优化,实现了有序性。

volatile的禁止重排序是使用了内存屏障。内存屏障就是在需要禁止重排序的前后加入相关指令,我是这样理解的:CPU会因为优化对相关指令重排序,或者流水线方式的话存在并行执行,加入内存屏障,就是告诉CPU,不用下功夫优化了,一步步按顺序执行。所以禁止重排序,一些优化就用不上了,性能肯定有下降。

在两个或者更多的线程需要访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,没必要使用volatile。

需要注意的是,由于volatile不能保证操作的原子性,因此,一般情况下volatile不能代替sychronized。

synchronized 关键字和 volatile 关键字的区别?

1、volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。

2、volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。实际开发中使用 synchronized 关键字的场景还是更多一些。

3、多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞

4、volatile关键字能保证数据的可见性,但不能保证数据的原子性。(一旦需要对变量进行自增这样的非原子操作,就不会保证这个变量的原子性)synchronized关键字两者都能保证

5、volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。

JMM(Java内存模型)

Java内存模型,就是在底层处理器内存模型的基础上,定义自己的多线程语义。

Java内存模型规定,对于多个线程共享的变量,存储在主内存当中,每个线程都有自己独立的工作内存(比如CPU的寄存器),线程只能访问自己的工作内存,不可以访问其他线程的工作内存。

工作内存中保存了主内存共享变量的副本,线程要操作这些共享变量,只能通过操作工作内存中的副本来实现,操作完毕之后再同步回到主内存当中。

为了屏蔽掉各种硬件和操作系统的内存访问差异,通过JMM来实现让Java程序在各种平台下都能达到一致的并发效果。并发编程的三个特性:可见性、原子性、有序性。JMM主要解决这三个问题,实现缓存一致性。

可见性:可见别人的修改,A改了共享变量, B知道A改了,并把自己工作内存更新了。 原子性:一组操作要么全做,要么全不做。 有序性:源代码–>编译器优化的重排–> 指令并行也可能会重排–> 内存系统也会重排—> 执行处理器在进行指令重排的时候,考虑:数据之间的依赖性!也就是单线程没问题,但是多线程之间可能会互相依赖,不可重排。

JMM可以通过

volatile - 保证可见性和有序性

synchronized - 保证可见性和有序性; 通过管程(Monitor)保证一组动作的原子性

1.synchronized的可见性

JMM关于synchronized的两条规定:

  1)线程解锁前,必须把共享变量的最新值刷新到主内存中

  2)线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值

   (注意:加锁与解锁需要是同一把锁)

 线程A和B竞争锁资源,线程A先拿到锁进入方法修改共享变量,在解锁前会将当前工作内存的变量写会主内存,然后释放锁资源;线程B在获取锁后,会清空当前工作内存,重新从主内存中拷贝变量副本,从而实现可见性。

2 synchronized的原子性

原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束。synchronized底层由于采用了字节码指令monitorenter和monitorexit来隐式地使用这lock和unlock两个操作,使得其操作具有原子性。

3 synchronized的有序性

 根据前面也知道,volatile的有序性表现在禁止指令重排。而synchronized有序性表现在as-if-serial语义,但as-if-serial语义不能确保多线程情况下的禁止指令重排。如单例中的双重检验锁写法:

Fork/Join分支合并框架

Java7提供了的一个用于并行执行任务的框架

Fork:把一个复杂任务拆分

Join:把拆分任务的结果合并

countdownlatch和cyclicbarrier

juc下的并发工具类

  1. CountDownLatch 的计数器是大于或等于线程数的,而CyclicBarrier是一定等于线程数

  2. CountDownLatch 放行由其他线程控制而CyclicBarrier是由本身来控制的

countdownlatch:允许一个获多个线程等待其他线程完成操作。

cyclicbarrier:可循环使用的屏障,让一组线程到达一个屏障时被阻塞,直到最后一个到达才会开门。

AQS

给代码加锁,是java中处理并发问题的重要手段。

java中的很多锁都是基于抽象类AQS(AbstractQueuedSynchronizer)实现的。

AQS就是一个并发包的基础组件,用来实现各种锁,各种同步组件的。

AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。

CAS

要说Java中的CAS,还是要先说一下自旋锁,有人认为二者是同一样东西,但我认为CAS操作是实现自旋锁的一部分. 在锁的基础上,未进入临界区的线程应该处于一种怎样的状态,有两种情况,一是阻塞自身线程,等待释放锁,这明显就是互斥锁,第二种是不阻塞自身,而是在一个循环中等待释放锁,而且一直访问查看是否释放锁.这就是自旋锁. 自旋锁的好处是避免了多余的线程上下文切换,执行速度快,缺点也和这个有关系,如果一个线程持有锁太久,导致其他锁一直循环等待,浪费了CPU. Java中CAS操作,也就是compare and swap,即比较,如果满足期待,就完成设置或交换.具体的方法就是Unsafe包下面的一系列compareAndSwap方法,也就是各种原子类也提供了conpareAndSet方法.有一点需要注意,CAS其实是无锁的,它只是抱着一种乐观的态度,认为自己可以完成任务,但是多个线程同时对一个变量进行CAS操作,只有一个操作可以完成.未完成的线程不会挂起,只是收到一个通知,告知失败.说它是无锁,是指硬件层面是无锁的,CAS操作只是一个的指令. ABA问题: 是指在一个线程进行CAS修改初始值为A的变量,这个操作的目的是将A换成B,如果最后成功换成了B,那能代表程序的执行就是正确的吗?是不行的,如果在获取A这个状态后,有其他线程将A换成B,又换成A,然后原来的线程继续进行,将A换成B.如果只是字面量还可以,但是如果这里的A是一种状态呢,就可能有别的问题. 解决ABA问题,只要规定状态变换的方向不可逆就可以了,或者给每一个状态添加一个时间戳等等. CAS和自旋锁,上面并没有说清楚,我说CAS是实现自旋锁的一部分,也说了自旋锁的定义,但怎么个实现法.其实很简单,我们将CAS操作放在一个循环条件中,将这个循环分别作为加锁和释放锁的操作逻辑.这里的锁只是逻辑上的,硬件层面没有锁.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值