6 多线程锁
6.1 锁的八个问题演示
class Phone {
public static synchronized void sendSMS() throws Exception {
//停留4秒
TimeUnit.SECONDS.sleep(4);
System.out.println("------sendSMS");
}
public synchronized void sendEmail() throws Exception {
System.out.println("------sendEmail");
}
public void getHello() {
System.out.println("------getHello");
}
}
-
标准访问,先打印短信还是邮件
- ------sendSMS
- ------sendEmail
-
停 4 秒在短信方法内,先打印短信还是邮件
- ------sendSMS
- ------sendEmail
-
新增普通的 hello 方法,是先打短信还是 hello
- ------getHello
- ------sendSMS
-
现在有两部手机,先打印短信还是邮件
- ------sendEmail
- ------sendSMS
-
两个静态同步方法,1 部手机,先打印短信还是邮件
- ------sendSMS
- ------sendEmail
-
两个静态同步方法,2 部手机,先打印短信还是邮件
- ------sendSMS
- ------sendEmail
-
1 个静态同步方法,1 个普通同步方法,1 部手机,先打印短信还是邮件
- ------sendEmail
- ------sendSMS
-
1 个静态同步方法,1 个普通同步方法,2 部手机,先打印短信还是邮件
- ------sendEmail
- ------sendSMS
分析:两点:
- 是否用的同一把锁
- 锁的范围是怎样的?
6.2 总结
- 两个线程的创建之间间隔了100ms,则短信线程先创建,邮件后创建
- sleep不会释放锁,两个线程一起等待4秒后,顺序和1一样
- hello不同步的,SMS等待4秒,所以先Hello
- 两部手机,两个同步监视器,互相不干扰,SMS睡4秒,所以先email
- 两个静态方法,锁是静态类的Class,所以顺序和1一样,因为同一把锁
- 两个静态方法,两部手机,但是锁还是一个,和5一样
- 一个锁是Class,一个锁是this,互相不影响,所以email快,SMS睡4秒所以慢
- 两把锁,几个手机都一样,锁不同,顺序和7一样。
具体表现为以下 3 种形式。
对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的 Class 对象。
对于同步方法块,锁是 Synchonized 括号里配置的对象
6.3 公平锁和非公平锁
例如:ReentrantLock的无参构造,就是一个非公平锁,会出现一个线程消耗所有资源,其他线程饿死的问题。
// 非公平锁演示
private Lock lock = new ReentrantLock();
// 或这样也是非公平锁
private Lock lock = new ReentrantLock(false);
// 公平锁
private Lock lock = new ReentrantLock(true);
公平锁与非公平锁优缺点:
- 非公平锁:
- 效率高
- 线程饿死
- 公平锁:
- 阳光普照
- 效率相对低
- 想要效率高:选非公平锁
- 想要公平:公平锁
公平锁与非公平锁源码实现:
- 非公平锁:占据锁之后之间进行后续操作
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
- 公平锁:里面有一个acquires 参数,并且判断时先执行hasQueuedPredecessors()方法,相当于先礼貌的问一句,这里有人吗;没人就继续操作,有人就会排队。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
6.4 可重入锁(递归锁)
synchronized(隐式)和Lock(显示)都是可重入锁
synchronized的加锁和释放都是自动的,所以是隐式的;Lock是显式的。
简单讲:如果一个线程拿到了锁,那么所有需要这个锁的地方,他都可以进入了
可重入锁,又称为递归锁,是一种递归无阻塞的同步机制。"可重入"的意思是如果一个线程已经持有了一个锁,那么这个线程可以再次获取同一把锁而不会被锁阻塞。这个特性可以在递归或者需要多次访问同步资源的场合简化编程。
在Java中,synchronized和ReentrantLock都是可重入锁。比如:
public class Example {
public synchronized void method1() {
method2();
}
public synchronized void method2() {
// do something
}
}
在这个例子中,一个线程进入method1
方法后,得到了对象锁,然后它可以再次进入method2
方法而不会阻塞,因为它已经持有了对象锁。
同样的,使用ReentrantLock也能达到类似的效果:
import java.util.concurrent.locks.ReentrantLock;
public class Example {
private final ReentrantLock lock = new ReentrantLock();
public void method1() {
lock.lock();
try {
method2();
} finally {
lock.unlock();
}
}
public void method2() {
lock.lock();
try {
// do something
} finally {
lock.unlock();
}
}
}
在这个例子中,线程进入method1
方法后,获取了锁,然后它可以进入method2
方法并再次获取锁,而不会被阻塞。需要注意的是,每次调用lock()
方法,引用计数就加1,每次调用unlock()
方法,引用计数就减1。只有当引用计数为0时,锁才真正被释放。
这种锁的好处是,同一个线程可以多次获取同一把锁,避免了死锁。缺点是可能导致锁保持得过久,从而影响系统性能。
LocK可重入锁演示:
//Lock演示可重入锁
Lock lock = new ReentrantLock();
//创建线程
new Thread(()->{
try {
//上锁
lock.lock();
System.out.println(Thread.currentThread().getName()+" 外层");
try {
//上锁
lock.lock();
System.out.println(Thread.currentThread().getName()+" 内层");
}finally {
//释放锁
lock.unlock();
}
}finally {
//释放做
lock.unlock();
}
},"t1").start();
在这段代码里面,如果不释放锁,后面的会获取不到锁。
6.5 死锁
- 定义:两个或者两个以上进程在执行过程中,因为争夺资源而造成一种互相等待的现象,如果没有外力干涉,他们无法再执行下去
- 产生死锁原因:
- 系统资源不足
- 进程运行推进顺序不合适
- 资源分配不当
- 验证死锁命令
- jps:ps -ef
- jstack :jvm自带堆栈工具