目录
并发笔记一:什么是线程不安全?
并发笔记二:线程中断机制
并发笔记三:线程的生命周期
并发笔记四:锁机制(一)
并发笔记四:锁机制(二)
1.隐式锁
隐式锁,又称线程同步,即synchronized
修饰的方法或代码块。
-
修饰方法
修饰方法时,放在范围修饰符(public等)之后,表示该方法线程同步。默认锁的对象就是当前对象本身。
private synchronized static void addOne(){
count ++;
}
- 修饰代码块
修饰代码块时,对某一代码块使用synchronized(Object)
,指定加锁对象
private synchronized void addOne(){
synchronized (this){
count ++;
}
}
相对显示锁不需要加锁和解锁的操作,故称之为隐式锁。
有一些隐式规则如下:
- 被
synchronized
修饰的方法或代码块被多线程访问时,同一时间只有一个线程可以访问该对象,其它线程需要等待当前线程完成后方可执行。 - 当一个线程访问
synchronized(this)
修饰的代码块时,其它线程仍可以访问该Object中的非synchronized(this)
代码块。 - 当一个线程访问Object中的
synchronized(this)
代码块时,其它线程对Object中的所有其它synchronized(this)
同步修饰的代码块将被阻塞。 - 一个线程访问Object的
synchronized(this)
同步代码块时,它就获得了该Object的对象锁,其它线程对该Object的同步代码块的访问都将阻塞。 - 以上规则对其它对象锁同样适用。
- 执行效率
synchronized
修饰时的效率:方法体修饰 <synchronized(this)
<synchronized(byte)
byte代表具体的小的对象值
2.显式锁 – Lock和ReentrantLock
2.1 Lock
Lock是java提供的无条件的、可轮询的、定时的、可中断的锁获取操作,所有加锁和解锁的操作都是显式的。包路径是java.util.concurrent.locks.Lock
,该类里的接口及作用如下:
/**
* Acquires the lock.
* 获取锁
* <p>If the lock is not available then the current thread becomes
* disabled for thread scheduling purposes and lies dormant until the
* lock has been acquired.
* 如果锁不可用,则出于线程调度的目的,将当前线程置于休眠状态直到获取锁。
*/
void lock();
/**
* Acquires the lock unless the current thread is
* {@linkplain Thread#interrupt interrupted}.
* 如果当前线程没有被中断,则获取锁
* <p>Acquires the lock if it is available and returns immediately.
* 如果锁可用,则获取锁并立即返回
* <p>If the lock is not available then the current thread becomes
* disabled for thread scheduling purposes and lies dormant until
* one of two things happens:
* 如果锁不可用,则出于线程调度的目的,将当前线程置于休眠状态直到出现以下两种情况之一:
* <ul>
* <li>The lock is acquired by the current thread; or
* 锁被当前线程获取
* <li>Some other thread {@linkplain Thread#interrupt interrupts} the
* current thread, and interruption of lock acquisition is supported.
* 其它线程中断了当前线程,并且支持对锁的中断。
* </ul>
*
* <p>If the current thread:
* <ul>如果当前线程:
* <li>has its interrupted status set on entry to this method; or
* 在进入这个方法时已经设置其中断状态
* <li>is {@linkplain Thread#interrupt interrupted} while acquiring the
* lock, and interruption of lock acquisition is supported,
* </ul>
* 或者在获取锁时被中断,并且支持对锁的中断。
* then {@link InterruptedException} is thrown and the current thread's
* interrupted status is cleared.
* 这样的话会抛出 InterruptedException 异常,并且清除当前线程的中断状态。
*/
void lockInterruptibly() throws InterruptedException;
/**
* Acquires the lock only if it is free at the time of invocation.
* 当调用该锁时,锁是空闲状态,则获取该锁。
* <p>Acquires the lock if it is available and returns immediately
* with the value {@code true}.
* 如果锁可用,则获取锁并立即返回true
* If the lock is not available then this method will return
* immediately with the value {@code false}.
* 如果锁不可用,这个方法会立即返回false
* <p>A typical usage idiom for this method would be:
* <pre> {@code
* Lock lock = ...;
* if (lock.tryLock()) {
* try {
* // manipulate protected state
* } finally {
* lock.unlock();
* }
* } else {
* // perform alternative actions
* }}</pre>
*
* This usage ensures that the lock is unlocked if it was acquired, and
* doesn't try to unlock if the lock was not acquired.
* 这个用法确保了当锁被获取是它是解锁的,并且当该锁没被获取时不会去尝试解锁。
* @return {@code true} if the lock was acquired and
* {@code false} otherwise
*/
boolean tryLock();
/**
* Acquires the lock if it is free within the given waiting time and the
* current thread has not been {@linkplain Thread#interrupt interrupted}.
* 当该线程在给定的时间内没有被中断,则获取锁。
* <p>If the lock is available this method returns immediately
* with the value {@code true}.
* If the lock is not available then
* the current thread becomes disabled for thread scheduling
* purposes and lies dormant until one of three things happens:
* <ul>
* 如果锁可用,则立即返回ture;如果锁不可用,则出于线程调度的目的,将当前线程置于休眠状态直到出现以下三种情况之一:
* <li>The lock is acquired by the current thread; or
* <li>Some other thread {@linkplain Thread#interrupt interrupts} the
* current thread, and interruption of lock acquisition is supported; or
* <li>The specified waiting time elapses
* </ul>
* 当前线程获取了该锁,或者其它线程中断了当前线程,并且支持对锁的中断;或者超过了指定的等待时间。
* <p>If the lock is acquired then the value {@code true} is returned.
* 如果锁被获取,则返回true
* <p>If the current thread:
* <ul>
* <li>has its interrupted status set on entry to this method; or
* <li>is {@linkplain Thread#interrupt interrupted} while acquiring
* the lock, and interruption of lock acquisition is supported,
* </ul>
* then {@link InterruptedException} is thrown and the current thread's
* interrupted status is cleared.
* 如果当前线程在进入方法时已经设置了其中断状态,或者在获取锁的过程中被中断,并且支持对锁的中断。
* 则抛出 InterruptedException 异常并清除当前线程的中断状态。
* <p>If the specified waiting time elapses then the value {@code false}
* is returned.
* If the time is
* less than or equal to zero, the method will not wait at all.
* 如果超过了指定的等待时间则返回false,如果指定等待时间小于等于0,则该方法不会在等待。
* <p><b>Implementation Considerations</b>
*
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
* Releases the lock.
* 释放锁
*/
void unlock();
/**
* Returns a new {@link Condition} instance that is bound to this
* {@code Lock} instance.
* 返回用来与此实例一起使用的Condition实例
*/
Condition newCondition();
2.2 ReentrantLock
ReentrantLock是Lock的实现类,是一个互斥同步器,具有可拓展能力。
使用方法:
public class Test {
public static void main(String[] args) {
LockTest lockTest = new LockTest();
for(int i =0;i < 2;i ++){
new Thread(()->lockTest.get()).start();
}
for (int j=0;j < 2;j++){
new Thread(()-> lockTest.put()).start();
}
}
}
public class LockTest {
final ReentrantLock lock = new ReentrantLock();
public void get(){
try {
lock.lock();
System.out.println("get方法用户开始操作,当前方法:" + Thread.currentThread().getName() + " get begin");
Thread.sleep(1000L);
System.out.println("get方法用户结束操作,当前方法:" + Thread.currentThread().getName() + " get end");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("get释放锁");
lock.unlock();
}
}
public void put(){
try {
lock.lock();
System.out.println("put方法用户开始操作,当前方法:" + Thread.currentThread().getName() + " put begin");
Thread.sleep(1000L);
System.out.println("put方法用户结束操作,当前方法:" + Thread.currentThread().getName() + " put end");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("put释放锁");
lock.unlock();
}
}
}
执行结果为:
get方法用户开始操作,当前方法:Thread-0 get begin
get方法用户结束操作,当前方法:Thread-0 get end
get释放锁
put方法用户开始操作,当前方法:Thread-3 put begin
put方法用户结束操作,当前方法:Thread-3 put end
put释放锁
get方法用户开始操作,当前方法:Thread-1 get begin
get方法用户结束操作,当前方法:Thread-1 get end
get释放锁
put方法用户开始操作,当前方法:Thread-2 put begin
put方法用户结束操作,当前方法:Thread-2 put end
put释放锁
一个线程执行完并释放锁,另一个线程才会获取锁,继续执行。如果将LockTest里的代码改动一下:
public class LockTest {
public void get(){
final ReentrantLock lock = new ReentrantLock();
try {
lock.lock();
System.out.println("get方法用户开始操作,当前方法:" + Thread.currentThread().getName() + " get begin");
Thread.sleep(1000L);
System.out.println("get方法用户结束操作,当前方法:" + Thread.currentThread().getName() + " get end");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("get释放锁");
lock.unlock();
}
}
public void put(){
final ReentrantLock lock = new ReentrantLock();
try {
lock.lock();
System.out.println("put方法用户开始操作,当前方法:" + Thread.currentThread().getName() + " put begin");
Thread.sleep(1000L);
System.out.println("put方法用户结束操作,当前方法:" + Thread.currentThread().getName() + " put end");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("put释放锁");
lock.unlock();
}
}
}
将new ReentrantLock
放在方法体内部,这样的话两个方法之间的锁是独立的,运行结果如下:
put方法用户开始操作,当前方法:Thread-3 put begin
put方法用户开始操作,当前方法:Thread-2 put begin
get方法用户开始操作,当前方法:Thread-1 get begin
get方法用户开始操作,当前方法:Thread-0 get begin
put方法用户结束操作,当前方法:Thread-3 put end
put释放锁
get方法用户结束操作,当前方法:Thread-0 get end
get方法用户结束操作,当前方法:Thread-1 get end
get释放锁
put方法用户结束操作,当前方法:Thread-2 put end
put释放锁
get释放锁
发现每次运行都不相同,说一下个人的理解:
当使用全局锁时:
在美国有个大教堂,神父每次只给一个人洗礼,教堂有一个锁着的大门(全局变量锁),为了避免种族歧视,神父随机点名,点到谁把钥匙给谁。如果某个白人拿到了钥匙(获取锁),那么他会打开大门进去,把大门插上(.lock()方法会将未获取到锁的线程置于休眠状态)防止别人进来打扰,做完洗礼后,出来把大门锁上(释放锁),把钥匙交给下一个神父点到的人。
这样的话每次就只有一个人做洗礼(每次只执行一个线程),线程之间互不干扰。
当使用独立锁时:
同样是上面的案例,这样持续一段时间后,黑人群体不乐意了,说神父好几次连续点了几名白人,这是种族歧视,黑人要求神父公平对待。神父没有办法,干脆在旁边又盖了一个教堂,将黑人和白人分开,这样黑人和白人都有自己的教堂(拥有各自的独立锁)不管你们自己怎么解决排队问题,两家教堂之间互不干扰(线程执行顺序不一致),所以线程运行顺序也是不一致的。
2.3 ReentrantReadWriteLock
从名字来看可以知道这是一个读写锁。ReadWriteLock接口提供了readLodk和WriteLock两种锁机制。其中readLock可以被多个线程读访问,是线程共享的,writeLock仅能有一个写线程访问,与读线程和写线程互斥。
读写锁存在以下机制:
(1)读-读不互斥:当只有读线程没有写线程是,线程可并发读,不会阻塞。
(2)读-写互斥:当有读线程时,写线程会被阻塞,反之亦然。就看谁先拿到锁了。
(3)写-写互斥:写线程之间互斥。
写一个测试代码,把之前测试Lock的代码修改一下,并加一个新的方法。
public class ReentrantReadWriteLockTest {
final ReentrantReadWriteLock rrwl = new ReentrantReadWriteLock();
private Map<String,Object> map = new HashMap<>();
public void get(){
try {
rrwl.readLock().lock();//读锁,其他线程只能读不能写,具有高并发。
System.out.println("get方法用户开始操作,当前方法:" + Thread.currentThread().getName() + " get begin");
Thread.sleep(1000L);
System.out.println("get方法用户结束操作,当前方法:" + Thread.currentThread().getName() + " get end");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("get释放锁");
rrwl.readLock().unlock();
}
}
public void put(){
try {
rrwl.writeLock().lock();
System.out.println("put方法用户开始操作,当前方法:" + Thread.currentThread().getName() + " put begin");
Thread.sleep(1000L);
System.out.println("put方法用户结束操作,当前方法:" + Thread.currentThread().getName() + " put end");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("put释放锁");
rrwl.writeLock().unlock();
}
}
public void readWrite(){
map.put("id",null);//假设是缓存数据
Object value = null;
rrwl.readLock().lock();
try {
value = map.get("id");
if(value == null){
rrwl.readLock().unlock();//要进行写操作了,读锁释放
rrwl.writeLock().lock();//获取写锁
try{
value = "这是一个测试value";//这里可以做一些操作,例如从数据库获取
}finally {
rrwl.writeLock().unlock();//写锁释放
}
rrwl.readLock().lock();//写操作完成并且已经释放了锁,获取读锁。
}
}finally {
rrwl.readLock().unlock();
}
System.out.println("测试读写方法的value:" + value);
}
}
public class Test {
public static void main(String[] args) {
final ReentrantReadWriteLockTest lockTest = new ReentrantReadWriteLockTest();
for(int i =0;i < 2;i ++){
new Thread(lockTest::get).start();
}
for (int j=0;j < 2;j++){
new Thread(lockTest::put).start();
}
for(int i = 0;i < 2;i++){
new Thread(lockTest::readWrite).start();
}
}
}
控制台结果:
get方法用户开始操作,当前方法:Thread-0 get begin
get方法用户开始操作,当前方法:Thread-1 get begin
get方法用户结束操作,当前方法:Thread-1 get end
get方法用户结束操作,当前方法:Thread-0 get end
get释放锁
get释放锁
put方法用户开始操作,当前方法:Thread-2 put begin
put方法用户结束操作,当前方法:Thread-2 put end
put释放锁
put方法用户开始操作,当前方法:Thread-3 put begin
put方法用户结束操作,当前方法:Thread-3 put end
put释放锁
测试读写方法的value:这是一个测试value
测试读写方法的value:这是一个测试value
从打印的效果来看,读锁是并发的,写锁是互斥的;获取读锁之前要释放写锁,获取写锁之前也要释放读锁,否则会发生阻塞。
2.4 显式锁StampedLock
StampedLock仅作了解即可。
StampedLock是jdk1.8新加入的一种锁机制,我们之前提到的ReentrantReadWriteLock
中,读写锁之间是互斥的,而StampedLock
则实现了读不阻塞写,那么在大量读少量写的情况下,StampedLock
可以极大的提高吞吐量,同时可以减少写饥饿现象。
看一个java的doc提供的例子:
public class StampedLockTest {
private double x,y;
private final StampedLock sl = new StampedLock();
void move(double deltaX,double deltaY) {// an exclusively locked method
//每次调用writeLock,stamp的值都会变化,方便对比
long stamp =sl.writeLock(); //写锁
try {
x +=deltaX;
y +=deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
double distanceFromOrigin() { // A read-only method
/**
* .tryOptimisticRead():Returns a stamp that can later be validated, or zero if exclusively locked.
* 返回一个稍后可验证的stamp,或者排他锁时返回零
*/
long stamp = sl.tryOptimisticRead();//获取乐观读锁
double currentX = x, currentY = y;//将两个字段读入本地局部变量
/**
* 关于validate方法:
* Returns true if the lock has not been exclusively acquired
* since issuance of the given stamp. Always returns false if the
* stamp is zero. Always returns true if the stamp represents a
* currently held lock. Invoking this method with a value not
* obtained from {@link #tryOptimisticRead} or a locking method
* for this lock has no defined effect or result.
* 若该锁自stamp发布以来未获取排他锁,则返回true,若stamp=0,返回false,
* 若stamp代表当前持有的锁,返回true.
* 当某值不是从tryOptimisticRead获取或该锁的锁方法没有确定的影响或结果时调用该方法
*/
if (!sl.validate(stamp)) {//检查发生乐观读锁后是否有写锁发生,如果读之前发生了写操作,则进入该方法
stamp = sl.readLock();//获取悲观读锁
try {
currentX = x;
currentY = y;
System.out.println("复制操作");
}finally{
sl.unlockRead(stamp);
}
}
double cx = currentX *currentX;
double cy = currentY *currentY;
double aaa = Math.sqrt( cx+ cy);
System.out.println(currentX + "," + currentX + "," + aaa);
return aaa;
}
//悲观锁案例
void moveIfAtOrigin(double newX, double newY) { // upgrade
long stamp = sl.readLock();
try {
//循环,检查当前状态是否符合
while (x == 0.0 && y == 0.0) {
//将读锁转换为写锁
long ws = sl.tryConvertToWriteLock(stamp);
//确认写锁是否成功
if (ws != 0L) {
stamp = ws;// 替换stamp
x = newX;//修改数据
y = newY;
break;
}
//转换为写锁失败
else {
//释放读锁
sl.unlockRead(stamp);
//显示获取写锁,然后再通过循环再试
stamp = sl.writeLock();
}
}
} finally {
sl.unlock(stamp);
}
}
}
main方法:
public class Test {
public static void main(String[] args) {
final StampedLockTest stampedLockTest = new StampedLockTest();
for(int i = 0;i < 2;i ++){
new Thread(()->{
stampedLockTest.move(5.00,5.00);
}).start();
}
Thread.sleep(1000L);
for (int i = 0;i < 2;i ++){
new Thread(()->{
stampedLockTest.distanceFromOrigin();
}).start();
}
}
}
首先不调用Thread.sleep(3000L);
方法,执行看看结果:
384
640
10.0,10.0,14.142135623730951
10.0,10.0,14.142135623730951
因为写操作1秒后才开始读操作,所以发生乐观读锁后没有写锁发生,故没有输出有写操作发生!
的字样。如果把Thread.sleep(1000L)
注释掉,让读写操作并发进行,则输出:
有写操作发生!
有写操作发生!
10.0,10.0,14.142135623730951
10.0,10.0,14.142135623730951
多运行几次,发现有可能会出现有写操作发生!
的字样,说明发生乐观读锁后有写锁发生,但是读锁并没有阻塞写锁,读写并发执行。