java中的常见锁

锁的种类:
        一、悲观锁
        重量级锁,会导致阻塞。每次在修改数据的时候,都认为其他线程会修改,所以都会加锁(读锁,写锁,行锁等),当其他线程需要访问数据的时候都会阻塞挂起.(类似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<Canditate> scoreUpdater = AtomicIntegerFieldUpdater.newUpdater(Canditate.class, "score");
    //检查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一样的线程同步的效果。

 

 

发布了18 篇原创文章 · 获赞 2 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_32285039/article/details/103674505