该文章Github地址:https://github.com/AntonyCheng/java-notes
在此介绍一下作者开源的SpringBoot项目初始化模板(Github仓库地址:https://github.com/AntonyCheng/spring-boot-init-template & CSDN文章地址:https://blog.csdn.net/AntonyCheng/article/details/136555245),该模板集成了最常见的开发组件,同时基于修改配置文件实现组件的装载,除了这些,模板中还有非常丰富的整合示例,同时单体架构也非常适合SpringBoot框架入门,如果觉得有意义或者有帮助,欢迎Star & Issues & PR!
上一章:由浅到深认识Java语言(32):多线程
42.多线程
生产者与消费者案例
生产者和消费者一边生产一边消费,即两条不同的线程共同操作同一个资源,该问题会面临线程安全问题,示例如下:
package top.sharehome.Test;
public class Demo {
public static void main(String[] args) {
Resourse r = new Resourse();
Produce produce = new Produce(r);
Customer customer = new Customer(r);
Thread t0 = new Thread(produce);
Thread t1 = new Thread(customer);
t0.start();
t1.start();
}
}
/**
* 定义资源对象
*/
class Resourse {
//该计数器用来记录生产的个数
int count;
//该标记用来提示生产者或者消费者工作
//true:生产好了,等待消费
//false:消费好了,等待生产
boolean flag;
}
/**
* 生产者线程
*/
class Produce implements Runnable {
private Resourse r;
public Produce(Resourse r) {
this.r = r;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (r) {
if (r.flag == true) {
try {
r.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
r.count++;
System.out.println("生成第" + r.count + "个");
r.flag = true;
r.notify();
}
}
}
}
/**
* 消费者线程
*/
class Customer implements Runnable {
private Resourse r;
public Customer(Resourse r) {
this.r = r;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (r) {
if (r.flag == false) {
try {
r.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费第" + r.count + "个");
r.flag =false;
r.notify();
}
}
}
}
打印效果如下:
多生产和多消费
示例代码如下:
package top.sharehome.Test;
public class Demo {
public static void main(String[] args) {
Resourse r = new Resourse();
Produce produce = new Produce(r);
Customer customer = new Customer(r);
new Thread(produce).start();
new Thread(produce).start();
new Thread(produce).start();
new Thread(customer).start();
new Thread(customer).start();
new Thread(customer).start();
}
}
/**
* 定义资源对象
*/
class Resourse {
//该计数器用来记录生产的个数
int count;
//该标记用来提示生产者或者消费者工作
//true:生产好了,等待消费
//false:消费好了,等待生产
boolean flag;
}
/**
* 生产者线程
*/
class Produce implements Runnable {
private Resourse r;
public Produce(Resourse r) {
this.r = r;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (r) {
if (r.flag == true) {
try {
r.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
r.count++;
System.out.println(Thread.currentThread().getName()+"生成第" + r.count + "个");
r.flag = true;
r.notify();
}
}
}
}
/**
* 消费者线程
*/
class Customer implements Runnable {
private Resourse r;
public Customer(Resourse r) {
this.r = r;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (r) {
if (r.flag == false) {
try {
r.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"消费第" + r.count + "个");
r.flag =false;
r.notify();
}
}
}
}
打印效果如下:
安全问题产生的原因:
- 线程本身就是一个新创建的方法栈内存,并不代表具体的功能,所以在线程唤醒时,CPU并不会分类选择,而是随机选择,即正常顺序应该是生产者生产后消费者消费,但是CPU不认识谁是生产者,谁是消费者,所以会随机选择;
- 线程的唤醒 notify() 并不能指定唤醒谁,但是会优先唤醒第一个等待的线程;
- 被唤醒的线程,已经进行过 if 判断,一旦醒来就会继续执行,不会理会我们所设置的标志 flag;
解决办法:
由于Java并不能控制 CPU等硬件,所以第一条原因我们没有办法进行优化;
所以我们能做的就是改进唤醒机制,为了避免一个线程唤醒后同类型线程再被唤醒不判别就开始运行,我们可以统一重置一下运行顺序,即在同一时间唤醒全部线程,给予所有线程等可能性的被选概率,然后将 if 判断改为 while 循环判断,防止不满足标识符要求就开始运行;
全部重新唤醒的方法是 对象.notifyAll()
;
示例如下:
package top.sharehome.Test;
public class Demo {
public static void main(String[] args) {
Resourse r = new Resourse();
Produce produce = new Produce(r);
Customer customer = new Customer(r);
new Thread(produce).start();
new Thread(produce).start();
new Thread(produce).start();
new Thread(customer).start();
new Thread(customer).start();
new Thread(customer).start();
}
}
/**
* 定义资源对象
*/
class Resourse {
//该计数器用来记录生产的个数
int count;
//该标记用来提示生产者或者消费者工作
//true:生产好了,等待消费
//false:消费好了,等待生产
boolean flag;
}
/**
* 生产者线程
*/
class Produce implements Runnable {
private Resourse r;
public Produce(Resourse r) {
this.r = r;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (r) {
while (r.flag == true) {
try {
r.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
r.count++;
System.out.println(Thread.currentThread().getName()+"生成第" + r.count + "个");
r.flag = true;
r.notifyAll();
}
}
}
}
/**
* 消费者线程
*/
class Customer implements Runnable {
private Resourse r;
public Customer(Resourse r) {
this.r = r;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (r) {
while (r.flag == false) {
try {
r.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"消费第" + r.count + "个");
r.flag =false;
r.notifyAll();
}
}
}
}
打印效果如下:
sleep()和wait()的区别
- sleep() 在休眠的过程中,同步锁不会丢失,不会释放;
- wait() 在等待时会释放同步锁,被唤醒之后需要重新获得同步锁才能开始执行;
生产者和消费者案例性能问题
- wait() 方法和 notify() 方法,本地方法调用 OS 的功能,和操作系统交互,JVM 找到 OS,把线程停止,频繁等待与唤醒,导致 JVM 和 OS 交互的次数过多;
- notifyAll() 方法唤醒全部的线程,会造成巨大的线程资源浪费,为了一个线程不得已唤醒了全部的线程;
阻塞队列方案(两大接口优化)
用 Lock 接口替换了同步锁 synchronized,提供了更加灵活,性能更好的锁定操作;
用 Condition 集合接口中 await() 方法替换掉需要设置在 synchronized 中的 wait() 方法;
用 Condition 集合接口中 signal() 方法替换掉需要设置在 synchronized 中的 notify() 方法;
问题分析如下:
由于类似于生产者和消费者的案例具有极其严重的性能问题,而该性能问题主要是由于 wait() 方法和 notify() 或者 notifyAll() 方法的自身缺陷所造成的,其次就是 CPU 并不能分辨出哪一条进程是生产者,哪一条进程是消费者而导致的资源争夺问题,阻塞队列就是来解决这两种问题的;
优化需求:
- 减少或者阻断程序通过本地方法和 OS 的交互,减少操作系统的运行压力;
- 将每一类进程做好分类,用容器装起来,避免资源的相互争夺;
优化思路:
- 我们可以用使用 Lock 接口中的 lock() 方法和 unlock() 方法,来替换掉同步代码块,以便能够更加灵活地上锁和解锁;
- 我们可以用 Condition 接口作为容器,分别容纳两类线程;该接口也成为线程的阻塞队列;
- Condition 容器是一个队列集合(先进先出)接口,专门用来装线程对象,通过 Lock 接口中的 newCondition() 方法返回一个 Condition 对象而产生,即获得该队列的前提是先有锁,一把锁让两个或多个队列分别轮流使用;
- 再调用 Condition 对象下特有的 await() 方法和 signal() 方法来分别替换 wait() 和 notify() ;
- await() 让线程释放锁,同时进入队列;
- signal() 让线程再次获得锁,同时退出队列
示例如下:
package top.sharehome.Test;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo {
public static void main(String[] args) {
Resourse r = new Resourse();
Produce pro = new Produce(r);
Customer cus = new Customer(r);
new Thread(pro).start();
new Thread(pro).start();
new Thread(pro).start();
new Thread(cus).start();
new Thread(cus).start();
new Thread(cus).start();
}
}
/**
* 定义资源对象
*/
class Resourse {
//该计数器用来记录生产的个数
private int count;
//该标记用来提示生产者或者消费者工作
//true:生产好了,等待消费
//false:消费好了,等待生产
private boolean flag;
private Lock lock = new ReentrantLock();
//生产者线程的阻塞队列
private Condition pro = lock.newCondition();
//消费者线程的阻塞队列
private Condition cus = lock.newCondition();
/**
* 生产者线程
*/
public void getCount() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取锁
lock.lock();
//无限等待,直至标识符改变
while (flag) {
try {
pro.await();
//使用Condition集合中特有的等待方法,随即释放锁
//同时将生产者线程放入了Condition集合之中
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.count++;
System.out.println(Thread.currentThread().getName() + "生成第" + this.count + "个");
//修改标签,完成生产
this.flag = true;
//唤醒消费线程队列的一个
cus.signal();
//释放锁
lock.unlock();
}
/**
* 消费者线程
*/
public void setCount() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取锁
lock.lock();
//无限等待,直至标识符改变
while (!this.flag) {
try {
cus.await();
//使用Condition中特有的等待方法,释放锁
//同时将消费者线程放入了Condition集合之中
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "消费第" + this.count + "个");
//修改标签,完成消费
this.flag = false;
//唤醒生产线程队列的一个
pro.signal();
//释放锁
lock.unlock();
}
}
/**
* 生产者类
*/
class Produce implements Runnable {
private Resourse r;
public Produce(Resourse r) {
this.r = r;
}
@Override
public void run() {
while (true) {
r.getCount();
}
}
}
/**
* 消费者类
*/
class Customer implements Runnable {
private Resourse r;
public Customer(Resourse r) {
this.r = r;
}
@Override
public void run() {
while (true) {
r.setCount();
}
}
}
打印效果如下:
Lock锁的实现原理
这个技术不开源,技术的名称叫做轻量级锁;
这种锁效率较高,因为使用的是 CAS 锁技术(Compare And Swap),也称为自旋锁;
图示如下:
若此时多出一个自减的 ThreadC,那么会出现一个更严重的问题,即 ABA 问题,也就是说在线程 A 自旋的过程中,线程 B 和线程 C 又自增又自减,线程 A 判断时会认为该值没有被改变,从而停止自旋,进行赋值,这是错误的;
若想解决 ABA 问题,我们需要加入一个只增不减的 version 值,即”版本号“,依此判断该变量是否被修改过;
CAS 锁的存在意义在于提高性能,但是不排除一些特殊的情况,例如该线程因为不明原因运行较其他线程慢,那么 CAS 就失去了存在意义,反而会拖慢进程,所以 JDK 对于此类问题做出了限制,当竞争的线程大于等于 10 ,或者单个线程自旋超过 10 次的时候,JDK 会强制取消 CAS 锁,升级其为重量级锁,即让 OS 锁定 CPU 和内存的总线,synchronized 同步锁就是一种重量级锁,所以它的一些方法能够直接和 OS 交互,效率较低;