《Java 并发编程》共享模型之管程

《Java 并发编程》专栏索引 👉 《Java 并发编程》进程与线程 👉《Java 并发编程》共享模型之管程 👉《Java 并发编程》共享模型之内存 👉《Java 并发编程》共享模型之无锁 👉《Java 并发编程》共享模型之不可变 👉《Java 并发编程》线程池

@[TOC](《Java 并发编程》共享模型之管程)

🚀1. 共享带来的问题

🚁1.1 临界区

  • 一个程序运行多个线程本身没有问题
  • 问题出在多个线程访问共享资源 (1)多个线程读共享资源其实也没有问题 (2)在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  • 一段代码内如果存在对共享资源的多线程读写操作,称这块代码块为临界区

例如,下面代码中的临界区

java 复制代码
static int counter = 0
static void increment() {
	// 临界区
	counter++;
}
static void decrement() {
	// 临界区
	counter--;
}

🚁1.2 竞态条件

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。

🚀2. synchronized 解决方案

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronized、lock
  • 非阻塞式的解决方案:原子变量

这里使用阻塞式的解决方案:synchronized 来解决上述问题,即俗称的【对象锁】 ,它采用互斥的方式让同一时刻最多只有一个线程能持有【对象锁】,其他线程再想获取这个【对象锁】时就会阻塞住 。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心上下文切换

值得注意的是,虽然 Java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同,需要一个线程等待其他线程运行到某个点

🚁2.1 synchronized 语法

java 复制代码
synchronized(对象) {   //线程1,线程2(blocked)
	临界区
}

案例代码

java 复制代码
static int counter = 0; 
//创建一个公共对象,作为对象锁的对象
static final Object room = new Object();
 
public static void main(String[] args) throws InterruptedException {    
	Thread t1 = new Thread(() -> {        
    for (int i = 0; i < 5000; i++) {            
        synchronized (room) {     
        	counter++;            
       	 }       
 	   }    
    }, "t1");
 
    Thread t2 = new Thread(() -> {       
        for (int i = 0; i < 5000; i++) {         
            synchronized (room) {            
            	counter--;          
            }    
        } 
    }, "t2");
 
    t1.start();    
    t2.start(); 
    t1.join();   
    t2.join();    
    log.debug("{}",counter); 
}

可以做这样的类比:

  • synchronized 中的对象,可以想象为一个房间,有唯一入口房间只能一次进入一个人进行计算,线程 t1 和 t2 想象成两个人
  • 当线程 t1 执行到 synchronized 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行 counter++
  • 这时如果 t2 也运行到了 synchronized 时,它发现门锁住了,只能在门外等待,发生了线程上下文切换,阻塞住了
  • 这中间即使 t1 的 CPU 时间片不幸用完,被踢出了门外,这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片才能开门进入
  • 当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 counter-- 代码

🚁2.2 synchronized 加在方法上

  1. 加在成员方法上
java 复制代码
public class Test {
	//在方法上加上synchronized关键字
	public synchronized void test() {
	
	}
	//等价于
	public void test() {
		synchronized(this) {
		
		}
	}
}
  1. 加在静态方法上
java 复制代码
public class Test {
	//在静态方法上加上synchronized关键字
	public synchronized static void test() {
	
	}
	//等价于
	public void test() {
		synchronized(Test.class) {
		
		}
	}
}

🚀3. 变量的线程安全分析

成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能够改变,又分为两种情况 (1)如果只有读操作,则线程安全 (2)如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否安全?

  • 局部变量是线程安全的
  • 局部变量引用的对象则未必线程安全 (1) 如果该对象没有逃离方法的作用访问,它是线程安全的 (2) 如果该对象逃离方法的作用范围,需要考虑线程安全

局部变量线程安全性分析

java 复制代码
public static void test1() {
	int i = 10;
	i++;
}

每个线程调用 test1() 方法时,局部变量 i 会在每个线程的栈帧内存中被创建多份,因此不存在共享,是线程安全的。 然而,局部变量的引用却有所不同,先看一个成员变量的例子

java 复制代码
public class ThreadUnsafe {
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;

    ArrayList<String> list = new ArrayList<>();
    public void method1(int loopNumber) {
        for (int i = 0; i < loopNumber; i++) {
            //{临界区,会产生竞态条件
            method2();
            method3();
            //}
        }
    }
    private void method2() {
        list.add("1");
    }
    private void method3() {
        list.remove(0);
    }

    public static void main(String[] args) {
        ThreadUnsafe test = new ThreadUnsafe();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            },"Thread" + i).start();
        }
    }
}

运行之后,可能有一种情况,method2 还未 add,method3 便开始 remove 就会报错:

Bash 复制代码
Exception in thread "Thread0" Exception in thread "Thread1" java.lang.ArrayIndexOutOfBoundsException: -1
	at java.util.ArrayList.remove(ArrayList.java:507)
	at com.czh.concurrent.ThreadUnsafe.method3(ThreadUnsafe.java:26)
	at com.czh.concurrent.ThreadUnsafe.method1(ThreadUnsafe.java:18)
	at com.czh.concurrent.ThreadUnsafe.lambda$main$0(ThreadUnsafe.java:33)
	at java.lang.Thread.run(Thread.java:748)
java.lang.ArrayIndexOutOfBoundsException: -1
	at java.util.ArrayList.add(ArrayList.java:465)
	at com.czh.concurrent.ThreadUnsafe.method2(ThreadUnsafe.java:23)
	at com.czh.concurrent.ThreadUnsafe.method1(ThreadUnsafe.java:17)
	at com.czh.concurrent.ThreadUnsafe.lambda$main$0(ThreadUnsafe.java:33)
	at java.lang.Thread.run(Thread.java:748)

分析:

  • 无论哪个线程中的 method2,引用的都是同一个对象中的 list 成员变量
  • method3 与 method2 分析相同

将 list 修改为局部变量

java 复制代码
public class ThreadUnsafe {

    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;


    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            //{临界区,会产生竞态条件
            method2(list);
            method3(list);
            //}
        }
    }

    private void method2(ArrayList<String> list) {
        list.add("1");
    }

    private void method3(ArrayList<String> list) {
        list.remove(0);
    }

    public static void main(String[] args) {
        ThreadUnsafe test = new ThreadUnsafe();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            },"Thread" + i).start();
        }
    }
}

分析:

  • list 是局部变量,每个线程调用时会创建其不同实例,没有共享
  • 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
  • method3 的参数分析与 method2 相同

方法修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会带来线程安全?

  • 情况1:有其他线程调用 method2 和 method3
  • 情况2:在情况1的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即
java 复制代码
public class ThreadUnsafe {

    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;


    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            //{临界区,会产生竞态条件
            method2(list);
            method3(list);
            //}
        }
    }

    private void method2(ArrayList<String> list) {
        list.add("1");
    }

    public void method3(ArrayList<String> list) {
        list.remove(0);
    }

    public static void main(String[] args) {
        ThreadUnsafe test = new ThreadUnsafe();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            },"Thread" + i).start();
        }
    }
}

class ThreadSafeSubClass extends ThreadUnsafe {
    @Override
    public void method3(ArrayList<String> list) {
        new Thread(()->{
            list.remove(0);
        });
    }
}

从这个例子可以看出 private 或 final 提供【安全】的意义所在,体会开闭原则中的【闭】。

常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector (List的线程安全实现类)
  • Hashtable(Hash的线程安全实现类)
  • java.util.concurrent 包下的类

这里的线程安全是指,多个线程调用它们同一个实例的某个方法时,是线程安全的,也可以理解为:

java 复制代码
Hashtable table = new Hashtable();
new Thread(()->{
	table.put("key", ""value1);
}).start();

new Thread(()->{
	table.put("key", "value2");
}).start();
  • 它们的每个方法是原子的
  • 但是它们多个方法的组合不是原子的,可能会出现线程安全问题
java 复制代码
Hashtable table = new Hashtable();
//线程1,线程2
if (table.get("key") == null) {
	table.put("key", value);
}

不可变类线程安全性

  • String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
  • String 有 replace,substring 等方法【可以】改变值,那么这些方法又是如何保证线程安全的呢?
  • 这是因为这些方法的返回值都创建了一个新的对象,而不是直接改变 String、Integer 对象本身

🚀4. Monitor 概念

当线程执行到临界区代码时,如果使用了 synchronized,会先查询 synchronized 中所指定的对象 (obj) 是否绑定了 Monitor.

  • 如果没有绑定 ,则会先去与 Monitor 绑定,并且将 Owner 设为当前线程
  • 如果已经绑定 ,则会去查询该 Monitor 是否已经有了 Owner (1) 如果没有,则将 Owner 与将当前线程绑定 (2) 如果有,则放入 EntryList,进入阻塞状态(blocked)
  • 当 Monitor 的 Owner 将临界区中代码执行完毕后,Owner 便会被清空,此时 EntryList 中处于阻塞状态的线程会被叫醒并竞争,此时的竞争是非公平的

注意:

  • 对象在使用了 synchronized 后与 Monitor 绑定时,会将对象头中的 Monitor Word 置为 Monitor 指针
  • 每个对象都会绑定一个唯一的 Monitor,如果 synchronized 中所指定的对象 (obj) 不同,则会绑定不同的 Monitor

🚀5. synchronized 原理进阶

Java 对象头格式

64 位虚拟机 Mark Word 结构如下:

🚁5.1 轻量级锁(用于优化 Monitor 这类的重量级锁)

轻量级锁使用场景:当一个对象被多个线程所访问,但访问的时间是错开的(不存在竞争),此时就可以使用轻量级锁来优化。

  • 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录对象 ,内部可以存储锁定对象的 mark word (不在一开始就使用 Monitor)

  • 让锁记录中的 Object Reference 指向锁对象(Object),并尝试用 CAS 去替换 Object 中的mark word,将此 mark word 放入 lock record 中保存

  • 如果 CAS 替换成功,则将 Object 的对象头替换为锁记录的地址和状态 00(轻量级锁状态),并由该线程给对象加锁

🚁5.2 锁膨胀

  • 如果一个线程在给一个对象加轻量级锁时,CAS 替换操作失败(因为此时其他线程已经给对象加了轻量级锁),此时该线程就会进入锁膨胀过程
  • 此时便会给对象加上重量级锁(使用 Monitor) 将对象头的 Mark Word 改为 Monitor 的地址,并且状态改为 01 (重量级锁) 并且该线程放入 EntryList 中,并进入阻塞状态 (blocked)

🚁5.3 自旋优化

重量级锁竞争时,还可以使用自旋来优化,如果当前线程在自旋成功(使用锁的线程退出了同步块,释放了锁),这时就可以避免线程进入阻塞状态。

  • 第一种情况
  • 第二种情况

🚁5.4 偏向锁(用于优化轻量级锁重入)

轻量级锁在没有竞争时,每次重入(该线程执行的方法中再次锁住该对象)操作仍需要 CAS 替换操作,这样会导致性能降低。

所以引入了偏向锁对性能进行优化:在第一次 CAS 时会将线程的 ID 写入对象的 Mark Word中。此后发现这个线程 ID 就是自己的,就表示没有竞争,就不需要再次 CAS ,以后只要不发生竞争,这个对象就归该线程所有。 偏向状态

  • Normal:一般状态,没有加任何锁,前面 62 位保存的是对象的信息,最后 2 位为状态(01),倒数第 3 位表示是否使用偏向锁(未使用:0)
  • Biased:偏向状态,使用偏向锁,前面 54 位保存的当前线程的 ID,最后 2 位为状态(01),倒数第 3 位表示是否使用偏向锁(使用:1)
  • Lightweight:使用轻量级锁,前 62 位保存的是锁记录的指针,最后两位为状态(00)
  • Heavyweight:使用重量级锁,前 62 位保存的是 Monitor 的地址指针,后两位为状态(10)
  • 如果开启了偏向锁(默认开启) ,在创建对象时,对象的 Mark Word 后三位应该是 101
  • 但是偏向锁默认是有延迟的,不会在程序一启动就生效,而是会在程序运行一段时间(几秒之后),才会对创建的对象设置为偏向状态
  • 如果没有开启偏向锁,对象的 Mark Word 后三位应该是 001

以下几种情况会使对象的偏向锁失效

  • 调用对象的 hashCode 方法
  • 多个线程使用该对象
  • 调用了 wait/notify 方法(调用 wait 方法会导致锁膨胀而使用重量级锁)

🚁5.5 批量重偏向

  • 如果对象虽然被多个线程访问,但是线程间不存在竞争,这时偏向 T1 的对象仍有机会重新偏向 T2,重偏向会重置Thread ID
  • 当撤销超过 20 次后(超过阈值),JVM 会觉得是不是偏向错了,这时会在给对象加锁时,重新偏向至加锁线程

🚁5.6 批量撤销

当撤销偏向锁的阈值超过 40 以后,就会将整个类的对象都改为不可偏向的

🚀6. wait/notify

🚁6.1 原理

  • 锁对象调用 wait 方法(obj.wait),会释放对象的锁,使当前线程进入 WaitSet 中,变为 WAITING 状态
  • 处于 BLOCKED 和 WAITING 状态的线程都为阻塞状态,CPU 都不会分给他们时间片。但是有所区别: BLOCKED 状态的线程是在竞争对象时,发现 Monitor 的 Owner 已经是别的线程了,此时就会进入 EntryList 中,并处于BLOCKED状态 然而,WAITING 状态的线程是获得了对象的锁,但是自身因为某些原因需要进入阻塞状态时,锁对象调用了 wait 方法而进入了 WaitSet 中,处于 WAITING 状态
  • BLOCKED 状态的线程会在锁被释放的时候被唤醒 ,但是处于 WAITING 状态的线程只有被锁对象调用了 notify 方法 (obj.notify/obj.notifyAll),才会被唤醒

wait 和 notify 都是线程之间进行协作的手段,都属于 Object 对象的方法,必须获得此对象的锁,才能调用这几个方法,示例代码如下:

java 复制代码
public class Test {
    final static Object obj = new Object();
    
    public static void main(String[] args) {
        new Thread(()->{
            synchronized (obj) {
                System.out.println("执行...");
                try {
                    obj.wait(); //让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("其他代码...");
        }).start();

        new Thread(()->{
            synchronized (obj) {
                System.out.println("执行...");
                try {
                    obj.wait(); //让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("其他代码...");
            }
        }).start();

        //主线程两秒后执行
        sleep(2);
        System.out.println("唤醒 obj 上其他线程");
        synchronized (obj) {
            obj.notify();  //唤醒obj上一个线程
            //obj.notifyAll();  //唤醒obj上所有等待线程
        }
    }
}

🚁6.2 使用 wait/notify 的正确姿势

wait 和 sleep 的区别:

  • sleep 是 Thread 类的静态方法,wait 是 Object 的方法,Object 又是所有类的父类,所以所有类都有 wait 方法
  • sleep 在阻塞(睡眠)的时候不会释放锁,而 wait 在阻塞的时候会释放锁
  • sleep 不需要与 synchronized 一起使用,而 wait 需要与 synchronized 一起使用(对象被锁以后才能使用)

wait 与 sleep 的相同点:

  • 阻塞状态都为 TIMED_WAITING

什么时候适合使用 wait

  • 当线程不满足某些条件,需要暂停运行时,可以使用 wait,这样会将对象的锁释放,让其他线程能够继续运行。如果此时使用 sleep,会导致所有线程都进入阻塞,导致所有线程都没法运行,直到当前线程 sleep 结束后,运行完毕,才能得到执行

使用 wait/notify 的注意点

  • 当有多个线程在运行时,对象调用了 wait 方法,此时这些线程都会进入 WaitSet 中等待。如果这时使用了 notify 方法,可能会造成虚假唤醒(唤醒的不是满足条件的等待线程),这时就需要使用 notifyAll 方法
java 复制代码
synchronized (LOCK) {
	while(//不满足条件,一直等待,避免虚假唤醒) {
		LOCK.wait();
	}
	//满足条件后再运行
}
synchronized (LOCK) {
	//唤醒所有等待线程
	LOCK.notifyAll();
}

🚀7. 模式之保护性暂停

定义

保护性暂停(Guarded Suspension)用在一个线程等待另一个线程的执行结果。 要点:

  • 有一个结果需要从一个线程传递到另一个线程,让它们关联同一个 Guarded Object
  • 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者-消费者)
  • JDK 中,join 的实现、Future 的实现,采用的就是 Guarded Suspension 模式
  • 因为要等待另一方的结果,因此归类到同步模式

案例代码如下

java 复制代码
public class Test {
	public static void main(String[] args) {
		String hello = "hello thread!";
		Guarded guarded = new Guarded();
		new Thread(()->{
			System.out.println("想要得到结果");
			synchronized (guarded) {
				System.out.println("结果是:"+guarded.getResponse());
			}
			System.out.println("得到结果");
		}).start();

		new Thread(()->{
			System.out.println("设置结果");
			synchronized (guarded) {
				guarded.setResponse(hello);
			}
		}).start();
	}
}

class Guarded {
	/**
	 * 要返回的结果
	 */
	private Object response;
	
    //优雅地使用 wait/notify
	public Object getResponse() {
		//如果返回结果为空就一直等待,避免虚假唤醒
		while(response == null) {
			synchronized (this) {
				try {
					this.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
		return response;
	}

	public void setResponse(Object response) {
		this.response = response;
		synchronized (this) {
			//唤醒休眠的线程
			this.notifyAll();
		}
	}

	@Override
	public String toString() {
		return "Guarded{" +
				"response=" + response +
				'}';
	}
}

带超时判断的暂停

java 复制代码
public Object getResponse(long time) {
	synchronized (this) {
		//获取开始时间
		long currentTime = System.currentTimeMillis();
		//用于保存已经等待了的时间
		long passedTime = 0;
		while(response == null) {
			//看经过的时间-开始时间是否超过了指定时间
			long waitTime = time - passedTime;
			if(waitTime <= 0) {
				break;
			}
			try {
                  	//等待剩余时间
				this.wait(waitTime);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			//获取当前时间
			passedTime = System.currentTimeMillis()-currentTime		
           }
	}
	return response;
}

join 源码------使用保护性暂停模式

java 复制代码
public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

🚀8. park & unpark

🚁8.1 基本使用

park/unpark 都是 LockSupport 类中的的方法

java 复制代码
//暂停线程运行
LockSupport.park;

//恢复线程运行
LockSupport.unpark(thread);
java 复制代码
public static void main(String[] args) throws InterruptedException {
		Thread thread = new Thread(()-> {
			System.out.println("park");
            //暂停线程运行
			LockSupport.park();
			System.out.println("resume");
		}, "t1");
		thread.start();

		Thread.sleep(1000);
		System.out.println("unpark");
    	//恢复线程运行
		LockSupport.unpark(thread);
}

🚁8.2 特点

与 Object 的 wait/notify 相比

  • wait/notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park&unpark 不必
  • park&unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】
  • park&unpark 可以先 unpark,而 wait & notify 不能先 notify
  • park 不会释放锁,而 wait 会释放锁

🚁8.3 原理

每个线程都有一个自己的 park 对象,并且该对象由 _counter, _cond,__mutex 组成

情况 1:先调用 park,再调用 unpark

先调用 park

  • 线程运行时,会将 park 对象中的 _counter 的值设为 0
  • 调用 park 时,会先查看 counter 的值是否为 0,如果为 0,则将线程放入阻塞队列 cond 中
  • 放入阻塞队列后,会再次将 counter 设置为 0

然后再调用 unpark

  • 调用 unpark 方法后,会将 counter 的值设置为 1
  • 去唤醒阻塞队列 cond 中的线程
  • 线程继续运行并将 counter 的值设为 0

情况 2:先调用 unpark,再调用 park

先调用 unpark

  • 会将 counter 设置为 1(运行时0)

再调用 park

  • 查看 counter 是否为 0
  • 因为 unpark 已经把 counter 设置为 1,所以此时将 counter 设置为 0,但不放入阻塞队列 cond 中

🚀9. 线程状态转换

假设有线程 Thread t

情况一:NEW --> RUNNABLE

  • 当调用 t.start() 方法时,由 NEW --> RUNNABLE

情况二:RUNNABLE <--> WAITING

  • 当调用了 t 线程用 synchronized(obj) 获取了对象锁后 (1)调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING (2)调用 obj.notify() ,obj.notifyAll() ,t.interrupt() 时:如果竞争锁成功,t 线程从 WAITING --> RUNNABLE;如果竞争锁失败,t 线程从 WAITING --> BLOCKED

情况三:RUNNABLE <--> WAITING

  • 当前线程 调用 t.join() 方法时,当前线程 从 RUNNABLE --> WAITING 注意是当前线程t 线程对象的监视器上等待

  • t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE 情况

情况四: RUNNABLE <--> WAITING

  • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE

情况五: RUNNABLE <--> TIMED_WAITING t 线程用 synchronized(obj) 获取了对象锁后

  • 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING
  • t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时 (1)竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE (2)竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED

情况六:RUNNABLE <--> TIMED_WAITING

  • 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING 注意是当前线程在 t 线程对象的监视器上等待

  • 当前线程等待时间超过了 n 毫秒,或 t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING --> RUNNABLE

情况七:RUNNABLE <--> TIMED_WAITING

  • 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
  • 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE

情况八:RUNNABLE <--> TIMED_WAITING

  • 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线 程从 RUNNABLE --> TIMED_WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING--> RUNNABLE

情况九:RUNNABLE <--> BLOCKED

  • t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
  • obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED

情况十: RUNNABLE <--> TERMINATED

  • 当前线程所有代码运行完毕,进入 TERMINATED

🚀10. 多把锁

将锁的粒度细分

  • 优点,可以增强并发度
  • 缺点,如果一个线程需要同时获得多把锁,就容易发生死锁

🚀11. 活跃性

定义:因为某种原因,使得代码一直无法执行完毕,这样的现象叫做活跃性。

🚁11.1 死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁

t1 线程获得 A 对象锁,接下来想获取 B 对象的锁, t2 线程获得 B 对象锁,接下来想获取 A 对象 的锁, 例:

java 复制代码
public static void main(String[] args) {

	final Object A = new Object();
	final Object B = new Object();
	
	new Thread(()->{
		synchronized (A) {
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			synchronized (B) {
			
			}
		}
	}).start();

	new Thread(()->{
		synchronized (B) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			synchronized (A) {

			}
		}
	}).start();
}

发生死锁的四个必要条件

  • 互斥条件:在一段时间内,一种资源只能被一个进程所使用。
  • 请求和保持条件:进程已经拥有了至少一种资源,同时又去申请其他资源。因为其他资源被别的进程所使用,该进程进入阻塞状态,并且不释放自己已有的资源。
  • 不可抢占条件:进程对已获得的资源在未使用完成前不能被强占,只能在进程使用完后自己释放。
  • 循环等待条件:发生死锁时,必然存在一个进程------资源的循环链。

定位死锁的方法

  • 检测死锁可以使用 jconsole 工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁。 省略中间的一些信息,找到最后一段信息,末尾处出现 Found 1 deadlock

注意点 :避免死锁要注意加锁顺序;另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 Linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 top -Hp 进程 id 来定位是哪个线程,最后再用 jstack 排查。

哲学家就餐问题

有 5 位哲学家,围坐在圆桌旁。他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。 如果筷子被身边的人拿着,自己就得等待。

筷子类

java 复制代码
class Chopstick {
    String name;

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

    @Override
    public String toString() {
        return "筷子{"+name+"}";
    }
}

哲学家类

java 复制代码
class Philosopher extends Thread {

    Chopstick left;
    Chopstick right;

    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }

    private void eat() {
        System.out.println("eating...");
        this.right = right;
    }

    @Override
    public void run() {
        while (true) {
            //获得左手筷子
            synchronized (left) {
                //获得右手筷子
                synchronized (right) {
                    //吃饭
                    eat();
                }
                //放下右手筷子
            }
            //放下左手筷子
        }
    }
}

避免死锁的方法

在线程使用锁对象时,顺序加锁即可避免死锁

🚁11.2 活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如

避免活锁的方法:在线程执行时,中途给予不同的间隔时间即可。

死锁与活锁的区别

  • 死锁是因为线程互相持有对象想要的锁,并且都不释放,最后到时线程阻塞,停止运行的现象。
  • 活锁是因为线程间修改了对方的结束条件,而导致代码一直在运行,却一直运行不完的现象。

🚁11.3 饥饿

某些线程因为优先级太低,导致一直无法获得资源的现象,在使用顺序加锁时,可能会出现饥饿现象。

🚀12. 可重入锁

和 synchronized 相比具有的的特点

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁 (先到先得)
  • 支持多个条件变量( 具有多个 waitset)

基本语法

java 复制代码
//获取ReentrantLock对象
private ReentrantLock lock = new ReentrantLock();
//加锁
lock.lock();
try {
	//需要执行的代码
}finally {
	//释放锁
	lock.unlock();
}

可重入

  • 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
  • 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

可打断

  • 如果某个线程处于阻塞状态,可以调用其 interrupt 方法让其停止阻塞,获得锁失败 。简而言之就是:处于阻塞状态的线程,被打断了就不用阻塞了,直接停止运行
java 复制代码
public static void main(String[] args) {

	ReentrantLock lock = new ReentrantLock();
	Thread t1 = new Thread(()-> {
		try {
			//加锁,可打断锁
			lock.lockInterruptibly();
		} catch (InterruptedException e) {
			e.printStackTrace();
               //被打断,返回,不再向下执行
			return;
		}finally {
			//释放锁
			lock.unlock();
		}

	});

	lock.lock();
	try {
		t1.start();
		Thread.sleep(1000);
		//打断
		t1.interrupt();
	} catch (InterruptedException e) {
		e.printStackTrace();
	} finally {
		lock.unlock();
	}
}

锁超时

  • 使用 lock.tryLock 方法会返回获取锁是否成功。如果成功则返回 true,反之则返回 false
  • 并且 tryLock 方法可以指定等待时间,参数为:tryLock(long timeout, TimeUnit unit),其中timeout 为最长等待时间,TimeUnit 为时间单位
  • 归纳就是,获取失败了、获取超时了或者被打断了,不再阻塞,直接停止运行

不设置等待时间,立刻失败

java 复制代码
public static void main(String[] args) {
	ReentrantLock lock = new ReentrantLock();
	Thread t1 = new Thread(()-> {
           //未设置等待时间,一旦获取失败,直接返回false
		if(!lock.tryLock()) {
			System.out.println("获取失败");
               //获取失败,不再向下执行,返回
			return;
		}
		System.out.println("得到了锁");
		lock.unlock();
	});


	lock.lock();
	try{
		t1.start();
		Thread.sleep(3000);
	} catch (InterruptedException e) {
		e.printStackTrace();
	} finally {
		lock.unlock();
	}
}

设置等待时间

java 复制代码
public static void main(String[] args) {
	ReentrantLock lock = new ReentrantLock();
	Thread t1 = new Thread(()-> {
		try {
			//判断获取锁是否成功,最多等待1秒
			if(!lock.tryLock(1, TimeUnit.SECONDS)) {
				System.out.println("获取失败");
				//获取失败,不再向下执行,直接返回
				return;
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
			//被打断,不再向下执行,直接返回
			return;
		}
		System.out.println("得到了锁");
		//释放锁
		lock.unlock();
	});


	lock.lock();
	try{
		t1.start();
		//打断等待
		t1.interrupt();
		Thread.sleep(3000);
	} catch (InterruptedException e) {
		e.printStackTrace();
	} finally {
		lock.unlock();
	}
}

公平锁

  • 在线程获取锁失败,进入阻塞队列时,先进入的会在锁被释放后先获得锁。这样的获取方式就是公平的
java 复制代码
//默认是不公平锁,需要在创建时指定为公平锁
ReentrantLock lock = new ReentrantLock(true);

条件变量

  • synchronized 中也有条件变量,就是 waitSet 等待队列 ,当条件不满足时进入waitSet 等待
  • ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量,这就好比,synchronized 是那些不满足条件的线程都在一间休息室等消息,而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

使用要点:

  • await 前需要获得锁
  • await 执行后,会释放锁,进入 conditionObject 等待
  • await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行
java 复制代码
static Boolean judge = false;
public static void main(String[] args) throws InterruptedException {
	ReentrantLock lock = new ReentrantLock();
	//获得条件变量
	Condition condition = lock.newCondition();
	new Thread(()->{
		lock.lock();
		try{
			while(!judge) {
				System.out.println("不满足条件,等待...");
				//等待
				condition.await();
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			System.out.println("执行完毕!");
			lock.unlock();
		}
	}).start();

	new Thread(()->{
		lock.lock();
		try {
			Thread.sleep(1);
			judge = true;
			//释放
			condition.signal();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}

	}).start();
}

🚀13. 同步模式之顺序控制

java 复制代码
static final Object LOCK = new Object();
//判断先执行的内容是否执行完毕
static Boolean judge = false;
public static void main(String[] args) {
	new Thread(()->{
		synchronized (LOCK) {
			while (!judge) {
				try {
					LOCK.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println("2");
		}
	}).start();

	new Thread(()->{
		synchronized (LOCK) {
			System.out.println("1");
			judge = true;
               //执行完毕,唤醒所有等待线程
			LOCK.notifyAll();
		}
	}).start();
}

交替输出(wait/notify 版本)

java 复制代码
public class Test {
	static Symbol symbol = new Symbol();
	public static void main(String[] args) {
		new Thread(()->{
			symbol.run("a", 1, 2);
		}).start();

		new Thread(()->{
			symbol.run("b", 2, 3);

		}).start();
		symbol.run("c", 3, 1);
		new Thread(()->{

		}).start();
	}
}

class Symbol {
	public synchronized void run(String str, int flag, int nextFlag) {
		for(int i=0; i<loopNumber; i++) {
			while(flag != this.flag) {
				try {
					this.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println(str);
			//设置下一个运行的线程标记
			this.flag = nextFlag;
			//唤醒所有线程
			this.notifyAll();
		}
	}

	/**
	 * 线程的执行标记, 1->a 2->b 3->c
	 */
	private int flag = 1;
	private int loopNumber = 5;

	public int getFlag() {
		return flag;
	}

	public void setFlag(int flag) {
		this.flag = flag;
	}

	public int getLoopNumber() {
		return loopNumber;
	}

	public void setLoopNumber(int loopNumber) {
		this.loopNumber = loopNumber;
	}
}

🚀14. ThreadLocal

ThreadLocal 是 JDK 包提供的,它提供线程本地变量 ,也就是如果创建了一个ThreadLocal 变量 ,那么访问这个变量的每个线程都会有这个变量的一个本地副本 。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。

使用

java 复制代码
public class ThreadLocalTest {
   public static void main(String[] args) {
      // 创建ThreadLocal变量
      ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
      ThreadLocal<User> userThreadLocal = new ThreadLocal<>();

      // 创建两个线程,分别使用上面的两个ThreadLocal变量
      Thread thread1 = new Thread(()->{
         // stringThreadLocal第一次赋值
         stringThreadLocal.set("thread1 stringThreadLocal first");
         // stringThreadLocal第二次赋值
         stringThreadLocal.set("thread1 stringThreadLocal second");
         // userThreadLocal赋值
         userThreadLocal.set(new User("Cristiano", 37));

         // 取值
         System.out.println(stringThreadLocal.get());
         System.out.println(userThreadLocal.get());
          
          // 移除
		 userThreadLocal.remove();
		 System.out.println(userThreadLocal.get());
      });

      Thread thread2 = new Thread(()->{
         // stringThreadLocal第一次赋值
         stringThreadLocal.set("thread2 stringThreadLocal first");
         // stringThreadLocal第二次赋值
         stringThreadLocal.set("thread2 stringThreadLocal second");
         // userThreadLocal赋值
         userThreadLocal.set(new User("Lionel", 34));

         // 取值
         System.out.println(stringThreadLocal.get());
         System.out.println(userThreadLocal.get());
      });

      // 启动线程
      thread1.start();
      thread2.start();
   }
}

class User {
   String name;
   int age;

   public User(String name, int age) {
      this.name = name;
      this.age = age;
   }

   @Override
   public String toString() {
      return "User{" +
            "name='" + name + '\'' +
            ", age=" + age +
            '}';
   }
}
Bash 复制代码
thread1 stringThreadLocal second
thread2 stringThreadLocal second
User{name='Cristiano', age=37}
User{name='Lionel', age=34}
null

从运行结果可以看出

  • 每个线程中的 ThreadLocal 变量是每个线程私有的,而不是共享的
  • ThreadLocal 其实就相当于其泛型类型的一个变量,只不过是每个线程私有的,stringThreadLocal被赋值了两次,保存的是最后一次赋值的结果
  • ThreadLocal可以进行以下几个操作: set 设置值 get 取出值 remove 移除值

原理

java 复制代码
public class Thread implements Runnable {
	...
    ThreadLocal.ThreadLocalMap threadLocals = null;

    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    ...
}
java 复制代码
static class ThreadLocalMap {
   static class Entry extends WeakReference<ThreadLocal<?>> {
       /** The value associated with this ThreadLocal. */
       Object value;

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

Thread 类中有一个 threadLocals 和一个 inheritableThreadLocals,它们都是 ThreadLocalMap 类型的变量,而 ThreadLocalMap 是一个定制化的 Hashmap。在默认情况下,每个线程中的这两个变量都为 null.

ThreadLocal 中的方法

java 复制代码
public void set(T value) {
	//获取当前线程
    Thread t = Thread.currentThread();
    //获得ThreadLocalMap对象, 返回Thread类中的threadLocals
    ThreadLocalMap map = getMap(t);
    if (map != null)
    	//ThreadLocal自生的引用作为key,传入的值作为value
        map.set(this, value);
    else
        createMap(t, value);
}
void createMap(Thread t, T firstValue) {
    // 创建的同时设置想放入的值
    // threadLocal自生的引用作为key,传入的值作为value
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
java 复制代码
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();
}
private T setInitialValue() {
     T value = initialValue();
     Thread t = Thread.currentThread();
     ThreadLocalMap map = getMap(t);
     if (map != null)
         map.set(this, value);
     else
         createMap(t, value);
     return value;
}
java 复制代码
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
}
  • 在每个线程内部都有一个名为 threadLocals 的成员变量,该变量的类型为 HashMap,其中 key 为我们定义的 ThreadLocal 变量的 this 引用,value 则为我们使用 set 方法设置的值。每个线程的本地变量存放在线程自己的内存变量 threadLocals 中
  • 只有当前线程第一次调用 ThreadLocal 的 set 或者 get 方法时才会创建 threadLocals(inheritableThreadLocals 也是一样)。其实每个线程的本地变量不是存放在 ThreadLocal 实例里面,而是存放在调用线程的 threadLocals 变量里面

从 ThreadLocal 的源码可以看出,无论是 set、get、还是 remove,都是相对于当前线程操作

java 复制代码
Thread t = Thread.currentThread();

因此 ThreadLocal 无法从父线程传向子线程,所以 InheritableThreadLocal 出现了,它能够让父线程中 ThreadLocal 的值传给子线程。

也就是从 main 所在的线程,传给 thread1 或 thread2

java 复制代码
public class Test {
   public static void main(String[] args) {
      ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
      InheritableThreadLocal<String> stringInheritable = new InheritableThreadLocal<>();

      // 主线程赋对上面两个变量进行赋值
      stringThreadLocal.set("this is threadLocal");
      stringInheritable.set("this is inheritableThreadLocal");

      // 创建线程
      Thread thread1 = new Thread(()->{
         // 获得ThreadLocal中存放的值
         System.out.println(stringThreadLocal.get());

         // 获得InheritableThreadLocal存放的值
         System.out.println(stringInheritable.get());
      });

      thread1.start();
   }
}

运行结果

Bash 复制代码
null
this is inheritableThreadLocal

InheritableThreadLocal 的值成功从主线程传入了子线程,而 ThreadLocal 没有。

原理

java 复制代码
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    // 传入父线程中的一个值,然后直接返回
    protected T childValue(T parentValue) {
        return parentValue;
    }

  	// 返回传入线程的inheritableThreadLocals
    // Thread中有一个inheritableThreadLocals变量
    // ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

 	// 创建一个inheritableThreadLocals
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}
  • InheritableThreadLocal 继承了 ThreadLocal,并重写了三个方法。InheritableThreadLocal 重写了createMap 方法,那么现在当第一次调用set方法时,创建的是当前线程的inheritableThreadLocals 变量的实例而不再是 threadLocals。当调用 getMap 方法获取当前线程内部的 map 变量时,获取的是 inheritableThreadLocals 而不再是 threadLocals
  • 当父线程创建子线程时,构造函数会把父线程中 inheritableThreadLocals 变量里面的本地变量复制一份保存到子线程的 inheritableThreadLocals 变量里面
相关推荐
徐小夕3 分钟前
10k Star 的开源 AI 记忆引擎:6 行代码,用图谱+向量打造永不遗忘的 AI
前端·后端·github
czlczl200209253 分钟前
拒绝 DTO 爆炸:详解 Spring Boot 参数校验中的“分组校验” (Validation Groups) 技巧
java·spring boot·后端
兔丝6 分钟前
FastAdmin框架SSE实时消息推送实现教程
后端
悟空码字7 分钟前
SpringBoot动态脱敏实战,从注解到AOP的优雅打码术
java·后端
小鸡脚来咯13 分钟前
springboot项目包结构
java·spring boot·后端
爱学习的小可爱卢15 分钟前
JavaEE进阶——SpringBoot日志从入门到精通
java·spring boot·后端
Clarence Liu28 分钟前
Go Context 深度解析:从源码到 RESTful 框架的最佳实践
开发语言·后端·golang
踏浪无痕30 分钟前
Nacos到底是AP还是CP?一文说清楚
分布式·后端·面试
踏浪无痕32 分钟前
深入JRaft:Nacos配置中心的性能优化实践
分布式·后端·面试
我梦见我梦见我37 分钟前
CentOS下安装RocketMQ
后端