公平锁和非公平锁###
公平锁是指多个线程在等待同一个锁时,必须按照申请的先后顺序来依次获得锁。
公平锁的好处是等待锁的线程不会饿死,但是整体效率相对低一些;
非公平锁的好处是整体效率相对高一些,但是有些线程可能会饿死或很早就在等待锁,但要很久才能获得锁。
公平锁是严格按照请求锁顺序排队来获得锁的,而非公平锁是可以抢占的,即如果某个时刻有线程需要获取锁,这个时候刚好锁可用,name这个线程会直接抢占,而这时阻塞在等待队列的线程则不会被唤醒。
公平锁可以使用new ReentrantLock(true)实现,默认是非公平锁。
自旋锁###
Java的线程是映射到操作系统的原生线程上,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态装换需要耗费很多的处理器时间,对于代码简单的同步块,状态转换消耗的时间有可能比用户代码执行的时间还长。
在很多应用上,共享数据的锁定状态只会持续很短的时段时间,为了这点时间挂起和恢复现场并不值得。如果物理机器上有一个以上的处理器,能让两个或以上的线程同时并行执行,就可以让后面请求锁的那个线程“稍等片刻”,但不放弃处理器的执行时间,看看持有锁的线程是否很快会释放锁。为了让线程等待,只需让线程执行一个忙循环(自旋),这就是自旋锁。
自旋等待不能代替阻塞。自旋等阿迪本身虽然避免了线程切换的开销,但它要占用处理器时间,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好,泛指,如果锁被占用的时间很长,name自旋的线程只会白白浪费处理器资源。因此,自旋等待的时间必须有一个限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当使用传统的方式去挂起线程。
JDK6开始默认开启自旋锁,并引用自使用的自旋锁。自适应意味着自旋的时间不再固定了,而是前一次在锁上的自旋时间及锁的拥有者的状态来决定。
自旋是在轻量级锁中使用的,在重量级锁中,线程不使用自旋。
如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,name虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。比如:100次循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能忽略掉自旋过程,以避免浪费处理器自旋。
自旋锁和互斥锁类似,在任何时刻,最多只能有一个保持者。不同的是,互斥锁,如果资源被占用,资源申请着只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,调用者就一直在循环在哪里看是否该自旋锁的保持者释放锁。
锁消除###
锁消除是在虚拟机JIT在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判断依据是来源于逃逸分析的数据支持。如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无需进行。
public String concatString(String s1, String s2){
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
上面代码中,append()方法通过synchronized实现了线程同步的。但是sb对象不会逃逸出去,即只会在方法体内被访问。虽然这里有锁,但是可以被安全的消除,在即时编译之后,这段代码就会忽略所有的同步而直接执行。
锁粗化###
原则上,推荐将同步块的作用范围限制的尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使得需要听不的操作数量尽可能小。如果存在锁禁止,那么等待的线程也能尽快拿到锁。大部分时候这是对的,但是,如果一些列的联系操作都是同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中,那么即使没有线程竞争,频繁地进行互斥操作也导致不必要的性能损耗。
上面的cincatString()方法,如果sb对象定义在方法体外,那么就会有线程竞争,但是每个append()操作都是对同一对象反复加锁解锁,那么虚拟机探测到这样的情况的话,会把加锁同步的范围扩展到整个操作序列的外部,即扩展到第一个append()操作之前和最后一个append()操作之后,这样的锁范围扩展的操作称之为锁粗化。
可重入锁###
可重入锁也叫递归锁,指的是同一线程外层函数获得锁之后,内层递归函数仍然有获得该锁的代码,但不受影响。
Java 的ReentrantLocak和synchronized都是可重入锁,可重入锁最大的作用就是避免死锁。
注意:Java同步关键词是可重入的,这意味着如果一个Java同步方法调用另一个需要相同的锁的同步方法,当前线程持有锁,能直接进入,不需要有获取锁。
类锁和对象锁###
synchronized修饰普通方法、静态方法、代码块。
synchronized修饰普通方法,锁是对象本身。
偏向锁、轻量级锁和重量级锁###
synchronized的偏向锁、轻量级锁和重量级锁是通过Java对象头实现的。
Java对象的内存布局分为:对象头、实例数据和对齐填充。而对象头分为“Mark Word”和类型指针klass。Mark Word是关键,默认情况起存储对象是HashCode、分代年龄和锁标记位。
悲观锁和乐观锁###
悲观锁:假装会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
乐观锁:假装不会发生冲突,只在提交操作时检测是否违反数据完整性。(使用版本号或者时间戳类配合实现)
共享锁和排它锁###
共享锁:如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排它锁。获准共享锁的事务只能读数据,不能修改数据。
排它锁:如果事务T对数据A加上排它锁,则其他事物不能对A加任何类型锁。获得排它锁的事务即能读数据又能修改数据。
读写锁###
读写锁是一个资源能够被多个读线程访问,或被一个写线程访问但不能同时存在读线程。
互斥锁###
就是指一次最多只能有一个线程持有的锁。synchronized和Lock就是互斥锁。
无锁###
分段锁###
ConcurrentHashMap中采用了分段锁
闭锁###
闭锁是一种同步工具类,可以延迟线程的进度直到其达到终止状态,闭锁的作用相当于一扇门,在闭锁到达结束状态之前,这扇门一直关闭着,并且没有任何线程通过,当到达结束状态时,这扇门会打开允许所有线程通过。当闭锁到达结束状态后,将不会再改变状态,这扇门将永远保持打开状态。
死锁###
两个或者两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,他们将无法推进下去。
死锁会让你的程序挂起,无法完成任务。
死锁发生必须满足4个条件:
互斥条件:一个资源每次只能被一个线程访问
请求和保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺
循环等待条件:若干线程之间形成一种头尾详解的循环等待资源关系。
避免死锁最简单的方法就是组织循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序操作来避免死锁。
活锁###
LiveLock是一种形式活跃性问题,该问题尽管不会阻塞线程,但也不能继续执行,因为线程将不断重复执行相同的操作,而且总会失败。
活锁通常发送在处理事务消息的应用程序中:如果不能成功处理某个消息,name消息处理机制将回滚整个事务,并将它重新放到队列的开头;由于这条消息又被放回到队列开头,因此处理器将被反复调用,并返回相同的结果。