常见锁策略、锁优化及死锁

文章目录

        • Ⅰ常见的锁策略
          • (1)乐观锁和悲观锁
          • (2)读写锁
          • (3)自旋锁(Spin Lock)
          • (4)可重入锁
        • Ⅱ CAS(Compare and Swap)
          • (1) CAS的缺点
          • (2)优点:
          • (3)CAS在java中的应用
        • Ⅲ 锁优化
          • (1)适应性自旋(Adaptive Spinning)
          • (2)锁消除
          • (3)锁粗化
          • (4)轻量级锁
          • (5)偏向锁
        • Ⅳ java.util.concurrent(juc)包
            • (1) java.util.concurrent.locks.*:实现锁的一些工具
            • (2) java.util.concurrent.atomic.*:一些原子类
            • (3) 可以使多线程看似有返回值的类—— Callable接口类
            • (4)工具类:信号量、内存栅栏
            • (5)线程池相关实现:Executor, ExecutorService,Executors, ThreadPoolExecutor
            • (6)线程安全的数据结构:ConcurrentHashMap等
            • (7)多线程框架:ForkJoinPool
        • Ⅴ 死锁
            • (1)死锁产生的四个必要条件:
            • (2)死锁代码实现
            • (3)预防死锁
            • (4)避免死锁
            • (5)检测死锁。
            • (6)解除死锁
            • (7)用信号量去解决死锁问题

Ⅰ常见的锁策略

在Java中可以通过 循环CAS的方式来实现 原子操作

(1)乐观锁和悲观锁

乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现

乐观锁实现:

  • 使用版本号(Version)机制是乐观锁最常用的一种实现方式。版本号(Version)机制就是为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
  • CAS就是采用的就是乐观锁的思想。

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

  • synchronized锁就可以认为是悲观锁,当一个线程获得锁时,其余线程都会进入堵塞状态。

悲观锁的问题:总是需要竞争锁,进而导致发生线程切换,挂起其他线程;所以性能不高。
乐观锁的问题:并不总是能处理所有问题,所以会引入一定的系统复杂度。

适用场景:
乐观锁适用于写比较少的情况下(多读场景),即冲突很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是写多的情况,一般会经常产生冲突,所以一般多写的场景下用悲观锁就比较合适。

(2)读写锁

synchronized和ReentrantLock都是排他锁,这些锁在同一时刻只允许一个线程进行访问。而 读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

互斥原则——写独占,读共享:

  • 读-读能共存
  • 读-写不能共存
  • 写-写不能共存

一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。 在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。Java并发包提供读写锁的实现是ReentrantReadWriteLock,下面是ReentrantReadWriteLock的特性。
常见锁策略、锁优化及死锁_第1张图片
常用方法:

  • ReentrantReadWriteLock.ReadLock: 读锁由方法 readLock()返回。
  • ReentrantReadWriteLock.WriteLock: 写锁由方法 writeLock()返回。

读写锁的使用方式:

public class Cache {
     
    static Map<String, Object> map = new HashMap<String, Object>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock r = rwl.readLock();  //读锁
    static Lock w = rwl.writeLock();  //写锁:相当于互斥锁
    // 获取一个key对应的value
    public static final Object get(String key) {
     
              r.lock();
              try {
     
                        return map.get(key);
              } finally {
     
                      r.unlock();
              }
      }
      // 设置key对应的value,并返回旧的value
      public static final Object put(String key, Object value) {
     
              w.lock();
              try {
     
                      return map.put(key, value);
              } finally {
     
                      w.unlock();
              }
      }
      // 清空所有的内容
      public static final void clear() {
     
              w.lock();
              try {
     
                      map.clear();
              } finally {
     
                      w.unlock();
              }


上述示例中,Cache组合一个非线程安全的HashMap作为缓存的实现,同时使用读写锁的读锁和写锁来保证Cache是线程安全的。在读操作get(String key)方法中,需要获取读锁, 这使得并发访问该方法时不会被阻塞。 写操作put(String key, Object value)方法和clear()方法,在更新HashMap时必须提前获取写锁,当获取写锁后,其他线程对于读锁和写锁的获取均被阻塞,而只有写锁被释放之后,其他读写操作才能继续。Cache使用读写锁提升读操作的并发性,也保证每次写操作对所有的读写操作的可见性,同时简化了编程方式。

读写锁的实现:
读写锁同样依赖自定义同步器来实现同步功能 。回想ReentrantLock中自定义同步器的实现,同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器 >需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。

如果在一个整型变量上维护多种状态,就一定需要“按位切割使用”这个变量,读写锁将变量切分成了两个部分,高16位表示读,低16位表示写。

注意:
RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。目的也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。

(3)自旋锁(Spin Lock)

由于多线程的核心是CPU的时间分片,所以同一时刻只能有一个线程获取到锁。那么就面临一个问题,那么没有获取到锁的线程应该怎么办?

通常有两种处理方式:一种是没有获取到锁的线程就一直循环等待判断该资源是否已经释放锁,这种锁叫做自旋锁。它不用将线程阻塞起来(NON-BLOCKING);还有一种处理方式就是把自己阻塞起来,等待重新调度请求,这种叫做互斥锁

注意:

  • 使用自旋锁是考虑到在抢锁失败后,过不了多久锁就会被释放,所以可以让线程循环等待一段时间,而不用将线程阻塞起来等待重新调度。

自旋锁的缺点:
如果之前的锁没有很快被释放,则线程就一直循环等待,其实这是在消耗CPU 资源,长期在做无用功。

(4)可重入锁

重入锁ReentrantLock,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。Java中synchronized也是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

ReentrantLock虽然没能像synchronized关键字一样支持隐式的重进入,但是在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。

1)重进入
重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实现需要解决以下两个问题。

  • 线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
  • 锁的最终释放。 线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行 计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。

常见锁策略、锁优化及死锁_第2张图片

	public static ReentrantLock lock = new ReentrantLock();
    public static int i = 0;
    public void run() 
  {
     
        for (int j = 0;j<100000;j++) 
     {
     
            lock.lock(); //A线程获取了一个Lock锁
            lock.lock(); // 当A线程再次请求Lock锁时,可以以请求成功
            try {
     
                i++;
            }finally {
     
                lock.unlock();
                lock.unlock();
            }
        }
    }

ReentrantLock是通过组合自定义同步器来实现锁的获取与释放.

公平锁模型和非公平锁模型的区别:
公平锁模型,公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。即当A线程状态值变为0,释放锁时,按顺序由队列中的B线程先请求锁。

非公平锁模型:当线程A执行完之后,要唤醒线程B是需要时间的,而且线程B醒来后还要再次竞争锁,所以如果在切换过程当中,来了一个线程C,那么线程C是有可能获取到锁的,如果C获取到了锁,B就只能继续休眠了。

公平锁与非公平锁的优缺点:
公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。

ReentrantLock和Synchronized的区别?
相同点:
两个都是可重入锁,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善)。
不同点:

synchronized ReentrantLock
构成 它是java语言的关键字,是原生语法层面的互斥,需要jvm实现 JDK 1.5之后提供的API层面的互斥锁类
实现 通过JVM加锁解锁 API层面的加锁解锁,需要手动释放锁。
代码编写 采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用,更安全, ReentrantLock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。需要lock()和unlock()方法配合try/finally语句块来完成,
灵活性 锁的范围是整个方法或synchronized块部分 Lock因为是方法调用,可以跨方法,灵活性更大
是否可中断 不可中断,除非抛出异常 1 lockInterruptibly()放代码块中,调用interrupt()方法可中断;2 设置超时方法 tryLock(long timeout, TimeUnit unit),时间过了就放弃等待;
是否公平锁 非公平锁 两者都可以,默认公平锁,构造器可以传入boolean值,true为公平锁,false为非公平锁,
便利性 Synchronized的使用比较方便简洁,由编译器去保证锁的加锁和释放 需要手工声明来加锁和释放锁,
适用情况 资源竞争不是很激烈的情况下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronize,另外可读性好 ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态

参考博客:https://blog.csdn.net/qq_40551367/article/details/89414446

Ⅱ CAS(Compare and Swap)

CAS(Compare and swap): 是一种无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。本质上CAS是CPU提供的一个原子指令,有了硬件的支持可以让软件更加高效的实现并发编程的原子性。

一个CAS 涉及到以下操作:
我们假设内存地址V,旧的预期值A,需要修改的新值B。

  1. 比较A 与内存地址V的值 是否相等。(比较)
  2. 如果比较相等,则将内存地址V中的值修改为B。(交换)
  3. 返回操作是否成功。

CAS举例:

1)内存地址V中存放着值为10的变量

2)此时线程1要把变量值加1,对线程1来说,旧的预期值A=10,要修改的新值B=11

3)在线程1提交更新之前,另外一个线程2提前一步将内存地址V中的变量值率先更新成了11

4)线程1此时开始提交更新,首先进行A和内存地址V中的值比较,发现A不等于此时内存地址V中的值11,提交失败

5)线程1尝试重新获取内存地址V的当前值,并重新计算想要修改的值,对线程1来说,此时旧的预期值A=11,要修改的新值B=12,这个重新尝试的过程叫做自旋

6)这一次比较幸运,没有其他线程更改内存地址V中的值,线程1进行compare,发现A和内存地址V中的值相同

7)线程1进行Swap,把内存地址V中的值替换为B,也就是12


问题1:如何保证获取的当前值是内存中的最新值?(如果每次获得的当前值不是内存中的最新值,那么CAS机制将毫无意义)

答: 用volatile关键字修饰变量,使得每次对变量的修改操作完成后一定会先写回内存,保证了每次获取到值都是内存中的最新值!

问题2: 如何保证Compare和Swap过程中的原子性(如果Compare和Swap过程不是原子性操作,那么CAS机制也毫无意义)?

Compare和Swap过程的原子性是通过unsafe类来实现的,unsafe类为我们提供了硬件级别的原子操作!

(1) CAS的缺点

1) ABA 问题
如果一个内存地址V初次读取的值是A值,并且在准备赋值的时候检查到它仍然是A值,而如果在这段时间它的值被改为其他值,然后又改回A,那CAS操作会误认为它从来没有被修改过。

举个例子:
如果实际中利用CAS机制从取款机上取钱,假如账户开始有100元,在取款机上取走50,取款机出现问题一共提交了两次请求(线程1,线程2),第二次请求(线程2)在执行时因为某种原因被阻塞了,这时候有人往你的账户打了50元,线程2恢复了可执行状态,这个时候就会出现问题,原本线程2应该执行失败的,但是比较后仍然与旧值一致,这样就造成了账户实际上扣款了两次!

解决方法:加入版本信息,例如携带AtomicStampedReference 之类的时间戳作为版本信息,检查A是否发生变更。 在Compare阶段不仅比较预期值和此时内存中的值,还要比较两个变量的版本号是否一致,只有当版本号一致才进行后续操作。

2)只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。
当对一个共享变量操作的时候,可以使用带有自旋(循环)的CAS方法来保证原子性操作,但是如果是多个变量共享的时候,可以封装到对象中,但是开销太大,或者是直接使用synchronized锁来保证原子性。

3)CPU开销大
在并发量比较高的情况下,自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。

(2)优点:

可以避免优先级倒置和死锁等危险.使用CAS就可以不用加锁来实现线程的安全性。

(3)CAS在java中的应用

java.util.concurrent.atomic包下的类都是采用CAS来实现的无锁。

注:
从思想上来说,Synchorized属于悲观锁,悲观的认为程序中的并发多,所以严防死守,CAS机制属于乐观锁,乐观的认为程序中并发少,让线程不断的去尝试更新

Ⅲ 锁优化

锁优化技术,如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁膨胀(Lock Coarsening)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)等,这些技术都是为了在线程之间更高效地共享数据及解决竞争问题,从而提高程序的执行效率。

(1)适应性自旋(Adaptive Spinning)

在JDK 6中对自旋锁的优化,引入了自适应自旋。自适应意味着自旋的时间不再是固定的了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。

(2)锁消除

锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。

(3)锁粗化

为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是在某些情况下,一个程序对同一个锁高频地进行请求、同步与释放,会消耗掉一定的系统资源。虽然单次同步操作的时间可能很短,但是锁的请求、同步与释放本身带来的性能损耗反而不利于系统性能的优化了。锁粗化就是告诉我们任何事情都要有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。

(4)轻量级锁

轻量级锁是JDK 6时加入的新型锁机制,它名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就被称为“重量级”锁。轻量级锁并不是用来代替重量级锁的,它设计的初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。如果确实存在锁竞争,除了互斥量的本身开销外,还额外发生了CAS操作的开销。因此在有竞争的情况下,轻量级锁反而会比传统的重量级锁更慢。

注意:
轻量级锁就是将大部分操作交给用户态来完成。

(5)偏向锁

如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了。偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。

synchronized背后的原理:
如果检测到理论上只有一个线程——锁消除
如果检测到锁粒度很细,但同时加锁反复是同一个线程——锁粗化
如果是第一个线程,则先使用偏向锁,直到其他线程开始抢锁
偏向锁如果开始被其他线程获取,那么就膨胀成轻量级锁。

Ⅳ java.util.concurrent(juc)包

(1) java.util.concurrent.locks.*:实现锁的一些工具
(2) java.util.concurrent.atomic.*:一些原子类

AtomicBoolean,AtomicInteger,AtomicIntegerArray,AtomicLong,AtomicReference,AtomicStampedReference
拿AtomicInteger 举例,常见方法有

addAndGet(int delta);   i += delta;
decrementAndGet(); --i;
getAndDecrement(); i--;
incrementAndGet(); ++i;
getAndIncrement(); i++;
非原子的:
int i=0;
i++;

原子的:
AtomicInteger i = new AtomicInteger(0);
int v = i.getAndIncrement();
(3) 可以使多线程看似有返回值的类—— Callable接口类

Callable接口类似于Runnable ,从名字就可以看出来了,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值.

class MyThread implements Callable<String> {
     
	private int ticket = 10 ; // 一共10张票
	@Override
	public String call() throws Exception {
     
		while(this.ticket>0){
     
			System.out.println("剩余票数:"+this.ticket -- );
		}
		return "票卖完了,下次吧。。。" ;
	}
}

public class TestDemo {
     
	public static void main(String[] args) throws Exception {
     
		FutureTask<String> task = new FutureTask<>(new MyThread()) ;
		new Thread(task).start();
		new Thread(task).start();
		System.out.println(task.get());
	}
}
(4)工具类:信号量、内存栅栏

1)Semaphore——信号量

public class SemaphoreTest {
         
   // 最多5 个信号量
   private static final Semaphore avialable = new Semaphore(5); 
  public static void main(String[] args) {
         
       ExecutorService pool = Executors.newFixedThreadPool(10);    
       Runnable r = new Runnable() {
         
           public void run(){
         
               try {
         
                   avialable.acquire();    //阻塞   
                   Thread.sleep(10 * 1000);    
                   System.out.println(Thread.currentThread().getName());    
                   avialable.release();    
              } catch (InterruptedException e) {
         
                   e.printStackTrace();    
              }    
          }    
      };
       
       for(int i=0;i<10;i++){
         
           pool.execute(r);    
      }  
       pool.shutdown();    
  }  
}

如果线程能够获得信号量就继续执行,如果得不到就阻塞在avialable.acquire()这里,只有等到其他线程释放信号量时才有机会拿到。

应用:

  • 利用信号量实现进程互斥
    为使多个进程能互斥地访问某临界资源,只需为该资源设置一互斥信号量Semaphore,并设其初始值为1,然后将各进程访问该资源的临界区置于avialable.acquire();avialable.release()操作之间即可。

2)CountDownLatch
CountDownLatch相当于一个线程计数器

public class Demo {
     
   public static void main(String[] args) throws Exception {
     
       CountDownLatch latch = new CountDownLatch(10);
       Runnable r = new Runable() {
     
           @Override
           public void run() {
     
			  try {
     
                   Thread.sleep(Math.random() * 10000);
                   latch.countDown();
              } catch (Exception e) {
     
                   e.printStackTrace();
              }
          }
      };
       for (int i = 0; i < 10; i++) {
     
           new Thread(r).start();
      }
  // 必须等到10 人全部回来
       latch.await();
       System.out.println("比赛结束");
  }
}

A线程调用了latch.await()会阻塞,只有10个线程执行完latch.countDown()后,A线程才继续执行。

(5)线程池相关实现:Executor, ExecutorService,Executors, ThreadPoolExecutor

ThreadPoolExecutor构造方法的参数:
corePoolSize:正式员工的数量
maximumPoolSize:正式工和临时工的数量
keepAliveTime+unit:临时工的最多空闲时间
workQueue:传递任务的队列
threadFactory:构建线程的工厂,方便修改线程池中的线程名字、优先级等属性。
handler:任务失败时的处理方法

  • ThreadPoolExecutor.AbortPolicy: 抛出异常(默认)

  • ThreadPoolExecutor.DiscardPolicy:直接丢弃任务

  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队伍队列中最老的任务

  • ThreadPoolExecutor.CallerRunsPolicy:提交任务的线程自己处理

(6)线程安全的数据结构:ConcurrentHashMap等

1)大部分数据结构都是线程不安全的

ArrayList,LinkedList,HashMap,
HashSet,TreeMap,TreeSet,
StringBuilder

2) 线程安全的有
Vector, Stack, HashTable, StringBuffer,但是这些带有synchronized的版本属于早期的类,性能都不是很高。

3)线程安全的:BlockingQueue,BlockingDeque
具体的实现类:

ArrayBlockingQueue, LinkedListBlockingQueue,
DelayQueue, LinkedBlockingDeque,
LinkedTransferQueue, PriorityBlockingQueue,
SynchronousQueue.

4). 不属于juc下的线程安全的数据结构

List list = Collections.synchronizedList(new ArrayList(...));
Coolections.synchronizedMap();
Collections.synchronizedSet();

5). CopyWriteArrayList
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器是一种读写分离的容器。

CopyOnWrite的应用场景
读多写少的情况。

CopyOnWrite的缺点

  • 内存占用变多,因为维护了2份相同的数组。
  • 数据一致性问题,在读取的时候,可能是从旧的数组读取到的。

6). ConcurrentHashMap
ConcurrentHashMap是一个经常被使用的数据结构,它的设计与实现非常精巧,大量的利用了volatile,final,CAS等lock-free技术来减少锁竞争对于性能的影响,无论对于Java并发编程的学习还是Java内存模型的理解,ConcurrentHashMap的设计以及源码都值得非常仔细的阅读与揣摩。

纯key模型 key-value模型 时间复杂度
搜索树(红黑树) TreeSet TreeMap O(logn)
哈希表 HashSet HashMap O(1)

Hash:

HashMap<Integer,String> map = new HashMap<>();
map.put(1,"张飞")

put的过程:

  • 根据key计算其hash值
  • hash值可能会大于数组下标的有效范围,所以需要将hash值变成有效下标。
  • 根据下标,去数组对应的位置,找到链表的头,然后在链表中进行put.

如何将hash值转换为合法的下标:
前提:数组的长度n是2的幂次方
(n-1)&hash就可以得到一个合法的下标

自定义类作为HashMap的key:
覆写hashcode方法
覆写equals方法
两个对象a和b,如果a.equals(b)为true,则a.hashCode() == b.hashCode()

HashMap的树化过程是比较少见的。每个节点的单链表长度一般都不会太长,也就是说树化的可能性极低,只有当链表的长度过长时,才会转化为搜索树。

HashMap不是线程安全的,里面的size是共享的,并且可以被修改。ConcurrentHashMap是线程安全的。而HashTable和Collections.synchronizedMap虽然也是线程安全的,但是它们采用的是粗粒度锁,也就是只抢夺一把锁,无法做到并发,效率会比较低。ConcurrentHashMap把锁细化了,同时还使用了很多CAS操作,减少了锁的使用,所以两个线程可以同时在不同的两个链表中进行put。
ConcurrentHashMap在扩容时,A线程,B线程都进行插入时,需要将元素搬到新数组中,达到一个扩容的并发。

ConcurrentHashMap的速度快:

  • CAS操作,减少了锁的使用
  • 锁粒度的细化,提升了并发能力
  • 扩容时,增加了并发扩容能力
(7)多线程框架:ForkJoinPool

Ⅴ 死锁

死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

(1)死锁产生的四个必要条件:

1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

(2)死锁代码实现
import java.util.Date;
 
public class LockTest {
     
   public static String obj1 = "obj1";
   public static String obj2 = "obj2";
   public static void main(String[] args) {
     
      LockA la = new LockA();
      new Thread(la).start();
      LockB lb = new LockB();
      new Thread(lb).start();
   }
}
class LockA implements Runnable{
     
   public void run() {
     
      try {
     
         System.out.println(new Date().toString() + " LockA 开始执行");
         while(true){
     
            synchronized (LockTest.obj1) {
     
               System.out.println(new Date().toString() + " LockA 锁住 obj1");
               Thread.sleep(3000); // 此处等待是给B能锁住机会
               synchronized (LockTest.obj2) {
     
                  System.out.println(new Date().toString() + " LockA 锁住 obj2");
                  Thread.sleep(60 * 1000); // 为测试,占用了就不放
               }
            }
         }
      } catch (Exception e) {
     
         e.printStackTrace();
      }
   }
}
class LockB implements Runnable{
     
   public void run() {
     
      try {
     
         System.out.println(new Date().toString() + " LockB 开始执行");
         while(true){
     
            synchronized (LockTest.obj2) {
     
               System.out.println(new Date().toString() + " LockB 锁住 obj2");
               Thread.sleep(3000); // 此处等待是给A能锁住机会
               synchronized (LockTest.obj1) {
     
                  System.out.println(new Date().toString() + " LockB 锁住 obj1");
                  Thread.sleep(60 * 1000); // 为测试,占用了就不放
               }
            }
         }
      } catch (Exception e) {
     
         e.printStackTrace();
      }
   }
}

以上代码运行输出结果为:

Tue May 05 10:51:06 CST 2015 LockB 开始执行
Tue May 05 10:51:06 CST 2015 LockA 开始执行
Tue May 05 10:51:06 CST 2015 LockB 锁住 obj2
Tue May 05 10:51:06 CST 2015 LockA 锁住 obj1

简化以下死锁的实现:

public class Main {
     

    public static void main(String[] args)  {
     
        Object o1 = new Object();
        Object o2 = new Object();

        new Thread(()->{
     
            synchronized (o1){
     
                System.out.println("我拿到了o1锁");
                try {
     
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
                System.out.println("我在等待o2锁");
                synchronized (o2){
     
                    System.out.println("我拿到了o2");
                }
            }
        }).start();

        new Thread(()->{
     
            synchronized (o2){
     
                System.out.println("我拿到了o2锁");
                try {
     
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
                System.out.println("我在等待o1锁");
                synchronized (o1){
     
                    System.out.println("我拿到了o1");
                }
            }
        }).start();
    }
}

结果:

我拿到了o1锁
我拿到了o2锁
我在等待o1锁
我在等待o2锁

查看死锁情况:
jps(查看当前java进程的小工具):拿到当前Main进程的id号
常见锁策略、锁优化及死锁_第3张图片

jstack 12124:(查看线程)查看当前进程中的死锁情况

常见锁策略、锁优化及死锁_第4张图片

(3)预防死锁

去破坏产生死锁的四个必要条件来预防死锁,可能会导致系统资源利用率和系统吞吐量降低。

1)破坏互斥条件
如果允许系统资源都能共享使用,则系统不会进入死锁状态。

但是破坏互斥条件而预防死锁的方法不太可行,因为在有的场合应该保护这种互斥性,如打印机等临界资源只能互斥使用。

2)破坏不可抢占条件
当一个进程请求新的资源而得不到满足时,它必须释放已经保持的所有资源,待以后需要时再重新申请。

释放已获得的资源可能造成前一阶段工作的失效,反复地申请和释放资源会增加系统开销,降低系统吞吐量。这种方法常用于状态易于保存和恢复的资源,如CPU的寄存器及内存资源,一般不能用于打印机之类的资源。

3)破坏请求和保持条件
进程在运行前一次申请完它所需要的全部资源,在它的资源未满足前,不把该进程投入运行。一旦投入运行后,这些资源就一直归它所有,也不再提出其他资源请求,这样就可以保证系统不会发生死锁。

预先静态分配方法: 这种方式实现简单,但缺点也显而易见,系统资源被严重浪费,其中有些资源可能仅在运行初期或运行快结束时才使用,甚至根本不使用。而且还会导致 “饥饿”现象,当由于个别资源长期被其他进程占用时,将致使等待该资源的进程迟迟不能开始运行。

4)破坏循环等待
资源有序分配法: 首先给系统中的资源(锁)编号,规定每个进程,必须按编号递增的顺序请求资源,同类资源一次申请完。也就是说,只要进程提出申请分配资源Ri,则该进程在以后的资源申请中,只能申请编号大于Ri的资源。

这种方法存在的问题是,编号必须相对稳定,这就限制了新类型设备的增加;尽管在为资源编号时已考虑到大多数作业实际使用这些资源的顺序,但也经常会发生作业使用资源的顺序与系统规定顺序不同的情况,造成资源的浪费;此外,这种按规定次序申请资源的方法,也必然会给用户的编程带来麻烦

(4)避免死锁

该方法同样是属于事先预防的策略,在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免发生死锁。

1)系统安全状态
避免死锁的方法中,允许进程动态地申请资源,但系统在进行资源分配之前,应先计算此次资源分配的安全性。若此次分配不会导致系统进入不安全状态,则将资源分配给进程; 否则,让进程等待。

2) 银行家算法
银行家算法是最著名的死锁避免算法。它提出的思想是:把操作系统看做是银行家,操作系统管理的资源相当于银行家管理的资金,进程向操作系统请求分配资源相当于用户向银行家贷款。操作系统按照银行家制定的规则为进程分配资源,当进程首次申请资源时,要测试该进程对资源的最大需求量,如果系统现存的资源可以满足它的最大需求量则按当前的申请量分配资源,否则就推迟分配。当进程在执行中继续申请资源时,先测试该进程已占用的资源数与本次申请的资源数之和是否超过了该进程对资源的最大需求量。若超过则拒绝分配资源,若没有超过则再测试系统现存的资源能否满足该进程尚需的最大资源量,若能满足则按当前的申请量分配资源,否则也要推迟分配。

(5)检测死锁。

这种方法并不须事先采取任何限制性措施,也不必检查系统是否已经进入不安全区,此方法 允许系统在运行过程中发生死锁。但可通过系统所设置的检测机构,及时地检测出死锁的发生,并精确地确定与死锁有关的进程和资源,然后采取适当措施,从系统中将已发生的死锁清除掉。

(6)解除死锁

这是与检测死锁相配套的一种措施。当检测到系统中已发生死锁时,须将进程从死锁状态中解脱出来。常用的实施方法是 撤销或挂起一些进程,以便回收一些资源,再将这些资源分配给已处于阻塞状态的进程,使之转为就绪状态,以继续运行。死锁的检测和解除措施,有可能使系统获得较好的资源利用率和吞吐量,但在实现上难度也最大。

剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
撤消进程: 可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指 优先级、运行代价、进程的重要性和价值 等。

(7)用信号量去解决死锁问题

为了解决这个问题,我们不使用显示的去锁,而用信号量去控制。信号量可以控制资源能被多少线程访问,这里我们指定只能被一个线程访问,就做到了类似锁住。而 信号量可以指定去获取的超时时间,我们可以根据这个超时时间,去做一个额外处理。

对于无法成功获取的情况,一般就是重复尝试,或指定尝试的次数,也可以马上退出。

参考文章:死锁及解决办法

你可能感兴趣的:(多线程,多线程,java,并发编程)