一 线程同步(会)
1 临界资源问题
临界资源
在⼀个进程中, 多个线程之间是可以资源共享的。 如果在⼀个进程中的⼀个资源同时被多个线程访问, 这个资源就是⼀个临界资源。
如果多个线程同时访问临界资源, 会对这个资源的值造成影响。
临界资源问题出现的原因就是多个线程在同时访问⼀个资源, 因此解决⽅案也很简单, 就是不让多个线程同时访问即可。
在⼀个线程操作⼀个资源的时候, 对这个资源进⾏“上锁”, 被锁住的资源, 其他的线程⽆法访问。
类似多个⼈去公共卫⽣间, 每⼀个⼈在进到卫⽣间的时候, 都会从⾥⾯进⾏反锁。 此时, 其他⼈如果也需要使⽤这个卫⽣间, 就得在⻔外等待。
2 线程锁
线程锁, 就是⽤来“锁住”⼀个临界资源, 其他的线程⽆法访问。 在程序中, 可以分为对象锁和类锁
对作为锁的对象的要求:
1.必须是对象
2.必须保证被多个线程共享
对象锁: 任何的普通对象或者this, 都可以被当做是⼀把锁来使⽤。 但是需要注意, 必须要保证不同的线程看到的锁, 需要是同⼀把锁才能⽣效。 如果不同的线程看到的锁对象是不⼀样的, 此时这把锁将没有任何意义。
注意: 不能直接使⽤匿名对象作为锁,因为这样每次都是在重新new,要保证锁是被⼤家共享.
类锁: 可以将⼀个类做成锁, 使⽤类.class (类的字节码⽂件对象)来作为锁。因为类的字节码⽂件的使⽤范围太⼤,所以⼀般我们不使⽤他作为锁,只有在静态⽅法中.
3 synchronized
如果在⼀个⽅法中, 所有的逻辑, 都需要放到同⼀个同步代码段中执⾏。 这样的⽅法, 可以直接做成同步⽅法。
1 同步⽅法
⾮静态同步⽅法,使⽤的对象锁(this)
是某个对象实例内,synchronized aMethod(){}可以防⽌多个线程同时访问这个对象的synchronized⽅法(如果⼀个对象有多个synchronized⽅法,只要⼀个线程访问了其中的⼀个synchronized⽅法,其它线程不能同时访问这个对象中任何⼀个synchronized⽅法)。这时,不同的对象实例的synchronized⽅法是不相⼲扰的。也就是说,其它线程照样可以同时访问相同类的另⼀个对象实例中的synchronized⽅法
静态同步⽅法,使⽤的类锁(当前类的.class⽂件)
是某个类的范围,synchronized static aStaticMethod{}防⽌多个线程同时访问
这个类中的synchronized static ⽅法。它可以对类的所有对象实例起作⽤。
静态同步函数在进内存的时候不会创建对象,但是存在其所属类的字节码⽂件对象,属于class类型的对象,所以静态同步函数的锁是其所属类的字节码⽂件对象
4 同步代码块
synchronized关键字⽤于⽅法中的某个区块中,表示只对这个区块的资源实⾏互斥访问。
同步代码段, 是来解决临界资源问题最常⻅的⽅式。 将⼀段代码放⼊到同步代码段中, 将这段代码上锁。第⼀个线程抢到了锁标记后, 可以对这个紧接资源上锁, 操作这个临界资源。 此时其他的线程再执⾏到synchronized的时候, 会进⼊到锁池, 直到持有锁的线程使⽤结束后, 对这个资源进⾏解锁。 此时, 处于锁池中的线程都可以抢这个锁标记, 哪⼀个线程抢到了, 就进⼊到就绪态, 没有抢到锁的线程, 依然处于锁池中。
同步代码块⼉的构成:
synchronized(锁(对象)){
同步的代码
}
二 线程通信(会)
1 线程通信-打印机打印实例
实例:打印机打印
实现功能:不断输⼊不断输出
总结:需要给输⼊任务和输出任务同时加⼀把锁,保证两个任务之间是同步的给两个任务加⼀把锁:可以是desc或者Object.class
不建议使⽤Object.class:由于Object的使⽤范围太⼤,可能造成不必要的错误.desc合适,因为他只被当前的两个任务共享.
注意:对于当前的情况只给⼀个线程加锁,⽆法实现两个线程的同步
示例代码:
/*
分析:
两个线程:输⼊线程和输出线程
两个任务区:输⼊任务,输出任务
⼀份数据
*/
public class Demo2 {
public static void main(String[] args) {
//数据对象
Desc desc = new Desc();
//创建两个任务
Input input = new Input(desc);
Output output = new Output(desc);
//创建线程
Thread in = new Thread(input);
Thread out = new Thread(output);
//开启线程
in.start();
out.start();
}
}
//数据类
class Desc {
String name;
String sex;
}
//输⼊任务
class Input implements Runnable{
Desc desc;
public Input(Desc desc) {
this.desc = desc;
}
int i = 0;
public void run() {
while (true){
synchronized (desc) {
if (i == 0) {
desc.name = "特没谱";
desc.sex = "男";
} else {
desc.name = "安倍⼩三";
desc.sex = "⼥";
}
i = (i + 1) % 2;
}
}
}
}
//输出任务
class Output implements Runnable{
Desc desc;
public Output(Desc desc) {
this.desc = desc;
}
public void run() {
while (true){
synchronized (desc) {
System.out.println(desc.name + " " +
desc.sex);
}
}
}
}
2 线程通信功能优化
实例:打印机打印
功能:对⼀次输⼊⼀次输出代码的改进
总结:进⾏了代码优化
⾯向对象的精髓:谁的活⼉谁⼲,不是你的活⼉不要⼲
将数据准备的活⼉从输⼊任务输出任务提出来,放⼊数据类Desc2
示例代码:
public class Demo4 {
public static void main(String[] args) {
//创建了⼀份数据
Desc2 desc = new Desc2();
//创建了输⼊任务和输出任务对象
Input2 input = new Input2(desc);
Output2 output = new Output2(desc);
//创建输⼊线程和输出线程
Thread in = new Thread(input);
Thread out = new Thread(output);
//开启线程
in.start();
out.start();
}
}
//数据类
class Desc2{
String name;
String sex;
boolean flag = false;//⽤于执⾏唤醒等待的切换
//负责输⼊
public void setData(String name,String sex) {
if (flag == true) {//当flag值为true,就让当前的线程处于等待状
态
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}//当执⾏这⾏代码的时候,这⾥对应的是哪个线程,就操作的是哪个
线程
}
this.name = name;
this.sex = sex;
flag = !flag;
notify();//唤醒的是通⼀把锁下的线程,因为现在只有⼀个输⼊线程,⼀
个输出线程.所以这⾥唤醒的是输出线程
//当线程池中没有被当前的锁标记的线程可唤醒时,我们成为空唤醒,空唤
醒不影响程序的执⾏.
}
//负责输出
public void getData() {
if (flag == false) {//让输出线程等待
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("姓名:"+name+" 性别:"+sex);
flag = ! flag;
notify();//唤醒的是输⼊线程
}
}
//输⼊任务
class Input2 implements Runnable{
Desc2 desc;
public Input2(Desc2 desc) {
this.desc = desc;
}
@Override
public void run() {
int i=0;
while (true) {
synchronized (desc) {
if (i == 0) {
desc.setData("超超", "男");
}else {
desc.setData("欣欣", "⼥");
}
i=(i+1)%2;
}
}
}
}
//输出任务
class Output2 implements Runnable{
Desc2 desc;
public Output2(Desc2 desc) {
this.desc = desc;
}
@Override
public void run() {
while (true){
synchronized (desc) {
desc.getData();
}
}
}
}
三 ⽣产者消费者模式 (重点)
⽣产者消费者问题是研究多线程程序经典问题之⼀,它描述是有⼀块缓冲区作为仓库,⽣产者可以将产品放⼊仓库,消费者则可以从仓库中取⾛产品。在Java中⼀共有四种⽅法⽀持同步,其中前三个是同步⽅法,⼀个是管道⽅法。
(1)Object的wait() / notify()⽅法 (2)Lock和Condition的await() / signal()⽅法
(3)BlockingQueue阻塞队列⽅法 (4)PipedInputStream /PipedOutputStream
这⾥只对第⼀种第⼆种做讲解,其他的有兴趣的⼩伙伴可以⾃⼰进阶学习
咱们前⾯通过---打印机打印实例--的深⼊理解,最终的代码实现的就是⽣产者消费者模
式,对应的是单⽣产者单消费者.下⾯我们就使⽤标准模型代码理解⼀下.
1 单⽣产者消费者(会)
/*
* 单⽣产者单消费者
* 需要的线程:两个---⼀个⽣产线程⼀个消费线程
* 需要的任务:两个---⼀个⽣产任务⼀个消费任务
* 需要数据:⼀份---产品
*/
public class Demo5 {
public static void main(String[] args) {
//准备数据
Product product = new Product();
//准备任务
Producer producer = new Producer(product);
Consumer consumer = new Consumer(product);
//准备⽣产线程消费线程
Thread pro = new Thread(producer);
Thread con = new Thread(consumer);
//开启线程
pro.start();
con.start();
}
}
//创建数据类--产品
class Product {
String name;//名字
double price;//价格
int number;//数量
//标识--控制唤醒等待
boolean flag = false;
//准备⽣产
public synchronized void setProduce(String name,double
price){
if (flag == true){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name;
this.price = price;
System.out.println(Thread.currentThread().getName()+"
⽣产了:"+this.name+" 价格:"+this.price+" 数量:"+this.number);
number++;
flag = !flag;
notify();
}
//准备消费
public synchronized void getConsume(){
if (flag == false){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"
消费了:"+this.name+" 价格:"+this.price);
flag = !flag;
notify();
}
}
//创建⽣产任务
class Producer implements Runnable{
Product product;
public Producer(Product product) {
this.product = product;
}
@Override
public void run() {
while (true) {
product.setProduce("bingbing", 10);
}
}
}
//创建消费任务
class Consumer implements Runnable{
Product product;
public Consumer(Product product) {
this.product = product;
}
@Override
public void run() {
while (true) {
product.getConsume();
}
}
}
2 多⽣产者多消费者(了解)
总结:将单⽣产者单消费者代码不做更改,直接再添加⼀个⽣产线程,⼀个消费线程,就形成了多⽣产者多消费者多消费者.
出现的错误1
错误描述:当有两个⽣产线程,两个消费线程同时存在的时候,有可能出现⽣产⼀次,消费多次或者⽣产多次消费⼀次的情况.
原因:当线程被重新唤醒之后,没有判断标记,直接执⾏了下⾯的代码
解决办法:将标记处的if改成while
出现的错误2
问题描述:继续运⾏程序,会出现死锁的情况(4个线程同时处于等待状态)
原因:唤醒的是本⽅的线程,最后导致所有的线程都处于等待状态.
解决办法:将notify改成notifyAll.保证将对⽅的线程唤醒
示例代码:
/*
* 多⽣产者多消费者
* 需要的线程:四个---两个⽣产线程两个消费线程
* 需要的任务:两个---⼀个⽣产任务⼀个消费任务
* 需要数据:⼀份---产品
*
* ⽣产任务与消费任务共⽤⼀个数据--产品类
* 要求:最终也要实现⼀次⽣产⼀次消费
*/
public class Demo6 {
public static void main(String[] args) {
//准备数据
Product1 product = new Product1();
//准备任务
Producer1 producer = new Producer1(product);
Consumer1 consumer = new Consumer1(product);
//准备⽣产线程消费线程
Thread pro1 = new Thread(producer);
Thread pro2 = new Thread(producer);
Thread con1 = new Thread(consumer);
Thread con2 = new Thread(consumer);
//开启线程
pro1.start();
con1.start();
pro2.start();
con2.start();
}
}
//创建数据类--产品
class Product1 {
String name;//名字
double price;//价格
int number;//数量
//标识--控制唤醒等待
boolean flag = false;
//准备⽣产
public synchronized void setProduce(String name,double
price){
while (flag == true){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name;
this.price = price;
System.out.println(Thread.currentThread().getName()+"
⽣产了:"+this.name+" 价格:"+this.price+" 数量:"+this.number);
number++;
flag = !flag;
//notify();
notifyAll();
}
//准备消费
public synchronized void getConsume(){
while (flag == false){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"
消费了:"+this.name+" 价格:"+this.price);
flag = !flag;
//notify();
notifyAll();
}
}
//创建⽣产任务
class Producer1 implements Runnable{
Product1 product;
public Producer1(Product1 product) {
this.product = product;
}
@Override
public void run() {
while (true) {
product.setProduce("bingbing", 10);
}
}
}
//创建消费任务
class Consumer1 implements Runnable{
Product1 product;
public Consumer1(Product1 product) {
this.product = product;
}
@Override
public void run() {
while (true) {
product.getConsume();
}
}
}
四 Lock锁
1为什么使⽤Lock锁?
在我们使⽤synchronized进⾏同步的时候,锁对象是Object类的对象,使⽤的wait,notify⽅法都来⾃Object类,但是咱们知道并不是所有的对象都会⽤到同步,所以这样⽤法不太合理,⽽且锁相关的功能很多,Lock就是将锁⾯向对象的结果.不光是将锁⾯向对象了,同时将wait,notify等⽅法也做了⾯相对象处理.形成了Condition接⼝.当我们想实现多⽣产者多消费者模式时,可以使⽤Lock实现同步,同时配合Condition接⼝实现唤醒等待.
//创建锁对象
Lock lock = new ReentrantLock();
//⽤于⽣产任务的Condition
Condition proCon = lock.newCondition();
//⽤于消费任务的Condition
Condition conCon = lock.newCondition();
2:⽐较synchronized和Lock
⽐较synchronized和Lock
1.synchronized:从jdk1.0就开始使⽤的同步⽅法-称为隐式同步
synchronized(锁对象){//获取锁 我们将锁还可以称为锁旗舰或者监听器
同步的代码
}//释放锁
2.Lock:从jdk1.5开始使⽤的同步⽅法-称为显示同步
原理:Lock本身是接⼝,要通过他的⼦类创建对象⼲活⼉
常⽤⼦类:ReentrantLock
使⽤过程:
⾸先调⽤lock()⽅法获取锁
进⾏同步的代码块⼉
使⽤unlock()⽅法释放锁
使⽤的场景:
当进⾏多⽣产者多消费者的功能时,使⽤Lock,其他的都使⽤synchronized
使⽤效率:Lock⾼于synchronized
⽐较Object多wait,notify和Condition的await,signal