Concurrent Programming —— Pessimistic Lock and Monitor

Concurrent Programming

Concurrent Programming —— Introduction
Concurrent Programming —— Pessimistic Lock and Monitor
Concurrent Programming —— JMM(Java Memory Model)
Concurrent Programming ——Thread Pool
Concurrent Programming —— JUC(java.util.concurrent)

前言

文本将介绍Java解决线程安全问题的方法中的悲观锁模式以及Monitor对象

多个线程访问共享资源,对共享资源进行读写操作时发生指令交错,就会出现并发问题,一段代码块内如果存在对共享资源的多线程读写操作,称这段代码为临界区

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

避免临界区的竞态条件发生的方法:
阻塞式解决方法:synchronizedlock
非阻塞式解决方法:原子变量
在这里插入图片描述

1. synchronized

使用方法

synchronized(object){
	//task1
}

synchronized中的对象,可以想象为一个房间,只有唯一入口,每次只能一个人进入进行计算, 当多个线程想进入这个房间运行代码时,只有一个线程能获得锁进入房间,这就保证了共享数据只有一个线程在进行读写操作,没有获得锁的线程就会进入Blocked状态,需要等锁释放后才能再去竞争这个锁

不同的对象,也代表着不同的房间,如果希望只有只有一个线程能进行操作,就需要所有线程都synchronized同一个对象

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

Object key1 = new Object();
Object key2 = new Object();
int count = 10;
Thread t1 = new Thread(()->{
	synchronized(key){
		for(int i = 0; i < 500; i++){
			count++;
		}
	}
	
});

Thread t2 = new Thread(() -> {
	synchronized(key2){
		for(int i = 0; i < 500; i++){
			count--;
		}
	}
});

t1.start();
t2.start();

由于key1 和 key2 是不同的对象,因此t1和t2线程不会有竞争,两个线程都能同时对count进行修改操作,这就会产生线程安全问题,t1和t2线程中synchronized的对象要相同才能起到保护共享数据的作用

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

synchronized也可以加在方法上,成员方法上的synchronized 锁的是this对象,静态方法上的synchronized 锁的是类对象

Java中有许多线程安全的类,如String,基本数据类型的包装类,StringBuffer等,这些类中的方法都是线程安全,但是当多个方法组合时不一定是线程安全的

Hashtable<String,String> table = new Hashtable<>();
	Thread t1 = new Thread(()->{
		if (table.get("key")==null){
			table.put("key","1");
		}
	});
	Thread t2 = new Thread(()->{
		if (table.get("key")==null){
			table.put("key","2");
		}
	});
t1.start();
t2.start();

Hashtable是线程安全类,类中的方法也是线程安全,代码中两个线程都是去查找table中是否有key元素如果没有则添加到table中,这两个方法组合在一起就不一定是线程安全的了,当t1先执行get方法时,table对象是上锁的,t2线程无法对它进行操作,但当t1线程的get方法结束后,锁就释放了,准备执行put方法,但是t2线程抢到了锁,也开始去判断table是否有key元素,这时t1线程并没有执行put方法,所以也是true,t=准备进行put操作,这就会导致t1和t2线程都执行put操作,导致数据不一致引发了线程安全问题,因此线程安全的方法组合在一起不能确保操作的原子性,也会导致线程不安全

2. Monitor

Java对象在内存中都是由两部分组成,一部分是对象头,另一部分是对象的成员变量

对象头在32位系统下的结构:
在这里插入图片描述
在32位系统下,对象头占64位,8个字节,其中4个字节是Mark Word,另外4个字节是Klass Word,Klass Word是指向这个对象所存储的Class

Mark Word 结构:
在这里插入图片描述
其中25位用来表示hashcode,4位用来表示对象年龄,1位用来表示是不是偏向锁,最后2位用来表示锁的状态
图中列出了对象的5种状态,按照顺序依次为:正常状态,偏向锁状态,轻量级锁状态,重量级锁状态和标记状态(标记后开始等待垃圾回收器进行回收)

每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)以后,该对象头的Mark Word中就被设置指向Monitor对象的指针

当有线程占有锁的时候,Monitor的Owner就会设置为线程,只能有一个Owner
如果其它线程也来执行synchronized 就会进入EntryList,变成Blocked状态

在这里插入图片描述

2.1轻量级锁

应用场景:一个对象虽然有多线程访问,但多线程访问的时间是错开的(没有竞争),那么可以使用轻量级锁来优化

轻量锁对使用者是透明的,语法依旧是synchronized

对象变成轻量级锁的过程:

  1. 创建锁记录,每个线程都的栈帧都包含一个锁记录的结构,内部可以存储锁对象的Mark Word
  2. 让锁记录Object reference指向锁对象,并尝试用cas替换Object的Mark word,让Mark word的值存入锁记录
  3. 如果cas(compare and set/swap)替换成功,对象头存储了锁记录地址和状态00,表示由该线程给对象加锁
  4. 如果失败由两种情况:
    如果是其它线程已经持有了该Object的轻量锁,这时表明有竞争,进入膨胀过程
    如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数在这里插入图片描述
    锁膨胀:如果在尝试加轻量锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量锁变为重量锁
    在这里插入图片描述
    当Thread-1进行轻量加锁时,Thread-0已经对该锁加了轻量级锁这时Thread-1加轻量锁失败,进入膨胀锁流程:
    为Object对象申请Monitor 锁,让Object指向重量锁地址
    然后让自己进入Monitor的EntryList 和Blocked状态
    当Thread-0退出同步块解锁时,使用cas将Mark word的值恢复给对象头,会失败。
    这时会进入重量级锁解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中Blocked线程

2.2 重量级锁

重量级锁是线程开销最大的锁,当一个线程获得重量锁后,别的线程再来尝试获取锁时,就会进入阻塞状态,CPU不再对阻塞线程进行调度,当锁释放后,需要去唤醒阻塞状态的线程再来竞争锁,唤醒和阻塞线程的都需要消耗很多时间

重量级锁竞争时,会进行自旋优化,如果当前线程自旋成功(即这时持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞,会根据上次自旋是否成功来增加或减少自旋次数,比较智能

2.3偏向锁

轻量锁在没有竞争时,每次重入仍然需要执行CAS操作,Java 6中引入来偏向锁来做进一步优化,只要第一次使用CAS线程ID设置到对象的Mark Word头,之后发现这个线程的ID是自己就表示没有竞争,不用重新CAS,以后只要不发生竞争,这个对象就归该线程所有,在Mark Word中会用23位来记录获得锁的线程

一个对象创建时,如果开启了偏向锁(默认开启),那么对象创建后,markword值即最后3位为101,这时他的thread、epoch、age都为0

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

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

hashcode()方法会禁用偏向锁,因为偏向锁没有足够的位置存储hashcode(需要31位)

Java用锁顺序:优先偏向锁,然后轻量锁,最后重量锁

当其他线程使用偏向锁对象时,会将偏向锁升级为轻量锁

3. 锁对象控制线程的方法

synchronized(obj){
	//使当前线程进入等待状态
	obj.wait()
	//随机唤醒等待中的一个线程
	obj.notify()
	//唤醒所有等待的线程
	obj.notifyAll()
}

调用wait方法,线程就会进入Monitor对象的WaitSet中变为Waiting状态
在Owner线程调用notify或者notifyAll唤醒后,线程进入EntryList重新竞争,不会立刻获得锁

Sleep和Wait的区别:

  1. sleepThread方法,而waitObject的方法
  2. sleep不需要强制和synchronized结合使用,wait需要和synchronized一起用
  3. sleep在睡眠的同时,不会释放对象的锁,但wait在等待的时候会释放对象锁
  4. sleepwait的线程状态相同都是TIMED_WAITING

3.1 LockSupport类中的方法

Java中LockSupport类提供了一些方法可以直接控制线程,不需要配合对象的Monitor就可以直接控制线程

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

与Object的wait & notify对比:

  1. waitnotifynotifyAll必须配合Object Monitor一起使用,而parkunpark不需要
  2. park unpark是以线程为单位来阻塞和唤醒的,而notify随机唤醒一个等待线程
  3. park unpark 可以先unpark,而waitnotify 不能先notify

每个线程都有一个Parker对象,有_counter, _cond和_mutex组成,_counter 来判断线程是否需要休息 _cond用来存放休息的线程,当_counter = 0说明需要休息,调用unpark后 _counter 会变为1

3.2锁活跃性

死锁:

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

Thread t1 = new Thread(()->{
	synchronizef(A){
		synchoronized(B){
		}
	}
});
Thread t2 = new Thread(()->{
	synchronizef(B){
		synchoronized(A){
		}
	}
});

死锁后,线程不会停止会一直运行

监测死锁可以用 jconsole工具,或者是用jps定位进程ID,再用 jstack定位死锁

活锁:
两个线程互相改变对象的结束条件,最后谁也无法结束

int count=10;
Thread t1 = new Thread(()->{
	while(count>0){
		count--;
	}
}).start();
Thread t2 = new Thread(()->{
	while(count<20){
		count++;
	}
}).start();

t1和t2线程都在改变count的值,导致count的值无法大于20和小于0,使得两个线程都一直在运行

饥饿问题:
一个线程由于优先级太低,始终得不到CPU调度执行,也不能够结束

4. ReentrantLock

对于synchronized 具备以下特点:

  1. 可中断
  2. 可以设置超时时间
  3. 可以设置为公平锁
  4. 支持多个条件变量
  5. 与synchronized 一样,都支持可重入

使用方法

ReentrantLock lock = new ReentrantLock();
//获取锁
lock.lock()
try{
	//临界区
}finally{
	//释放锁
	lock.unlock()
}

可打断:
t1在等待过程中,其他线程可以用interrupt()方法打断 t1线程的等待
lock()方法是不可打断的,lockInteruptibly()方法可以被打断

锁超时:
等待一段时间后还是无法获得锁,就放弃等待,通过tryLock()方法尝试获得锁,返回boolean值,false 表示没有获得锁,没有获得锁就结束等待,也支持可打断,可以用来解决死锁问题

ReentrantLock A = new ReentrantLock();
ReentrantLock B = new ReentrantLock();
Thread t1 = new Thread(()->{
	if(A.tryLock()){
		try{
			if(B.tryLock()){
				try{
					//执行方法
				}finally{
					B.unlock();
				}
			}
		}finally{
			A.unlock();
		}
	}
}).start();
Thread t2 = new Thread(()->{
	if(B.tryLock()){
		try{
			if(A.tryLock()){
				try{
					//执行方法
				}finally{
					A.unlock();
				}
			}
		}finally{
			B.unlock();
		}
	}
}).start();

在无法获取另外一个锁的时候就会放弃等待并释放自己手中的锁

公平锁
ReentrantLock默认是不公平的(不按照阻塞队列的顺序来分配释放的锁),本意是为了解决饥饿问题,
构造时传入true来变成公平锁,公平锁会降低并发度

条件变量
不满足条件时,进入waitSet等待
ReentrantLock支持多条件变量,不同的条件有不同的waitSet(休息室),可以按照waitSet(休息室)来唤醒

ReentrantLock lock = new ReentrantLock();
//创建条件变量
Condition cond1 = lock.newCondition();
//await前需要获得锁
//进入等待
cond1.await(); 
//唤醒cond1中的某一个线程
cond1.singal();
//唤醒cond1中的所有线程
cond1.singalAll();

Concurrent Programming

Concurrent Programming —— Introduction
Concurrent Programming —— Pessimistic Lock and Monitor
Concurrent Programming —— JMM(Java Memory Model)
Concurrent Programming ——Thread Pool
Concurrent Programming —— JUC(java.util.concurrent)

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值