锁的种类:
一、悲观锁
重量级锁,会导致阻塞。每次在修改数据的时候,都认为其他线程会修改,所以都会加锁(读锁,写锁,行锁等),当其他线程需要访问数据的时候都会阻塞挂起.(类似java中的synchronized)如:排他锁,互斥锁
二、 乐观锁
本质是无锁,效率比较高、无阻塞、无等待、重试
通常在数据库表设计的时候,会有一个version(版本)字段,每次在做写操作的时候,会先查这个版本号,然后那这个版本号当做条件去做修改,修改的时候也将版本号进行更新(如+1),如果此时有别人对同一条数据做了修改,版本号就会+1,而导致此时无法修改,就会进行重试。(与java中的SAS无锁机制的思想类似)
可重入性(synchronized和lock都是重入锁)
递归锁,外层方法的锁可以传递到内层方法,而不需要再次创建新的锁,解决死锁问题
synchronized(可见性+原子性)内置锁(重量级)
同步方法:
一、使用的是this锁:
public synchronized void sale() {
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "张票");
trainCount--;
}
}
二、同步代码块(推荐使用):
synchronized(对象)//这个对象可以为任意对象
{
需要被同步的代码
}
静态同步函数:
public static void sale() {
synchronized (ThreadTrain3.class) {
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "张票");
trainCount--;
}
}
}
静态的同步函数使用的锁是该函数所属字节码文件对象可以用 getClass方法获取,也可以用当前类名.class 表示。
lock显示锁(轻量级)
ReentrantLock()重入锁
手动加锁lock(),释放锁unlock()
tryLock()
带参数tryLock(long time, TimeUnit unit);
第一个是等待时长,第二个是时间单位
等待相应时长返回结果
不带参数
立即返回结果
lockInterruptibly();获得锁,但优先相应中断
等待和唤醒
需要Condition condition = lock.newCondition();锁对象.condition.await()
需要Condition condition = lock.newCondition();锁对象.condition..signal()
ReentrantLock(true)
公平锁,大家排队获取锁
ReadWriteLock读写锁
读写分离锁
读-读 不互斥:读读之间不阻塞
读-写互斥:读阻塞写,写也会阻塞读
写-写互斥:写写阻塞
读的操作远远大于写的操作,则读写锁就可以发挥最大的功效,提高系统性能。
互斥锁与自旋锁:互斥锁就是悲观锁,有等待会阻塞;自旋锁就是乐观锁,效率高不等待,循环监测锁的标志,要注意死循环。
错误的加锁
如锁对象为Integer类型,则会造成线程不安全,以为Integer属于不变对象,对它进行i++其实就是创建一个新对象,所以会导致不安全问题
CAS无锁机制
AutomicInteger(原子类)线程安全,没有上锁,使用了CAS无锁技术
Compare and swap:即比较再交换
三个参数(V,E,N)。V:表示需要更新变量(主内存);E:预期值(本地内存);N新值。当V=E的时候,说明没有被别的线程改过,将V的值设置为新值,若V!=E,则重新刷新主内存,循环比较(自旋锁)
缺点:ABA问题,如将A值改为B然后又改回A,则会被误认为没有修改过,java并发包中提供了一个带有标记的原子应用类AtomicStampedReference,它可以通过控制变量值的版本(时间戳)来保证CAS的正确性。注意死循环
无锁数组
AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray(使用方法查看API)
让普通变量也享受原子操作
AtomicIntergerFieldUpdater:
public class AtomicIntegerFieldUpdaterDemo {
public static class Canditate {
int id;
volatile int score;
}
public final static AtomicIntegerFieldUpdater
//检查Updater是否工作正确
public static AtomicInteger allScore = new AtomicInteger(0);
public static void main(String[] args) {
final Canditate stu = new Canditate();
Thread[] t = new Thread[10000];
for (int i = 0; i < 10000; i++) {
t[i] = new Thread() {
@Override
public void run() {
if (Math.random() > 0.4) {
scoreUpdater.incrementAndGet(stu);
allScore.incrementAndGet();
}
}
};
t[i].start();
}
for (int i = 0; i < 10000; i++) {
System.out.println("score=" + stu.score);
System.out.println("allScore=" + allScore);
}
}
}
注意事项
Updater只能修改它可见范围内的变量。因为Updater使用反射得到这个变量。如果变量不可见,就会出错。如score申明为private,就不行了为了确保变量被正确的读取,它必须是volatile类型的由于CAS操作会通过对象实例中的偏移量直接进行赋值,因此,不支持static字段
锁的优化
减小锁持有时间
只在必要时进行同步
有助于降低锁冲突的可能性,进而提升系统的并发能力
锁的粗化
与减少锁的持有时间思想相反,把所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步次数。因为不断的请求锁也会浪费资源。(具体使用锁粗化还是减少锁的持有时间,根据具体情况进行选择)如:
public void demoMethod(){
synchronized(lock){
//do sth.
}
}
//其他操作
synchronized(lock){
//do sth.
}
}
改为
public void demoMethod(){
synchronized(lock){
//do sth.
//其他操作
}
}
减小锁粒度
概念:缩小锁定对象的范围,从而减少锁冲突的可能性。进而提高系统的并发能力
如ConcurrentHashMap:需要增加一个新的表项,并不是对整个HashMap加锁,而是首先根据hashcode得到该表项应该被存放到拿个段中,然后对该段加锁,并完成put()操作。在多线程环境中,如果多个线程同时进行put()操作,只要被加入的表项不存放在同一个段中,,则线程间便可以做到真正的并行。由于默认16个段,因此,运气好的话可以同时接受16个线程同时插入,从而大大提供其吞吐量。
问题:当系统需要取得全局锁时,其消耗的资源互比较多,如:size()方法,需要获取所有段的所有锁
只有在类似于size()获取全局信息的方法调用不频繁时,这种减小粒度的方法才能真正意义上提高系统吞吐量。
java虚拟机对锁的优化
锁偏向
核心思想:如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无须再做任何同步操作。
适用场合:同一个线程请求相同的锁。
缺点:对于锁竞争比较激烈的场合,效果不佳。因为每次请求都是不同的线程请求相同的锁,锁偏向会失效,因此还不如不启用偏向锁
使用方法:在java虚拟机参数-XX:+UseBiasedLocking可以开启偏向锁
轻量级锁
当偏向锁失败,虚拟机并不会立即挂起,而是使用轻量级锁的优化手段,它只是简单地将对象头部作为指针,指向持有锁的线程堆栈内部,来判断一个线程释放持有对象锁。如果获得轻量级锁成功,则可以顺利进入临界区。如果失败,表示其他线程争夺到了锁,那么当前线程的锁请求就会膨胀为重量级锁
自旋锁
锁膨胀后,虚拟机为了避免线程真实地在操作系统层面挂起,虚拟机就会使用自旋锁。由于当前线程暂时无法获得锁,但是什么时候获得锁是一个未知数。如果贸然的将线程挂起,可能是一种得不偿失的操作。因此系统会进行一次赌注:它会假设在不久的将来,线程可以得到这把锁。因此虚拟机会让当前线程做几个空循环(自旋的含义),经过若干次循环后,如果可以得到锁,那么久顺利进入临界区,如果还不能获得锁,才会真实地将线程在操作系统层面挂起。
锁消除
彻底的锁优化,java虚拟机在JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。可以节省毫无意义的请求锁时间。(可能使用了JDK的API造成不竞争的锁)
关键技术:逃逸分析,就是观察某一个变量是否会逃出某一个作用域
在-server模式下,使用-XX:+DoEscapeAnalysis参数打开逃逸分析。
使用-XX:EliminateLocks参数可以打开锁消除。
synchronized和lock的区别:synchronized重量级,不可控制lock轻量级,可控,灵活性高,使用多。
synchronized和volatile的区别
synchronized保证内存可见性和操作的原子性
volatile只能保证内存可见性
volatile不需要加锁,比synchronized更轻量级,也不会阻塞线程
volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化(如编译器重排序的优化)
volatile是变量的修饰符,仅能用于变量,而synchronized是一个方法或块的修饰符
volatile本质是在告诉JVM当前变量在寄存器中的值是不确定的,使用前需要先从主存中读取,因此可以实现可见性。而对n=n+1,n++等操作时,volatile关键字将失效,不能起到像synchronized一样的线程同步的效果。