Programación concurrente: inicio de subprocesos, interbloqueo, seguridad de subprocesos, ThreadLocal

¡Continúe creando, acelere el crecimiento! Este es el quinto día de mi participación en el "Nuggets Daily New Plan · June Update Challenge", haz clic para ver los detalles del evento


1 Cómo iniciar un hilo

Solo hay dos formas de iniciar un hilo.

Modo 1: Heredar Thread, luego llamar al start()inicio.

private static class PrimeThread extends Thread {
    @Override
    public void run() {
        System.out.println("thread extend Thread---name:" + Thread.currentThread().getName());
    }
}

PrimeThread thread = new PrimeThread();
thread.start();
复制代码

Método 2: Implementar Runnabley luego entregar Threadpara comenzar.

private static class PrimeRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("thread implements Runnable---name:" + Thread.currentThread().getName());
    }
}

PrimeRunnable runnable = new PrimeRunnable();
new Thread(runnable).start();
复制代码

Otros, como los grupos de subprocesos, FutureTasketc., pertenecen al empaquetamiento o encapsulamiento de estos dos.

Y Threadlos comentarios del código fuente también escriben claramente que hay dos formas de crear hilos:

* <p>
* There are two ways to create a new thread of execution. One is to
* declare a class to be a subclass of <code>Thread</code>. This
* subclass should override the <code>run</code> method of class
    * <code>Thread</code>. An instance of the subclass can then be
    * allocated and started. For example, a thread that computes primes
    * larger than a stated value could be written as follows:
* <hr><blockquote><pre>
*     class PrimeThread extends Thread {
*         long minPrime;
*         PrimeThread(long minPrime) {
*             this.minPrime = minPrime;
*         }
*
*         public void run() {
*             // compute primes larger than minPrime
*             &nbsp;.&nbsp;.&nbsp;.
*         }
*     }
* </pre></blockquote><hr>
* <p>
* The following code would then create a thread and start it running:
* <blockquote><pre>
*     PrimeThread p = new PrimeThread(143);
*     p.start();
* </pre></blockquote>
* <p>
* The other way to create a thread is to declare a class that
    * implements the <code>Runnable</code> interface. That class then
        * implements the <code>run</code> method. An instance of the class can
            * then be allocated, passed as an argument when creating
            * <code>Thread</code>, and started. The same example in this other
            * style looks like the following:
* <hr><blockquote><pre>
*     class PrimeRun implements Runnable {
*         long minPrime;
*         PrimeRun(long minPrime) {
*             this.minPrime = minPrime;
*         }
*
*         public void run() {
*             // compute primes larger than minPrime
*             &nbsp;.&nbsp;.&nbsp;.
*         }
*     }
* </pre></blockquote><hr>
 * <p>
复制代码

2 El estado del hilo

El estado de un hilo en Java se divide en 6 tipos:

1. Inicial (NUEVO): Se crea un nuevo subproceso, pero start()no se ha llamado al método.

2. EJECUTABLE: Los dos tipos de carga en el subproceso de Java, LISTO y EN EJECUCIÓN, generalmente se denominan "en ejecución".

Después de que se crea el objeto de subproceso, otros subprocesos (como el subproceso principal) llaman al método start () del objeto. El subproceso en este estado se ubica en el grupo de subprocesos ejecutables y obtiene el derecho a usar la CPU. En este momento , está en estado listo (READY), estado listo El subproceso pasa a EJECUTAR después de obtener el intervalo de tiempo de la CPU.

3. Bloqueado (BLOCKED): Indica que el hilo está bloqueado en el candado.

4. En espera (WAITING): Un subproceso que entra en este estado necesita esperar a que otros subprocesos realicen algunas acciones específicas (notificaciones o interrupciones).

5. Tiempo de espera excedido (TIMED_WAITING): este estado es diferente de WATING, puede regresar por sí mismo después del tiempo especificado.

6. Terminado (TERMINATED): Indica que el hilo ha sido ejecutado.

El ciclo de vida del hilo es el siguiente:

3 punto muerto

3.1 Concepto

Deadlock es un fenómeno en el que dos o más procesos se bloquean debido a la competencia por los recursos o por la comunicación entre ellos durante el proceso de ejecución, si no hay una fuerza externa, no podrán avanzar. En este punto, se dice que el sistema está en un estado de interbloqueo o el sistema tiene un interbloqueo.

死锁是必然发生在多操作者(M>=2个)情况下,争夺多个资源(N>=2个,且N<=M)才会发生这种情况。很明显,单线程自然不会有死锁。

死锁还有几个要求:

  1. 争夺资源的顺序不对,如果争夺资源的顺序是一样的,也不会产生死锁。
  2. 争夺者拿到资源不放手。

3.1.1 学术定义

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

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

理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。只要打破四个必要条件之一就能有效预防死锁的发生。

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

避免死锁常见的算法有有序资源分配法、银行家算法。

示例代码:

/**
 * @Description: 死锁的产生
 * @CreateDate: 2022/3/15 2:31 下午
 */
public class NormalDeadLock {

    /**
     * 第1个锁
     */
    private static final Object LOCK_1 = new Object();
    /**
     * 第2个锁
     */
    private static final Object LOCK_2 = new Object();

    /**
     * 第1个拿锁的方法 先去拿锁1,再去拿锁2
     *
     * @throws InterruptedException 中断异常
     */
    private static void method1() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        synchronized (LOCK_1) {
            System.out.println(threadName + " get LOCK_1");
            Thread.sleep(100);
            synchronized (LOCK_2) {
                System.out.println(threadName + " get LOCK_2");
            }
        }
    }

    /**
     * 第2个拿锁的方法 先去拿锁2,再去拿锁1,这就导致方法1和方法2各拿一个锁,然后互不相让,都不释放自己的锁,造成了互斥,就产生了死锁
     *
     * @throws InterruptedException 中断异常
     */
    private static void method2() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        synchronized (LOCK_2) {
            System.out.println(threadName + " get LOCK_2");
            Thread.sleep(100);
            synchronized (LOCK_1) {
                System.out.println(threadName + " get LOCK_1");
            }
        }
    }

    /**
     * 子线程PrimeThread1
     */
    private static class PrimeThread1 extends Thread {
        private final String name;

        public PrimeThread1(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            Thread.currentThread().setName(name);
            try {
                method1();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 子线程PrimeThread2
     */
    private static class PrimeThread2 extends Thread {
        private final String name;

        public PrimeThread2(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            Thread.currentThread().setName(name);
            try {
                method2();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        PrimeThread1 thread1 = new PrimeThread1("PrimeThread1");
        PrimeThread2 thread2 = new PrimeThread2("PrimeThread2");

        thread1.start();
        thread2.start();
    }

}
复制代码

执行后,可以看到控制台没有结束运行,看不到Process finished with exit code 0,但是又一直处于静止状态。

PrimeThread1 get LOCK_1
PrimeThread2 get LOCK_2
复制代码

3.2 危害

  1. 线程不工作了,但是整个程序还是活着的。
  2. 没有任何的异常信息可以供我们检查。
  3. 一旦程序发生了发生了死锁,是没有任何的办法恢复的,只能重启程序,对正式已发布程序来说,这是个很严重的问题。

3.3 解决方案

关键是保证拿锁的顺序一致。

两种解决方式:

1、内部通过顺序比较,确定拿锁的顺序。

比如上述示例代码中,可以让方法1和方法2同时都先拿锁1,然后再去拿锁2,就能解决死锁问题。

private static void method1() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        synchronized (LOCK_1) {
            System.out.println(threadName + " get LOCK_1");
            Thread.sleep(100);
            synchronized (LOCK_2) {
                System.out.println(threadName + " get LOCK_2");
            }
        }
}

private static void method2() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        synchronized (LOCK_1) {
            System.out.println(threadName + " get LOCK_1");
            Thread.sleep(100);
            synchronized (LOCK_2) {
                System.out.println(threadName + " get LOCK_2");
            }
        }
}
复制代码

修改后后,可以看到程序能正常执行。

PrimeThread1 get LOCK_1
PrimeThread1 get LOCK_2
PrimeThread2 get LOCK_1
PrimeThread2 get LOCK_2

Process finished with exit code 0
复制代码

2、采用尝试拿锁的机制。

示例代码:

/**
 * @Description: 尝试拿锁,解决死锁问题
 * @CreateDate: 2022/3/15 2:57 下午
 */
public class TryGetLock {
    /**
     * 第1个锁
     */
    private static final Lock LOCK_1 = new ReentrantLock();
    /**
     * 第2个锁
     */
    private static final Lock LOCK_2 = new ReentrantLock();

    /**
     * 方法1 先尝试拿锁1,再尝试拿锁2,拿不到锁2的话连同锁1一起释放
     *
     * @throws InterruptedException 中断异常
     */
    private static void method1() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        Random r = new Random();
        while (true) {
            if (LOCK_1.tryLock()) {
                System.out.println(threadName + " get LOCK_1");
                try {
                    if (LOCK_2.tryLock()) {
                        try {
                            System.out.println(threadName + " get LOCK_2");
                            System.out.println("method1 do working...");
                            break;
                        } finally {
                            LOCK_2.unlock();
                        }
                    }
                } finally {
                    LOCK_1.unlock();
                }
            }
            //注意:这里需要给个很短的间隔时间去让其他线程拿锁,不然可能会造成活锁
            Thread.sleep(r.nextInt(3));
        }
    }

    /**
     * 方法2 先尝试拿锁2,再尝试拿锁1,拿不到锁1的话连同锁2一起释放
     *
     * @throws InterruptedException 中断异常
     */
    private static void method2() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        Random r = new Random();
        while (true) {
            if (LOCK_2.tryLock()) {
                System.out.println(threadName + " get LOCK_2");
                try {
                    if (LOCK_1.tryLock()) {
                        try {
                            System.out.println(threadName + " get LOCK_1");
                            System.out.println("method2 do working...");
                            break;
                        } finally {
                            LOCK_1.unlock();
                        }
                    }
                } finally {
                    LOCK_2.unlock();
                }
            }
            Thread.sleep(r.nextInt(3));
        }
    }

    private static class PrimeThread1 extends Thread {
        private final String name;

        public PrimeThread1(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            Thread.currentThread().setName(name);
            try {
                method1();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private static class PrimeThread2 extends Thread {
        private final String name;

        public PrimeThread2(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            Thread.currentThread().setName(name);
            try {
                method2();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        PrimeThread1 thread1 = new PrimeThread1("PrimeThread1");
        PrimeThread2 thread2 = new PrimeThread2("PrimeThread2");

        thread1.start();
        thread2.start();
    }
}
复制代码

执行结果:

PrimeThread2 get LOCK_2
PrimeThread1 get LOCK_1
PrimeThread2 get LOCK_2
PrimeThread2 get LOCK_1
method2 do working...
PrimeThread1 get LOCK_1
PrimeThread1 get LOCK_2
method1 do working...

Process finished with exit code 0
复制代码

4 其他线程安全问题

4.1 活锁

两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有的锁释放的过程。

解决办法:每个线程休眠随机数,错开拿锁的时间。

如上边的尝试拿锁示例代码中,如果不加随机sleep,就会造成活锁。

4.2 线程饥饿

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

5 ThreadLocal

5.1 与Synchonized的比较

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

5.2 ThreadLocal的使用

ThreadLocal类接口很简单,只有4个方法:

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

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

示例代码:

/**
 * @Description: 使用ThreadLocal
 * @CreateDate: 2022/3/15 3:37 下午
 */
public class UseThreadLocal {

    private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

    private void startThreadArray() {
        Thread[] threads = new Thread[3];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new PrimeRunnable(i));
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }
    }

    private static class PrimeRunnable implements Runnable {
        private final int id;

        public PrimeRunnable(int id) {
            this.id = id;
        }

        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            THREAD_LOCAL.set("线程" + id);
            System.out.println(threadName + ":" + THREAD_LOCAL.get());
        }
    }

    public static void main(String[] args) {
        UseThreadLocal useThreadLocal = new UseThreadLocal();
        useThreadLocal.startThreadArray();
    }
}
复制代码

5.3 ThreadLocal的内部实现

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
复制代码
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
复制代码
ThreadLocal.ThreadLocalMap threadLocals = null;
复制代码

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

看下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;
            
            //类似于map的key、value结构,key就是ThreadLocal,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.
         * 用数组保存了Entry,因为可能有多个变量需要线程隔离访问
         */
        private Entry[] table;
复制代码

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

        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            // Android-changed: Use refersTo()
            if (e != null && e.refersTo(key))
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }
复制代码
        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)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

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

回顾get方法,其实就是拿到每个线程独有的ThreadLocalMap,然后再用ThreadLocal的当前实例,拿到Map中的相应的Entry,然后就可以拿到相应的值返回出去。当然,如果Map为空,还会先进行Map的创建,初始化等工作。

关注木水小站 (zhangmushui.cn)和微信公众号【木水Code】,及时获取更多最新技术干货。

Supongo que te gusta

Origin juejin.im/post/7102969152477855780
Recomendado
Clasificación