提高程序运行效率
下载、数据库连接池
Thread
、实现Runnble
,覆写run
方法callable
覆写call
方法 该方法有返回值支持泛型T,可以抛出异常,通常与线程池一起使用ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<String> submit = executorService.submit(new Callable<String>(){
@Override
public String call() throws Exception {
return "执行结果";
}
});
String result = submit.get();
GC
垃圾回收器)、非守护线程(用户线程)main
线程不是守护线程。当所有非守护线程都结束工作时,守护线程会随着JVM
一同结束工作。setDaemon(true)
将该线程设置为守护线程。class TestRunnable implements Runnable{
public void run(){
try{
Thread.sleep(1000);// ① 守护线程阻塞1秒后运行
FileOutputStream os=new FileOutputStream(new File("daemon.txt"),true);
os.write("daemon".getBytes());
}
catch(Exception e1){
e1.printStackTrace();
}
}
}
public class DaemonTest {
public static void main(String[] args) {
Thread thread=new Thread(new TestRunnable());
thread.setDaemon(true); //设置守护线程
thread.start(); //开始执行分进程
}
}
参考:Java中守护线程的总结
wait
当前线程进行阻塞,放弃锁,将CPU
的执行权让出
notify notifyAll
将持有该资源的阻塞状态的线程唤醒,并释放当前对象的锁(不是立马释放,此方法执行完毕才会释放),只是唤醒(让这些被唤醒的线程拥有重新获取锁的机会,而不是继续阻塞)
需与 synchronized
一起使用
参考:java锁之wait,notify
wait,notify
是object
的方法,语义是释放锁,并阻塞当前线程(当前线程进入锁的阻塞队列,等待拥有这个锁的其他线程唤醒)。
synchronized
中维护了锁,其中有个monitor
的概念,也叫监视器,每个对象都有monitor
。
wait
方法而阻塞,是进入阻塞队列notify/notifyAll
是将阻塞队列中的线程加入到同步队列synchronized
底层对同步代码块进行加锁时,使用了monitorEnter、monitorExit
,所以wait
为什么要和synchronized
一起使用join
相当于插队 让该线程先知行。 join
方法是synchroniezd
关键字修饰的,底层原理是wait
.
一个案列:配合守护线程、join
一起看
//thread1.join();
//若在主线程中执行此语句后,主线程运行thread1的join方法,主线程进入阻塞,一直等到thread1线程执行完毕,主线程再次执行。
class TestRunnable implements Runnable{
public void run(){
try{
Thread.sleep(1000);//守护线程阻塞1秒后运行
System.out.println("子线程写完了");
}
catch(Exception e1){
e1.printStackTrace();
}
}
}
public class DaemonTest {
public static void main(String[] args) throws Exception{
Thread thread=new Thread(new TestRunnable());
thread.setDaemon(true); //设置守护线程
thread.start(); //开始执行分进程
thread.join();
System.out.println("hello");
}
}
yield
暂停当前正在执行的线程,使其从运行状态切换到就绪状态,注意此方法可能最终没效果,因为转换到就绪状态后,依然有机会获取CPU
的调度。
sleep
当前线程进入休眠,可指定时间,并没有释放锁,暂时让出CPU
的资源,等到指定时间,再次得到CPU
的调度
对比:
sleep
是Thread
的方法,需处理异常,不会释放锁;wait
是 Object
的方法,会释放锁
原子性:一个操作或多个操作要么全部执行,要么全部不执行;
可见性:多个线程在对一个变量进行操作时,该变量的值会对其他线程可见。Volatile
有序性:多线程程序执行时,为提高程序运行效率,运行编译器和处理器会对指令进行优化,即重排序。多线程下对指令顺序重排可能会产生问题
线程执行可以有返回值,以上有例子说明了Callable
用法,但如果在主线程中等待子线程的执行结果,此时主线程时阻塞的,影响了程序的执行效率。
使用场景:图片的加载,刚开始加载图片时,图片未加载好,可显示为模糊。使用子线程1加载图片,子线程2等待加载结果,并将结果替换为原先模糊的图片,而不是使用主线程去等待子线程1的执行结果
主内存、工作内存;定义了线程间通讯时,线程间的可见性。
工作方式:
堆、栈(虚拟机栈、本地方法栈)、程序计数器、方法区
int(-128~127)
outOfMemoryError
,栈区域还会 stackOverFlowError
多线程环境下,共享变量值发生错乱,程序未按预期执行
加锁、同步
当线程释放锁时,会将线程对应的本地内存空间的值刷新到主内存中;
当线程获取锁时,会将线程对应的本地内存空间的值置为无效,从而使得被监视器保护的临界区的代码需从主内从中去获取。
多个线程竞争资源,锁无法释放
破坏死锁四个必要条件:请求和保持,不剥夺,循环等待,互斥条件
本地线程变量,每一个线程维护自己的本地变量,类似于Map数据结构实现,key
为当前线程,val
为Obj
变量,支持泛型
数据库连接Connection
。存在内存泄漏的风险
synchronized
可以理解为时间换空间;threadLocal
可以理解为空间换时间
当使用大量的线程时,这些线程都使用 threadLocal
本地线程变量,互不影响。假设程序运行结束,这些本地线程变量的 entry
没有被回收,threadLocal
内部实现是一个类Map的结构,threadLocalMap
,threadLocal
引用了该map,那么很可能造成堆内存溢出。
其实内存泄漏的原因是entry
,强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
,导致value
对应的Object
一直无法被回收,产生内存泄露。
threadLocal
对象被回收,threadLocal
引用的 threadLocalMap
的 entry
的 k
为强引用,在GC
时没有被回收,如果没有手动回收,产生内存泄漏;threadLocal
对象被回收,threadLocal
引用的threadLocalMap
的entry
的k
为弱引用,在GC
时被回收,那么k
为空,entry
变成
键为null
的entry
,导致entry
不能被回收,最终value
没被回收,产生内存泄漏;threadLocal
提供的get、set、remove
方法在使用时,会将内存中,k
为null
的v
清除;k
为null
对应的v
会在threadLocalMap
调用set、get、remove
时被清除1.为了解决多线程中可见性的问题,JMM中存在一个原则:Happens before
原则。不要求前一个操作在另一操作前发生,要求前一个操作的结果可以被后一个操作获取(对后一个操作可见)。
2.As if serial
语义:为了提高程序运行效率,JVM
可能会对代码进行指令重排序(不影响结果的前提下),它不会对有依赖关系的指令重排序,保证了在单线程下数据安全,且提高了运行效率。
3.CPU
缓存一致性协议(MESI
)
高速运转的CPU
进行数据读写时会与主内存进行交互,但内存的处理的速度与CPU
处理的速度不是一个量级,差距很大。便在他们之间引入了缓冲区的概念。
CPU CACHE模型 | 程序局部执行原理 |
---|---|
每一个CPU
修改内存数据的步骤:
1)将数据从内存中复制一份到当前CPU的cache中
2)在cache
中更新数据
3)将更新的数据刷新到内存中
多个cpu
在操作主内存中同一个共享变量时,会出现数据不一致性的问题,解决办法如下:
1)总线加锁,粒度太大,不好控制
2)MESI
缓存一致性协议
a.读操作:不做任何事情,将cache
的值读到寄存器
b.写操作:通知其他CPU
将该变量的cache line
置为无效,其他的cpu
要访问这个变量的时候只能从内存中获取
4.原理
禁止指令重排序是添加了内存屏障;
可见性:对使用volatile
修饰的共享变量进行写操作时,会使用CPU
提供的lock
前缀指令,主要做两件事:
1)将当前处理器缓存中的数据写回到系统内存;
2)这个回写操作会使 其他CPU
中缓存该内存地址的数据置为无效
5.为什么不能保证原子性
对任意单个volatile
变量的读/写具有原子性,对于类似volatile++
的操作不具备原子性。
在《Java并发编程的艺术》中有这一段描述:“在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。”我们需要注意的是,这里的修改操作,是指的一个操作。
6.使用场景:
1)作为状态标志、开关等
2)双重检查锁定DCL(double-checked-locking)
,单例模式懒汉式中会用到
volatile
保证变量可见性;禁止指令重排序,不能保证原子性,不会发生阻塞,底层使用Lock
synchronzied
作用在方法上,或同步代码块上,能保证变量的可见性,能保证原子性,可能因等待锁发生阻塞,底层使用 monitor
synchronzied
在1.6之前代价较大,同步之后并行变成串行1.使用
2.原理
java
对象头 和 monitor
是实现关键
例:使用同步代码块,锁主的是类对象
public class Demo1{
private static int count = 0;
public static void main(String[] args) {
synchronized (Demo1.class) {
inc();
}
}
private static void inc() {
count++;
}
}
编译之后,查看字节码文件
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class com/thread/SynchronizedDemo
2: dup
3: astore_1
4: monitorenter //注意这个
5: invokestatic #3 // Method inc:()V
8: aload_1
9: monitorexit //注意这个
10: goto 18
13: astore_2
14: aload_1
15: monitorexit //注意这个
16: aload_2
17: athrow
18: return
占用 monitor
概述:
线程在获取锁的时候,实际上就是获得一个监视器对象(monitor)
,monitor
可以认为是一个同步对象,所有的Java
对象是天生携带 monitor
。而monitor
是添加Synchronized
关键字之后独有的。synchronized
同步块使用了monitorenter
和monitorexit
指令实现同步,这两个指令,本质上都是对一个对象的监视器(monitor)
进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized
所保护对象的监视器。 线程执行到monitorenter
指令时,会尝试获取对象所对应的monitor
所有权,也就是尝试获取对象的锁,而执行monitorexit
,就是释放monitor
的所有权。其实wait/notify
等方法也依赖于monitor
对象,这就是为什么wait/notify
只能配合synchronized
使用。第二个monitorExit
是异常出口。另外以上代码是同步代码块上的过程,若是同步方法,反编译后是没有monitorEnter
和monitorExit
的,而是在方法上加了个标记,ACC_SYNCHRONIZED
,JVM
通过ACC_SYNCHRONIZED
标识,就可以知道这是一个需要同步的方法,进而执行上述同步的过程。
占用monitor
过程:
1.如果monitor
的进入数为0
,则该线程进入monitor
,然后将进入数设置为1
,该线程即为monitor
的所有者
2.如果线程已经占有该monitor
,只是重新进入,则进入monitor
的进入数加1
3.如果其他线程已经占用了monitor
,则该线程进入阻塞状态,直到monitor
的进入数为0
,再重新尝试获取monitor
的所有权
对象被创建在堆中,对象在内存中存储布局方式可以分为3个区域:
对象头(Header)
、实例数据(Instance)
、对齐填充(Padding)
对象头中主要包括 类型指针 和 标记字段。
类型指针指向类元数据,虚拟机通过该指针确定这个对象是哪个类的实例对象
标记字段中存储对象自身运行时的数据,包括哈希码、GC
分代年龄、锁状态标志、线程持有的锁、偏向线程Id。
使用同步来保证多线程并发时,数据安全问题。保证了安全性但降低了效率,从日常说法上来说,synchronized
是重量级锁,不过在JDK1.6
之后对此进行了优化,锁的种类可以分为
无锁、偏向锁、轻量级锁、重量级锁。也就是synchronized
在使用时并不是一直都是重量级锁,锁的状态会根据线程的竞争速度升级。
在大多数情况,加锁的代码块不仅仅不存在多线程竞争,而且总是由同一个线程多次获得。JDK1.6
之后对此进行了优化,为了减少获得锁和释放锁带来的性能开销,引入了偏向锁和轻量级锁的概念。即锁的状态会根据线程的竞争速度升级。
CAS
重新偏向但前线程for
循环,不会阻塞),直到拥有锁的线程释放锁,这个线程可以马上获取锁。自旋锁,原地等待进行for
循环操作时是会消耗CPU
的,所以轻量级锁适用于同步代码块执行很快的场景,这样线程原地自旋等待很短的时间就能获取锁了。所以轻量级锁出现的条件就是同步代码执行的时间不能过长,否则等待锁的线程for
循环太长时间反而会消耗CPU
资源。JDK1.6
之前自旋默认的次数是10
次,1.6
之后引入了自适应自旋锁,根据前一次在同一个锁上自旋等待的时长以及锁的拥有者决定。即上一次自旋等待时间较短,那么认为本次等待差不多的时间即可获取到锁,那么虚拟机认为此次等待获取到锁的概率很大,进而允许自旋等待相对更长的时间,反之亦然(等待时间太长,自旋多次太消耗CPU
资源,锁升级为重量级锁,线程直接阻塞)。CAS
操作替换对象头,如果成功,即没有竞争发生。如果失败,表示锁存在竞争,锁升级为重量级锁与synchronized
异同syn
属于重量级锁,阻塞性的获取锁,Lock
可以非阻塞性的获取锁,且可中断性的获取锁(在等待锁的阻塞过程中终止);sync
锁的获取释放是根据代码的执行自动的,Lock
锁的获取与释放需要手动;sync 是互斥锁,Lock
可以实现互斥锁也可实现共享锁(多个线程获取同一把锁);ReentrantLock
、Synchronized都属于可重入锁
ReentrantLock
Condition
wait、notify
更高级的功能。每一个Condition
对象维护着一个同步队列和等待队列(FIFO)
,进入 Lock
锁代码块中,调用condition.await()
方法,此时持有该锁的线程进入等待队列,并释放锁。当其他线程调用signal()
时,唤醒在该condition
上注册的等待线程,即从在condition
的等待队列中出列,等待这个线程lock方法释放完毕后,出列的线程,加入同步队列获取竞争锁的机会,成功获取锁之后从await()
后执行。ReadWriteLock
JDK
使用 先获取写入锁,然后获取读取锁,最后释放写入锁 这个步骤,是为了提高获取锁的效率,而不是所谓的可见性。 悲观锁:对并发修改出现的情况持有悲观态度,即认为此种情况出现的情况较多,当前线程持有锁,其他线程不允许在持有锁,syn
和写锁都属于悲观锁
乐观锁:对并发修改出现的情况持有乐观态度,或较少的出现,或者不加锁,CAS
锁,read
锁都属于乐观锁
以AtomicInteger
为例,Integer
的原子类,底层使用了CAS(compare and swap,比较并交换)
,无锁化编程,实际上使用了自旋锁。以对变量a进行自增的操作,++a并不是原子操作,但底层在此操作之后,调用了unsafe的compateAndSwap方法,该方法是native,基于的是CPU 的 CAS指令来实现的。
核心:变量v、valueOffset(指向变量的偏移地址,可理解为变量v的内存地址)、expectVal(期望值)、updateVal(更新值)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
使用了volatile
修饰变量值,多线程可见。unsafe
类是一个很特殊的类,功能比较特殊,可以操作对象属性、获得对象偏移地址、线程的挂起与恢复、CAS
操作等。
获取unsafe
实例,并获取对象的偏移地址。unsafe
是 CAS
的核心类,因为Java
无法直接访问底层操作系统,而是通过本地(native)
方法来访问,但JVM
还是开了个后门,使用 unsafe
,它提供了硬件级别的原子操作。
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
以上是核心代码,原子类中的操作基本上都是基于这个来做的。
this
当前对象valueOffset
偏移地址,unsafe
类操作可得到value
值expect
期望的值update
更改的值/**
* Atomically sets to the given value and returns the old value.
*
* @param newValue the new value
* @return the previous value
*/
public final long getAndSet(long newValue) {
return unsafe.getAndSetLong(this, valueOffset, newValue);
}
Unsafe.class:
public final int getAndSetInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var4));
return var5;
}
进行do-while
操作,不断的比较,原理就是这样。AtomicReference
支持泛型的,原理与Integer
一样。
CAS
存在的问题:
1.ABA
问题
变量 1→2→1
,CAS
并不知道变量经历了2的过程。加版本号、时间戳等方法解决。引入AtomicStampedReference
(相比于AtomicReference
加了时间戳)来解决这个问题
2.性能消耗问题
较多线程访问变量并更新时,自旋等待尝试更新却一直失败,循环往复,带来性能消耗。
3.不能保证代码块的原子性
只能保证单个变量原子性操作,多个变量一起操作的话,可以使用synchronized
代替CAS
。
使一个线程等待其他线程全部执行完毕后再执行;通过一个计数器来实现,计数器的初始值是设置的线程的数量,每当一个线程执行完毕后,计数器的值就-1
,当计数器的值为0
时,处于阻塞状态的线程恢复执行。
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };
//将count值减1
public void countDown() { };
案列:
public class Demo1 {
private static void tt(CountDownLatch countDownLatch){
try {
synchronized (Demo1.class){
countDownLatch.countDown();//自减
System.out.println("thread counts = " + (countDownLatch.getCount())+","+Thread.currentThread().getName());
Thread.sleep(1000);
}
countDownLatch.await();
System.out.println("所有线程执行结束" + countDownLatch.getCount()+","+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception{
System.out.println("主线程开始执行");
ExecutorService pool = Executors.newCachedThreadPool();
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0;i<5;i++){
pool.execute(()->{
tt(countDownLatch);
});
}
pool.shutdown();
countDownLatch.await(2, TimeUnit.SECONDS);
System.out.println("主线程结束执行");
}
}
res:
主线程开始执行
thread counts = 4,pool-1-thread-1
thread counts = 3,pool-1-thread-5
主线程结束执行
thread counts = 2,pool-1-thread-4
thread counts = 1,pool-1-thread-3
thread counts = 0,pool-1-thread-2
所有线程执行结束0,pool-1-thread-5
所有线程执行结束0,pool-1-thread-3
所有线程执行结束0,pool-1-thread-4
所有线程执行结束0,pool-1-thread-1
所有线程执行结束0,pool-1-thread-2
循环屏障,循环往复可利用的屏障。所有线程都到达屏障处,才会继续往下执行。
//parties 是参与线程的个数
public CyclicBarrier(int parties)
//Runnable 参数,这个参数的意思是最后一个到达线程要做的任务
public CyclicBarrier(int parties, Runnable barrierAction)
//线程已到达屏障,线程挂起
public int await() throws InterruptedException, BrokenBarrierException
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException
案列:
public class Demo1 {
static CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
public static void main(String[] args) {
ExecutorService pool = Executors.newCachedThreadPool();
for(int i = 0;i<2;i++){
pool.execute(()->{
try {
synchronized (Demo1.class){
Thread.sleep(1000);
}
System.out.println("ready,"+Thread.currentThread().getName());
cyclicBarrier.await();
System.out.println("continue,"+Thread.currentThread().getName());
}catch (Exception e) {
e.printStackTrace();
}
});
}
pool.shutdown();
}
}
res:
ready,pool-1-thread-1
ready,pool-1-thread-4
ready,pool-1-thread-3
ready,pool-1-thread-2
continue,pool-1-thread-2
continue,pool-1-thread-4
continue,pool-1-thread-1
continue,pool-1-thread-3
CyclicBarrier
的用途是让一组线程互相等待,直到全部到达某个公共屏障点才开始继续工作。CyclicBarrier
是可以重复利用的。CyclicBarrier
指定的任务是进行 barrier
处最后一个线程来调用的,如果在执行这个任务发生异常时,则会传播到此线程,其它线程不受影响继续正常运行。CountDownLatch
区别:CountDownLatch
是一个线程(或者多个),等待另外 N
个线程完成某个事情之后才能执行;CyclicBarrier
是 N
个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。CountDownLatch
的计数器只能使用一次。而 CyclicBarrier
的计数器可以使用 reset()
方法重置;CyclicBarrier
能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。CountDownLatch
采用减计数方式;CyclicBarrier
采用加计数方式。也称之为许可证管理器。API
其实都差不多。
acquire()、acquire(permits)
当前线程尝试去阻塞的获取 permits
个许可证。( permits >= 1)
此过程是阻塞的,它会一直等待许可证,直到发生以下任意一件事:
1.当前线程获取了n
个可用的许可证,则会停止等待,继续执行。
2.当前线程被中断,则会抛出 InterruptedException
异常,并停止等待,继续执行。
acquierUninterruptibly()、acquireUninterruptibly(permits)
当前线程尝试去阻塞的获取 permits
个许可证。 ( permits >= 1)
此过程是阻塞的,它会一直等待许可证,直到发生以下:
当前线程获取了n
个可用的许可证,则会停止等待,继续执行。
释放与此相对。
/**
* 网上查询到,使用semaphore的一个很好的例子:
* 食堂打饭案列:一共两个打饭窗口,20个学生按次序排队打饭。
* 学生1-10:无论等待多久,都会等待,直到打到饭
* 学生11-15:等待1s,超时则回宿舍吃泡面
* 学生16-20:中断阻塞式排队打饭
*/
public class Demo2 {
static class Student implements Runnable {
private Semaphore semaphore;
private int no;//编号
private int type;//三类学生
public Student(Semaphore semaphore, int no, int type) {
this.semaphore = semaphore;
this.no = no;
this.type = type;
}
@Override
public void run() {
switch (type) {
case 1:
try {
semaphore.acquire();
Thread.sleep(1000);
System.out.println(no + "号学生排队打到饭了");
semaphore.release();
} catch (Exception e) {
}
break;
case 2:
try {
if (semaphore.tryAcquire(new Random().nextInt(10000), TimeUnit.MILLISECONDS)) {
System.out.println(no + "号学生排队打到饭了");
semaphore.release();
} else {
System.out.println(no + "号学生排队等了1s,没打到饭了,回宿舍吃饭");
}
} catch (Exception e) {
}
break;
case 3:
try {
semaphore.acquire();
Thread.sleep(4000);
System.out.println(no + "号学生排队打到饭了");
semaphore.release();
} catch (Exception e) {
System.out.println(no + "号学生排队有异常被中断");
}
break;
default:
break;
}
}
}
public static void main(String[] args) throws Exception {
Semaphore semaphore = new Semaphore(2, true);
Thread[] threadArr = new Thread[5];
for (int i = 1; i < 21; i++) {
if (i < 11) {
new Thread(new Student(semaphore, i, 1)).start();
} else if (i < 16 && i > 10) {
new Thread(new Student(semaphore, i, 2)).start();
} else {
Thread thread = new Thread(new Student(semaphore, i, 3));
threadArr[20 - i] = thread;
thread.start();
}
}
Thread.sleep(2000);
for (Thread thread : threadArr) {
thread.interrupt();
}
}
}
它提供了一个FIFO
队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件,常见的有:ReentrantLock
、CountDownLatch等
。
AQS
是一个抽象类,主要是通过继承的方式来使用,它本身没有实现任何的同步接口,仅仅是定义了同步状态的获取以及释放的方法来提供自定义的同步组件。
核心:
1.如果当前线程竞争锁失败,则将当前线程封装成node
节点,添加到CLH
同步队列,并阻塞该线程;
CAS
讲tail
重新指向新的尾部节点head
节点指向新获得锁得节点pre
节点指向null
线程安全的支持阻塞性的添加或移除数据。
ArrayBlockingQueue
1
把锁ReentrantLock
,2
个condition
用于阻塞/唤醒take
和put(wating takes,wating puts)
,维护了1
个数组,2
个下标用于添加元素和取出元素(take_index,put_index)
,size()int
变量LinkedBlockingQueue
2
把ReentrantLock
锁(take,put)
,2
个condition(take,put)
,链表中维护node
,take,put
操作分别加锁,队列长度使用原子类维护PriorityBlockingQueue
SynchronousQueue
DelayQueue
ConcurrentLinkedQueue
基于链表的数据结构,使用CAS
操作进行入队或出队的节点更新。使用 CAS
非阻塞算法解决了当前节点与 next
节点之间的安全连接和对当前节点的赋值。
添加:找到tail
节点,调用 casNext()
,只有一个线程会成功,并发下其他线程进行此操作会失败,自旋重新进行此操作。
移除:找到head
节点,将其返回,并设置新的head
节点(原head.next
)。同样也是自旋进行该操作。
如何实现线程安全且非阻塞:保证可见性和原子性
内部维护 Node
节点,单向的链表结构,设置 head,tail
节点,使用 volatile
修饰,保证了可见性。
tail.casSetNext()
CAS
设置尾节点保证操作原子性且非阻塞。
copyOnWriteArrayList
基于数组实现。应用于 读多写少 的场景。可能会出现脏读(读取时,有写入)。
关键字:ReentrantLock 、volatile array
读操作无锁,写操作加锁
初始化:初始化一个长度为0的数组
添加:copy
一个数组,将长度+1
,将添加的值赋值,并将copy
的数组赋给array
移除:同上,也是copy
一个数组,长度-1
,并将新copy的数组赋给array
获取:同一般数组
即:在更新、移除、添加的操作会copy
新数组来实现。
concurrentHashMap
1.7 与 1.8
1.7:
segment分段锁,每一个segment
理解为一个片段(可重入锁),其内为数组+链表结构。不同的片段中,并发操作不影响,加锁的粒度较大。最大支持segment数目的并发。
put
操作加锁,get
不加锁(volatile)Node
节点中,val、next
都是volatile
修饰。volatile
只能报账基本数据类型值修改可见性,引用类型(数组,bean等)只能保证引用的可见性。
1.8:
1.7中segment
加锁粒度太大,1.8
的concurrentHashMap
以1.8
的hashMap
数据结构为基础,使用synchronized+cas+volatile
来降低锁的粒度,锁住的是table
的首节点。
get
原理与1.7
类似,put
机制如下:
死循环table
1.table为空,初始化
若是有其他线程也在对其初始化,该线程yield,有个标志位
若是没有,CAS设置标志位,并对table进行初始化
2.根据hash值获取f=table[index]
3.若f为null
CAS插入该node,casTabAt
4.检测标志位f.hash==moved
helpTransfer帮助扩容
5.table不为null,且hash桶处有冲突,加同步锁,锁住f,将粒度降到最低
内部有链表与树的转换
6.根据插入数据后判断是否扩容
HashTable
线程安全的,基本上所有方法上添加了synchronized
关键字。数组(默认长度11)+链表
Entry< k,v >[] table
;
count
; //table数组中所有entry的个数,可理解为key的个数
capacity
;//默认11
thresHold
;//阈值 = capacityloadFactor
loadFactor
;//负载因子 0.75
reHash
;//扩容,数组逆向添加到新的(扩容过)数组中,一次扩容(2n+1)
HashTabale< k,v >
根据k.hashCode 返回的int 值,在将此值与数组长度取模
int hash = key.hashCode
int index = (hash & 0x7FFFFFFF) % table.length //做绝对值并取模
for(Entry e = table[index];e != null;e = e.next ){
if(e.hash == hash && e.key == key){
return e.v
}
}
HashMap
线程不安全,数组+链表,链表长度>8,链表转为红黑树。put操作插入链表节点使用头插法
1.7在多线程扩容时会有链表死循环的可能,1.8无。1.7在扩容时,使用的头插法,1.8改为尾插法。
int hash = key.hashCode ^ (key.hashCode <<< 16) //低16位异或高16位
int index = hash & (table.length-1) //等同于取模,二进制效率高
//避免冲突,分布均匀
线程的的使用从创建开始到使用,最终到结束,都是需要人工去处理的,在使用时,从 new Thread()
开始 到 线程运行.start()
,都需要去自己处理,并且创建完成之后,并没有对使用过的线程做后续处理,在使用新的线程,也是新创建,无法管理。
总而言之:提高复用性;解耦(线程的创建和执行分开);减少资源消耗(频繁手动创建)
Executor
顶级接口
ExecutorService
次级接口,继承顶级接口
AbstractExecutorService
实现类,实现了基本所有的方法
ThreadPoolExecutor
继承上类
参数:corePoolSize
(核心),maximumPoolSize
(最大),keepAliveTime
(存活时长),unit
(左参数单位),workQueue
(阻塞队列,存储任务)
FIXED:new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>())
SINGLED:new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>())
CACHED:new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>())
SCHEDULE:super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue())
阻塞队列简单分为:
LinkedListBlockingQueue
无界阻塞队列ArrayBlockingQueue
有界阻塞队列SynchronousQueue
无缓冲阻塞队列 ,出列一个才能入列一个,可以避免在执行有依赖关系的任务出现锁DiscardPolicy
丢弃阻塞队列中的任务AbortPolicy
丢弃阻塞队列中的任务,并抛出异常 (默认策略)DiscardOldestPolicy
丢弃阻塞队列中最老的任务(最先进入的)CallerRunspolicy
由调用线程执行该任务1.每来一个任务,就会创建一个线程去执行该任务,知道线程数到达核心数。
2.核心数到达之后,再来任务,则将任务添加到阻塞队列中,等待核心线程空闲获取去执行
3.核心数到达,且阻塞队列满之后,再来任务,那么再次创建线程执行该任务,直到线程池中线程数达到最大线程数
4.最大线程数到达之后,在来任务,则按照拒绝策略执行
Executors
中提供的方法来创建线程池(FIXED,SINGLED..)
;Executors
提供的方法创建,而是自行使用ThreadpoolExecutor
自己定义FIXED,SINGLE
方式创建线程,因为阻塞队列无界,当任务量巨大时,任务积压在阻塞队列中,造成OOM
CACHED,SCHEDULE
方式创建线程,允许创建的线程Integer.MAX
则允许创建大量的线程,造成OOM
IO
密集型 任务 CPU
空闲,磁盘很忙 线程数 = 2 * CPU
个数CPU
密集型 任务 需要耗费大量的CPU
进行计算 线程数 = CPU
个数