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());elseacquire(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 orderNode 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自带堆栈工具