java多线程
文章目录
一、生产者消费者模型
这里采用的是最基本的生产者消费者模型,生产者生产资源,消费者消耗资源。
隐蔽知识点:生产者消费者模型一般不使用集合来模拟,这是因为集合会无限扩容,使用数组可以很好的展现效果
基本模型搭建如下:还未添加线程的同步机制
// 资源类
class Resource{
// 定义容器用来存放数据或取数据的空间
private Object[] data = new Object[1]; // 方便演示所以仅开辟一个空间
private int num = 1;
// 存放数据的方法
public void save(){
data[0] = "data"+num; // 正常而言应该是通过参数传入数据,这里为了演示将其写死,并通过num来体现生产的不同东西
System.out.println(Thread.currentThread().getName()+"正存放的数据为"+data[0]);
num++;
}
// 获取数据的方法
public void get(){
System.out.println(Thread.currentThread().getName()+"正取的数据为"+data[0]);
data[0]=null;
}
}
// producter
class Product implements Runnable{
// 在生产者类中提供构造方法,目的是在创建线程的时候可以明确线程将来操作的资源本身
private Resource r;
public Product(Resource r){
this.r = r;
}
public void run(){
while(true){
// 无限的生产资源
r.save();
}
}
}
// consumer
class Consumer implements Runnable{
private Resource r;
public Consumer(Resource r){
this.r = r;
}
public void run(){
while(true){
// 无限的消耗资源
r.get();
}
}
}
class ThreadDemo {
public static void main(String[] args) throws Exception {
// 创建生产的任务对象
Resource r = new Resource();
Product p = new Product(r);
Consumer c = new Consumer(r);
Thread t1 = new Thread(p);// 生产者线程
Thread t2 = new Thread(c);// 消费者线程
t1.start();
t2.start();
}
}
二、生产者消费者模型的安全问题
由于cpu会在线程间切换,导致可能出现某个线程未完全跑完代码就切换到隔壁线程了。如消费者刚修改数据为null,就切换到生产者打印结果,那么打印结果自然为生产者生产了数据null。
Thread-0正存放的数据为 null
Thread-1正取的数据为 null
解决办法:加锁。
1.同步代码块
代码如下:区别就在于创建了锁,并用synchronized这一同步代码块来为线程同步区域加了锁。save方法与get方法用的是同一把锁,可以完成同步。
当然,这个代码也是存在问题的,它描述的场景是这样的,生产者可以一直生产,每次都会替换掉之前生成的产品;消费者可以一直消费,哪怕产品是null也会消费。也即是无限生产/无限消费,显然是不合理的。
class Resource{
// 定义容器用来存放数据或取数据的空间
private Object[] data = new Object[1]; // 方便演示所以仅开辟一个空间
private int num = 1;
private Object lock = new Object(); // 创建锁
// 存放数据的方法
public void save(){
synchronized(lock){
data[0] = "data"+num; // 正常而言应该是通过参数传入数据,这里为了演示将其写死,并通过num来体现生产的不同东西
System.out.println(Thread.currentThread().getName()+"正存放的数据为 "+data[0]);
num++;
}
}
// 获取数据的方法
public void get(){
synchronized(lock){
System.out.println(Thread.currentThread().getName()+"正取的数据为 "+data[0]);
data[0]=null;
}
}
}
三、生产者消费者模型的同步问题
也即线程间通信问题。
无限生产/消费问题想要解决,我们思考一下情景。
生产者在生产时应该确定此时没有产品;消费者消费时应该确定此时有产品。也即是加入判断,在不满足条件时使线程等待。
线程间通信使用锁的wait与notify方法。也即是PV操作。
下列代码可以完成单生产者单消费者问题。
class Resource{
// 定义容器用来存放数据或取数据的空间
private Object[] data = new Object[1]; // 方便演示所以仅开辟一个空间
private int num = 1;
private Object lock = new Object(); // 创建锁
// 存放数据的方法
public void save(){
synchronized(lock){
if(data[0]!=null){
// 如果有数据,那么生产者应该等待消费者取走
try {
lock.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
data[0] = "data"+num; // 正常而言应该是通过参数传入数据,这里为了演示将其写死,并通过num来体现生产的不同东西
System.out.println(Thread.currentThread().getName()+"正存放的数据为 "+data[0]);
num++;
// 生产数据后要通知消费者可以消费了
lock.notify();
}
}
// 获取数据的方法
public void get(){
synchronized(lock){
if(data[0]==null){
// 如果没有数据,那么消费者应该等待生产者生产
try {
lock.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"正取的数据为 "+data[0]);
data[0]=null;
// 消费数据后要通知生产者可以生产了
lock.notify();
}
}
}
四、生产者消费者模型的多生产多消费问题
上述代码只能完成单生产者单消费者问题,一旦开辟多个生产者/消费者,那么生产者唤醒锁的时候有可能唤醒了另一个生产者,那么就再次出现了无线生产问题。
落实在代码上,是因为线程被唤醒后会直接执行if后的代码。
解决方法:将 if 改为 while。一定可以解决问题,但是也有隐患,发生了死锁,所有线程都wait了。
死锁原因:唤醒的时候唤醒了自己的同伴,由于判断条件改为了while,导致同步再次判断依然被wait,最终导致没有可以执行的线程。
解决方法:JDK5之前只能采用notifyAll方法唤醒所有线程。唤醒的同伴会再次被wait,对方线程会干活,但是这种方式效率低。更希望的结果是唤醒对方线程。
或者使用JDK5中的condition接口,完成不同的等待唤醒机制监控不同的接口。
class Resource{
// 定义容器用来存放数据或取数据的空间
private Object[] data = new Object[1]; // 方便演示所以仅开辟一个空间
private int num = 1;
private Object lock = new Object(); // 创建锁
// 存放数据的方法
public void save(){
synchronized(lock){
while(data[0]!=null){
// 如果有数据,那么生产者应该等待消费者取走
try {
lock.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
data[0] = "data"+num; // 正常而言应该是通过参数传入数据,这里为了演示将其写死,并通过num来体现生产的不同东西
System.out.println(Thread.currentThread().getName()+"正存放的数据为 "+data[0]);
num++;
// 生产数据后要通知消费者可以消费了
lock.notify();
}
}
// 获取数据的方法
public void get(){
...
}
}
五、lock+condition解决生产者消费者模型
同步问题要么使用synchronized同步代码块+notify/notifyAll来解决,要么使用lock+condition来解决,其中lock对应synchronized,condition对应notify。
如果使用lock+condition方法来解决问题,就和OS课程上学习的PV十分接近,代码如下:主要更改的地方有锁的创建、加锁去锁、PV操作。
此方法效率更高。
class Resource{
// 定义容器用来存放数据或取数据的空间
private Object[] data = new Object[1]; // 方便演示所以仅开辟一个空间
private int num = 1;
// 创建锁, Lock是一个接口,ReentrantLock是该接口的实现类
private Lock l = new ReentrantLock();
// 给Lock接口下绑定多个用于监视线程的Condition对象
private Condition proCon = l.newCondition(); // 监视生产者线程,要绑定在锁l之上
private Condition conCon= l.newCondition(); // 监视消费者线程,要绑定在锁l之上
// 存放数据的方法
public void save(){
l.lock();// 手动上锁
try{ // 使用try-finally的写法是为了一定执行unlock方法,否则出现意外如exception,那么可以不会unlock,就出问题了
while(data[0]!=null){
// 如果有数据,那么生产者应该等待消费者取走
try {
proCon.await();// P是await()
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
data[0] = "data"+num; // 正常而言应该是通过参数传入数据,这里为了演示将其写死,并通过num来体现生产的不同东西
System.out.println(Thread.currentThread().getName()+"正存放的数据为 "+data[0]);
num++;
// 生产数据后要通知消费者可以消费了
conCon.signal();// V是signal()
}finally{
// 手动解锁
l.unlock();
}
}
// 获取数据的方法
public void get(){
...
}
}
完整代码
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
// 资源类
class Resource{
// 定义容器用来存放数据或取数据的空间
private Object[] data = new Object[1]; // 方便演示所以仅开辟一个空间
private int num = 1;
// 创建锁, Lock是一个接口,ReentrantLock是该接口的实现类
private Lock l = new ReentrantLock();
// 给Lock接口下绑定多个用于监视线程的Condition对象
private Condition proCon = l.newCondition(); // 监视生产者线程,要绑定在锁l之上
private Condition conCon= l.newCondition(); // 监视消费者线程,要绑定在锁l之上
// 存放数据的方法
public void save(){
l.lock();// 手动上锁
try{ // 使用try-finally的写法是为了一定执行unlock方法,否则出现意外如exception,那么可以不会unlock,就出问题了
while(data[0]!=null){
// 如果有数据,那么生产者应该等待消费者取走
try {
proCon.await();// P是await()
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
data[0] = "data"+num; // 正常而言应该是通过参数传入数据,这里为了演示将其写死,并通过num来体现生产的不同东西
System.out.println(Thread.currentThread().getName()+"正存放的数据为 "+data[0]);
num++;
// 生产数据后要通知消费者可以消费了
conCon.signal();// V是signal()
}finally{
// 手动解锁
l.unlock();
}
}
// 获取数据的方法
public void get(){
l.lock();
try{
while(data[0]==null){
// 如果没有数据,那么消费者应该等待生产者生产
try {
conCon.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"正取的数据为 "+data[0]);
data[0]=null;
// 消费数据后要通知生产者可以生产了
proCon.signal();
}finally{
l.unlock();
}
}
}
// producter
class Product implements Runnable{
// 在生产者类中提供构造方法,目的是在创建线程的时候可以明确线程将来操作的资源本身
private Resource r;
public Product(Resource r){
this.r = r;
}
public void run(){
while(true){
// 无限的生产资源
r.save();
}
}
}
// consumer
class Consumer implements Runnable{
private Resource r;
public Consumer(Resource r){
this.r = r;
}
public void run(){
while(true){
// 无限的消耗资源
r.get();
}
}
}
class ThreadDemo {
public static void main(String[] args) throws Exception {
// 创建生产的任务对象
Resource r = new Resource();
Product p = new Product(r);
Consumer c = new Consumer(r);
Thread t1 = new Thread(p);// 生产者线程
Thread t11 = new Thread(p);// 生产者线程
Thread t2 = new Thread(c);// 消费者线程
Thread t21 = new Thread(c);// 消费者线程
t1.start();
t2.start();
t11.start();
t21.start();
}
}