目录
什么是JUC?
通过写售票代码回顾Synchronized和认识Lock
Synchronized
Lock
synchorized与Lock锁的区别
Synochorized的生产者和消费者问题
虚假唤醒问题
Lock中的生产者和消费者问题
深入理解Java锁
在Java 5.0时提供了 java.lang.concurrent 这个包,这个包简称JUC包,提供了一些处理并发编程下的一些工具类。
售票这段代码在学习多线程的时候是必定绕不开的一环,多个线程同时去争抢票这个资源。
那么我们再写写这个卖票的代码,思路核心是多线程操作同一资源类,资源类是单独的,没有任何附属操作,它就只有属性和方法。
资源类 Ticket
/**
* 资源类 Ticket
*/
class Ticket2 {
private int ticket = 30;
public void sale() {
if (ticket>0){
System.out.println(Thread.currentThread().getName() + "售出第" + ticket-- + "张票" + "还剩" + ticket + "张票");
}
}
}
线程类:使用了匿名内部类来减少了代码量
public static void main(String[] args) {
Ticket2 ticket = new Ticket2();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}
}, "A").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 40; i++) {
ticket.sale();
} }
}, "B").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 40; i++) {
ticket.sale();
} }
}, "C").start();
}
但推荐使用Lambda表达式,看起来更加简洁。
public static void main(String[] args) {
Ticket2 ticket = new Ticket2();
new Thread(()->{ for (int i = 0; i < 40; i++) { ticket.sale(); } }, "A").start();
new Thread(()->{ for (int i = 0; i < 40; i++) { ticket.sale(); } }, "B").start();
new Thread(()->{ for (int i = 0; i < 40; i++) { ticket.sale(); } }, "C").start();
}
目前为止,代码是线程不安全的,要想实现线程同步,初学的时候使用的是Sychorized,同步代码块或者方法,在资源类的方法上加上synchronize即可,这就是一个普通的线程同步方法。
public synchronized void sale() {
if (ticket>0){
System.out.println(Thread.currentThread().getName() + "售出第" + ticket-- + "张票" + "还剩" + ticket + "张票");
}
}
学习J.U.C,就是学习java.util.concurrent包下的一些工具类来解决并发的问题。
接下来使用Lock类来解决线程安全问题。
Java 1.8中的Lock接口的三个实现类。
我们现在使用的是RenntrantLock,它需要显示的加锁和解锁,并需要与trycatch一起使用。
这样,就使用了Lock来实现了线程安全。
/**
* 资源类 Ticket
*/
class Ticket2 {
private int ticket = 30;
// 实例化ReentrantLock
ReentrantLock lock = new ReentrantLock();
public void sale() {
// 开启锁
lock.lock();
try {
if (ticket>0){
System.out.println(Thread.currentThread().getName() + "售出第" + ticket-- + "张票" + "还剩" + ticket + "张票");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭锁
lock.unlock();
}
}
}
解释一下可重入锁:
可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁
生产者和消费者问题用于理解线程之间的通讯,其思路是一个共同资源类被多线程操作并且交替执行。
如果不能理解如何写这种多线程操作并且交替执行的代码,先想想思路:
/**
* 生产者与消费者问题
* 问题:现在两个线程,可以操作初始值为零的一个变量
* 实现一个线程对该变量+1,一个线程对该变量-1
* 实现10轮交替,变量初始值为0
*
*
* 思路:高内聚低耦合的思路,线程操作资源类
* ---> 判断/业务/通知
*
* @author Claw
* @date 2020/6/8 16:36.
*/
public class ConsumersAndProducers {
public static void main(String[] args) {
Product product = new Product();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
product.toProduct();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
product.toConsume();
}
}, "B").start();
}
}
/**
* 资源类
*/
class Product {
private int product = 0;
/**
* 生产者
*/
public synchronized void toProduct() {
// 判断等待
if (product !=0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 业务操作
product++;
System.out.println(Thread.currentThread().getName()+"------->"+product);
// 通知
this.notifyAll();
}
/**
* 消费者
*/
public synchronized void toConsume() {
// 判断等待
if (product==0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 业务操作
product--;
System.out.println(Thread.currentThread().getName()+"------->"+product);
// 通知
this.notifyAll();
}
结果:
A------->1
B------->0
A------->1
B------->0
A------->1
B------->0
A------->1
B------->0
A------->1
B------->0
A------->1
B------->0
A------->1
B------->0
A------->1
B------->0
A------->1
B------->0
A------->1
B------->0
这样看似没有什么问题,但如果有更多的线程进来,会导致一个问题,叫做虚假唤醒。
在主方法中新加两个线程
new Thread(() -> {
for (int i = 0; i < 10; i++) {
product.toProduct();
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
product.toConsume();
}
}, "D").start();
执行结果:在这里截取了结果异常的一段,可以看到出现了异常的数量2和3。
这是为什么呢?API里解释了线程也可以被唤醒,而不会被通知,中断或超时,这就是所谓的虚假唤醒,等待应该总是出现在循环之中。
在上面的代码中,条件判断使用了if而不是while,这导致了虚假唤醒。
多线程的交互必须用while
While的本质是循环+判断,线程被唤醒后,需要重新判断是否符合运行条件。
而if只判断了一次
看看这个错误的结果,为什么两个线程没有问题,而4个线程出现了问题?
A和C是生产者,B和D是消费者
当A线程生产完毕后,ABCD四个线程都准备操作product,理想情况下,生产一个消费一个是我们想要的模式,所以我们会想着生产线程完毕,消费的线程进来。但事实上不是如此,当A线程生产完毕后,进来的是生产者线程C,此时product为1,我们使用if作为条件判断,当product !=0时,条件判断为true,C线程执行到wait()处,开始等待,释放了锁。此时product仍然为1,ABD三条线程开始想操作product,现在进入方法的仍然是A,执行到wait()后开始等待。
然后A和C两个生产线程都进行等待了,剩下的线程只剩下消费者线程。此时product为1,消费线程消费完product,唤醒其他线程,因为和C两个线程已经等待许久,系统会优先让A和C先执行,此时product为0,但因为使用了if语句作为条件判断,已经不满足条件了,但if不会判断第二次,唤醒后的A和C两个线程同时进行了++,product由1变成2。
所以需要使用while作为条件判断。
上面的synchorized来体会的生产者消费者的问题并不是对JUC学习的目的,而是回顾。
J.U.C中,Lock替代了synchronized方法和语句的使用,Condition替代了Object监视器方法的使用。
synchronized让线程通讯的方法是:wait(),notify(),notifyAll().
Condition的与此相对应的方法是:await(),sign(),signAll()
Condition是一种广义上的条件队列。他为线程提供了一种更为灵活的等待/通知模式,线程在调用await方法后执行挂起操作,直到线程等待的某个条件为真时才会被唤醒。Condition必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个Condition的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现。
参考:JAVA并发-Condition
所以使用Lock来实现线程同步和通讯中,生产者和消费者问题的代码是这样的:
/**
* 资源类
*/
class Product2 {
private int product = 0;
// 实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
// 实例化Condition
private Condition condition = lock.newCondition();
/**
* 生产者
*/
public synchronized void toProduct() {
// 判断等待
while (product !=0){
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 业务操作
product++;
System.out.println(Thread.currentThread().getName()+"------->"+product);
// 通知
condition.signalAll();
}
/**
* 消费者
*/
public synchronized void toConsume() {
// 判断等待
while (product==0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 业务操作
product--;
System.out.println(Thread.currentThread().getName()+"------->"+product);
// 通知
condition.signalAll();
}
}
这样看来,与使用synchronize并没有什么优势的地方。
但Condition的强大之处是它可以为多个线程建立不同的Condition,让线程精准通讯,比如控制A线程执行完以后执行B,B执行完以后执行C。
/**
* 需求:多线程之间按顺序调用,实现A-B-C
* 三个线程启动,要求如下
*
* AA打印3次,BB打印3次,CC打印3次
* 循环3次
*
* @author Claw
* @date 2020/6/9 3:14.
*/
public class DataShare {
public static void main(String[] args) {
ShareResources sr = new ShareResources();
new Thread(() -> {
for (int i = 0; i < 3; i++) {
sr.printA();
}
}, "线程1").start();
new Thread(() -> {
for (int i = 0; i < 3; i++) {
sr.printB();
}
}, "线程2").start();
new Thread(() -> {
for (int i = 0; i < 3; i++) {
sr.printC();
}
}, "线程3").start();
}
}
/**
* 资源类
*/
class ShareResources {
// 实例化ReentrantLock
ReentrantLock lock = new ReentrantLock();
// 实例化 Condition
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();
Condition c3 = lock.newCondition();
/**
* 标志位:A:1 B:2 C:3
*/
private int number = 1;
public void printA() {
// 加锁
lock.lock();
// 判断等待
try {
while (number != 1) {
c1.await();
}
// 业务
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + "AA");
}
// 设置标志位
number = 2;
// 通知
c2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() {
// 加锁
lock.lock();
try {
while (number != 2) {
// 判断等待
c2.await();
}
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + "BBB");
}
// 标志位更改
number = 3;
// 通知
c3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() {
// 加锁
lock.lock();
try {
while (number != 3) {
// 判断等待
c3.await();
}
// 业务
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + "CCC");
}
// 标志位更改
number = 1;
// 通知
c1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
请看这段代码:
public class SynchronizedTest {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> { phone.sendEmail(); }).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> { phone.sendMsg(); }).start();
}
}
/**
* 资源类
*/
class Phone {
public synchronized void sendEmail() {
System.out.println("发邮件");
}
public synchronized void sendMsg() {
System.out.println("发短信");
}
}
结果:发邮件-->发短信
在sendEmail中让线程休眠4秒
public synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发邮件");
}
结果: 仍然为发邮件--->发短信
为什么?问题1和问题2是一样的
一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中一个synchronized方法了,其他线程只能等待,换句话说,某一个时刻内,其他线程都不能进入到当前对象的其他synchronized方法。
锁的是当前对象this(也就是资源类Phone),被锁定后,其他线程线程都不能进入到当前对象的其他的synchronized方法。
两个方法用的都是同一把锁,所以一次只能进入一个线程。
/**
* 资源类
*/
class Phone {
public synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发邮件");
}
public synchronized void sendMsg() {
System.out.println("发短信");
}
// 增加一个普通方法
public void hello(){
System.out.println("hello");
}
}
让线程去调用发邮件以及hello的方法
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> { phone.sendEmail(); }).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> { phone.hello(); }).start();
}
结果:
为什么?
因为普通方法与同步锁无关
再实例化一个Phone类,一个实例对象去调用发邮件,一个实例去调用发短信,那么结果是什么?
public class SynchronizedTest {
public static void main(String[] args) {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> { phone.sendEmail(); }).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> { phone2.sendMsg(); }).start();
}
}
/**
* 资源类
*/
class Phone {
public synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发邮件");
}
public synchronized void sendMsg() {
System.out.println("发短信");
}
public void hello(){
System.out.println("hello");
}
}
结果:
为什么?
因为是两个对象,不是再共用一把锁,不冲突也不影响。
将资源类的发邮件和发短信改为静态同步方法
public class SynchronizedTest {
public static void main(String[] args) {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> { phone.sendEmail(); }).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> { phone.sendMsg(); }).start();
}
}
/**
* 资源类
*/
class Phone {
public static synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发邮件");
}
public static synchronized void sendMsg() {
System.out.println("发短信");
}
public void hello(){
System.out.println("hello");
}
}
结果:
为什么?
synchronized修饰的同步方法,锁的是当前对象this,而对于静态同步方法,锁的就是整个Person类
如果是两个手机,执行结果如何?这个跟上面的方法是一个意思。静态同步方法锁的对象是当前类的Class对象,因此谁先拿到锁,谁先执行。
public static synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发邮件");
}
public synchronized void sendMsg() {
System.out.println("发短信");
}
为什么?
静态同步方法和普通同步方法不是同一把锁,因此调用方法时不再需要等待。
为什么?
因为既不是同一把锁,也不是争抢同一资源,不会互相影响。