5. 多线程锁
5.1 类锁与对象锁
关于Java中synchronized 用在实例和对象方法上面存在一定的区别。
5.1.1 无static时
在不加static时,synchronized默认的锁对象为this,也就是调用该方法的那个对象,对于同一个对象调用的不同的同步方法,无论如何休眠,先执行结束都是先拿到锁的那个线程。
public class Phone {
public 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");
}
}
public static void main(String[] args) {
// 一个对象只有一个锁
Phone phone = new Phone();
new Thread(() -> {
try {
phone.sendSMS();
} catch (InterruptedException e) {
}
}, "AA").start();
new Thread(() -> {
phone.sendEmail();
}, "BB").start();
}
输出结果
------sendSMS
------sendEmail
运行后的结果:先短信后邮件,代码中线程AA运行sendSMS()
方法时,休眠了1秒,BB线程没有马上执行逻辑,而是等到现场AA运行完毕锁释放后,才去执行锁。
5.1.2 两个实例对象
public static void main(String[] args) throws Exception {
// 一个对象只有一个锁
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
try {
phone.sendSMS();
} catch (InterruptedException e) {
}
}, "AA").start();
Thread.sleep(100);
new Thread(() -> {
phone2.sendEmail();
}, "BB").start();
}
结果
------sendEmail
------sendSMS
结果是邮件、短信。这意味着线程BB并没有等待AA执行完后在执行,而是在AA睡眠的时候线程BB执行执行了。原因是,sendEmail()
sendSMS()
,是属于两个不同对象。而实例方法的锁是属于对象的,因此锁才不会生效。除非是同一个对象调用这个两个方法。
5.1.3 带static与不带static
-
如果一个是static修饰的静态方法,另一个是实例方法。使用同一个对象的调用
sendEmail()
、sendSMS()
public class Phone { public static synchronized void sendSMS() throws InterruptedException { // 停留4秒 TimeUnit.SECONDS.sleep(4); System.out.println("------sendSMS"); } public synchronized void sendEmail() { System.out.println("------sendEmail"); } public void getHello() { System.out.println("------getHello"); } }
public static void main(String[] args) throws InterruptedException { // 一个对象只有一个锁 Phone phone = new Phone(); new Thread(() -> { try { phone.sendSMS(); } catch (InterruptedException e) { throw new RuntimeException(e); } }, "AA").start(); Thread.sleep(100); new Thread(() -> { phone.sendEmail(); }, "BB").start(); }
结果
------sendEmail ------sendSMS
结果为:
sendEmail()
、sendSMS()
,通过结果来看,同一个phone对象调用sendEmail()
、sendSMS()
,sendEmail()
并没有等待sendSMS()
,原因是static修饰的锁是类锁,锁住的是整个Class,非static修饰的,锁住的是Class的实例信息。所以每当创建一个新对象,虽然访问的是同一个方法,但锁不会管用。这里虽然使用的事同一个对象,但是锁存在于不同的地方,一个锁在对象中,一个在Class中。所以,其实锁没有失效,只是使用的方式不对。5.1.4 static修饰两个synchronized同对象
使用static 修饰两个synchronized方法。最后通过同一个对象用两个线程调用。
public class Phone { public static synchronized void sendSMS() throws InterruptedException { // 停留4秒 TimeUnit.SECONDS.sleep(4); System.out.println("------sendSMS"); } public static synchronized void sendEmail() { System.out.println("------sendEmail"); } public void getHello() { System.out.println("------getHello"); } }
public static void main(String[] args) throws Exception { // 一个对象只有一个锁 Phone phone = new Phone(); new Thread(() -> { try { phone.sendSMS(); } catch (InterruptedException e) { throw new RuntimeException(e); } }, "AA").start(); Thread.sleep(100); new Thread(() -> { phone.sendEmail(); }, "BB").start(); }
结果
------sendSMS ------sendEmail
结果为:
sendSMS()
、sendEmail()
。原因也很简单,因为两个方法都在Class中,所以调用不同的方法,作用还是在整个Class中。5.1.5 static修饰两个synchronized不同对象
public static void main(String[] args) throws InterruptedException { // 一个对象只有一个锁 Phone phone = new Phone(); Phone phone2 = new Phone(); new Thread(() -> { try { phone.sendSMS(); } catch (InterruptedException e) { } }, "AA").start(); Thread.sleep(100); new Thread(() -> { phone2.sendEmail(); }, "BB").start(); }
结果
------sendSMS ------sendEmail
结果为:
sendSMS()
、sendEmail()
,static修饰的方法可以不通过对象调用,因为static修饰的变量、代码块、方法在类加载期间就已经分配了内存了,而非static修饰的变量、代码块、方法需要在创建对象的时候分配内存,所以我们不需要创建对象也可以调用静态方法。而这里使用两个对象调用,最后结果是sendSMS
、sendEamil
的原因就是,虽然表面上看其起来是两个对象,但是在内存指针中其实phone
与phone2
对象指向是同一块Class的信息,同时也包含了类锁的信息。
5.2 公平锁与非公平锁
- 公平:每个线程获取锁的机会是平等的。常见公平锁对象:
new ReentrantLock(true)
、new ReentrantReadWriterLock(true)
等。 - 非公平:每个线程获取锁的机会是不平等的。常见非公平锁对象:
synchronized
、new ReentrantLock(false)
、new ReentrantReadWriteLock(false)
5.2.1 非公平锁synchronized
public class LockDemo {
public static void main(String[] args) {
OptionDemo optionDemo = new OptionDemo();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
optionDemo.test();
}
}, "AA").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
optionDemo.test();
}
}, "BB").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
optionDemo.test();
}
}, "CC").start();
}
}
class OptionDemo {
private int number = 10;
public synchronized void test() {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + " :减掉前:" + (number--) + "剩下:" + number);
}
}
}
结果:
AA :减掉前:10剩下:9
BB :减掉前:9剩下:8
BB :减掉前:8剩下:7
BB :减掉前:7剩下:6
BB :减掉前:6剩下:5
BB :减掉前:5剩下:4
AA :减掉前:4剩下:3
AA :减掉前:3剩下:2
AA :减掉前:2剩下:1
AA :减掉前:1剩下:0
说明:在一段时间内较大概率输出线程AA或者线程BB,根本没有线程CC持有的机会,体现了非公平性。
5.2.2 非公平锁ReentrantLock(false)
public class LockDemo {
public static void main(String[] args) {
OptionDemo optionDemo = new OptionDemo();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
optionDemo.test();
}
}, "AA").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
optionDemo.test();
}
}, "BB").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
optionDemo.test();
}
}, "CC").start();
}
}
class OptionDemo {
Lock lock = new ReentrantLock(false);
private int number = 10;
public void test() {
lock.lock();
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + " :减掉前:" + (number--) + "剩下:" + number);
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
结果:
AA :减掉前:10剩下:9
AA :减掉前:9剩下:8
AA :减掉前:8剩下:7
AA :减掉前:7剩下:6
AA :减掉前:6剩下:5
AA :减掉前:5剩下:4
AA :减掉前:4剩下:3
CC :减掉前:3剩下:2
CC :减掉前:2剩下:1
CC :减掉前:1剩下:0
说明:在一段时间内较大概率输出线程AA或者线程CC,根本没有线程BB持有的机会,体现了非公平性。
5.2.3公平锁ReentrantLock(true)
public class LockDemo {
public static void main(String[] args) {
OptionDemo optionDemo = new OptionDemo();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
optionDemo.test();
}
}, "AA").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
optionDemo.test();
}
}, "BB").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
optionDemo.test();
}
}, "CC").start();
}
}
class OptionDemo {
Lock lock = new ReentrantLock(true);
private int number = 30;
public void test() {
lock.lock();
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + " :减掉前:" + (number--) + "剩下:" + number);
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
结果:
AA :减掉前:30剩下:29
CC :减掉前:29剩下:28
AA :减掉前:28剩下:27
CC :减掉前:27剩下:26
AA :减掉前:26剩下:25
CC :减掉前:25剩下:24
AA :减掉前:24剩下:23
CC :减掉前:23剩下:22
AA :减掉前:22剩下:21
CC :减掉前:21剩下:20
AA :减掉前:20剩下:19
BB :减掉前:19剩下:18
CC :减掉前:18剩下:17
AA :减掉前:17剩下:16
BB :减掉前:16剩下:15
CC :减掉前:15剩下:14
AA :减掉前:14剩下:13
BB :减掉前:13剩下:12
CC :减掉前:12剩下:11
AA :减掉前:11剩下:10
BB :减掉前:10剩下:9
CC :减掉前:9剩下:8
AA :减掉前:8剩下:7
BB :减掉前:7剩下:6
CC :减掉前:6剩下:5
BB :减掉前:5剩下:4
BB :减掉前:4剩下:3
BB :减掉前:3剩下:2
BB :减掉前:2剩下:1
BB :减掉前:1剩下:0
说明:均匀的输出三个线程的信息,每个线程都有平均的持有线程的机会。
5.3 可重入锁
5.3.1 什么是可重入锁
可重入锁是一种特殊的互斥锁,他可以被同一个线程多次获取,而不会产生死锁。
- 首先它是互斥锁:任意时刻,只有一个线程。即假设A线程已经获取到锁,在A线程释放这个锁之前,B线程无法获取这个锁,B要获取这个锁就会进入阻塞状态。
- 其次,它可以被同一个线程多次持有。即,假设A线程已经获取这个锁,如果A线程在释放锁之前又一次请求,那么是能够再次获取到的。
- synchronized 关键字锁也是可重入锁。
5.3.2 同一个线程多次持有
public static void main(String[] args) {
Lock lock = new ReentrantLock();
new Thread(() -> {
try {
// 上锁
lock.lock();
System.out.println(Thread.currentThread().getName() + "外层");
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "中层");
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "内层");
} finally {
// 释放锁
// lock.unlock();
}
} finally {
// 释放锁
// lock.unlock();
}
} finally {
// 释放锁
// lock.unlock();
}
}, "t1").start();
}
结果:
t1外层
t1中层
t1内层
说明:
如上面的代码所所示,当线程"t1"第一次获取锁"lock"同时在没有获取到内层和中层锁以及释放锁的情况下还能继续往下执行,并且不会产生死锁,这就是可重入锁的第一个特性,可被一个线程多次持有。
5.3.2 一种特殊的互斥锁
public static void main(String[] args) {
// 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() + "中层");
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "内层");
} finally {
// 释放锁
// lock.unlock();
}
} finally {
// 释放锁
lock.unlock();
}
} finally {
// 释放锁
lock.unlock();
}
}, "t1").start();
// 创建新线程
new Thread(() -> {
lock.lock();
System.out.println("aaa");
lock.unlock();
},"t2").start();
}
结果:
t1外层
t1中层
t1内层
说明:
如上面代码所示,线程"t1"获取到锁,在锁没有释放前"t2"线程是无法获取到锁的。
5.3.3 synchronized 关键字重入锁
public static void main(String[] args) {
Object o = new Object();
new Thread(()->{
synchronized (o){
System.out.println(Thread.currentThread().getName()+"外层");
synchronized (o){
System.out.println(Thread.currentThread().getName()+"中层");
synchronized (o){
System.out.println(Thread.currentThread().getName()+"内层");
}
}
}
},"t1").start();
}
结果:
t1外层
t1中层
t1内层
说明:
synchronized 关键字锁也是可重入锁,与ReentrantLock一样。
5.4 死锁
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源释放。由于线程被无限期地阻塞,因此程序不能正常终止。
- 互斥使用,即当资源资源被一个线程使用(占用)时,别的线程不能使用
- 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源自能有资源占有者主动释放。
- 请求和保持,即当资源请求者在请求其他资源的同时保持对原有资源的占有。
- 循环等待,即存在一个等待队列:P1占用P2的资源,P2占有P3的资源,P3占用P1的资源。这样就形成了一个等待环路。
当上述四个条件都成立的时候,便形成了死锁。当然死锁的情况下,如果上诉任何一个条件被打破,便可让死锁消失。
解决死锁问题的方法是:一种是synchronized,一种是Lock显示锁的实现。
而如果不恰当使用了锁,且出现同时要锁多个对象时,会出现死锁的情况,如下:
public class DeadLockTest {
public static String obj1 = "obj1";
public static String obj2 = "obj2";
public static void main(String[] args) {
new Thread(new LockA()).start();
new Thread(new LockB()).start();
}
}
class LockA implements Runnable {
@Override
public void run() {
try {
System.out.println(new Date() + "LockA 开始执行");
while (true) {
synchronized (DeadLockTest.obj1) {
System.out.println(new Date() + "LockA 锁住 obj1");
Thread.sleep(3000);
synchronized (DeadLockTest.obj2) {
System.out.println(new Date() + "LockA 锁住 obj2");
// 为测试,占用了就不放
Thread.sleep(60 * 1000);
}
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
class LockB implements Runnable {
@Override
public void run() {
try {
System.out.println(new Date() + "LockB 开始执行");
while (true) {
synchronized (DeadLockTest.obj2) {
System.out.println(new Date() + "LockB 锁住 obj2");
// 此处等待是给A锁住机会
Thread.sleep(300);
synchronized (DeadLockTest.obj1) {
System.out.println(new Date() + "LockB 锁住 obj1");
// 为了测试,占用了就不放
Thread.sleep(60 * 1000);
}
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
以上代码运行输出结果为:
Wed Jul 06 19:40:15 CST 2022LockA 开始执行
Wed Jul 06 19:40:15 CST 2022LockB 开始执行
Wed Jul 06 19:40:15 CST 2022LockA 锁住 obj1
Wed Jul 06 19:40:15 CST 2022LockB 锁住 obj2
此时死锁产生。
为了解决这个问题,我们不能使用显示去锁,我们用信号量去控制。
信号量可以控制资源能被多少线程访问,这里我们指定只能被一个线程访问,就做到了类似锁住。而信号量可以指定去获取的超时的时间,我们可以根据这个超时时间,去做一个额外处理。
对于无法成功的情况,一般就是重复尝试,或指定尝试的次数,也可以马上退出。
Wed Jul 06 20:36:18 CST 2022 LockB 开始执行
Wed Jul 06 20:36:18 CST 2022 LockA 开始执行
Wed Jul 06 20:36:18 CST 2022 LockB 锁住 obj2
Wed Jul 06 20:36:18 CST 2022 LockA 锁住 obj1
Wed Jul 06 20:36:19 CST 2022 LockA 锁 obj2 失败
Wed Jul 06 20:36:19 CST 2022 LockB 锁 obj1 失败
Wed Jul 06 20:36:20 CST 2022 LockA 锁住 obj1
Wed Jul 06 20:36:20 CST 2022 LockA 锁住 obj2
Wed Jul 06 20:36:29 CST 2022 LockB 锁住 obj2
Wed Jul 06 20:36:29 CST 2022 LockB 锁住 obj1