一篇文章了解公平锁、非公平锁、重入锁、自旋锁、读写锁(Java)1.0

锁是什么

synchronize

  • vt :使同步;使同时发生.
  • vi :与…同时发生;与…一起发生,

多线程共享一个内存资源,每个线程都从主内存copy资源到自己的工作内存中,大家都各改各的资源,再写入到主内存中,线程并发执行下,就可能会导致主内存数据失帧,产生的数据与我们预期的不一致。synchronize就是使线程的工作内存与主内存的数据进行同步。

公平锁、非公平锁

就是线程是否按时间有序的获取锁,还是无序的获取锁。Object的notify()和notifyAll()方法都是唤醒正在阻塞获取锁的线程,前者随机唤醒一个线程获取锁,后者唤醒全部线程去争抢锁。都是随机的获取锁,可知synchronized同步是非公平锁。
公平锁的实现靠Lock对象,我们再他的构造方法中可以设置是否是公平锁。

public ReentrantLock(boolean var1) {
        this.sync = (ReentrantLock.Sync)(var1 ? new ReentrantLock.FairSync() : new ReentrantLock.NonfairSync());
    }
重入锁(递归锁)

线程可以进入任何一个它己经拥有的锁所同步着的代码块,递归比较好理解,就是最外层获取到锁,那么子方法的锁都一并获取到,下面是代码演示。其实锁都是重入锁,这个概念理解一下即可,如果不是则非常容易照成死锁。

public class Test {

    public static void main(String[] args) throws Exception {
        Test test = new Test();

        new Thread(() -> {
            test.test1();
        }, "t1").start();

        new Thread(() -> {
            test.test2();
        }, "t2").start();

        new Thread(() -> {
            test.test3();
        }, "t3").start();
    }

    private synchronized void test1() {
        System.out.println(Thread.currentThread().getName() + " sync. invoke test1()");
        test2();
    }

    private synchronized void test2() {
        System.out.println(Thread.currentThread().getName() + " sync. invoke test2()");
        test3();
    }

    private synchronized void test3() {
        System.out.println(Thread.currentThread().getName() + " sync. invoke test3()");
    }
}
自旋锁

自:线程自己
旋:循环
锁:同步

相对于互斥锁,如果资源已经被占用,资源申请者只能进入阻塞状态。但是自旋锁不会引起调用者阻塞,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁。自旋锁的效率远高于互斥锁。我们可以拿一个JUC中原子引用包的AtomicInteger源码来演示。(这个没有加synchronized、lock,主要靠判断当前主内存的值是否符合我们的预期值,如果是则修改,这个同步是靠我们的预期值来维护的)

public final int getAndUpdate(IntUnaryOperator var1) {
        int var2;
        int var3;
        do {
            var2 = this.get();
            var3 = var1.applyAsInt(var2);
        } while(!this.compareAndSet(var2, var3));

        return var2;
    }
读写锁

读写锁把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。它允许同时有多个读者来访问共享资源,能提高并发性。而写者是排他性的。

只要理解:读读可以共享锁(多线程并发读取),读写不能共存(多线程阻塞,单线程运行),写写不能共存(多线程阻塞,单线程运行)。

代码演示:并发执行,写不能共存,只能顺序执行。完毕后,读取时可以并发读取。

public class Test {

    public static void main(String[] args) throws Exception {
        Cache cache = new Cache();

        for (int i = 0; i < 5; i++) {
            int finalI = i;
            new Thread(() -> {
                cache.put(finalI, Thread.currentThread().getName());
            }, "put" + String.valueOf(i)).start();
        }

        for (int i = 0; i < 5; i++) {
            int finalI = i;
            new Thread(() -> {
                cache.get(finalI);
            }, "get" + String.valueOf(i)).start();
        }
    }
}
//资源类
class Cache {
	//共享数据
    private volatile Map<Integer, String> map = new HashMap<>();
	//读写锁
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    void put(Integer key, String value) {
    	//写锁定
        lock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t 正在写入 = " + key);
            //模拟 暂停一会
            try {
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //写数据
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "\t 写入完成 = ");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.writeLock().unlock();
        }
    }

    void get(Integer key) {
    	//读锁
        lock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t 正在读取 = ");
            //模拟 暂停一会
            try {
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //读数据
            Object o = map.get(key);
            System.out.println(Thread.currentThread().getName() + "\t 读取完成 = " + o);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.readLock().unlock();
        }
    }
}
死锁

多线程的编程过程中,我们首先要保证线程安全性,对共享变量进行加锁。但是加太多锁,并发性又加降低。为了保证并发性能,所以加锁的同时也要注意锁的粒度,尽可能让锁的粒度变细(就像synchronized尽量不要放到方法上,要放到具体的同步代码块上)。但是锁的粒度变细,就会碰到死锁的问题。
代码演示:单线程请求下没有问题,但是多线程情况下就会又线程安全问题(假设from同时向多个帐号转账,可能会出现余额少减的情况,因为amount并没有同步,多线程都可以读写)。所以我们要加锁,保证from不会少减,to不会少增。

public class Test {
	//模拟 转账
    void transfer(Account from, Account to, int amount) {
    synchronized (from) {
            synchronized (to) {
		    	//转出 扣减
		        from.setAmount(from.getAmount() - amount);
		        //转入 增加
		        to.setAmount(to.getAmount() + amount);
          }
       }
    }
}
class Account {
    void setAmount(int amount) {

    }
    int getAmount() {
        return 0;
    }
}

进一步分析:线程在运行方法时,是可以在任何地方进行线程切换的,甚至是在一条语句中间。如果设想用户AB同时相互转账,恰好两个线程都获取到的第一行的锁,然后又在等待获取第二行的锁。这样子就发生死锁了。

总结:死锁的条件

  • 互斥等待
  • hold and wait 拿到锁 等待另外的锁
  • 循环等待
  • 无法剥夺的等待 没有超时时间

满足上面4条就会发生死锁,我们只要破除其中一个就可以避免死锁。

  • 破除互斥等待,一般无法破除,要保证同步。
  • 破除hold and wait,只要一次性获取所有锁资源,设置一个全局锁。
  • 破除循环等待,按顺序获取资源,可以用主键排序获取锁。
  • 破除无法剥夺的等待,设置超时时间。一般不推荐,耗时浪费资源。

上述几种破除都可以,但是都有各自的利弊,开发就是解决一个问题又会引出另外一个问题,再去解决,螺旋向上。

万一发生了死锁,我们在项目中又怎么去排查呢?我们先写个正真的死锁代码。

public class DeadLockDemo {
    public static void main(String[] args) {
    	//俩锁 让其相互等待
        String lockA = "A";
        String lockB = "B";

        new Thread(() -> {
        	//获取A锁
            synchronized (lockA) {
                System.out.println(Thread.currentThread().getName() + "\t 拿到lockA锁,等待lockB锁释放");
                //等待其他线程获取B锁
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //再去获取B锁
                synchronized (lockB) {
                    System.out.println(Thread.currentThread().getName() + "\t 计算");
                }
                System.out.println(Thread.currentThread().getName() + "\t 完成");
            }
        }, "AAA").start();

        new Thread(() -> {
        	//获取B锁
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + "\t 拿到lockB锁,等待lockA锁释放");
                //等待其他线程获取A锁
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //获取A锁
                synchronized (lockA) {
                    System.out.println(Thread.currentThread().getName() + "\t 计算");
                }
                System.out.println(Thread.currentThread().getName() + "\t 完成");
            }
        }, "BBB").start();
    }
}

执行结果

AAA	 拿到lockA锁,等待lockB锁释放
BBB	 拿到lockB锁,等待lockA锁释放

分析死锁:

jps -l	#查看java进程信息,查询PID
jstack PID #查看具体的堆栈信息

下面是打印出来的,结论发现一个死锁()

Java stack information for the threads listed above:
===================================================
"BBB":
        at 死锁.DeadLockDemo.lambda$main$1(DeadLockDemo.java:45)
        - waiting to lock <0x00000000d5ce8bd0> (a java.lang.String)
        - locked <0x00000000d5ce8c00> (a java.lang.String)
        at 死锁.DeadLockDemo$$Lambda$2/1769597131.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
"AAA":
        at 死锁.DeadLockDemo.lambda$main$0(DeadLockDemo.java:30)
        - waiting to lock <0x00000000d5ce8c00> (a java.lang.String)
        - locked <0x00000000d5ce8bd0> (a java.lang.String)
        at 死锁.DeadLockDemo$$Lambda$1/97730845.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

也可以用jconsole.exe,选择进程,点击线程,检测死锁。
在这里插入图片描述
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值