start方法和run()方法的区别:
start()方法来启动线程;
run() 线程体;
java的锁升级条件
如何多线程打印ABC
通过两个锁(不推荐,可读性和安全性比较差)
1、如何保证线程轮流打印
使用sychcronize方法多每个线程上锁,设置上一个对象和当前对象锁,每次执行打印任务,需要同时获取到这两个锁资源,才可以执行打印功能;线程的开启,需要按顺序开启,每个开启,都需要主线程等待1s;在执行完之后,一次释放锁资源。
缺点:这种假设依赖于JVM中线程调度、执行的顺序,所以需要手动控制他们三个的启动顺序,即Thread.Sleep(100)。
2、使用reentrantLock和condition的配合,实现线程的控制
1、如何保证线程轮流打印
reentrantLock的condition可以对线程唤醒;方法中,设置当前的condition和nextcondition, condition的signal/signalall来唤醒;condition.await来释放锁并等待唤醒
3、通过一个锁和一个状态变量来实现(推荐)
关键:每个线程打印的状态位status用static修饰;使用while判断状态位,使用synchronize结合wait和notify对锁的释放和唤醒
多任务执行完再执行后面的任务
CyclicBarrier
countDown
future
本文介绍并发编程的基础知识:线程介绍,jmm模型,java的锁实现及其原理,线程池的参数配置,并发工具的介绍。
1、线程的介绍:
线程的创建方式有: 1、继承Thread类创建线程类;
2、实现Runnable接口;
3、实现Callable接口通过FutureTask包装器来创建Thread线程
4、使用ExecutorService、Callable、Future实现有返回结果的线程
实现runable接口、thread继承、实现callable接口、线程池
callable有返回值,会抛异常
callable需要搭配futuretask,将futuretask放入thread中,开启线程 futuretask.isdone() 判断是否完成; futuretask.get() 阻塞的方法
线程的一共有4种状态,分别是 新状态,可运行状态、运行状态、阻塞/等待/睡眠、
线程的状态图:
四种状态的解释:
1、新状态:线程对象已经创建,还没有在其上调用start()方法。
2、可运行状态:当线程有资格运行,但调度程序还没有把它选定为运行线程时线程所处的状态。当start()方法调用时,线程首先进入可运行状态。在线程运行之后或者从阻塞、等待或睡眠状态回来后,也返回到可运行状态
3、运行状态:线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。
4、等待/阻塞/睡眠状态:这是线程有资格运行时它所处的状态。实际上这个三状态组合为一种,其共同点是:线程仍旧是活的,但是当前没有条件运行。换句话说,它是可运行的,但是如果某件事件出现,他可能返回到可运行状态。
5、死亡态:当线程的run()方法完成时就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
本地方法栈服务的对象是JVM执行的native方法,而java方法栈服务的是JVM执行的java方法。
每个线程都有自己的线程栈
2.堆栈(也就是平时所说的栈stack):用来存放基本数据类型和引用数据类型的实例的(也就是实例对象的在堆中的首地址,Person p = new Person; p存贮在堆栈中,值为@23651dff。还有就是堆栈是线程独享的。每一个线程都有自己的线程栈。
睡眠: Thread.sleep(longmillis)和Thread.sleep(long millis, int nanos)静态方法强制当前正在执行的线程休眠(暂停执行),以“减慢线程”。当线程睡眠时,线程没有释放资源,在苏醒之前不会返回到可运行状态。当睡眠时间到期,则返回到可运行状态。
阻塞的情况分三种: 等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池中”。进入这个状态后是不能自动唤醒的,必须依靠其他线程调用notify()或者notifyAll()方法才能被唤醒。
同步阻塞:运行的线程在获取对象的(synchronized)同步锁时,若该同步锁被其他线程占用,则JVM会吧该线程放入“锁池”中。
其他阻塞:通过调用线程的sleep()或者join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新回到可运行状态
sleep和wait的区别:
1、sleep不会交出锁对象
不释放已获取的锁资源,如果sleep方法在同步上下文中调用,那么其他线程是无法进入到当前同步块或者同步方法中的;wait方法释放对象资源
2、sleep可通过调用interrupt()方法来唤醒休眠线程。wait需要等待notify,notifyall,才会唤醒,进入可执行状态
3、sleep是作用于当前线程,wait是作用于对象本身
4、sleep不需要再同步方法内使用,wait需要在同步的上下文中调用
5、sleep是静态方法,wait是实例方法
threadlocal的原理和风险
ThreadLocal主要为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量 不同线程取到的变量副本都是由线程本身的提供的,存储在线程本身,只是借助ThreadLocal去获取,不是存放于 ThreadLocal 一旦在ThreadLocal的createMap函数中初始化之后,这个threadlocals就初始化了。以后每次ThreadLocal对象想要访问变量的时候,比如set函数和get函数,都是先通过getMap(Thread t)函数,先将线程的map取出,然后再从这个在线程(Thread)中维护的map中取出数据或者存入对应数据
在不同的线程的threadlocals变量中,都会有一个以你所声明的那个线程局部变量threadlocal作为键的key-value
如果线程的threadlocal存在强引用,就
ThreadLocal为什么会内存泄漏
ThreadLocal在ThreadLocalMap中是以一个弱引用身份被Entry中的Key引用的,因此如果ThreadLocal没有外部强引用来引用它,那么ThreadLocal会在下次JVM垃圾收集时被回收。这个时候就会出现Entry中Key已经被回收,出现一个null Key的情况,外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。此时value-->object是强引用的关系,但是此时的value的key已经为null了,如果不主动清除,就只能等到线程结束了,对应的object对象才有可能被删除 因此如果当前线程的生命周期很长,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread --> ThreadLocalMap-->Entry-->Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。
ThreadLocal如何防止内存泄漏?
每次使用完ThreadLocal,都调用它的remove()方法,清除数据。 在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就需要清理。
jmm{java内存模型}
以上是jmm的内存模型图 内存模型的运行规则: 1、对于多个线程共享的内存,都存放在主内存中; 2、线程不能直接操作主内存,只能操作工作内存读取的主内存副本; 3、线程间的通信,只能通过主内存,不可以直接传递数据; 4、对主内存的修改,只能通过修改工作内存,再通过工作内存写进主内存。
内存可见性:当一个线程在对某个对象状态修改时,其他线程可以获取这个信息,可以通过增加修饰符volatile实现;
volatile关键字: 1、保证变量的内存可见性(当一个线程对被volatile修饰的变量修改时,改线程的工作内存回立即更新值到主内存,并且让其他线程的副本值失效),其他线程需要重新到主内存读取 2、局部阻止重排序的发生
new AtomicInteger(0);原子自增方法,使用了自旋锁和cas来实现 volattile是java虚拟机提供的轻量级同步机制 保证可见性 不保证原子性 禁止指令重排序
cas: AtomicInteger,getAndIncrement,compareAndSet讲解: AtomicInteger atomicInteger = new AtomicInteger(5); atomicInteger.compareAndSet(5,2018); atomicInteger.getAndIncrement();
上面的方法是通过cas来实现的:
cas是compareAndSet的缩写:比较交换(先比较,再确定是否修改) AtomicInteger的值, 如果AtomicInteger=A,那么atomicInteger.compareAndSet(A,B)后,返回结果是true,AtomicInteger=B 如果AtomicInteger=C,那么atomicInteger.compareAndSet(A,B)后,返回结果是false,AtomicInteger=A AtomicInteger是主物理内存的值,atomicInteger.compareAndSet(A,B)
因为cas是通过unsafe类去修改内存的值,该事是所以线程都可见的,就是unsafe类是调用系统底层的资源执行。
jvm通过汇编语言去操作内存地址的值,(类似于通过机器语言的有序性实现了分布式锁)
下图解释原子性自增的原理:
使用cas保证数据的原子性,通过while方法,将内存的最新值和原始获取的值对比,如果没修改,就直接执行加操作,否则重新获取内存的最新值再执行加操作。这里的核心是通过cas对内存执行操作。(也就是while不断地地去刷新比较,直到值没有改变才去更新),如果有10个线程并发修改,也就最多执行10即可修改到值
cas的缺点:(通过牺牲cpu的性能来换取高效率)
2.只能实现一个共享变量的原子性操作,不像synchroniz,可以代码块枷锁
3、aba问题
关于aba问题:
如果两个线程同时操作一个内存的值,分别是1,2线程,在1对内存A,赋值B,再赋值A,在2线程看来,该内存是没有被操作过的,但是实际已被操作,所以可能会出现问题(cas忽略了中间的情况,可能会出现问题)
解决aba问题
原子引用atomicReference,可以对对象进行原子操作
atomicStampedReference的使用
atomicStampedReference.compareAndSet(10,100,atomicStampedReference.getStamp()
atomicStampedReference的参数,
cas-->unsafe-->cas底层原理-->aba-->原子引用更新-->如何规避aba问题
java的锁实现及其原理
可重入锁、自旋锁、独占锁(写锁)/共享锁(读锁)/互斥锁、
- 可重入锁:指程序获取了外面的锁后,同一个线程也可以获得内部的锁,不会发生阻塞
reenterlock / synchronize 是典型的可重入锁;
可重入锁的最大优点就是不会发生死锁,因为线程在进入某个方法时,已经获取了里面嵌套的所有 锁,不存在线程间相互拿不到锁的情况;
- 自旋锁:自旋锁时线程会不断地i循环去获取锁,而不会阻塞(交出cpu使用权),从而避免了上下文的切换, 缺点是循环会消耗cpu (通过 do while 循环来实现)
使用 automicreference 实现对象的自旋;原子引用,使用可cas比较
-
独占锁(写锁)/共享锁(读锁)/互斥锁
-
独占锁:指锁只被一个线程只持有,reentrantrock和synchronice都是独占锁
共享锁:多线程可读,不可写
-
ReentrantLock默认是创建非公平锁. Lock lock = new ReentrantLock(true); 为公平锁
-
reentrantreadwritelock其= 分为写锁和读锁,在多线程的环境,有线程进入了获取了写锁,那么读锁设置的代码块就被锁死, 如果读锁的代码块先获得锁,那么所有的读操作可以同时执行,但是写操作被上锁
读时是读锁是共享锁,
写时是写锁是独占锁
读锁的共享锁保证了高效的并发,
读写锁,支持读读,写的时候上锁
synchronize有三种锁(锁的膨胀),会根据情况升级锁,最初是偏向锁,会升级到轻量级锁和重量级锁
1.偏向锁(对象头有个标记位,记录哪个线程获得锁,每个线程进入前,会检查这个记录,确实是否能进入) 乐观锁。对象头有一个标志 MarkWord 负责记录哪个线程获得了锁,如果已经记录了线程A,线程A可以直接进入,但是线程B进入时,会检测线程A是否活动。 不活动:把标志MarkWord指向自己B。然后进入同步代码块。 活动:等待线程A执行到安全点,挂起线程A。执行线程B。最后膨胀为轻量级锁。
2.轻量级锁(获得锁的信息记录在线程上,对象头指向该栈针位置)通过cas自旋的方式获取锁资源,这样修不需要交出cpu使用权,减少cpu上下切换的开销,但是如果自旋的线程过多,时间过长,会给cpu带来压力。此时膨胀为重量级锁合适
乐观锁。升级轻量级锁后,线程A获得锁时,会在线程A的栈帧中创建“lock record”来记录(复制)对象头的MarkWord,并将对象头的MarkWord 更新为指向“lock record”的指针。如果更新失败了就会自旋有限次数,仍然失败就会膨胀为重量级锁。 3.重量级锁(非公平锁) 悲观锁。如果线程A获取了对象的锁,其它线程会自旋有限次数,失败后线程B被阻塞挂起。线程A释放锁后唤醒所有阻塞的线程,一起竞争对象锁。阻塞线程之间不会排队等待,没有”先到先得“的原则。 其实上面所有的锁,都是“对象锁”
ychronize的实现,依赖于jvm的objectmonitor方法 monitor机制,在java对象中,实现了wiat方法和notify方法,
objectmonitor中存储了对象头锁的状态,waitset(wait线程队列),entrylist(阻塞线程的队列)
synchronize的互斥性:修饰的对象,只能被一个线程访问,其他线程阻塞等待
可见性:其他线程后续可以知道当前线程的操作结果
重练级锁底层实现原理:
通过monitor每个对象都与monitor关联,同步时,修改相关标志位,但是修改标志为的指令是调用操作系统的方法实现,被阻塞的线程,需要挂起,等待调度,这个过程,用户态和内核态需要来回切换,消耗性能
在java1.6以后,已经有优化,性能开销低了 实现如下: 刚开始使用乐观锁 cas是自旋,获取锁资源(轻量级锁)
如果一定时间没有获取到,就膨胀为重量级锁
偏向锁,轻量级锁,重量锁,是jvm智能实现的
下面是对象的mark标记位,锁升级后,不会降级
在轻量级锁中,object对象和栈的指针,是相互指向的
锁的升级条件( 偏向锁-->轻量级锁:线程A活动:等待线程A执行到安全点,挂起线程A。执行线程B。最后膨胀为轻量级锁。
轻量级锁-->重量级锁 : 如果更新失败了就会自旋有限次数,仍然失败就会膨胀为重量级锁。)
三种锁的区别,
偏向锁,锁的信息维护在对象头中
轻量级锁,维护了锁的等待队列消息,栈中也有执行对象的引用
重量级锁,通过jvm的objectMonitor,底层操作系统修改标志了实现的,涉及到用户态和内核态的切换
synchronic和reentratlock的区别:
1、synchronic(关键字)是jvm层面的,lock(具体类)是java的api,再juc包下
2、synchronic的代码块是不可以中断的,除非抛异常或者完成运行;reentrantlock是可以设置超时时间的,trylock(时间),或者使用interrupt去中断
3、reentratlock可以设置公平锁和非公平锁,但是synchronic是非公平锁
4、synchronic只能唤醒全部的线程;reentratlock可以分批唤醒,精确唤醒;
lock结合condition可以实现精确地控制线程执行
线程池
Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。 Executor 接口对象能执行我们的线程任务。 ExecutorService接口继承了Executor接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。 线程池都有哪些: Executors newCachedThreadPool()(缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量,可以进行自动线程回收) Executors newFixedThreadPool(int)(创建固定大小的线程池) Executors newSingleThreadExecutor()(线程池中只有一个线程) ScheduledExecutorService newScheduledThreadPool() : 创建固定大小的线程,可以延迟或定时的执行任务。
threadpoolexcutor(再工作中,这三个线程池都不要用)
Excutors.newfixedThreadpool:固定线程数(适合执行较长的任务) Excutor.newsingleThreadExcutor:单线程(一个任务一个任务执行的场景) EXecutor.newCachedThreadPool:多线程,线程变化灵活(短期的,负载较轻的小程序)
线程池的参数:
ThreadpoolExcutor( int corepoolsize:常驻核心线程数
int maxnumpoolsize线程池能同时容纳执行的最大线程数,
long keepalive:多余线程的存活时间
Timeunit unit:多余线程的存活时间单位 当前线程池数量超过core'poolsize时,当空闲线程到达keepalivetime时,多余的线程会被销毁,直到剩下
corethreadthreadsize
blockingqueue:任务队列,别提交,但是没有被执行的线程
threadfactory:生成线程
handler:拒绝策略?
首先,被提交的任务放到核心线程执行;如果核心线程都用满,线程需要放到队列中等待;如果线程池队列满了,会开启最大线程处理数,如果队列还是满,就执行拒绝策略;拒绝策略1、抛出异常,2、直接丢弃3、将队列中等待最久的抛弃,将这个任务加入到队列中;4、绝对的任务,由调用的线程去处理
使用 threadpoolExcutor()设计线程池 工作中,实际是不用这些线程池,是自己去设计一个线程池,我们去调用
线程池配置合理的线程数: 可以分为场景
cpu密集型:cpu数+1,减少上下文切换
io密集型由于cpu不用一直执行任务,配置尽可能多的线程 cpu核数*2 或者 核数/1-阻塞系数 阻塞系数一般是 0.8-0.9
juc的工具类:countdownlach、cycribal、semophore
- countdownlach:只有前面的线程都执行完了,才可以执行下面的线程
- cycribal和countdownlach相反,是计数到了一个值,才可以执行下面的程序
- semophore相对于cycribal和countdownlach,可以复用
并发容器
ConcurrentHashMap: 减小锁粒度是指缩小锁定对象的范围,从而减小锁冲突的可能性,从而提高系统的并发能力 ConcurrentHashMap,它内部细分了若干个小的 HashMap,称之为段(Segment)。默认情况下 一个 ConcurrentHashMap 被进一步细分为 16 个段,既就是锁的并发度。 并不是将整个 HashMap 加锁,而是首 先根据 hashcode 得到该表项应该存放在哪个段中,然后对该段加锁,并完成 put 操作。在多线程 环境中,如果多个线程同时进行put操作,只要被加入的表项不存放在同一个段中,则线程间可以做到真正的并行。
阻塞队列:当添加数据时,如果已经满了,增加的线程就会阻塞;获取的时候,如果队列里面市空的,获取的线程就会阻塞
:
currenthashmap在1.7和1.8的区别
Fork/Join 框架
-守护线程(Daemon)和用户线程(User)
唯一的区别是判断虚拟机(JVM)何时离开,Daemon是为其他线程提供服务,如果全部的User Thread已经撤离,Daemon 没有可服务的线程,JVM撤离
线程问题排查:
死锁:死锁的编码和定位分析
jps查看java线程号
jstack 线程号
问题: