注册

并发编程-线程的启动、死锁、线程安全、ThreadLocal

1 线程的启动方式


线程的启动方式只有两种。


方式1:继承Thread,然后调用start()启动。


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();

方式2:实现Runnable,然后交给Thread去启动。


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();

其他的比如线程池、FutureTask等都属于这两种的包装或封装。


并且Thread源码的注释中也清楚的写了有两种方式创建线程:


* <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 线程的状态


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


1、初始(NEW):新创建了一个线程,但是还没有调用start()方法。


2、运行(RUNNABLE):Java线程中将就绪(READY)和运行中(RUNNING)两种装填笼统的称为“运行”。


线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法,该状态的线程位于可运行线程池中,获取CPU的使用权,此时处于就绪状态(READY),就绪状态的线程在获得CPU时间片后变为运行中状态(RUNNING)。


3、阻塞(BLOCKED):表示线程阻塞于锁。


4、等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。


5、超时等待(TIMED_WAITING):该状态不同于WATING,它可以在指定的时间后自行返回。


6、终止(TERMINATED):表示该线程已经执行完毕。


线程生命周期如下:



3 死锁


3.1 概念


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


死锁是必然发生在多操作者(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的创建,初始化等工作。


作者:木水Code
链接:https://juejin.cn/post/7102969152477855780
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册