慢慢挣,今天比昨天更有钱,明天比今天有钱,后天比明天有钱。
0.思维导图
6.多线程锁
synchronized实现同步的基础:Java中的每一个对象都可以作为锁。
具体表现为以下3中形式
- 对于普通同步方法,锁是当前实例对象。
- 对于静态同步方法,锁是当前类的Class对象。
- 对于同步方法块,锁是Synchronized括号里配置的对象。
6.1 synchronized 锁的八种情况总结
视频案例
class Phone {
// 模拟发送短信操作
public synchronized void sendSMS() throws InterruptedException {
// 停留3秒在短信内
TimeUnit.SECONDS.sleep(3);
System.out.println("------sendSMS------");
}
// 模拟发送邮件操作
public synchronized void sendEmail() {
System.out.println("------sendEmail------");
}
// 普通方法 打招呼
public void sayHello() {
System.out.println("------sayHello------");
}
}
public class ThreadDemo {
public static void main(String[] args) {
Phone phone = new Phone();
Phone phone1 = new Phone();
// 创建一个线程,调用Phone的sendSMS()方法
new Thread(() -> {
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
// 创建一个线程,调用Phone的sendEmail()方法
new Thread(() -> {
try {
// phone.sayHello();
// phone.sendEmail();
phone1.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
}
).start();
}
}
不同案例输出的结果分析
- 1.标准访问,先打印短信还是邮件
输出结果
------sendSMS------
------sendEmail------
原因分析
按线程启动顺序获取锁释放锁。
- 2.停3秒在短信方法内,先打印短信还是邮件
输出结果
------sendSMS------
------sendEmail------
原因分析
此时获取同一个对象锁,第一次获取锁后等待3秒,执行完释放锁之后,第二个线程才能获取同一把对象锁。
- 3.新增普通的hello方法,在线程2中调用,是先发短信还是hello
输出结果
------sayHello------
------sendSMS------
------sendEmail------
原因分析
线程A获取到锁,开始等待,因为线程B执行不许要获取同步锁,所以先输出sayHello,然后经过3秒,线程A等待结束,执行输出操作并释放锁。
- 4.现在有两部手机,先打印短信还是邮件
输出结果
------sendEmail------
------sendSMS------
原因分析
线程A和B获取到不同的对象锁,之间没有竞争关系,因此B线程先输出,A先等待结束之后输出。
- 5.两个静态同步方法,一部手机,先打印短信还是邮件
输出结果
------sendSMS------
------sendEmail------
原因分析
静态方法,是Class锁,线程A和B争夺同一把类锁,线程A 先获取锁,因此A等待3秒执行输出释放锁之后,B才获取到锁并执行输出。
- 6.两个静态同步方法,两部手机,先打印短信还是邮件
输出结果
------sendSMS------
------sendEmail------
原因分析
线程A和B通过不同对象争夺同一把类锁,线程A 先获取锁,因此A等待3秒执行输出释放锁之后,B才获取到锁并执行输出。
- 7.一个静态同步方法,一个普通同步方法,一部手机,先打印短信还是邮件
输出结果
------sendEmail------
------sendSMS------
原因分析
线程A获取类锁,执行等待,期间线程B获取对象锁,执行输出并释放对象锁,线程A等待结束执行输出释放类锁。(因为获取的锁对象不同,不存在竞争,按照时间顺序输出)
- 8.一个静态同步方法,一个普通同步方法,两部手机,先打印短信还是邮件
输出结果
------sendEmail------
------sendSMS------
原因分析
通过不同对象获取不同的锁,不存在竞争。
6.2 公平锁和非公平锁
- 公平锁 :效率相对低 ,阳光普照,每一个线程都参与工作。
- 非公平锁:效率高,但是线程容易饿死,所有的工作,都由一个线程完成。
用法:在创建可重入锁ReentrantLock时,调用有参构造器,传入参数true设置为公平锁。
private final ReentrantLock lock = new ReentrantLock(true);
jdk源码
两者性能差距
公平锁询问,如果有人,则进行排队等待
6.3 可重入锁
可重入锁就是某个线程已经获得某个锁,可以重复获取同一个锁而不死锁,可重入锁也叫递归锁。
synchronized示例代码
public class ThreadDemo {
// 创建一个锁对象
static final Object myLock = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (myLock) {
System.out.println("这是第一层锁");
synchronized (myLock) {
System.out.println("这是第二层锁");
}
}
}).start();
}
}
输出结果
ReentrantLock示例代码
public class ThreadDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
new Thread(() -> {
lock.lock();
try {
System.out.println("这是第一层锁");
lock.lock();
try {
System.out.println("这是第二层锁");
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
}).start();
}
}
6.4 死锁
两个或以上的进程因为争夺资源而造成互相等待资源的现象称为死锁。如果没有外力干涉,他们无法继续执行。
产生死锁的原因
- 系统资源不足
- 进行运行推进顺序不合理
- 资源分配不当
死锁产生的四个必要条件
- 互斥使用:当资源被一个线程使用或者占用时,别的线程不能使用该资源。
- 不可抢占:获取资源的一方,不能从正在使用资源的一方抢占掠夺资源,资源只能被使用者主动释放。
- 请求保持:资源请求者在请求别的资源时,同时保持对已有资源的占有。
- 循环等待:即p1占有p2的资源,p2占有p3的资源,p3占有p1的资源,这样形成了一个等待环路。
死锁示例代码
public class ThreadDemo {
static Object a = new Object();
static Object b = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (a) {
System.out.println("外层,已经获取a,试图获取b");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (b) {
System.out.println("内层");
}
}
}, "A").start();
new Thread(() -> {
synchronized (b) {
System.out.println("外层,已经获取b,试图获取a");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (a) {
System.out.println("内层");
}
}
}, "B").start();
}
}
7.Callable接口
7.1 Runnable接口与Callable接口
使用 Runnable 创建的线程缺少的一项功能,当线程终止时(即 run()
完成时),我们无法使线程返回结果。为了支持此功能,Java 中提供了 Callable 接口,即线程终止(call()
执行完成时)后返回结果。
对比 | Runnable接口 | Callable 接口 |
---|---|---|
返回值 | 没有 | 有 |
抛出异常 | 没有 | 有 |
实现方法名称 | run() | call() |
7.2 Callable接口使用
代码示例
因为Thread的构造函数中没有Callable接口的参数设置,不可以直接替换,只能用FutureTask类来实现线程创建(FutureTask类既能传入Callable构造,又是Runnable接口的实现类)
class MyThread implements Callable {
@Override
public Integer call() throws Exception {
return 10086;
}
}
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask futureTask = new FutureTask<>(new MyThread());
//创建一个线程
new Thread(futureTask,"AA").start();
//调用FutureTask的get方法获取线程运行结果
System.out.println(futureTask.get());
System.out.println(Thread.currentThread().getName() + " is over");
}
}
8.JUC强大辅助类
8.1 减少计数CountDownLatch
CountDownLatch 方法使用说明
CountDownLatch(int count); //构造方法,创建一个值为count 的计数器。
await(); //阻塞当前线程,将当前线程加入阻塞队列。
countDown(); //对计数器进行递减1操作,当计数器递减至0时,当前线程会去唤醒阻塞队列里的所有线程。
案例
6个同学陆续离开教室之后,班长才能锁门。
如果不加 CountDownLatch类,会出现线程混乱执行,同学还未离开教室班长就已经锁门了。
不使用CountDownLatch,导致线程混乱
public class ThreadDemo {
public static void main(String[] args) {
for (int i = 0; i < 6; i++) {
new Thread(() -> System.out.println(Thread.currentThread().getName() + "同学离开教室"), String.valueOf(i)).start();
}
System.out.println(Thread.currentThread().getName() + "班长锁门");
}
}
运行结果
通过CountDownLatch计数,保证主线程输出语句最后执行
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "同学离开教室");
countDownLatch.countDown();
}, String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "班长锁门");
}
}
运行结果
8.2 循环栅栏CyclicBarrier
CyclicBarrier 方法使用说明
CyclicBarrier(int parties,Runnable barrierAction); //构造方法,创建一个值为parties的屏障。
await(); //当一个线程到了栅栏这里了,那么就将计数器减 1
代码示例
public class CyclicBarrierDemo {
public static final int NUMBER = 7;
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, () -> System.out.println("集齐七颗龙珠就可以召唤神龙"));
for (int i = 0; i < 7; i++) {
new Thread(() -> {
try {
System.out.println("第 " + Thread.currentThread().getName() + " 颗龙珠被收集到");
//等待
cyclicBarrier.await();
} catch (Exception e) {
throw new RuntimeException(e);
}
}, String.valueOf(i)).start();
}
}
}
运行结果
8.3 信号灯Semaphore
Semaphore 方法使用说明
Semaphore(int permits); //创建具有给定的许可数和非公平的公平设置的Semapore
acquire(); //从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断
release(); //释放一个许可,将其返回给信号量
【具体案例】
6辆汽车,停3个车位
代码示例
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "车抢到车位");
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println("--" + Thread.currentThread().getName() + "车离开车位");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
semaphore.release();
}
}, String.valueOf(i)).start();
}
}
}
运行结果