likes
comments
collection
share

并发编程-死锁/ThreadLocal

作者站长头像
站长
· 阅读数 10

死锁

死锁是指两个或多个进程在等待对方释放资源的情况下无限期地阻塞的现象,如下代码所示:

public static void main(String[] args) {
  final Object resource1 = "resource1";
  final Object resource2 = "resource2";

  Thread t1 = new Thread(() -> {
      synchronized (resource1) {
          System.out.println("Thread 1: locked resource 1");

          try {
              Thread.sleep(100);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }

          synchronized (resource2) {
              System.out.println("Thread 1: locked resource 2");
          }
      }
  });

  Thread t2 = new Thread(() -> {
      synchronized (resource2) {
          System.out.println("Thread 2: locked resource 2");

          try {
              Thread.sleep(100);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }

          synchronized (resource1) {
              System.out.println("Thread 2: locked resource 1");
          }
      }
  });

  t1.start();
  t2.start();
}

线程 t1 和 t2,它们都试图同时访问两个资源 resource1 和 resource2。线程 t1 首先获取了资源 resource1,然后等待 100 毫秒。在此期间,线程 t2 获取了资源 resource2。接下来,线程 t1 尝试获取资源 resource2,但是由于它已经被线程 t2 占用了,所以 t1 等待 t2 释放资源 resource2。同时,线程 t2 尝试获取资源 resource1,但是由于它已经被线程 t1 占用了,所以 t2 等待 t1 释放资源 resource1。这就导致了死锁的情况。

死锁发生的条件

  • 互斥,共享资源 X 和 Y 只能被一个线程占用
  • 占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
  • 不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
  • 循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。

死锁的解决方案

上述的四个条件中只要我们破坏其中一个就能避免死锁的发生,首先第一个我们是没有办法修改的,因为这个是业务需要,修改的话可能会对我们自己的业务造成影响出现bug。

  • 首先我们可以使用顺序锁,也就是都是先占有resource1然后再占有resource2,这样就不会出现死锁的情况,但是为了满足业务需求可能这个方案在实际开发中会用不上

  • 一次性获取所有资源上述代码我们可以添加一个分配资源的类如下图所示: public class Allocator {

         private List<Object> list=new ArrayList<>();
         synchronized  boolean apply(Object resource1,Object resource2){
             if(list.contains(resource1)||list.contains(resource2)){
                 return false;
             }
             list.add(resource1);
             list.add(resource2);
             return true;
         }
    
         synchronized void free(Object resource1,Object resource2){
             list.remove(resource1);
             list.remove(resource2);
         }
    
    
     }
    
    public class DeadlockExample {
        static Allocator allocator = new Allocator();
        public static void main(String[] args) {
            final Object resource1 = "resource1";
            final Object resource2 = "resource2";
    
            Thread t1 = new Thread(() -> {
                if(allocator.apply(resource1,resource2)){
                    synchronized (resource1) {
                        System.out.println("Thread 1: locked resource 1");
    
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                        synchronized (resource2) {
                            System.out.println("Thread 1: locked resource 2");
                        }
                    }
                    allocator.free(resource1,resource2);
                }
    
            });
    
            Thread t2 = new Thread(() -> {
                if(allocator.apply(resource1,resource2)){
                    synchronized (resource2) {
                        System.out.println("Thread 2: locked resource 2");
    
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                        synchronized (resource1) {
                            System.out.println("Thread 2: locked resource 1");
                        }
                    }
                    allocator.free(resource1,resource2);
                }
            });
            t1.start();
            t2.start();
        }
    }
    

    这样子也可以解决死锁的问题 并发编程-死锁/ThreadLocal

  • 还可以使用ReentrantLock解决死锁问题代码如下:

    
    public class NoDeadlockExample {
        private static final ReentrantLock lock1 = new ReentrantLock();
        private static final ReentrantLock lock2 = new ReentrantLock();
    
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                try {
                    if (lock1.tryLock()) {
                        System.out.println("Thread 1: locked resource 1");
    
                        if (lock2.tryLock()) {
                            System.out.println("Thread 1: locked resource 2");
                            // 业务逻辑
                            lock2.unlock();
                        }
                    }
                } finally {
                    lock1.unlock();
                }
            });
    
            Thread t2 = new Thread(() -> {
                try {
                    if (lock2.tryLock()) {
                        System.out.println("Thread 2: locked resource 1");
    
                        if (lock1.tryLock()) {
                            System.out.println("Thread 2: locked resource 2");
                            // 业务逻辑
                            lock2.unlock();
                        }
                    }
                } finally {
                    lock1.unlock();
                }
            });
    
            t1.start();
            t2.start();
        }
    }
    

    ReentrantLock 是 Java 中的一种可重入锁,如果某个线程无法获取到锁,则会立即释放已经获取到的锁并尝试重新获取锁,这样可以避免死锁的发生。

ThreadLocal

线程隔离机制。 ThreadLocal实际上一种线程隔离机制,也是为了保证在多线程环境下对于共享变量的访问的安全性 底层实现其实就是将共享变量拷贝一份存存到线程的工作内存,然后使用。

具体使用方式如下:

public class UserContextHolder {
    private static final ThreadLocal<UserContext> userContextThreadLocal = new ThreadLocal<>();

    public static UserContext getUserContext() {
        UserContext userContext = userContextThreadLocal.get();
        if (userContext == null) {
            userContext = new UserContext();
            userContextThreadLocal.set(userContext);
        }
        return userContext;
    }

    public static void clear() {
        userContextThreadLocal.remove();
    }
}

上面的代码定义了一个 UserContextHolder 类,其中包含了一个 ThreadLocal 对象 userContextThreadLocal,用于存储 UserContext 类型的变量副本。

当调用 getUserContext() 方法时,首先尝试从当前线程的变量副本中获取 UserContext 对象,如果不存在则创建一个新的 UserContext 对象,并将其存储到变量副本中。这样每个线程都有自己独立的 UserContext 对象,不会发生线程间数据共享的问题。

当需要清除线程变量时,可以调用 clear() 方法来删除当前线程的变量副本,避免内存泄漏。

通过 ThreadLocal 实现线程上下文的存储,可以避免将上下文对象作为参数传递给每个方法的麻烦,从而提高代码的可读性和可维护性。

ThreadLocal原理分析

首先看一下set源码如下

set源码

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}

首先是获取了一个ThreadLocalMap这个ThreadLocalMap就是当前线程的 threadLocals 并发编程-死锁/ThreadLocal 再看一下ThreadLocalMap结构

static class ThreadLocalMap {

    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

    /**
     * The initial capacity -- MUST be a power of two.
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * The table, resized as necessary.
     * table.length MUST always be a power of two.
     */
    private Entry[] table;

    /**
     * The number of entries in the table.
     */
    private int size = 0;

    /**
     * The next size value at which to resize.
     */
    private int threshold; // Default to 0
    .....
  }

发现其实最主要的就是一个table数组,数组中存着Entry对象其实就是一个键值对。 类似于 HashMap,它内部维护了一个 Entry 数组,每个 Entry 对象包含了一个 key 和一个 value,用于存储 ThreadLocal 对象和其对应的变量副本。 继续往下set看源码ThreadLocalMap存在则直接插入值不存在则创建 并发编程-死锁/ThreadLocal 并发编程-死锁/ThreadLocal 数组的初始大小是16 并发编程-死锁/ThreadLocal 然后threadLocalHashCode与运算获取到数组的下标然后存储

如果已经存在则直接set:

private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.refersTo(key)) {
            e.value = value;
            return;
        }

        if (e.refersTo(null)) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

如果对应的i中的key,就是我们想要则则直接赋值就可以了。如果Entry不是null 但是key是null,这里需要注意的是这个key就是ThreadLocal<?>实例,所以这里有人可能会奇怪这里不是一直持有的吗为什么会为null 并发编程-死锁/ThreadLocal 可以看到是这里是弱引用所以如果外面没有变量指向这个实例的话那么就会被释放,所以key就会变成null 最后再看replaceStaleEntry方法

private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.refersTo(key)) {
            e.value = value;
            return;
        }

        if (e.refersTo(null)) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

主要作用是清除被GC回收的key对象对应的Entry对象。当调用set方法时,如果发现当前线程对应的ThreadLocalMap对象中存在已经被GC回收的key对象对应的Entry对象,那么就会调用replaceStaleEntry方法来清除这些“脏”的Entry对象,以确保ThreadLocalMap中只保留有效的Entry对象。

具体来说,replaceStaleEntry方法会遍历引用队列中的所有Entry对象,对于每个Entry对象,如果其对应的key对象已经被GC回收,那么就将该Entry对象从ThreadLocalMap中移除。如果发现有多个Entry对象对应同一个key对象,那么只保留最新的那个Entry对象,将其余的Entry对象都移除

get源码

并发编程-死锁/ThreadLocal 并发编程-死锁/ThreadLocal 并发编程-死锁/ThreadLocal 所以此时就循环遍历查找

Thread.join

上文中再说Happens-Before可见性模型说到了Thread.join,为什么使用Thread.join可见呢。 底层其实就是用的是wait/notify,她会是当前线程阻塞一直等到上面的代码执行完之后才被唤醒: 并发编程-死锁/ThreadLocal

转载自:https://juejin.cn/post/7230838101076934693
评论
请登录