线程同步
1.概述
线程同步是多线程编程中的一个重要概念,它指的是在多线程环境中,通过一定的机制保证多个线程按照某种特定的方式正确、有序地执行。这主要是为了避免并发问题,如死锁、竞态条件、资源争用等,确保数据的一致性和完整性。
当多个线程共享同一份资源时,由于线程的执行顺序是不确定的,可能会出现线程安全问题。例如,两个线程同时对一个共享变量进行操作,可能会出现预期之外的结果。
如下:
小明和小弘对同一账号取钱,会出现余额为负的情况
package Synchronization;
//操作账户
public class Account {private String cardId;private Double amount;public Account(String cardId, Double amount) {this.cardId = cardId;this.amount = amount;}public void withDrawMoney(Double amount){if(this.amount>=amount){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}this.amount=this.amount-amount;System.out.println(Thread.currentThread().getName()+"来取钱成功!余额为"+this.amount);}else {System.out.println(Thread.currentThread().getName()+"来取钱失败!");}}public String getCardId() {return cardId;}public void setCardId(String cardId) {this.cardId = cardId;}public Double getAmount() {return amount;}public void setAmount(Double amount) {this.amount = amount;}
}
package Synchronization;public class Main {public static void main(String[] args) {Account account = new Account("w2xId", (double) 1000);//初始化账户//实例化小明取钱线程new Thread(new Runnable() {@Overridepublic void run() {account.withDrawMoney((double) 1000);}},"小明").start();//实例化小弘取钱线程new Thread(new Runnable() {@Overridepublic void run() {account.withDrawMoney((double) 1000);}},"小弘").start();}
}
结果:
为了避免这种情况,就需要对线程进行同步,即保证同一时刻只有一个线程可以对共享资源进行操作。
2.线程同步的三种方式
1.同步代码块
在Java中,同步代码块是一种确保线程同步的机制,它允许你指定一段代码只能由一个线程在任何给定时间执行。同步代码块是通过在代码块前加上synchronized关键字和一个锁对象来实现的。这个锁对象可以是任何对象,当线程尝试进入同步代码块时,它必须首先获得这个锁。
关键修改部分
//同步代码块
synchronized (this) {if(this.amount>=amount){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}this.amount=this.amount-amount;System.out.println(Thread.currentThread().getName()+"来取钱成功!余额为"+this.amount);}else {System.out.println(Thread.currentThread().getName()+"来取钱失败!");}
}
这里取this作为锁对象,this指代Account对象,也就是Acount对象为锁对象,且此程序只有一个Account实例化对象。
执行结果
2.同步方法
在Java中,同步方法也是一种实现线程同步的常用方式。通过在方法声明前加上synchronized关键字,可以确保该方法在任何时刻只被一个线程访问。同步方法会隐式地锁定当前实例(this),即如果两个线程同时访问同一个对象的同一个同步方法,那么只有一个线程能够执行该方法,另一个线程必须等待。
关键代码修改如下:
synchronized public void withDrawMoney(Double amount){//同步代码块if(this.amount>=amount){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}this.amount=this.amount-amount;System.out.println(Thread.currentThread().getName()+"来取钱成功!余额为"+this.amount);}else {System.out.println(Thread.currentThread().getName()+"来取钱失败!");}}
3.Lock锁
在Java中,除了使用内置的synchronized关键字实现同步外,还可以使用java.util.concurrent.locks包中提供的Lock接口及其实现类(如ReentrantLock)来实现更灵活和强大的线程同步。Lock接口提供了一种机制,可以显式地获取和释放锁,而不是像synchronized那样隐式地获取和释放锁。
使用Lock接口的主要优势包括:
- 灵活性:可以中断正在等待锁的线程,可以尝试获取锁而不必阻塞,以及可以释放锁,即使锁是由其他线程获取的。
- 可响应性:相比于synchronized,Lock通常具有更好的响应性,因为它允许更细粒度的锁控制。
- 条件支持:Lock接口与Condition接口一起使用,可以实现更复杂的线程同步需求。
Lock锁实现步骤:
1.创建Lock锁
2.加锁
3.解锁
关键代码修改如下:
package Synchronization;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class Account {private String cardId;private Double amount;private final Lock lock=new ReentrantLock();//1.创建锁对象public Account(String cardId, Double amount) {this.cardId = cardId;this.amount = amount;}
// synchronized public void withDrawMoney(Double amount){
// //同步代码块
// if(this.amount>=amount){
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// this.amount=this.amount-amount;
// System.out.println(Thread.currentThread().getName()+"来取钱成功!余额为"+this.amount);
// }else {
// System.out.println(Thread.currentThread().getName()+"来取钱失败!");
// }
//
// }
synchronized public void withDrawMoney(Double amount){lock.lock();//2.加锁if(this.amount>=amount){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}this.amount=this.amount-amount;System.out.println(Thread.currentThread().getName()+"来取钱成功!余额为"+this.amount);}else {System.out.println(Thread.currentThread().getName()+"来取钱失败!");}lock.unlock();//3.解锁}// public void withDrawMoney(Double amount){
// //同步代码块
// synchronized (this) {
// if(this.amount>=amount){
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// this.amount=this.amount-amount;
// System.out.println(Thread.currentThread().getName()+"来取钱成功!余额为"+this.amount);
// }else {
// System.out.println(Thread.currentThread().getName()+"来取钱失败!");
// }
// }
//
// }public String getCardId() {return cardId;}public void setCardId(String cardId) {this.cardId = cardId;}public Double getAmount() {return amount;}public void setAmount(Double amount) {this.amount = amount;}
}
线程通信
1.概述
生产者消费者模型在线程通信中是一个经典的应用场景。这个模型主要用来解决生产者和消费者之间的同步问题,确保两者之间的顺畅协作。在这个模型中,生产者负责生成数据并将其放入共享缓冲区,而消费者则从缓冲区中取出数据进行处理。
生产者消费者模型的关键点:
- 共享缓冲区:通常是一个队列或其他数据结构,用作生产者和消费者之间的通信媒介。生产者将生产的数据放入缓冲区,而消费者从缓冲区中取出数据进行处理。
- 同步机制:由于生产者和消费者可能同时访问缓冲区,因此需要一种同步机制来确保数据的一致性和避免竞态条件。这通常通过锁、信号量或其他同步原语来实现。在Java中,可以使用synchronized关键字、Lock接口或BlockingQueue来实现同步。
- 生产者和消费者的协作:当缓冲区满时,生产者需要等待缓冲区有空闲空间才能继续生产数据。同样,当缓冲区为空时,消费者需要等待缓冲区中有数据才能继续消费。这种协作通过线程间的通信来实现,生产者通知消费者缓冲区有新数据,消费者通知生产者缓冲区有空闲空间。
2.实例
三个生产者生产包子,两个消费者吃包子,每次生产者将一个包子放到桌子上并通知消费者,消费者拿取包子后通知生产者生产包子。
package Synchronization;import java.util.ArrayList;
import java.util.List;/*** 如果桌子上没有包子,则拿包子线程等待,唤醒其他所有线程* 如果桌子上有包子,则放包子线程等待,唤醒其他所有线程*/
public class Desk {private List<String> list=new ArrayList<>();//放包子,通过同步代码块,保证生产者只有一个在生产包子public synchronized void put(){String name = Thread.currentThread().getName();if(list.size()==0){list.add("生产了一个包子");System.out.println(name+list.get(0));try {Thread.sleep(2000);this.notifyAll();//唤醒所有的线程this.wait();//等待} catch (Exception e) {e.printStackTrace();}}else {this.notifyAll();//唤醒所有的线程try {this.wait();//等待} catch (InterruptedException e) {e.printStackTrace();}}}//拿包子,通过同步代码块,保证只有一个消费者拿取包子public synchronized void get(){String name = Thread.currentThread().getName();if(list.size()==1){list.clear();System.out.println(name+"拿了一个包子");try {this.notifyAll();//唤醒所有的线程this.wait();//等待} catch (Exception e) {e.printStackTrace();}}else {this.notifyAll();//唤醒所有的线程try {this.wait();//等待} catch (InterruptedException e) {e.printStackTrace();}}}
}
package Synchronization;public class CommunicationModel {public static void main(String[] args) {Desk desk=new Desk();//创建三个生产者线程new Thread(new Runnable() {@Overridepublic void run() {while (true){desk.put();}}},"厨师1").start();new Thread(new Runnable() {@Overridepublic void run() {while (true){desk.put();}}},"厨师2").start();new Thread(new Runnable() {@Overridepublic void run() {while (true){desk.put();}}},"厨师3").start();//创建两个消费者线程new Thread(new Runnable() {@Overridepublic void run() {while (true){desk.get();}}},"客人1").start();new Thread(new Runnable() {@Overridepublic void run() {while (true){desk.get();}}},"客人2").start();}
}