Java多线程、死锁、活锁、ThreadLocal源码分析

Java里的程序天生就是多线程的,启动线程的方式只有两种,继承Thead和实现Runnable接口

程序启动Main的线程


image.png
  • 线程进入到柱塞的情况只有在进入到synchronized的方法,在大多数的线程安全的集合或者是Map,在底层基本上会调用 Thread.yield()的方法,让出时间片嘛,就是线程允许执行的时间
image.png

Java中线程的状态分为6种:

  1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
  2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
    线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
  3. 阻塞(BLOCKED):表示线程阻塞于锁。
  4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。1
  5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
  6. 终止(TERMINATED):表示该线程已经执行完毕。
  • 关于 yield的方法,在 ConcurrentHashMap 类中的initTable方法中就调用了 Thread.yield()


    image.png
  • 等待唤醒机制:

    • wait(); 等待/冻结 :可以将线程冻结,释放CPU执行资格,释放CPU执行权,并把此线程临时存储到线程池
    • notify(); 唤醒线程池里面 任意一个线程,没有顺序;
    • notifyAll(); 唤醒线程池里面,全部的线程;在很多的开源框架,大部分都是 notifyALl,为什么,要保证全部唤醒,开源框架 wait的比较多,无法明确唤醒哪一个

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁

image.png
  • 死锁的危害
    1、线程不工作了,但是整个程序还是活着的
    2、没有任何的异常信息可以供我们检查。
    3、一旦程序发生了发生了死锁,是没有任何的办法恢复的,只能重启程序
  • 死锁的条件
    死锁是必然发生在多操作者(M>=2个)情况下,争夺多个资源(N>=2个,且N<=M)才会发生这种情况。
    所以,单线程自然不会有死锁;单资源,谁抢到就是谁的,但不会产生死锁。

  • 死锁还有几个要求:
    1、争夺资源的顺序不对,如果争夺资源的顺序是一样的,也不会产生死锁;
    2、争夺者拿到资源不放手。

  • 死锁的发生必须具备以下四个必要条件

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

  • 只要打破四个必要条件之一就能有效预防死锁的发生。
  • 打破互斥条件:改造独占性资源为虚拟资源,大部分资源已无法改造。
  • 打破不可抢占条件:当一进程占有一独占性资源后又申请一独占性资源而无法满足,则退出原占有的资源。
  • 打破占有且申请条件:采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待,这样就不会占有且申请。
  • 打破循环等待条件:实现资源有序分配策略,对所有设备实现分类编号,所有进程只能采用按序号递增的形式申请资源。
  • 避免死锁常见的算法有有序资源分配法、银行家算法

活锁?

两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有的锁释放的过程。
解决办法:每个线程休眠随机数,错开拿锁的时间。

线程饥饿

低优先级的线程,总是拿不到执行时间

ThreadLocal

ThreadLocal与Synchonized的比较

ThreadLocal和Synchonized都用于解决多线程并发訪问。可是ThreadLocal与synchronized有本质的差别。synchronized是利用锁的机制,使变量或代码块在某一时该仅仅能被一个线程訪问。而ThreadLocal为每个线程都提供了变量的副本,使得每个线程在某一时间訪问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。

image.png

image.png

image.png
image.png

image.png

先取到当前线程,然后调用getMap方法获取对应的ThreadLocalMap,ThreadLocalMap是ThreadLocal的静态内部类,然后Thread类中有一个这样类型成员,所以getMap是直接返回Thread的成员。

  • ThreadLocal的内部类ThreadLocalMap源码:
    image.png

可以看到有个Entry内部静态类,它继承了WeakReference,总之它记录了两个信息,一个是ThreadLocal类型,一个是Object类型的值。getEntry方法则是获取某个ThreadLocal对应的值,set方法就是更新或赋值相应的ThreadLocal对应的值。

image.png

image.png

回顾get方法,其实就是拿到每个线程独有的ThreadLocalMap

然后再用ThreadLocal的当前实例,拿到Map中的相应的Entry,然后就可以拿到相应的值返回出去。当然,如果Map为空,还会先进行map的创建,初始化等工作。

方法解析

  • 设置当前线程的线程局部变量的值。
• void set(Object value) 
  • 该方法返回当前线程所对应的线程局部变量。
• public Object get() 
  • 将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
• public void remove() 
  • 返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
• protected Object initialValue() 

RESOURCE代表一个能够存放String类型的ThreadLocal对象。此时不论什么一个线程能够并发访问这个变量,对它进行写入、读取操作,都是线程安全的。

public final static ThreadLocal RESOURCE = new ThreadLocal();

源码分析
ThreadLocal 说到底就是来安全访问线程的变量,不是这个线程的就访问不了。在Handler的底层用来Looper.prepareMainLooper();方法

    ////Looper的prepare方法,并且关联到主线程
    public static void prepareMainLooper() {
        //Only one Looper may be created per thread"
        // false意思不允许我们程序员退出(面向我们开发者),因为这是在主线程里面
        // TODO: 2018/5/17   
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            //把Looper设置为主线程的Looper
            sMainLooper = myLooper();
        }
    }

关于prepare(false)方法: Only one Looper may be created per thread 也就是说,一个线程只有一个Looper对象,要不然会抛出异常

   private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

我们可以自己实现这个功能对吧?如下的代码实现的过程

public class MyThreadLocal {
    /*存放变量副本的map容器,以Thread为键,变量副本为value*/

    private Map threadTMap = new HashMap<>();

    /**
     * 不去抢线程了,但是会去抢这个锁,对象锁,
     * 就好像很多的篮球放到很多的柜子里面,但是里面只有一把锁能开开
     *
     * @return
     */
    public synchronized T get(){
        return  threadTMap.get(Thread.currentThread());
    }

    /**
     * 这个锁很精华了
     * @param t
     */
    public synchronized void set(T t){
        threadTMap.put(Thread.currentThread(),t);
    }

}

但是这个Demo有个问题就是:不去抢线程了,但是会去抢这个锁,对象锁,就好像很多的篮球放到很多的柜子里面,但是里面只有一把锁能开开,这个是有很大的性能的问题的
在回头过来看这个问题:是以Thread.currentThread()为key的,是以当前的线程,

image.png
  • 以前看Handler的源码的时候,注意到一个奇怪的现象,这次又看到了,说下 ThreadLocalMap的扩容机制竟然是直接乘以2了,哈哈 有意思


    image.png

你可能感兴趣的:(Java多线程、死锁、活锁、ThreadLocal源码分析)