文章目录
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…