初识Java并发编程(二)

1.共享模型之管程

1.1共享带来的问题
Java的体现

​ 两个线程对初始值为0的静态变量了一个自增500次,一个自减500次,结果是0嘛?

public class Test05 {
	static int couner=0;

	public static void main(String[] args)throws InterruptedException {
		Thread t1=new Thread(()->{
			for(int i=0;i<5000;i++){
				couner++;
			}
		});

		Thread t2 = new Thread(()->{
			for (int i=0;i<5000;i++){
				couner--;
			}
		});

		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println(couner);

	}

}
问题分析

​ ​ ​ ​ ​ ​ 以上的结果可能是正数、复数、零。为啥呢?因为Java中静态变量的自增和自减不是原子操作,要彻底理解必须从字节码来分析。

​ ​ ​ ​ ​ ​ 例如,对于i++而言,实际会产生如下的JVM字节码指令:

getstatic i//获取静态变量i的值
iconst_1  //准备常量1
iadd //自增
putstatic i //将修改后的值存入静态变量i

​ ​ ​ ​ ​ ​ ​ 对应的i–类似:

getstatic i//获取静态变量i的值
iconst_1  //准备常量1
isub //自减
putstatic i //将修改后的值存入静态变量i

​ ​ ​ ​ ​ ​ ​ 对于如何在IDEA中查看字节码文件,请戳我

​ ​ ​ ​ ​ ​ ​ 在Java内存模型中,完成静态变量的自增自减需要在主存和工作内存中进行数据交换:

在这里插入图片描述

​ 如果是单线程以上8行代码是顺序执行的,没有什么问题:

在这里插入图片描述

​ 下面说明一下交错运行,出现负数的情况:

在这里插入图片描述

​ 出现正数的情况分析:
在这里插入图片描述

临界区Critical Section
  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源
    • 多个线程读共享资源其实也没问题
    • 在多个线程对共享资源读写操作时发生指令交错,就有问题了
  • 一段代码块如果存在对共享资源的多线程读写操作,就称这段代码块为临界区
竞争条件Race Condition

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

1.2 synchronized解决方案

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

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

​ ​ ​ ​ ​ ​ ​ 这里主要说的是synchronized,也就是对象锁,它采用互斥的方式让同一时刻最多只有一个线程能持有对象锁,其他线程再想获取这个对象锁的时候就会被阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文的切换。

ps:Java中互斥和同步都可以采用synchronized实现,但是互斥和同步是有区别的

▲互斥式保证临界区的竞争条件发生时,同一时刻只能有一个线程执行临界区的代码

▲同步是由于线程执行的先后、顺序不同,需要一个线程等待其他线程运行到某个点

synchronized

语法:

synchronized(对象){
	临界区
}

解决:

public class Test05 {
	static int couner=0;
	static Object lock=new Object();

	public static void main(String[] args)throws InterruptedException {
		Thread t1=new Thread(()->{
			for(int i=0;i<5000;i++){
				synchronized(lock){
				couner++;
			}
			}
		});

		Thread t2 = new Thread(()->{
			for (int i=0;i<5000;i++){
				synchronized (lock){
				couner--;
			}
			}
		});

		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println(couner);

	}

}

​ ​ ​ ​ ​ ​ ​ 我们new了一个Object类的变量,我突然想起来之前考OS的时候,那已经距离现在有一段时间了,我去问振哥,这个临界区加锁balala的问题,那时候不大明白为什么线程1和线程2对counter操作,我们对这个counter加锁不就完了呗,干嘛还在搞一个变量,对这个无关变量加锁。

​ ​ ​ ​ ​ ​ ​ 趁着这个功夫,我试了试,synchronized(couner),是不可行,理由是参数类型应该是Object的。那我换个思路,这样搞行不行,大家可以看看下面的代码。

public class Test05 {
	static int couner=0;
	static Object lock=new Object();

	public static void main(String[] args)throws InterruptedException {
		Thread t1=new Thread(()->{
			for(int i=0;i<5000;i++){
				synchronized(lock){
					couner++;
				lock=Integer.valueOf(couner);
			}
			}
		});

		Thread t2 = new Thread(()->{
			for (int i=0;i<5000;i++){
				synchronized (lock){
					couner--;
					lock=Integer.valueOf(couner);
			}
			}
		});

		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println(couner);

	}

}

​ ​ ​ ​ ​ ​ ​ 上锁的对象还是lock,但是多次实验,你会发现打印的结果也不一定是0,尽管我试了好多次打印都是0,大家可以想想为什么。以线程t1为例,t2如果加了,可能效果不太明显。我加了几句代码:

Thread t1=new Thread(()->{
			for(int i=0;i<5000;i++){
				synchronized(lock){
					System.out.println("t1:couner++前:"+lock.getClass());
					couner++;
					System.out.println("t1:couner++后"+lock.getClass());
					lock=Integer.valueOf(couner);
					System.out.println("t1:lock获取后"+lock.getClass());
				}
			}
		});

​ ​ ​ ​ ​ ​ ​ 然后我再次进行了输出,得到了这样的结果:

t1:couner++前:class java.lang.Object
t1:couner++class java.lang.Object
t1:lock获取后class java.lang.Integer
t1:couner++前:class java.lang.Integer
t1:couner++class java.lang.Integer
t1:lock获取后class java.lang.Integer
t1:couner++前:class java.lang.Integer
t1:couner++class java.lang.Integer
t1:lock获取后class java.lang.Integer
t1:couner++前:class java.lang.Integer
t1:couner++class java.lang.Integer
t1:lock获取后class java.lang.Integer
... ... ...

​ ​ ​ ​ ​ ​ ​ 由此我们知道,没办法保证couner=0的原因在于执行了lock=Integer.valueOf(couner);,这时候lock的类型变了。lock变量为Object类型的时候,只有第一次在t1线程中第一次循环执行给lock赋值之前。

如何理解
  • synchronized(对象)中的对象,可以想象为一个房间,有唯一的入口,房间只能1次进入1个人,线程t1和t2看做两个人。
  • 当线程t1执行到synchronized(room)的时候,就好比t1先进入了这个房间,并锁住了门拿走了钥匙,在门内执行couner++代码
  • 这时候如果t2也运行到了synchronized(room),他发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了
  • 这中间即使t1的时间片用完了,被踢出去了,这时候门还是锁住的,但要是还是在t1那里,t2线程还在阻塞状态进不来,只有下次轮到t1自己再次获得时间片的时候才能开门进入
  • 当t1执行完了synchronized{}中的代码,这时候会从房间里出来,并解开门上的锁,唤醒t2线程并且把要是给他,t2线程才能进去,锁住了门拿上要是,执行他的couner–代码。
思考

​ ​ ​ ​ ​ ​ ​ synchronized实际上使用了对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。

  • 如果把synchronized(obj)放在for循环外面怎么理解?
    • 以自增为例,以前的时候,synchronized相当于是对i++四条字节码指令进行保护,现在呢,相当于对循环和四条字节码指令进行保护,掐指一算也就是5000*4条指令保护。
  • 如果t1 synchronized(obj1)而t2 synchronized (obj2)会怎样运作?
    • 显然不行,相当于进入两个不同的房间,保护两个不同的东西。
  • 如果t1synchronized(obj)但是t2没有加会怎么样?
    • 输出结果还是不为0,没有提供原子性保护。
1.3 方法上的synchronized

1.加在普通方法上

class Test{
  public synchronized void test(){
    
  }
}

//等价于

class Test{
  public void test(){
    synchronized(this){
      
    }
  }
}

2.加载类方法上

class Test{
  public synchronized static void test(){
    
  }
}

//等价于

class Test{
  public static void test(){
    synchronized(this){
      
    }
  }
}

​ ​ ​ ​ ​ ​ ​ 虽然加在方法上,但还是锁对象,不过锁的是this对象。

所谓的“线程八锁”

​ to do

1.4 变量的线程安全分析
成员变量和静态变量是否线程安全?
  • 如果它们没有被共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能够改变,又分为2种情况
    • 如果只有读操作,安全
    • 如果有读写操作,则这段代码就是临界区,需要考虑线程安全
局部变量是否线程安全?
  • 局部变量是线程安全的
  • 但局部变量引用的对象未必
    • 如果该对象没有逃离方法的作用访问,则是线程安全的
    • 如果该方法逃离方法的作用范围,需要考虑线程安全
局部变量线程安全分析

​ ​ ​ ​ ​ ​ ​ 先看局部变量的情况,稍后分析局部变量引用。看到i++操作你可能会说,这个操作不是原子的啊,我们上次分析过了啊,但是请注意,多线程访问的时候,它可并不是作为一个共享变量的。

public static void test1(){
  int i=10;
  i++;
}

​ ​ ​ ​ ​ ​ ​ 每个线程调用test1()方法时局部变量i,会在每个线程的栈帧中被创建多份,因此不存在共享。

public static void test1(){
  descriptor:()V
  flags:ACC_PUBLIC,ACC_STATIC
  Code:
  	stack=1,locals=1,args_size=0
  		0:bipush     10
  		2:istore_0
  		3:iinc		0,1
  		6:return	
  	LineNumberTable:
  		line 10:0
  		line 11:3
  		line 12:6
  	LocalVariableTable:
  		Start Length Slot Name Signature
  		   3	4	  0	   1
}

​ ​ ​ ​ ​ ​ ​ 希望下面这幅图可以帮助更好的理解这个问题。

在这里插入图片描述
​ ​ ​ ​ ​ ​ ​ 说完局部变量,再说是局部变量引用,先看一个成员变量的例子。

import java.util.ArrayList;

public class TestThreadSafe {
	static final int THREAD_NUMBER=2;
	static final int LOOP_NUMBER=200;

	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+1)).start();
		}
	}
}


class ThreadUnsafe{
	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);
	}
}

​ ​ ​ ​ ​ ​ ​ 上面这个程序是有问题的(当然可能你在运行的时候没错,多运行看看),报错如下,这是因为线程2还没有执行add操作,线程1remove就会出现问题:

Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
	at java.util.ArrayList.rangeCheck(ArrayList.java:657)
	at java.util.ArrayList.remove(ArrayList.java:496)
	at exam.offer.day06.ThreadUnsafe.method3(TestThreadSafe.java:34)
	at exam.offer.day06.ThreadUnsafe.method1(TestThreadSafe.java:25)
	at exam.offer.day06.TestThreadSafe.lambda$main$0(TestThreadSafe.java:13)
	at java.lang.Thread.run(Thread.java:748)

​ ​ ​ ​ ​ ​ ​ 无论哪个线程中的method2()方法引用的都是同一个对象中的list成员变量。

在这里插入图片描述

​ ​ ​ ​ ​ ​ ​ 当我们如果把这个改成局部变量,是不是还会不安全呢?

public class TestThreadSafe {
	static final int THREAD_NUMBER=2;
	static final int LOOP_NUMBER=200;

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

class ThreadSafe{
	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("1");
	}
}

​ ​ ​ ​ ​ ​ ​ 通过运行可以知道,把list变量从成员变量改成局部变量之后,就没这个问题了,list是局部变量,每个线程调用的时候都会创建其不同实例,并不存在共享问题。而method2的参数是从method1中传递过来的,与method1中引用的是同一个对象。视频中还演示了一种,就是将method2和method3的方法改成public,其实也是不会出问题的。

在这里插入图片描述

​ ​ ​ ​ ​ ​ ​ 做一个延伸拓展,请看下面的代码,ThreadSafeSubClass继承ThreadSafe类,那么再次运行的时候会出现问题吗?显示是会的,在子类里面创建了一个新的线程,那么他肯定和旧的线程是共享list资源的,那么就会有安全问题哦。

class ThreadSafe{
	public final void method1(int loopNumber){
		ArrayList<String> list=new ArrayList<>();
		for(int i=0;i<loopNumber;i++){
			method2(list);
			method3(list);
		}
	}

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

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

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

/**运行结果如下:
Exception in thread "Thread-229" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
	at java.util.ArrayList.rangeCheck(ArrayList.java:657)
	at java.util.ArrayList.remove(ArrayList.java:496)
	at exam.offer.day06.ThreadSafeSubClass.lambda$method3$0(TestThreadSafe.java:59)
	at java.lang.Thread.run(Thread.java:748)
*/
常见线程安全类

​ ​ ​ ​ ​ ​ ​ 常见的比如String、Integer、StringBuffer、Random、Vector、HashTable、J.U.C包下的类。这里说的线程安全是指,多个线程调用他们同一个实例的某个方法时,是线程安全的。它们的每个方法都是原子的,但是并不意味着多个方法的组合也是原子的。

线程安全类方法的组合

​ ​ ​ ​ ​ ​ ​ 分析下面的代码是否线程安全?

HashTable table=new HashTable();
if(table.get("key")==null){
  table.put("key",value);
}

​ ​ ​ ​ ​ ​ ​ 只能保证get方法和put方法内部的所有代码是原子性的,但是没法保证get和put组合原子性。
在这里插入图片描述

不可变类线程安全性

​ ​ ​ ​ ​ ​ ​ String和Integer等都是不可变的类,因为其内部的状态不可以改变,因此i他们的方法都是线程安全的。但是如果看过String的源码,会发现里面有replace、substring等方法明明可以改变值啊,这些方法是怎么保证线程安全的呢?来瞅瞅substring方法的源码↓

public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }
    
    
public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
            if (count < 0) {
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= value.length) {
                this.value = "".value;
                return;
            }
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }

​ ​ ​ ​ ​ ​ ​ 实际上没改变旧的字符串,而是返回了一个复制的字符串this.value = Arrays.copyOfRange(value, offset, offset+count);

1.5 习题
卖票练习

to do…

转账练习

to do…

1.6 Monitor概念
Java对象头

to do…

Monitor

​ ​ ​ ​ ​ ​ ​ Monitor被翻译为监视器或者管程。每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级锁)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针。

​ ​ ​ ​ ​ ​ ​ Monitor的结构如下所示。
在这里插入图片描述

  • 刚开始Monotor中Owner为null
  • 当Thread-2执行synchronized(obj)就会将Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner
  • 在Thread-2上锁的过程中,如果Thread-3,Thread-4,Thread-5也来执行synchronized(obj),就会进入EntryList,状态为BLOCKED。
  • 图中WaitSet中的两个线程是之前获得过锁,但条件不满足进入WAITING状态的线程。
  • 注意:synchronized必须是进入同一个对象的monitor才有上述的效果,不加synchronized的对象不会关联监视器。

*原理之synchronized

​ ​ ​ ​ ​ ​ ​ 从字节码的角度去分析。

public class SynchronizedTest {
	static final Object lock=new Object();
	static int counter=0;

	public static void main(String[] args) {
		synchronized (lock){{
			counter++;
		}}
	}
}

​ ​ ​ ​ ​ ​ ​ 如果不会使用字节码可以看看戳我,对应的字节码为:

public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // lock引用(synchronized开始) lock:Ljava/lang/Object;
       3: dup
       4: astore_1						//lock引用->slot1
       5: monitorenter	//将lock对象MarkWord设置为Monitor指针
       6: getstatic     #3                  // Field counter:I
       9: iconst_1
      10: iadd
      11: putstatic     #3                  // Field counter:I
      14: aload_1
      15: monitorexit
      16: goto          24
      19: astore_2				//e->slot 2
      20: aload_1				//<-lock引用
      21: monitorexit			//将lock对象MarkWord重置,唤醒EntryList
      22: aload_2				//<-slot 2(2)
      23: athrow				//throw e
      24: return
    Exception table:
       from    to  target type
           6    16    19   any
          19    22    19   any
    LineNumberTable:
      line 8: 0
      line 9: 6
      line 10: 14
      line 11: 24
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0      25     0  args   [Ljava/lang/String;

  static {};
    Code:
       0: new           #4                  // class java/lang/Object
       3: dup
       4: invokespecial #1                  // Method java/lang/Object."<init>":()V
       7: putstatic     #2                  // Field lock:Ljava/lang/Object;

​ ​ ​ ​ ​ ​ ​ Exception table中第一行对6-16行进行检测,如果有问题就会跳到19行,将异常对象存储到一个临时变量里,然后加载lock引用,然后唤醒EntryList,抛出去异常。

*原理之synchronized进阶

1.轻量级锁

​ ​ ​ ​ ​ ​ ​ 轻量级锁的使用场景:如果一个对象虽然有多线程访问,但是多线程访问时错开的,那么可以使用轻量级锁优化。轻量级锁对于使用者来说是透明的,语法还是synchronized,假设有两个方法同步块,利用同一个对象加锁:

static fianl Object obj=new Object();
public static void method1(){
  synchronized(obj){
    //同步块A
    method2();
  }
}

public static void method2(){
  synchronized(obj){
    //同步块B
  }
}
  • 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
    在这里插入图片描述

  • 让锁记录中Object reference指向锁对象,并尝试用CAS替换Object的Mark Word,将Mark Word的值存入锁记录
    在这里插入图片描述

  • 如果CAS替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁,或许你现在还不清楚CAS(Compare AND SWAP),不过没关系,先记着CAS这是个非常重要的概念,后面会详细讲的!。
    在这里插入图片描述

  • 如果CAS失败,有两种情况

    • 如果是其他线程已经持有了该Object的轻量级锁,这时候表明有竞争,进入锁膨胀过程
    • 如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数
      在这里插入图片描述
  • 当退出synchronized代码块的时候,也就是解锁的时候,如果有取值为null的锁记录,表示有重入,这时候重置锁记录,表示重入计数-1
    在这里插入图片描述

  • 当退出synchronized代码块的时候,锁记录的值不是null,这时候使用CAS将Mark Word的值恢复给对象头

    • 成功:解锁成功
    • 失败:说明轻量级锁进行了锁膨胀或者已经升级为重量级锁,进入重量级锁的解锁过程

2.锁膨胀

​ ​ ​ ​ ​ ​ ​ 如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时候一种情况就是有其他线程为此对象加上了轻量级锁,这时候需要进行锁膨胀,将轻量级锁变成重量级锁。

static Object obj=new Object();
public static void method1(){
  synchronized(obj){
    //同步代码块
  }
}
  • 当Thread-1进行轻量级加锁的时候,Thread-0已经对该对象加了轻量级锁
    在这里插入图片描述

  • 这时候Thread-1加轻量级锁失败,进入锁膨胀流程

    • 即为Object对象申请Monitor锁,让Object指向重量级锁地址
    • 然后自己进入Monitor的EntryList BLOCKED

在这里插入图片描述

  • 当Thread-0退出同步块解锁的时候,使用CAS将Mark Word的值恢复给对象头,失败,这时候会进入重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中的BLOCKED线程

3.锁自旋化

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

​ ​ ​ ​ ​ ​ ​ 自旋重试成功的情况如下所示:
在这里插入图片描述

​ ​ ​ ​ ​ ​ ​ 自旋重试失败的情况如下所示:
在这里插入图片描述

  • Java6之后自旋锁是自适应的,比如对象刚刚的1次自旋操作成功,那么认为这次自旋成功的可能性会高,就多自旋几次,反之就减少自旋次数。
  • 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势
  • Java7后不能控制是否开启自旋功能

4.偏向锁

​ ​ ​ ​ ​ ​ ​ 轻量级锁在没有竞争的时候,每次重入仍然需要执行CAS操作。Java 6中引入了偏向锁进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS,以后只要不发生竞争,这个对象就归该线程所有。
在这里插入图片描述
在这里插入图片描述
偏向状态

​ ​ ​ ​ ​ ​ ​ 首先来回忆下对象头的格式。
在这里插入图片描述
​ ​ ​ ​ ​ ​ ​ 一个对象创建的时候:

1.如果开启了偏向锁(默认开启),那么对象创建之后,markword值为0x05即最后3位是101,这时候它的thread、epoch、age都是0

2.偏向锁默认是延迟的,不会在程序启动的时候立即生效,如果想避免延迟,可以加VM参数-XX:BiasedLockingStartupDalay=0来禁用延迟

3.如果没有开启偏向锁,那么对象创建之后,markword值位0x01即最后3位是001,这时候它的hashcode、age都是0,第一次用到hashcode才会赋值

4.处于偏向锁的对象解锁之后,线程id仍然存储在对象头中

5.禁用偏向锁-XX:-UserBiasedLocking

撤销-其他线程使用对象

​ ​ ​ ​ ​ ​ ​ 当有其它线程使用偏向锁对象的时候,会将偏向锁升级为轻量级锁。

撤销-调用对象hashCode

​ ​ ​ ​ ​ ​ ​ 调用了对象的hashCode,但偏向锁的对象Mark Word中存储的是线程id,如果调用hashCode会导致偏向锁被撤销。

  • 轻量级锁会在锁记录中记录hashcode

  • 重量级锁会在Monitor中记录hashCode

    在调用hashCode后使用偏向锁,记得去掉-XX:-UserBiasedLocking

批量重偏向

​ ​ ​ ​ ​ ​ ​ 如果对象虽然被多个线程访问,但没有竞争,这时候偏向了线程T1的对象仍然有机会重新偏向T2,重偏向会重置对象的Thread ID。

​ ​ ​ ​ ​ ​ ​ 当撤销偏向锁阈值超过20次后,JVM会这样觉得,我是不是偏向错了?于是会再给这些对象加锁的时候重新偏向到加锁线程。

批量撤销

​ ​ ​ ​ ​ ​ ​ 当撤销偏向锁阈值超过40次后,JVM后觉得,自己确实偏向错了,根本就不该偏向,于是整个类的所有对象变为不可偏向的,新建的对象也是不可偏向的。

5.锁消除
to do…

1.7 wait notify
*原理之wait/notify

在这里插入图片描述

  • Owner线程发现条件不满足,调用wait方法,即可进入WaitSet,变为WAITING状态
  • BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
  • BLOCKED线程会在Owner线程释放锁的时候唤醒
  • WAITING线程会在Owner线程调用notify或者notifyAll的时候唤醒,但唤醒后并不意味着力可获得锁,仍然需要进入EntryList重新竞争
API介绍
  • obj.wait()让进入object监视器的线程到waitSet等待
  • obj.notify()在object上正在waitSet等待的线程中挑一个唤醒
  • obj.notifyAll()让object上正在waitSet等待的线程全部唤醒

​ ​ ​ ​ ​ ​ 它们都是线程之间进行协作的手段,都属于Object对象的方法,必须获得此对象的锁,才能调用这些方法。可以试试,会报IllegalMonitorStateException异常。

1.8 认识wait notify的正确姿势
sleep(long n)和wait(long n)的区别

1)sleep是Thread方法,而wait是Object方法

2)sleep不需要强制和synchronized配合使用,但是wait需要和synchronized一起使用

3)sleep在睡眠的同时,不会释放对象锁的,但wait在等待的时候会释放对象锁

to do…

模式之保护性暂停

​ ​ ​ ​ ​ ​ ​ 用一个线程等待另一个线程的执行结果

要点:

1.有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject

2.如果有结果不断从一个线程到另一个线程那么可以使用消息队列(生产者/消费者)

3.JDK中,join的实现和Future的实现,采用的就是此模式

在这里插入图片描述

实现:

->测试类:

在这里插入图片描述

->执行下载功能的类:

在这里插入图片描述

->执行获取和产生结果的类
在这里插入图片描述
->运行结果
在这里插入图片描述

*模式之生产者消费者

​ to do…

1.9 Park&Unpark
基本使用

​ ​ ​ ​ ​ ​ ​ 它们都是LockSupport类中的方法

//暂停当前线程
LockSupport.park();
//恢复某个线程的运行
LockSupport.unpark(暂停线程对象);
特点

​ ​ ​ ​ ​ ​ ​ 与Object的wait和notify相比

  • wait、notify和notifyAll必须配合Object Monitor一起使用,而park和unpark不需要。
  • park和unpark是以线程为单位来阻塞和唤醒线程的,而notify只能随机唤醒一个等待线程,不那么精确
  • park和unpark可以先unpark,然鹅wait和notify不可能先notify。
*原理之park & unpark

​ ​ ​ ​ ​ ​ ​ 每个线程都有一个自己的Parker对象,由三部分组成:_counter, _cond, _mutex。

​ ​ ​ ​ ​ ​ ​ 线程就好像一个旅客,Parker就像是随身携带的背包,条件变量好比背包中的帐篷,_counter好比备用干粮(0耗尽,1充足)。调用park就是要看需不需要停下来歇一会,如果备用干粮耗尽,那么钻进帐篷休息,要是充足,那么继续前进;调用unpark,好比补充干粮,如果这时候线程还在帐篷里,就唤醒让他继续前进,如果线程仍在运行,下次调用park的时候,仅仅是消耗掉备用干粮,不需要停留继续前进,因为背包空间有限,多次调用unpark仅仅会补充一份备用干粮。

在这里插入图片描述
1.当前线程调用Unsafe.park()方法

2.检查_counter,本情况为0,这时候获得 _mutex互斥锁

3.线程进入_cond条件变量阻塞

4.设置_counter=0

在这里插入图片描述

1.调用Unsafe.unpark(Thread_0)方法,设置_counter为1

2.唤醒_cond条件变量中的Thread_0

3.Thread_0恢复运行

4.设置_counter为0

在这里插入图片描述

1.调用Unsafe.unpark(Thread_0)方法,设置_counter为1

2.当前线程调用Unsafe.park()方法

3.检查_counter,本情况为1,这时候线程无需阻塞,继续运行

4.设置_counter为0

1.10 重新理解线程状态转换

戳我了解

1.11 多把锁

​ ​ ​ ​ ​ ​ ​ 现在有个大房间,有两个人A和B,A想学习,B想睡觉,A和B都不想自己被打扰,也就是说A学习的时候不希望有人来干扰A(比如发出妈妈在这个大房子里唱歌…),B睡觉的时候也不希望别人干扰B(比如叫醒他)。

​ ​ ​ ​ ​ ​ ​ 那你可能直接就要给这个大房子上锁了,但是如果通过日志打印下时间,你会发现并发度很低,基本上是串行执行程序的,那咋办?让热爱学习A去书房学,让喜欢睡觉的B去卧室睡觉,完事~

​ ​ ​ ​ ​ ​ ​ 所以就需要多把锁,这样就相当于把锁的粒度细分了,好处呢就是可以增强并发度,坏处就是如果一个线程需要同时获得多把锁,容易发生死锁。

1.12 活跃性
死锁

​ ​ ​ ​ ​ ​ ​ 简单说就是A等待B,但是B等待A,构成一个闭环了,永远不会停下来,就发生死锁了。下面这段代码即将发生死锁。

import static java.lang.Thread.sleep;

public class DeadLock {
	public static void main(String[] args) {
		test1();

	}

	private static void test1() {
		Object a = new Object();
		Object b = new Object();
		Thread t1=new Thread(()->{
			synchronized (a){
				System.out.println("t1:lock a");
				try {
					sleep(2);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				synchronized (b){
					System.out.println("t1:lock b");
					System.out.println("t1:操作。。。");
				}
			}
		},"t1");

		Thread t2=new Thread(()->{
			synchronized (b){
				System.out.println("t2:lock b");
				try {
					sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				synchronized (a){
					System.out.println("t2:lock a");
					System.out.println("t2:操作。。。");
				}

			}
		},"t2");
		t1.start();
		t2.start();
	}
}
定位死锁

​ ​ ​ ​ ​ ​ ​ 检测死锁可以使用jconsole工具,或者使用jps定位进程id,再用jstack定位死锁。具体的操作点我

哲学家就餐问题

​ ​ ​ ​ ​ ​ to do…

活锁

​ ​ ​ ​ ​ ​ to do…

饥饿

​ ​ ​ ​ ​ ​ to do…

1.13 ReentrantLock

​ ​ ​ ​ ​ ​ ​ 相对于synchronized,ReentrantLock具备下列特点:

​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ 1.可中断

​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ 2.可以设置超时时间

​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ 3.可以设置为公平锁

​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ 4.支持多个条件变量

​ ​ ​ ​ ​ ​ ​ 和synchronized一样,都支持可重入。

​ ​ ​ ​ ​ ​ 基本语法:

//获得锁
reentrantLock.lock();
try{
  //临界区
}finally{
  //释放锁
  reentrantLock.unlock();
}
可重入

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

import java.util.concurrent.locks.ReentrantLock;

public class Test01 {
	private static ReentrantLock lock=new ReentrantLock();

	public static void main(String[] args) {
		lock.lock();
		try {
			System.out.println("main:enter");
			m1();
		}finally {
			lock.unlock();
		}
	}

	public static void m1(){
		lock.lock();
		try {
			System.out.println("enter m1");
			m2();
		}finally {
			lock.unlock();
		}
	}
	public static void m2(){
		lock.lock();
		try {
			System.out.println("enter m2");
		}finally {
			lock.unlock();
		}
	}

}

可打断
public class Test02 {
	private static ReentrantLock lock=new ReentrantLock();

	public static void main(String[] args) throws InterruptedException {
		Thread t1=new Thread(()->{
			try {
				//如果没有竞争那么此方法就会获取lock对象锁
				//如果有竞争就会进入阻塞队列,可以被其他线程用interrupt方法打断
				System.out.println("t1尝试获得锁");
				lock.lockInterruptibly();
			} catch (InterruptedException e) {
				e.printStackTrace();
				System.out.println("t1没获得锁,返回");
				return;
			}
			try{
				System.out.println("t1获取到锁了");
			}finally {
				lock.unlock();
			}

		},"t1");


		lock.lock();
		t1.start();


		Thread.sleep(1);
		System.out.println("打断t1");
		t1.interrupt();
	}
}
锁超时

​ ​ ​ ​ ​ ​ ​ 下面演示的是获取不到锁,立刻结束等待,而这一小结的标题是锁超时也是基于下面代码中的同一个方法tryLock()

public class Test03 {
	private static ReentrantLock lock=new ReentrantLock();

	public static void main(String[] args) {
		Thread thread = new Thread(()->{
			System.out.println("t1尝试获得锁....");
			/*
			当只有t1一个线程的时候自然能获得到锁,此时控制台的打印情况为:
			t1尝试获得锁....
			t1获得到锁...
			 */
			if(!lock.tryLock()){
				System.out.println("t1获取不到锁");
				return;
			}
			try{
				System.out.println("t1获得到锁...");
			}finally {
				lock.unlock();
			}
		},"t1");
		//主线程上锁
		lock.lock();
		System.out.println("主线程获得到锁...");
		thread.start();
	}
}

/**打印结果:
主线程获得到锁...
t1尝试获得锁....
t1获取不到锁
*/

​ ​ ​ ​ ​ ​ ​ 通过使用带参数的tryLock方法即可实现锁超时情况下的实现。

public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

​ ​ ​ ​ ​ ​ ​ 锁超时可以解决哲学家就餐的问题。

公平锁

​ ​ ​ ​ ​ ​ ​ ReentrantLock默认是不公平的。可以通过改变布尔值参数实现公平锁。

 public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
}

​ ​ ​ ​ ​ ​ ​ 公平锁一般没必要,会降低并发度,公平锁可以解决饥饿问题。

条件变量

​ ​ ​ ​ ​ ​ ​ synchronized中也有条件变量,就是waitSet休息室,当条件不满足的时候进入waitSet等待。

​ ​ ​ ​ ​ ​ ​ ReentrantLock的条件变量比synchronized强大的地方在于,它是支持多个条件变量的,这就好比:synchronized是哪些不满足条件的线程都在1间休息室等消息,而ReentrantLock支持多间休息室。

使用流程

  • await前需要获得锁
  • await执行后,会释放锁,进入conditionObject等待
  • await的线程被唤醒(或打断、或超时)取重新竞争Lock锁
  • 竞争Lock锁成功后,从await后继续执行

使用案例

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class Test04 {
	private static ReentrantLock ROOM=new ReentrantLock();
	static final Object room=new Object();
	static boolean hasCigarette=false;
	static boolean hasTakeout=false;

	//等待烟的休息室
	static Condition waitCigaretteSet=ROOM.newCondition();
	//等待外卖的休息室
	static Condition waitTakeoutSet=ROOM.newCondition();

	public static void main(String[] args) {

		new Thread(()->{
			ROOM.lock();
			try{
				System.out.println("大叔:烟送到了吗?"+hasCigarette);
				while(!hasCigarette){
					System.out.println("大叔,没烟,再等一会!");
					try{
						waitCigaretteSet.await();
					}catch (InterruptedException e){
						e.printStackTrace();
					}
				}
				System.out.println("[大叔]可以开始干活了");
			}finally {
				ROOM.unlock();
			}
		},"大叔").start();




		new Thread(()->{
			ROOM.lock();
			try{
				System.out.println("君君:外卖送到了吗?"+hasTakeout);
				while(!hasTakeout){
					System.out.println("君君,没外卖,再等一会!");
					try{
						waitTakeoutSet.await();
					}catch (InterruptedException e){
						e.printStackTrace();
					}
				}
				System.out.println("[君君]可以开始干活了");
			}finally {
				ROOM.unlock();
			}
		},"君君").start();

		new Thread(()->{
			ROOM.lock();
			try {
				hasTakeout=true;
				waitTakeoutSet.signal();
			}finally {
				ROOM.unlock();
			}
		},"送外卖的").start();


		new Thread(()->{
			ROOM.lock();
			try {
				hasCigarette=true;
				waitCigaretteSet.signal();
			}finally {
				ROOM.unlock();
			}
		},"送烟的").start();
	}
}

*同步模式之顺序控制

to do…

*同步模式之交替输出

to do…

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值