java 线程中常常遇到的锁的讲解

怎样产生死锁

  1. 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求该资源,则请求者只能等待,直至占有该资源的进程用毕释放。
  2. 请求和保持条件:指进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其它进程占有,此时请求进程阻塞,但又对自己获得的其它资源保持不放。
  3. 不剥夺条件:指进程已获得资源,在使用完之前,不能被剥夺,只能在使用完时由自己释放。
  4. 环路等待条件:指在发生死锁时,必然存在一个进程—资源的环形链,即进程集合(P0,P1,P2,…,Pn)中的P0正在等待一个P1占用的资源;P1正在等待一个P2占用的资源,……,Pn正在等待已被P0占用的资源。

相关链接

  1. java常用线程池讲解
  2. android handlerThread

如何避免死锁

  1. 加锁顺序
  2. 加锁时限
  3. 死锁检测
public class T2 {
    public String str="abc";

    static class R1 implements Runnable{
        public String str;
        public Object o1;
        public Object o2;

        public R1(String str,Object ob1,Object ob2) {
            this.str=str;
            this.o1=ob1;
            this.o2=ob2;
        }
        @Override
        public void run() {
            if (str.contains("a")){
                synchronized (o1){
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (o2){
                        System.out.println("aaa-bbb");
                    }
                }
            }
            if (str.contains("b")){
              synchronized (o2){
                  try {
                      Thread.sleep(2000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  synchronized (o1){
                      System.out.println("bbb-aaa");
                  }
              }
            }
        }
    }
    public static void main(String[] args){
        Object ob1=new Object();
        Object ob2=new Object();
        R1 r1=new R1("a",ob1,ob2);
        Thread thread1=new Thread(r1);
        thread1.start();

        R1 r2=new R1("b",ob1,ob2);
        Thread thread2=new Thread(r2);
        thread2.start();

    }


}

线程能提高程序的运行速度吗?

多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

1. 锁的分类

  • 公平锁、非公平锁
  • 可重入锁
  • 独享锁/共享锁
  • 互斥锁/读写锁
  • 乐观锁/悲观锁
  • 分段锁
  • 偏向锁/轻量级锁/重量级锁
  • 自旋锁

公平锁/非公平锁

  1. 公平锁是指多个线程按照申请锁的顺序来获取锁。公平锁不会产生饥饿锁,也就是只要排队等待,最终能等待到获取锁的机会。使用重入锁(默认是非公平锁)创建公平锁:
  2. 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
  3. 对于ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
    对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

ReenTrantLock实例

使用重入锁(默认是非公平锁)创建公平锁:

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
final ReentrantLock lock = new ReentrantLock();
final ReentrantLock lock = new ReentrantLock(false);

可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁

  1. 对于ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁。
  2. 对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。

可重入锁的实现要点

  1. 获取锁后,再次获取该锁时;判断当前线程是否是已经获取锁的线程,若是,则通过;否则,阻塞。
  2. 释放锁时,如果该线程获取了n(n>=1)次锁,则需要释放n次,才能释放成功,返回true;否则,返回false。
synchronized void setA() throws Exception{
    Thread.sleep(1000);
    setB();
}

synchronized void setB() throws Exception{
    Thread.sleep(1000);
}

上面的代码就是一个可重入锁的一个特点,如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁.

首先先看看ReentrantLock

1. 中断响应

对于synchronized块来说,要么获取到锁执行,要么持续等待。而重入锁的中断响应功能就合理地避免了这样的情况。比如,一个正在等待获取锁的线程被“告知”无须继续等待下去,就可以停止工作了。

2. 锁申请等待限时

可以使用 tryLock()或者tryLock(long timeout, TimeUtil unit) 方法进行一次限时的锁等待。

  1. 前者不带参数,这时线程尝试获取锁,如果获取到锁则继续执行,如果锁被其他线程持有,则立即返回 false ,也就是不会使当前线程等待,所以不会产生死锁。
  2. 后者带有参数,表示在指定时长内获取到锁则继续执行,如果等待指定时长后还没有获取到锁则返回false。
public class TryLockTest implements Runnable{
    public static ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        try {
            if (lock.tryLock(1, TimeUnit.SECONDS)) { // 等待1秒
                Thread.sleep(2000);  //休眠2秒
            } else {
                System.err.println(Thread.currentThread().getName() + "获取锁失败!");
            }
        } catch (Exception e) {
            if (lock.isHeldByCurrentThread()) lock.unlock();
        }

    }
    public static void main(String[] args) throws InterruptedException {
        TryLockTest test = new TryLockTest();
        Thread t1 = new Thread(test); t1.setName("线程1");
        Thread t2 = new Thread(test); t1.setName("线程2");
        t1.start();t2.start();
    }

}

独享锁()/共享锁

  1. 独享锁就是只能被一个线程所持有
  2. 共享锁是指能被多个线程所持有
  3. 对于JavaReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
  4. 读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
  5. 独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
  6. 对于Synchronized而言,是独享锁。

互斥锁/共享锁(读锁)/排它锁(写锁)

独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。

  1. 互斥锁在Java中的具体实现就是ReentrantLock.
  2. 读写锁在Java中的具体实现就是ReadWriteLock.
  3. 共享锁(S锁):共享 (S) 用于不更改或不更新数据的操作(只读操作),如 SELECT 语句。如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。
  4. 排他锁(X锁):用于数据修改操作,例如INSERT、UPDATE 或DELETE。确保不会同时同一资源进行多重更新。如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。
    我们在操作数据库的时候,可能会由于并发问题而引起的数据的不一致性(数据冲突)。

乐观锁/悲观锁

乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。

  1. 悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
  2. 乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。
  3. 悲观锁在Java中的使用,就是利用各种锁。
    乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

两种锁的使用场景

从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

乐观锁常见的两种实现方式

1. 版本号机制

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

2.CAS算法

即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数。

  1. 需要读写的内存值 V
  2. 进行比较的值 A
  3. 拟写入的新值 B

乐观锁的缺点

1. ABA问题

如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。

2.循环时间长开销大

自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

3 只能保证一个共享变量的原子操作

CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了 AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用 AtomicReference类把多个共享变量合并成一个共享变量来操作。

分段锁

分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作.

  1. 我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
  2. 当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
  3. 但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
  4. 分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

重量级锁/偏向锁/轻量级锁

这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

  1. 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
  2. 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
  3. 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

重量级锁Synchronized

1. Synchronized的作用

  1. 作用于方法时,锁住的是对象的实例(this)
  2. 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8则是metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程.
  3. synchronized作用于一个对象实例时,锁住的是所有以该对象为锁的代码块.

2. Synchronized的实现

自旋锁

在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

1. 自旋锁的时间阈值

自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!

  • JVM对于自旋周期的选择,jdk1.5这个限度是一定的写死的,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时JVM还针对当前CPU的负荷情况做了较多的优化
    1. 如果平均负载小于CPUs则一直自旋
    2. 如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞
    3. 如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞
    4. 如果CPU处于节电模式则停止自旋
    5. 自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差)
    6. 自旋时会适当放弃线程优先级之间的差异

自旋锁的优点

  1. 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快。
  2. 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

自旋锁使用实例

public class SpinLock {
    private AtomicReference<Thread> cas = new AtomicReference<Thread>();
    public void lock() {
        Thread current = Thread.currentThread();
        // 利用CAS
        while (!cas.compareAndSet(null, current)) {
            // DO nothing
        }
    }
    public void unlock() {
        Thread current = Thread.currentThread();
        cas.compareAndSet(current, null);
    }
}

ock()方法利用的CAS,当第一个线程A获取锁的时候,能够成功获取到,不会进入while循环,如果此时线程A没有释放锁,另一个线程B又来获取锁,此时由于不满足CAS,所以就会进入while循环,不断判断是否满足CAS,直到A线程调用unlock方法释放了该锁。

可重入的自旋锁和不可重入的自旋锁

上面代码是不支持重入的,它是不支持重入的,即当一个线程第一次已经获取到了该锁,在锁释放之前又一次重新获取该锁,第二次就不能成功获取到。由于不满足CAS,所以第二次获取会进入while循环等待,而如果是可重入锁,第二次也是应该能够成功获取到的
而且,即使第二次能够成功获取,那么当第一次释放锁的时候,第二次获取到的锁也会被释放,而这是不合理的。
为了实现可重入锁,我们需要引入一个计数器,用来记录获取锁的线程数。

ublic class ReentrantSpinLock {
    private AtomicReference<Thread> cas = new AtomicReference<Thread>();
    private int count;
    public void lock() {
        Thread current = Thread.currentThread();
        if (current == cas.get()) { // 如果当前线程已经获取到了锁,线程数增加一,然后返回
            count++;
            return;
        }
        // 如果没获取到锁,则通过CAS自旋
        while (!cas.compareAndSet(null, current)) {
            // DO nothing
        }
    }
    public void unlock() {
        Thread cur = Thread.currentThread();
        if (cur == cas.get()) {
            if (count > 0) {// 如果大于0,表示当前线程多次获取了该锁,释放锁通过count减一来模拟
                count--;
            } else {// 如果count==0,可以将锁释放,这样就能保证获取锁的次数与释放锁的次数是一致的了。
                cas.compareAndSet(cur, null);
            }
        }
    }
}

参考文献

  1. 博客园
  2. 重入锁:ReenTrantlock详解
  3. java 中的锁 – 偏向锁、轻量级锁、自旋锁、重量级锁
  4. 面试必备之深入理解自旋锁锁
  5. 死锁的产生以及如何避免死锁
  6. 线程死锁产生的必要条件
  7. 可重入锁的介绍
  8. 乐观锁以及悲观锁
  9. 怎样查看死锁
发布了16 篇原创文章 · 获赞 2 · 访问量 633
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 技术工厂 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览