目录
前言
本文是《Java并发视频入门》视频课程的笔记总结,旨在帮助更多同学入门并发编程。
本系列共五篇博客,本篇博客着重聊Sychronized关键字、线程间通信机制(Wait-Notify机制和生产者-消费者算法)。
Sychronized关键字
1. 数据不一致问题
/**
* 多线程售票系统,系统十张票,四个窗口去并行售票。
*/
public class Test1 {
//多线程问题主要是:多个线程操作同一个对象。
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket, "一号窗口");
Thread t2 = new Thread(ticket, "二号窗口");
Thread t3 = new Thread(ticket, "三号窗口");
Thread t4 = new Thread(ticket, "四号窗口");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Ticket implements Runnable {
private int index = 0;
private static final int MAX = 10;
@Override
public void run() {
while (index < MAX) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
index++;
System.out.println(Thread.currentThread().getId() + "的号码是:" + index);
}
}
}
结果输出:
15的号码是:4
17的号码是:4
16的号码是:4
14的号码是:4
15的号码是:5
14的号码是:7
17的号码是:6
16的号码是:7
14的号码是:9
16的号码是:9
17的号码是:9
15的号码是:8
14的号码是:10
17的号码是:12
16的号码是:11
15的号码是:12
出现三个问题:1.号码被略过;2.某个号码重复出现;3.号码最大值超过10。
号码跳过
号码重复出现
号码最大值超过10
2. synchronize关键字
2.1. 同步方法&同步代码块
数据不一致问题,究其原因是多个线程对同一对象的成员变量同时操作引起的。synchronize关键字提供排他机制,在同一时间点只有一个线程执行。
public class Test1 {
//多线程问题主要是:多个线程操作同一个对象。
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket, "一号窗口");
Thread t2 = new Thread(ticket, "二号窗口");
Thread t3 = new Thread(ticket, "三号窗口");
Thread t4 = new Thread(ticket, "四号窗口");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Ticket implements Runnable {
private int index = 0;
private static final int MAX = 10;
@Override
public synchronized void run() {
while (index < MAX) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
index++;
System.out.println(Thread.currentThread().getName() + "的号码是:" + index);
}
}
}
结果输出:
一号窗口的号码是:1
一号窗口的号码是:2
一号窗口的号码是:3
一号窗口的号码是:4
一号窗口的号码是:5
一号窗口的号码是:6
一号窗口的号码是:7
一号窗口的号码是:8
一号窗口的号码是:9
一号窗口的号码是:10
虽然以上代码解决了数据不一致问题,但仍存在两个问题:1.只有一个线程在执行;2.不够快。
解决方案如下:
class Ticket implements Runnable {
private int index = 0;
private static final int MAX = 10;
private Object lock = new Object();
@Override
public void run() {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//同步代码块
synchronized (lock) {
if (index >= MAX) {
break;
}
index++;
System.out.println(Thread.currentThread().getName() + "的号码是:" + index);
}
}
}
}
sychronized虽然都是加锁,但加锁的对象不同:
指定加锁对象:给指定对象加锁,进入同步代码块要获取该对象的锁;
作用于实例方法:相当于对当前实例加锁,进入同步代码前要获取到该实例的锁;
作用于静态方法:相当于对当前类加锁,进入同步代码前要获取该类的锁。
2.2. 锁重入
sychronized是可重入锁,当一个线程获取到一个锁之后,再次获取到锁,也是可以获取到的,再次获取时monitor计数器加一。假设其为不可重入锁,那么在调用Method1之后,在Method1中是无法调用Method2的。
public class Test5 {
public static void main(String[] args) {
method1();
}
private static synchronized void method1() {
System.out.println("方法一");
method2();
}
private static synchronized void method2() {
System.out.println("方法二");
method3();
}
private static synchronized void method3() {
System.out.println("方法三");
}
}
2.3.Synchronize使用事项
- synchronize锁的对象为null:错误
错误,每一个对象会和moitor关联,锁的本质是为了获取monitor关联的锁。对象为null,moniot无从谈起了。
class Ticket implements Runnable {
private Object lock = null;
@Override
public void run() {
synchronized (lock) {
//....
}
}
}
- synchronize作用域太大:不推荐
synchronize有排他性,所有线程必须串行经过synchronize保护区域,如果过大,会降低效率。
- synchronize锁了不同的对象:操作不同对象,不用加锁。
并发问题一定是指多个线程访问同一个实例,否则是绝不会出现并发问题的,锁是为了解决并发问题才存在的。假设每个线程操作的是不同对象,对象的锁也不是同一个锁,实际上还是并行代码。
- println方法是同步的
- 尽量不要使用String类型作为锁
字符串会从常量池查找,如果没有,则创建新对象。如果使用字符串常量作为锁,很有可能导致不同业务使用同一把锁,严重可以导致业务代码无法执行。锁一般全用Object关键字,最小化的对象。
2.4. 死锁
死锁是对锁使用不当产生的bug,当多个锁交叉使用时,很容易会产生死锁问题。如下所示:
public class Test6 {
public static void main(String[] args) throws InterruptedException {
DieLock dieLock = new DieLock();
dieLock.setUserName("a");
Thread t1 = new Thread(dieLock);
t1.start();
TimeUnit.MILLISECONDS.sleep(500);
dieLock.setUserName("b");
Thread t2 = new Thread(dieLock);
t2.start();
}
}
class DieLock implements Runnable {
private String userName;
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void setUserName(String userName) {
this.userName = userName;
}
@Override
public void run() {
if (userName.equals("a")) {
synchronized (lock1) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("userName是a");
}
}
}
if (userName.equals("b")) {
synchronized (lock2) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("userName是b");
}
}
}
}
}
检查哪里有死锁?
1.打开IDEA的Terminal;
2.输入"jps",列出所有线程;
3.jstack -l 3460
3. 小结
本小节主要介绍了因多线程导致的数据不一致问题(号码跳过、号码重复出现、号码超过阈值)、随后介绍了Sychronized关键字,主要从加锁对象(对象、实例方法、静态方法加锁)、锁重入、使用注意事项(加锁对象不能为null、作用域不能特别大、操作不同对象不用加锁以及尽量不要使用String类型锁),最后介绍了死锁。
线程间通信:
线程之间交换数据。
1.简单实现线程通信
public class Test1 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
new Thread(new Test1Thread1(list)).start();
new Thread(new Test1Thread2(list)).start();
}
}
class Test1Thread1 implements Runnable {
private List<String> list;
public Test1Thread1(List<String> list) {
this.list = list;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
list.add("aaa");
System.out.println("添加了" + (i + 1) + "个元素");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Test1Thread2 implements Runnable {
private List<String> list;
public Test1Thread2(List<String> list) {
this.list = list;
}
@Override
public void run() {
while (true) {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
if (list.size() == 5) {
System.out.println("集合大小超过5,线程退出");
break;
}
}
}
}
结果输出:
添加了1个元素
添加了2个元素
添加了3个元素
添加了4个元素
4
添加了5个元素
添加了6个元素
添加了7个元素
添加了8个元素
8
添加了9个元素
添加了10个元素
10
10
10
该代码存在以下严重问题:
- 轮训时间很小,浪费CPU资源;
- 轮训时间很长,得不到想要数据;
- 去掉间隔时间,线程2得不到想要数据。
很容易出现可见性问题,因此需要引入等待-通知机制。
2.等待/通知机制
wait/notify机制生活中比比皆是。eg:服务员和厨师的菜品传递过程。服务员得“等待”,厨师做好菜“通知”服务员取。
wait和notify是Object的方法,因此任意类都可以调用。wait和notify必须要加到锁内,且必须持有同一把锁;执行顺序为:开始wait->开始notify->结束notify->结束wait。
public class Test2 {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
new Thread(new Test2Thread1(lock)).start();
TimeUnit.SECONDS.sleep(3);
new Thread(new Test2Thread2(lock)).start();
}
}
class Test2Thread1 implements Runnable {
private Object lock;
public Test2Thread1(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
System.out.println("开始wait:" + System.currentTimeMillis());
try {
//执行wait,锁会得到释放。
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("结束wait:" + System.currentTimeMillis());
}
}
}
class Test2Thread2 implements Runnable {
private Object lock;
public Test2Thread2(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
System.out.println("开始notify:" + System.currentTimeMillis());
lock.notify();
System.out.println("结束notify:" + System.currentTimeMillis());
}
}
}
结果输出:
开始wait:1654358438754
开始notify:1654358441756
结束notify:1654358441757
结束wait:1654358441757
针对简单实现线程通信的代码作如下优化:
public class Test3 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Object lock = new Object();
new Thread(new Test3Thread1(list, lock)).start();
new Thread(new Test3Thread2(list, lock)).start();
}
}
class Test3Thread1 implements Runnable {
private List<String> list;
private Object lock = new Object();
public Test3Thread1(List<String> list, Object lock) {
this.list = list;
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
if (list.size() != 5) {
System.out.println("开始等待,此时list的长度为:" + list.size());
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("结束等待,此时list的长度为:" + list.size());
}
}
}
}
我们发现:添加至第五个元素时,线程2发出了通知,但是线程1并未立即执行,这是因为“线程2调用notify后,锁并没有立即释放。且被notify的线程只会进入就绪状态,何时执行取决于什么时候获取到CPI的执行权”。如何解决呢?
class Test3Thread1 implements Runnable {
private List<String> list;
private Object lock ;
public Test3Thread1(List<String> list, Object lock) {
this.list = list;
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
if (list.size() != 5) {
System.out.println("开始等待,此时list的长度为:" + list.size());
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("结束等待,此时list的长度为:" + list.size());
lock.notify();
}
}
}
}
class Test3Thread2 implements Runnable {
private List<String> list;
private Object lock;
public Test3Thread2(List<String> list, Object lock) {
this.list = list;
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 10; i++) {
list.add("aaa");
System.out.println("添加了" + list.size() + "个元素");
if (list.size() == 5) {
lock.notify();
System.out.println("满足需求,通知已经发出");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
结果输出:
开始等待,此时list的长度为:0
添加了1个元素
添加了2个元素
添加了3个元素
添加了4个元素
添加了5个元素
满足需求,通知已经发出
结束等待,此时list的长度为:5
添加了6个元素
添加了7个元素
添加了8个元素
添加了9个元素
添加了10个元素
3. wait和sleep的区别
两者都会使当前线程等待。wait方法执行后,锁会立即被释放;sleep方法不会释放锁,仅仅是等待一段时间。
4. wait和notify的其他方法
4.1.notify通知顺序
只会有一个线程被通知,当多个线程等待时,会通知最先调用wait的线程。
public class Test4 {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
new Thread(new Test4Thread1(lock)).start();
TimeUnit.SECONDS.sleep(1);
new Thread(new Test4Thread2(lock)).start();
TimeUnit.SECONDS.sleep(2);
new Thread(new Test4Thread3(lock)).start();
}
}
class Test4Thread1 implements Runnable {
private Object lock;
public Test4Thread1(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
System.out.println("线程1等待中...");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1被唤醒...");
}
}
}
class Test4Thread2 implements Runnable {
private Object lock;
public Test4Thread2(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
System.out.println("线程2等待中...");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程2被唤醒...");
}
}
}
class Test4Thread3 implements Runnable {
private Object lock;
public Test4Thread3(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
System.out.println("线程3开始唤醒...");
lock.notify();
System.out.println("线程3唤醒完毕...");
}
}
}
结果输出:
线程1等待中...
线程2等待中...
线程3开始唤醒...
线程3唤醒完毕...
线程1被唤醒...
4.2. notifyAll
上面案例,事先知道程序有两个待唤醒的线程,如果想唤醒两个线程,只需要调用两次notify即可,但实际开发中,我们并不知道有哪些线程待唤醒。使用notifyAll唤醒所有线程。notifyAll会按照后进先出算法唤醒所有wait状态的线程,即LIFO。(实际测试:JDK8后进先出,JDK15先进先出)
4.3. wait(long)
等待多久。
5. 生产者和消费者(*)
“生产者不停生产,消费者不停消费”,生产者消费者模型,是通过一个容器来解决生产者和消费者之间的强耦合问题。生产者和消费者并不直接接触,通过“货架”连接,这就是生产者-消费者模式。
5.1. 一生产一消费:操作值
使用一个值来作为货架,用来模拟“生产者和消费者”。
public class Test5 {
//货架
public static String value = "";
public static void main(String[] args) {
Object lock = new Object();
new Thread(new Test5Produce(lock)).start();
new Thread(new Test5Resume(lock)).start();
}
}
//生产者
class Test5Produce implements Runnable {
private Object lock;
public Test5Produce(Object lock) {
this.lock = lock;
}
@Override
public void run() {
while (true) {
synchronized (lock) {
// 当货架为空时,生产者要生产产品,消费者等待生产者往货架生产产品;
// 当货架满时,消费者可以从货架拿走商品,生产者等待货架的空位。
if (!"".equals(Test5.value)) {
System.out.println("生产者开始等待...");
try {
TimeUnit.SECONDS.sleep(1);
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Test5.value = System.currentTimeMillis() + "";
System.out.println("生产者生产值为:" + Test5.value);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.notify();
}
}
}
}
class Test5Resume implements Runnable {
private Object lock;
public Test5Resume(Object lock) {
this.lock = lock;
}
@Override
public void run() {
while (true) {
synchronized (lock) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if ("".equals(Test5.value)) {
System.out.println("消费者开始等待...");
try {
TimeUnit.SECONDS.sleep(1);
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费者获取值为:" + Test5.value);
Test5.value = "";
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.notify();
}
}
}
}
结果输出:
生产者生产值为:1654360356802
生产者开始等待...
消费者获取值为:1654360356802
消费者开始等待...
生产者生产值为:1654360362835
生产者开始等待...
消费者获取值为:1654360362835
.....
5.2.一生产一消费:操作栈
“一生产一消费”的弊端是:生产完一条后,必须等待消费后才能继续生产,生产者效能低下。
饭店中,厨师负责做菜,服务员负责递菜,餐台扮演“货架”。厨师无需关心服务员给顾客递菜,仅需将做好的菜放在餐台上。
“餐台”一般使用阻塞队列来实现,在这里我们使用栈。ArrayList线程不安全,因此需要封装一个线程安全的栈。
public class MyStack<T> {
private final List<T> list = new ArrayList<>();
public synchronized void put(T value) {
list.add(value);
}
public synchronized T pop() {
T t = list.get(0);
list.remove(0);
return t;
}
public synchronized int size() {
return list.size();
}
}
public class Test7 {
public static void main(String[] args) {
Object lock = new Object();
MyStack<String> myStack = new MyStack<>();
new Thread(new Test7Produce(lock, myStack)).start();
new Thread(new Test7Resume(lock, myStack)).start();
}
}
class Test7Produce implements Runnable {
private Object lock;
private MyStack<String> stack;
public Test7Produce(Object lock, MyStack<String> stack) {
this.lock = lock;
this.stack = stack;
}
@Override
public void run() {
while (true) {
synchronized (lock) {
if (stack.size() == 5) {
System.out.println("栈中数据超过5,生产者开始等待");
try {
TimeUnit.SECONDS.sleep(1);
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
String value = UUID.randomUUID().toString();
stack.put(value);
System.out.println("生产者生产值为:" + value);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.notify();
}
}
}
}
class Test7Resume implements Runnable {
private Object lock;
private MyStack<String> stack;
public Test7Resume(Object lock, MyStack<String> stack) {
this.lock = lock;
this.stack = stack;
}
@Override
public void run() {
while (true) {
synchronized (lock) {
if (stack.size() == 0) {
try {
System.out.println("栈中无元素,消费者开始等待");
TimeUnit.SECONDS.sleep(1);
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
String pop = stack.pop();
System.out.println("消费者获取元素:" + pop);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.notify();
}
}
}
}
结果输出:
生产者生产值为:7c050e37-1af8-476f-a40f-7bbc6eeaba97
生产者生产值为:4061a989-f89d-40a2-850e-cd89b9e69bb0
生产者生产值为:d186abfa-0647-401f-8995-8372b62f57c8
生产者生产值为:b942ecfe-2fcb-48fe-8040-333ea2f0591d
生产者生产值为:bf5d8891-6bf9-4075-bff4-821248c03f75
栈中数据超过5,生产者开始等待
消费者获取元素:7c050e37-1af8-476f-a40f-7bbc6eeaba97
消费者获取元素:4061a989-f89d-40a2-850e-cd89b9e69bb0
消费者获取元素:d186abfa-0647-401f-8995-8372b62f57c8
消费者获取元素:b942ecfe-2fcb-48fe-8040-333ea2f0591d
消费者获取元素:bf5d8891-6bf9-4075-bff4-821248c03f75
栈中无元素,消费者开始等待
生产者生产值为:804eb604-f059-4adc-ab11-fd9e4cb693e9
6. 人手一支笔“ThreadLocal”
变量的共享可以使用public static形式去实现,所有的线程都去访问这一个变量,但是共享的过程中会出现线程安全问题。
加锁虽能解决线程安全问题,但有些场景下性能确实不高。eg:100个人填写个人信息表,假如只有一支笔,大家需要挨个去填写,必须要保证大家不会去哄抢这根笔。
ThreadLocal能够保证线程从头到尾都共享的是一个全局变量,不会加锁,也不会出现线程安全问题。
public class Test10 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
User user = GlobalUser.user;
user.setAge(23);
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
user.setName("张三");
System.out.println(user);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
User user = GlobalUser.user;
user.setAge(24);
try {
TimeUnit.MILLISECONDS.sleep(120);
} catch (InterruptedException e) {
e.printStackTrace();
}
user.setName("李四");
System.out.println(user);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
User user = GlobalUser.user;
user.setAge(25);
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
user.setName("王五");
System.out.println(user);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
User user = GlobalUser.user;
user.setAge(26);
try {
TimeUnit.MILLISECONDS.sleep(180);
} catch (InterruptedException e) {
e.printStackTrace();
}
user.setName("赵六");
System.out.println(user);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
User user = GlobalUser.user;
user.setAge(27);
try {
TimeUnit.MILLISECONDS.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
user.setName("田七");
System.out.println(user);
}
}).start();
}
}
class GlobalUser {
public static User user = new User();
}
结果输出:
User{name='张三', age=27}
User{name='李四', age=27}
User{name='田七', age=27}
User{name='赵六', age=27}
User{name='王五', age=27}
明显存在问题,优化后代码。
public class Test11 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
User user = Test11GlobalUser.get().get();
user.setAge(23);
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
user.setName("张三");
System.out.println(user);
}
}).start();
//....
}
}
class Test11GlobalUser {
public static ThreadLocal<User> threadLocal = new ThreadLocal<>();
public static ThreadLocal<User> get() {
User user = threadLocal.get();
if (user == null) {
user = new User();
threadLocal.set(user);
}
return threadLocal;
}
}
结果输出:
User{name='张三', age=23}
User{name='李四', age=24}
User{name='田七', age=27}
User{name='赵六', age=26}
User{name='王五', age=25}
7. 小结
本小节我们介绍了线程通信的问题、Wait-Notify机制、Wait和Notify的其他方法,最重要的是生产者-消费者模型实现(操作值、操作栈)!最后我们介绍了ThreadLocal,它能能够保证线程从头到尾都共享的是一个全局变量,不会加锁,也不会出现线程安全问题。