一、线程的状态
New、Runnable、Teminated、TimeWaiting、Waiting、Blocked
Runnable又有 ready 和 running两个子状态
二、线程的几个基本方法
Thread.sleep(1000); 线程休眠,从Runnable进入TimeWaiting;1000ms后唤醒,又变回Runnable。
Thread.yield();让出一下cpu,然后继续抢。从running变成ready。
thread.join(); 从Runnable进入Waiting,thread结束后,被唤醒进入Runnalbe
三、Synchronized
1.可保证多线程时,共享变量的可见性、原子性和有序性。
2.原理是保证同时只有一个线程进入临界区。
依赖 monitorenter和monitorexit实现。monitor依赖操作系统的mutex lock 来实现互斥锁,获取到mutex则持有锁,mutex涉及到内核态与用户态的切换,增加系统开销。
3.在对象头用两位表示锁标志,详细如下图:
4.synchronized(o) 锁的是 o 对象,即moniter为o.this;作用在非static方法同理..
synchronized(static o ) 锁的是class,即moniter为o.class,相当于一个全局锁;作用在static方法同理..
5.synchronized是可重入的对象锁。
锁对象并非引用;可重入是为了避免死锁,例:子类调用父类同步方法如果不可重入,则会死锁。
6.程序中如果出现异常,锁将被释放。
即执行monitorexit,释放mutex。
7.锁升级,对照3中的图
=>无锁 锁标记 0 01
=>偏向锁:第一个访问锁的线程,未加锁,在偏向锁位记为1,同时记录线程ID。锁标志 1 01
=>轻量级锁:有线程征用,升级为自旋锁。默认自旋10次。锁标志 00
=>重量级锁:轻量级锁自旋10次后仍未获取到锁,则升级为重量级锁。锁标志10
8.自旋锁占用cpu,但是不访问操作系统,属于用户态。
重量级锁不占cpu,但是涉及到用户态和内核态的切换。
ps:加锁代码,执行时间长,线程数多的时候,系统锁更好。执行时间短,线程数少,用自旋锁。
四、wait()、notify()/notifyAll()
1.是本地方法,无法被重写。
2.配合synchronized使用,获取到锁后,才用这三个方法。
3.wait()会让出cpu;
notify()和notifyAll()虽然会唤醒wait()的线程,但不会让出cpu,会继续执行。所以一般用notify唤醒wait的线程时,都要主动退出临界区,以便被唤醒的线程进入临界区。
4.多线程判断状态时,用notify和wait时,判断一定要用while()而不是if(),因为wait被唤醒线程应该重新判断,而不是直接向下执行。如图:
public synchronized void put(T t) throw Execption {
while(lists.size() == MAX) { //用while而不是用if。被notify唤醒后还要判断,才可进行下面业务逻辑
this.wait();
}
//Todo 业务逻辑
}
5.依赖锁的monitor来实现的,所以只有在同步代码块或者同步方法中才能调用,否则抛异常。
6.调用wait(),线程会释放锁,线程会进入WaitSet等待队列,被notify()唤醒的时候是随机唤醒WaitSet中的一个线程。
五、锁粗化和锁消除
1.锁粗化
锁的请求、同步与释放本身会带来性能损耗。所以对同一个锁的高频、不间断的请求、同步与释放会消耗一定系统资源。所以把多次频繁的锁请求合并成一次,就会降低资源的消耗。
注意:锁粗化有可能把原本不需要加锁的代码放到锁中执行,所以如果代码执行的时间很长,就会大大提高锁的持有时间,这样锁粗化就不合理了。
2.锁消除
锁消除是指虚拟机即时编译器在运行时,检测到一些加锁的代码不可能存在共享数据竞争的锁进行消除。
想让锁消除生效,需要开启逃逸分析。
-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
-server 表示以server模式启动,只有server模型才会进行锁消除优化;+DoEscapeAnalysis表示开启逃逸分析;+EliminateLocks表示开启锁消除。
六、happens-before原则
1.程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
2.锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
3.volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
4.传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
5.线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
6.线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
7.线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
8.对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
七、volatile关键字
1.汇编层实际是通过lock指令实现的
2.保证线程可见性,本质上时是使用了cpu的MESI,即缓存一致性协议。
写时强制将修改值刷如内存
同时把其他线程的该volatile的缓存行置为无效
其他线程使用该volatile值时,发现缓存无效,则直接内存中获取
3.禁止指令重排序,使用内存屏障
内存屏障有四种 StoreStore、LoadLoad、StoreLoad、LoadStore四种屏障。
4.volatile的内存屏障策略非常严格保守,如下:
写操作前加入StoreStore,保证之前写操作对其他线程可见。
写操作后加入StoreLoad,保证写操作对其他线程可见。
读操作前加入LoadLoad,保证读操作完成后,才可进行后续读。
读操作后加入LoadStore,保证读操作完成后,才可进行后续写。
5.单例的双重检查写法,注意要用volatile。否则可能会有指令重排问题。因为new不是原子操作的。
public class Singleton {
private Singleton(){}
private static volatile Singleton singleton; //这里要用volatile
public static Singleton getInstance(){
if (singleton == null){
synchronized (Singleton.class){
if (singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
八、CAS 和 Atomic
1.compare and swap 比较替换
2.cas(OldVal,Expected,NewVal)
if(O == E) O=N;
else try again
3.cas是cpu原语支持的,是原子操作。
4.ABA问题,解决办法,加个版本号。 AtomicStampedReference可以解决。
5.简单了解 Unsafe类
6.简单了解LongAddr,采用分段锁