JUC笔记

线程是最小的调度单位,进程是最小的资源分配单位,在win中进程是不活动的。

继承Thread类重写run方法,start方法使用了多态直接调用重写过的run方法。

实现runnable接口,start方法使用了静态代理,使用的是Thread方法本身的run方法吗,间接调用target的run方法。

创建FutureTask对象,并传入实现Callable接口的对象,最终再传入给线程,以get方法获取返回值,主线程将一直等待get方法返回值。至于FutureTask对象其实也继承了Runnable接口。

直接调用run方法并没有开启新的线程,多线程必须用start方法。

sleep会让线程从running状态变为Timed Waiting状态,其他线程可以通过调用这个线程对象的interrupt方法进行唤醒,但是会抛出异常,所以要用catch块进行捕获,进行唤醒后的进一步操作。线程被唤醒并不会直接为运行状态,TimeUnit的sleep具有更好的可读性。

yeild让线程从running变成runnable状态,但是具体还是要看os的进程调度。

sleep属于阻塞模式就算cpu空闲也不会使它运行,但是yeild在空闲时就会运行。

cpu比较空闲时候,优先级和yeild作用不大。

sleep作用,放在whlie(true)循环中可以降低cpu占用率。

A线程中调用B线程的join方法,会使得A等到B运行之后再运行。

Interrupt可以打断sleep、wait、join阻塞的线程。打断后都会抛出异常。

每个线程都有一个打断标记,打断正在运行的线程标记就是true但是如果打断的是join、sleep、wait线程他们会吧标记重置为false。

打断正常运行中的线程,不会有什么结果,只是让这个线程知道了有别的线程想打断我,并且设置标志位。  可以利用这个标志位进行判断是否有人打断我,决定权在自己手上。

System(exit)结束整个程序。

isinterrupt方法不清除标记,interrupt会清楚标记。

park阻塞的线程被打断之后会被唤醒,但是标记不会被清除,如果不被清楚就无法再次被park,可以利用interrupt方法。

stop、resume、suspend不建议使用,会死锁。

主线程不等于Java进程。

守护线程,其他线程结束,即使守护线程没有执行完也会强制停止(垃圾回收线程)。

runnable阻塞状态是指调用了某些api,比如文件读取。BLOCKED(拿不到锁)、WAITING(join)、TIMED_WAITING(sleep)是对阻塞状态的细分。

sychronized和lock都是阻塞锁,语法:sychronized(锁对象),可以自己定义一个锁对象,并非锁对象一定就是你要操作的对象。时间片结束并不会导致锁的释放。

只有读取可以不加锁,一旦既有读取又有修改就必须加锁。

锁加在方法上(语法和加锁对象上不一样),相当于锁住的是this对象,加在静态方法上相当于是锁住class对象。sychronized只能锁对象。对于类中没有加锁的方法,外界调用的时候就没有锁这一个概念,正常使用就行。只有在遇到sychronized时候才会去尝试获取锁。class和this是两个东西,不干扰,不互斥。

对于锁this对象,只有是尝试获取同一个对象的加在成员方法上的锁才会有互斥。对于锁class对象,只有是尝试获取同一个对象的加在静态方法上的锁或者不同对象之间的静态方法时候才会有互斥。

局部变量是线程安全的,因为栈是线程私有的,每次不同的线程进入到同一个方法都会建立一个新栈帧,开辟新的局部变量,但是局部变量引用的对象不一定是线程安全的。

方法中的局部变量可以是final但是不能是static。

原子方法的组合不是线程安全的。

 加了final也不是线程安全的,因为一个对象可以被多个变量所引用。

Spring默认单例模式,因此上述也会有线程安全。(做成局部变量可解) 。

没有成员变量的对象一般是线程安全的(无状态)。局部变量一般可以一定程度上规避线程风险。

抽象类行为不确定,可能会引发线程安全问题,外星方法。

P76讲解moniter,必看!EntryList是非公平队列。

一个获得过锁的线程,运行代码块时候发现条件不够,为了防止自己一直占用锁,就可以使用wait暂时放弃锁,加入到wait队列中,然后等待条件满足,Owner发起notify通知队列中的线程,让其知道自己需要的条件已经满足,可以等这个锁释放时候再次获取。唤醒后加入entrylist重写排队,其实就相当于一个休息室(超时也是一样)。

必须是先成为owner才能调用notify、notifyall、wait。调用方法时锁对象.wait(),notify()随机唤醒一个。会导致虚假唤醒。while+notifyall解决虚假唤醒。

Sleep和Wait的区别,从宏观上说,其实sleep是在线程层面上的,线程sleep,加入到os的对应队列种,并且持有锁。wait是锁对象层级的,加入到锁对象的waitset上。

锁重入:一个已经持有了锁的线程再次申请锁。

对象头中的mark word 偏向锁是延迟生效的。

可偏向对象调用hashcode会撤销偏向锁,变为正常状态。轻量级锁的hashcode会存在于栈帧的锁记录里,重量级锁会存放于moniter中,而可偏向锁没有其他空间存放hashcode。

访问互斥资源如果是错开的,才能是偏向锁和轻量级锁,否则就是锁膨胀为重量级锁。

多个线程申请锁对象(错开情况),偏向锁会失效,成为normal。

调用wait、notify会将偏向锁升级为重量级锁。

一个类的偏向锁撤销次数多了,超过阈值(20)这个类的对象就会重新偏向。大于(40),将会撤销该类所有对象的偏向锁,新对象也是如此。

JIT会优化Java代码,如果锁作为局部对象没有逃逸,那么JVM进行解释时候会锁消除。

join底层其实就是利用了保护性暂停。

保护性暂停模式是一对一,生产者消费者模式(各种阻塞队列)是多对一。

生产者消费者模式中的“消息”是异步的,因为是将消息加入到消息队列中,消息并不是第一时间被消费,而保护性暂停是一对一的,就是只能存在一个消息。

先unpack也可以解锁pack,类似于sleep也是线程层面的。

notify不能在wait之前,interrupt也可以唤醒waitset中的线程。

 

 reentrantlock如果没有获得锁进入到阻塞队列可以被中断,但是需要永lockinterrupt方法去尝试获得锁,与wait不同,wait被打断的前提已经是获得锁了,二者打断的线程处在的位置不同。

 reentrantlock默认是不公平锁,构造时候可以设置。

可见性,由于JIT即时编译器可能会导致线程从缓存中查找外部变量,可以用volatile和sychronized保持可见性。

volatile适合一个线程写,多个线程读取。volatile的作用就是保证某个数据最新值,并不能解决指令交错。volatile和sychronized不同,不是保持原子性(同步),如果有printf也可以保证可见性,因为其中有sychronized。

可以用volatile来优化两阶段退出。

JVM会调整指令执行顺序,单线程没问题,多线程会有问题。指令重排的前提是,重排之后不影响结果。

sychronized可以保证有序性、可见性、原子性。

volatile可以防止之前的代码不会被重排序到volatile变量的后面,并且前面的操作都可以被volatile后面所见。sychronized保持有序性的前提是完全把共享变量交给sychronized管理,才不会有这些问题,但是其保持有序性并非是通过避免重排序。

volatile保持可见性、有序性依靠的是读写屏障:

 

CAS底层实现是cpu本身支持的原子操作,乐观锁,无锁乐观,无阻塞并发。

CAS其实是无锁的一直同步机制,首先获取变量的值,然后进行操作,在对数据进行更正的时候再次比较这个值和起初的值是否一直,一则则进行compareandset,这个是原子操作,若不一样则重新获取操作,再cas,注意cas的变量必须配合volitail,要保证线程改变的值即使刷入主存,以保证其他线程能获取到最新的值。

接口中如果有且仅有一个抽象方法(其他随意)就是函数式接口。

原子类就是保证每次线程操作都是原子性的,都不会出现交错问题。基于其进行cas更加准确,因为原子类里面使用了volitail。

Atomicxxx,包含一系列封装好的cas。其实就是内部利用了volitail以及CAS技术实现了数据操作的原子性,因此外部业务如果需要进行CAS操作,使用原子类进行操作更为完美。

反射可以获得无法直接使用的类。

不可变对象可以保证线程安全,因为外界没有方法去更改这个对象的值,即使多线程也无所谓。千万要注意是不可变对象是线程安全的,而并非说其引用变量是线程安全的,而是指底层的对象,无论有多少个线程都无法修改这个对象本身的值,即使线程a中指向了“abc”,底层是一个char数组,别的对象也无法修改这个数组。

具体来说 String a = new String("a");  b = a(引用泄露); 这时调用b的方法,最终其实底层会返回一个新的对象给b而非对a引用的对象进行了修改,二者互不影响。二者hashcode也不同。

不可变对象的改变都是通过新建对象来实现的,并非改了原有对象的值。

基本数据类型的变量存的就是值本身,只有包装类才有缓存。

 包装类、String、BIgdecimal都是不可变类型,都是线程安全的。

final是通过写屏障实现final的,final优化后在后续使用的时候直接用值代替。

使用CAS可以提高吞吐量,但是不能太多,要小于CPU核心数。

关于BigDecimal为什么还要加一层Atomicreference,虽然BigDecimal是不可变对象,但是操作之间并非是原子性的,但是用Atomicreference后,使用其API可以保证原子性。

Atomicreference,它可以保证你在修改对象引用时的线程安全性。保证操作的对象的我们想真正想操作的。但是有时候我们是要对引用的某个属性进行原子性保证,因此便提出原子数组、字段更新器这一概念。适用于以更新引用的方式更新数据的代码。

上图进入线程时,拿到的信用卡是版本一,但是到第三行可能信用卡的版本已经被其余线程更新了 ,由于第一行和第三行的信用卡版本不同,因此数据不同步。

下图的Atomicreference的API就可以在确保要更新时候的信用卡还是一开始拿到的信用卡才更新。

 超时等待其实就是不用外界唤醒了,自己醒,然后区entryset重新排队执行后面的代码。

救急线程是在核心线程繁忙、阻塞队列满的情况下才会触发,如果阻塞队列是无界的,那么有无法触发救急线程。核心线程是复用的。

固定线程池,队列无限,没有救急线程。

线程池中的线程都是非守护线程,不会随着主线程结束而结束。

缓存线程池,只有救急线程,任务队列是sychronousQueue(一手交钱一手交货)

单线程线程池适用于任务之间串行执行,与写在一个程序里的区别就是,如果是在一个程序中执行如果某个步骤报错,整个程序也就停止了,后面任务无法执行,但是如果使用线程池就会再创建一个线程来继续执行。与newfixthreadpool的区别是其使用了装饰器模式,将线程池作为参数传给构造方法,所以无法进行强转,并且无法修改其核心配置。

不同的任务放置在不同的线程池可以缓解饥饿问题。

Timer就是单线程实现任务调度,如果前一个任务出错那么后面的任务也会被影响。而schedulethreadpool就不会被影响,因为线程池就是可以保证时刻池中都有设置的线程存在,即使死掉了也会再生一个。

正常线程池如果出现错误时在主线程中是不会报错的。submit只有提交返回值的lambda才能有返回值和抛出异常。Future get方法获取结果或者异常。

scheduleFixedrate参数period是指一定时间(period)中运行这个任务。要想控制任务之间的间隔要使用delay。

ReentrantReadWritelock,写锁不支持条件变量,锁升级不能重入,即获取了读锁不能再获取写锁,但是降级可以重入,获取了写锁可以重入读锁。读写、写写 会互斥。读操作大于写操作可以考虑读写锁。

semaphore可以代替wait和notify。

想让主线程得到其他线程的结果就要用Future。

Countdownlatch与Join虽然可以达到一样的效果,但是对于线程池来说,join使用时需要知道线程的名字,繁琐,而Countdownlatch则不用,只需要倒数,并且Join是底层的API,麻烦。Countdownlatch不可以重用。想重用要用cyclicbarrier。

cyclicbarrier注意事项,线程池线程数要和cyclicbarrier一致,可能后续的任务先执行完毕导致计数归零,结果可能和预想不同。

cyclicbarrier第二个参数就是当计数器置为0时候运行该任务,从线程池中找到一个线程去执行。如果想一次cyclicbarrier执行两个任务(1S、2S),但是线程池中有三个线程则不会得到自己想要的结果 ,因为第一批会执行三个任务即1S、2S、(并不是执行参数二的任务,因为两秒的任务还没执行完毕,线程池的第三个线程会再次执行第一个一秒的任务)1S。

线程安全的集合是fail-safe机制。

concurrenthashmap是线程安全的,但是一定要注意,线程安全是指每个方法都是线程安全的,但是一个任务是多种操作才能完成的,就不是原子的了。所以要使用concurrenthashmap包装好的高级api。

用concurrenthashmap完成计数器,需要配合累加器,map.computeifabsent(累加器),这个方法会将累加器再返回,因此可以外部接受这个累加器的引用,在下一步利用累加器的ncrease方法进行累加(累加器是对象)。

hashmap,JDK7是头插法、JDK8是尾插法。

JDK7并发死链:多个线程同时进行扩容(JDK7),一个已经扩容完毕,另一个还没扩容,导致循环死链。

会先扩容,再转换为红黑树,结点少了还会转换为链表。JDK8,concurrenthashmap也是懒加载,构造器只计算table大小而不去创建,JDK7是直接创建。size必须是2的n次方,构造方法中会进行转换。

concurrenthashmap不能有空的键值。get不加锁,put发生冲突会锁住对应桶的头节点。

Longadder多个累加单元,减少cas自旋。

JDK7concurrenthashmap靠segment保证同步,创建时便会创建segment以及其第一个元素,并且segment不可扩容。hashtable扩容,链表如果只有一个元素就复用,以及rehash后和之前hashcode一样的结点也重用,其余结点新建。get也是不加锁。

统计size先三次对比,如果还是不能统一,就加锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值