Java多线程-03-线程同步
目录
一.线程简介
二.线程创建
三.线程状态
四.线程同步
五.线程协作
四.线程同步(重点+难点)
线程同步机制


我说:线程同步是为了实现多个线程操作同一个资源,同时避免因并发问题导致的数据紊乱
解决并发问题的思路(什么是线程同步)

队列和锁
-
线程排队
-
每个对象都有一把锁
生活例子类比:
排队上厕所,每个人锁上门使用厕所
线程同步的实现条件:
队列 + 锁
锁机制虽然解决了多个线程访问同一对象时的正确性,但也带来了一些问题

三大不安全案例
1.不安全的买票
public class Unsafe_BuyTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket,"A").start();
new Thread(buyTicket,"B").start();
new Thread(buyTicket,"C").start();
}
}
class BuyTicket implements Runnable{
private int ticketNumber = 10;
private boolean flag = true;//外部停止
@Override
public void run() {
while(true) buy();
}
private void buy(){
//判断是否有票
if(ticketNumber <= 0){
flag = false;
return;
}
//模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//买票
System.out.println(Thread.currentThread().getName() + "-->买到了第" + ticketNumber-- + "张票");
}
}

我的解释:
1.为什么会出现多个线程买到同一张票?

在某一时刻A,B,C都"看到"ticketNumber=10,于是把10"拷贝"到自己的工作内存,如果B先买了第10张票,那么实际的剩余票数应该为9,但是A,C的工作内存尚未"更新"还认为ticketNumber=10,所以A,C认为自己应该买第10张票
2.为什么会出现负数?
private void buy(){
//判断是否有票
if(ticketNumber <= 0){
flag = false;
return;
}
//买票
System.out.println(Thread.currentThread().getName() + "-->买到了第" + ticketNumber-- + "张票");
}
ticketNumber = 1,A,B,C都判断有票,B买了第1张,C买了第0张,A买了第-1张
2.不安全的取钱
public class Unsafe_Bank {
public static void main(String[] args) {
//账户
Accout accout = new Accout("买房存款", 100);
//取钱业务
//业务1:小明在accout中取50
Drawing draw_money_business_1 = new Drawing("小明", accout , 50);
//业务1:小红在accout中取100
Drawing draw_money_business_2 = new Drawing("小红", accout , 100);
//执行业务
draw_money_business_1.start();
draw_money_business_2.start();
}
}
//银行账户
class Accout{
public String accountName;
public int money;
public Accout(String accountName, int money){
this.accountName = accountName;
this.money = money;
}
}
//银行:模拟取钱(Drawing)
class Drawing extends Thread{
//要取哪个账户里的钱
private Accout accout;
//要取多少钱
private int wantDraw;
//成功取到多少钱
private int successDrawed = 0;
//构造器(“谁”要在哪个“账户”取多少“钱”)
public Drawing(String personName, Accout accout, int wantDraw){
//调用父类的构造器,线程的名字 = personName
super(personName);
this.accout = accout;
this.wantDraw = wantDraw;
}
//取钱
@Override
public void run() {
//判断能不能取
if(accout.money - wantDraw < 0){
//this.getName() = Thread.currentThread().getName()
System.out.println(this.getName() + "-->" + accout.accountName + "账户余额不足");
return;
}
//模拟延时(所有线程都会停在这一步,增加后面代码发生并发问题的概率)
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
accout.money = accout.money - wantDraw;//更新账户余额
successDrawed = wantDraw;
System.out.println(accout.accountName + "账户余额为:" + accout.money
+ " " + this.getName() + "-->取到了" + successDrawed);
}
}


3.不安全的集合
public class Unsafe_List {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
//创建10000条线程并把线程的名字添加到list
for (int i = 0; i < 10000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
//让主线程睡一会再输出结果,保证其他的线程都跑完了(把自己的名字加到list里)
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
为什么list的size不是10000?
我的解释:
在同一瞬间有多个线程在操作list的同一个位置
例如A,B同时往list的第995个位置add数据,A写完后主线程还没来得及执行i++,B就又往995写了(把A的覆盖掉了)
正确的情况应该类似于A写995位置,B写996位置
同步方法及同步块
同步方法
同步块
把上一节中三个不安全的例子改为安全的
1.安全的买票
synchronized 同步方法,锁的是this (拥有这个同步方法的类)
public class Safe_BuyTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket,"A").start();
new Thread(buyTicket,"B").start();
new Thread(buyTicket,"C").start();
}
}
class BuyTicket implements Runnable{
private int ticketNumber = 10;
private boolean flag = true;//外部停止
@Override
public void run() {
while(true) buy();
}
//synchronized 同步方法,锁的是this,即BuyTicket
private synchronized void buy(){
//判断是否有票
if(ticketNumber <= 0){
flag = false;
return;
}
//模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//买票
System.out.println(Thread.currentThread().getName() + "-->买到了第" + ticketNumber-- + "张票");
}
}
2.安全的取钱
我说:
同步块 synchronized (Obj){}
Obj是要锁的实例对象
同步块会被线程按序执行,A把这个块跑完了,B才能去跑,即同步块只允许有一个线程在执行块内代码
如何判断锁哪个对象?
这个对象会被多个线程修改
同步块中应该放哪些代码?
对这个对象进行修改操作的代码
public class Safe_Bank {
public static void main(String[] args) {
//账户
Accout accout = new Accout("买房存款", 100);
//取钱业务
//业务1:小明在accout中取50
Drawing draw_money_business_1 = new Drawing("小明", accout , 50);
//业务1:小红在accout中取100
Drawing draw_money_business_2 = new Drawing("小红", accout , 100);
//执行业务
draw_money_business_1.start();
draw_money_business_2.start();
}
}
//银行账户
class Accout{
public String accountName;
public int money;
public Accout(String accountName, int money){
this.accountName = accountName;
this.money = money;
}
}
//银行:模拟取钱(Drawing)
class Drawing extends Thread{
//要取哪个账户里的钱
private Accout accout;
//要取多少钱
private int wantDraw;
//成功取到多少钱
private int successDrawed = 0;
//构造器(“谁”要在哪个“账户”取多少“钱”)
public Drawing(String personName, Accout accout, int wantDraw){
//调用父类的构造器,线程的名字 = personName
super(personName);
this.accout = accout;
this.wantDraw = wantDraw;
}
//取钱
@Override
public void run() {
//同步块,是对account对象进行修改,所以要锁account
//同步块中的代码会被线程按序执行,A把这个块跑完了,B才能去跑
synchronized (accout){
//判断能不能取
if(accout.money - wantDraw < 0){
//this.getName() = Thread.currentThread().getName()
System.out.println(this.getName() + "-->" + accout.accountName + "账户余额不足");
return;
}
//模拟延时(所有线程都会停在这一步,增加后面代码发生并发问题的概率)
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
accout.money = accout.money - wantDraw;//更新账户余额
successDrawed = wantDraw;
System.out.println(accout.accountName + "账户余额为:" + accout.money
+ " " + this.getName() + "-->取到了" + successDrawed);
}
}
}
3.安全的集合
public class Safe_List {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
//创建10000条线程并把线程的名字添加到list
for (int i = 0; i < 10000; i++) {
new Thread(()->{
//锁list
synchronized (list){
list.add(Thread.currentThread().getName());
}
}).start();
}
//让主线程睡一会再输出结果,保证其他的线程都跑完了(把自己的名字加到list里)
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
小结
-
同步方法
synchronized作为关键字修饰方法,锁的是this
-
同步块
synchronized (Obj){},锁的是Obj
-
锁谁?
被多个线程修改的对象
-
涉及修改被锁对象的代码
死锁
死锁举例
public class DeadLock {
public static void main(String[] args) {
WeaponChoice player_1_Choice = new WeaponChoice("player-1", 0);
WeaponChoice player_2_Choice = new WeaponChoice("player-2", 1);
player_1_Choice.start();
player_2_Choice.start();
}
}
//武器一:步枪(rifle)
class Rifle{
}
//武器二:手枪(pistol)
class Pistol{
}
//武器库中只有一把手枪和一把步枪
//有两种选择方式
//choice = 1 使用手枪1秒 然后使用步枪
//choice = 0 使用步枪1秒 然后使用手枪
class WeaponChoice extends Thread{
//只有一把手枪和一把步枪,需要的资源只有一份,用static来修饰
static Rifle rifle = new Rifle();
static Pistol pistol = new Pistol();
private String playerName;
private int choiceCode;
public WeaponChoice(String playerName, int choiceCode){
this.playerName = playerName;
this.choiceCode = choiceCode;
}
@Override
public void run() {
try {
equipmentWeapon();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//给玩家装备分配武器
private void equipmentWeapon() throws InterruptedException {
//先使用1秒步枪 再使用手枪
if(choiceCode == 0){
//一个同步块拥有两个对象的锁 rifle 和 pistol
synchronized (rifle){
System.out.println(this.playerName + "-->获得了 步枪 的锁");
Thread.sleep(1000);//睡眠1s 可以放大死锁发生的可能性(两个线程都锁住了对方期待的资源)
synchronized (pistol){
System.out.println(this.playerName + "-->获得了 手枪 的锁");
}
}
}
//先使用1秒手枪 再使用步枪
if(choiceCode == 1){
//一个同步块拥有两个对象的锁 rifle 和 pistol
synchronized (pistol){
System.out.println(this.playerName + "-->获得了 手枪 的锁");
Thread.sleep(1000);//睡眠1s 可以放大死锁发生的可能性(两个线程都锁住了对方期待的资源)
synchronized (rifle){
System.out.println(this.playerName + "-->获得了 步枪 的锁");
}
}
}
}
}
问题代码
rifle和pistol资源都只有一个
A先拿了rifle的锁,B先拿了pistol的锁
之后A想要pistol的锁,B想要rifle的锁
对A而言,现在pistol是被B锁住的所以A要等待
对B而言,现在rifle是被A锁住的所以B要等待
A无法得到pistol的锁,同步块代码没有执行完,也就无法释放rifle的锁
B无法得到rifle的锁,同步块代码没有执行完,也就无法释放pistol的锁
形成死锁
// A线程
...
//一个同步块拥有两个对象的锁 rifle 和 pistol
synchronized (rifle){
System.out.println(this.playerName + "-->获得了 步枪 的锁");
Thread.sleep(1000);//睡眠1s 可以放大死锁发生的可能性(两个线程都锁住了对方期待的资源)
synchronized (pistol){
System.out.println(this.playerName + "-->获得了 手枪 的锁");
}
}
...
---------------------------------------------
// B线程
...
//一个同步块拥有两个对象的锁 rifle 和 pistol
synchronized (pistol){
System.out.println(this.playerName + "-->获得了 手枪 的锁");
Thread.sleep(1000);//睡眠1s 可以放大死锁发生的可能性(两个线程都锁住了对方期待的资源)
synchronized (rifle){
System.out.println(this.playerName + "-->获得了 步枪 的锁");
}
}
...
如何修改
我说:
线程一次只拿一个锁,将自己持有的锁释放了才能去拿别的锁
简单的来说就是:同步块里面不能有同步块
// A线程
...
synchronized (rifle){
System.out.println(this.playerName + "-->获得了 步枪 的锁");
Thread.sleep(1000);//睡眠1s 可以放大死锁发生的可能性(两个线程都锁住了对方期待的资源)
}
synchronized (pistol){
System.out.println(this.playerName + "-->获得了 手枪 的锁");
}
...
------------------------------------------------------
// B线程
...
synchronized (pistol){
System.out.println(this.playerName + "-->获得了 手枪 的锁");
Thread.sleep(1000);//睡眠1s 可以放大死锁发生的可能性(两个线程都锁住了对方期待的资源)
}
synchronized (rifle){
System.out.println(this.playerName + "-->获得了 步枪 的锁");
}
...

避免死锁的方法
(ps:字打错了是线程不是进程)
Lock锁
常用的是
ReentrantLock(可重入锁)
ReentrantLock类实现了Lock接口,可以显式加锁,释放锁
推荐格式

//定义lock锁
private final ReentrantLock lock = new ReentrantLock();
...
try {
lock.lock();//加锁
//被锁机制保障线程安全的代码
}finally {
lock.unlock();//解锁
}
还是以买票为例
public class Lock_Test {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket,"A").start();
new Thread(buyTicket,"B").start();
new Thread(buyTicket,"C").start();
}
}
class BuyTicket implements Runnable{
private int ticketNumber = 10;
private boolean flag = true;//外部停止
//定义lock锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true) buy();
}
private void buy(){
try {
lock.lock();//加锁
//判断是否有票
if(ticketNumber <= 0){
flag = false;
return;
}
//模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//买票
System.out.println(Thread.currentThread().getName() + "-->买到了第" + ticketNumber-- + "张票");
}finally {
lock.unlock();//解锁
}
}
}
对比
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 上周热点回顾(3.3-3.9)
· AI 智能体引爆开源社区「GitHub 热点速览」