线程同步
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() {
@Override
public void run() {
account.withDrawMoney((double) 1000);
}
},"小明").start();
//实例化小弘取钱线程
new Thread(new Runnable() {
@Override
public 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() {
@Override
public void run() {
while (true){
desk.put();
}
}
},"厨师1").start();
new Thread(new Runnable() {
@Override
public void run() {
while (true){
desk.put();
}
}
},"厨师2").start();
new Thread(new Runnable() {
@Override
public void run() {
while (true){
desk.put();
}
}
},"厨师3").start();
//创建两个消费者线程
new Thread(new Runnable() {
@Override
public void run() {
while (true){
desk.get();
}
}
},"客人1").start();
new Thread(new Runnable() {
@Override
public void run() {
while (true){
desk.get();
}
}
},"客人2").start();
}
}