线程同步问题
一、线程同步
- 多个线程操作一个对象
1.并行并发
- 并行是指两个或者多个事件在同一时刻发生,而并发是指两个或多个事件在同一时间间隔发生。
- 并发:同一个对象被多个线程同时操作
2.线程同步
- 现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题,比如,食堂排队打饭,最天然的解决方法就是排队,一个个来。
- 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时候就需要线程同步,线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池,形成队列,等待前面线程使用完毕,下一个线程再使用。
3.队列和锁
- 队列+锁(类比上厕所,进去后要锁门才能保证安全)才能保证线程同步的安全性
- 由于同一进程的多个线程共享一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被 访问时的正确性,在访问时加入锁机制 synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可,存在以下问题
- 一个线程持有锁会导致其他所有需要此锁的线程挂起
- 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
- 如果一个优先级高的线程等待一个优先级㐴的线程释放锁,会导致优先级倒置,引起性能问题。
二、三大线程不安全案例
1.模拟买票
-
线程不安全,票num可能出现负数
-
package safe; //模拟买票,线程不安全 public class TicketsBuy { public static void main(String[] args) { BuyTicket ticket=new BuyTicket(); new Thread(ticket,"我").start(); new Thread(ticket,"你").start(); new Thread(ticket,"黄牛党").start(); } } class BuyTicket implements Runnable{ private int ticketsNum=10; boolean flag=true;//通过外部判断线程停止 @Override public void run() { while(flag){ buy(); } } public void buy(){ if (ticketsNum<=0) { flag = false; return;//结束 } try { Thread.sleep(100);//模拟延时,放大问题 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"买到了第"+ticketsNum--+"张票"); } }
-
-
不安全的原因:
- 三个人可能同时或其中两个人拿到了票,到最后一张票时,三个人发现都还可以拿,所以三个人都各拿了一张,最后导致票数为负数。同时操作了一块内存。
2.模拟银行取钱
-
会出现负数,线程不安全,同时对一个对象进行操作了
package safe; //模拟两个人同时去银行取钱,对同一个账户进行操作 public class unSafeBank { public static void main(String[] args) { Account account=new Account(100,"10086"); BankDrawing you=new BankDrawing(account,50,"你"); BankDrawing me=new BankDrawing(account,100,"我"); you.start(); me.start(); } } //账户 class Account{ int moneyNum; String cardID; public Account(int moneyNum, String cardID) { this.moneyNum = moneyNum; this.cardID = cardID; } } //模拟银行取钱 class BankDrawing extends Thread{ Account account;//账户 //取了多少钱 int drawingMoney; //目前手里有多少钱 int nowMoney; public BankDrawing(Account account,int drawingMoney,String name){ super(name);//传给父类的构造方法 this.account=account; this.drawingMoney=drawingMoney; } @Override public void run() { if (account.moneyNum-drawingMoney<0){ System.out.println("账户里的钱不够了"); return; } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } account.moneyNum=account.moneyNum-drawingMoney; nowMoney=nowMoney+drawingMoney; System.out.println("账户余额为:"+account.moneyNum); //this.getName()=Thread.currentThread().getName(),因为继承了Thread System.out.println(this.getName()+"目前手里有"+nowMoney+"钱"); } }
-
3.arraylist是线程不安全的
package safe;
import java.util.ArrayList;
public class unsafeList {
public static void main(String[] args) {
ArrayList<String> arrayList=new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
arrayList.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(10);//如果休眠时间足够长,结果为10000
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(arrayList.size());//输出结果不是10000,还有未执行完成的
}
}
三、线程同步方法及代码块
-
由于我们可以通过private关键字来保证数据对象只能被方法(get\set)访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种方法:synchronized方法及synchronized块
public synchronized void method(){}
-
synchronized方法控制对“对象”的访问,每个对象都有一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程阻塞,方法一但执行,就独占该锁,直到方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
-
缺陷:若将一个大的方法声明为synchronized将会影响效率。
-
方法里面需要修改的内容才需要锁,锁得太多,浪费资源。
-
同步块:
- synchronized(obj){}
- obj称为同步监视器
- obj可以是任何对象,但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器是this,就是这个对象本身,或者是class(反射)
- 同步监视器的执行过程
- 1.第一个线程访问,锁定同步监视器,执行其中的代码
- 2.第二个线程访问,发现同步监视器被锁定,无法访问
- 3.第一个线程访问完毕,解锁同步监视器
- 4.第二个线程访问,发现同步监视器没有锁,然后锁定并访问
-
针对上面三个案例进行同步
-
取票:
-
package safe; //模拟买票,线程不安全 public class TicketsBuy { public static void main(String[] args) { BuyTicket ticket=new BuyTicket(); Thread you= new Thread(ticket, "你"); Thread me = new Thread(ticket, "我"); Thread party = new Thread(ticket, "黄牛党"); you.setPriority(5); me.setPriority(5); party.setPriority(5); you.start(); me.start(); party.start(); } } class BuyTicket implements Runnable{ private int ticketsNum=1000; boolean flag=true; @Override public void run() { while(flag){ try { buy(); } catch (InterruptedException e) { e.printStackTrace(); } } } public synchronized void buy() throws InterruptedException { if (ticketsNum<=0) { flag = false; return;//结束 } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"买到了第"+ticketsNum--+"张票"); } } //如果sleep是在buy里,则线程拿到锁执行完成后会由CPU调度的执行哪个线程,三个线程都在run方法里的buy前面进行等待
-
package safe; //模拟买票,线程不安全 public class TicketsBuy { public static void main(String[] args) { BuyTicket ticket=new BuyTicket(); Thread you= new Thread(ticket, "你"); Thread me = new Thread(ticket, "我"); Thread party = new Thread(ticket, "黄牛党"); you.setPriority(5); me.setPriority(5); party.setPriority(5); you.start(); me.start(); party.start(); } } class BuyTicket implements Runnable{ private int ticketsNum=1000; boolean flag=true; @Override public void run() { while(flag){ try { buy(); } catch (InterruptedException e) { e.printStackTrace(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } public synchronized void buy() throws InterruptedException { if (ticketsNum<=0) { flag = false; return;//结束 } System.out.println(Thread.currentThread().getName()+"买到了第"+ticketsNum--+"张票"); } } //如果放在run方法里,cpu调度线程,第一个线程进入拿到锁,第二三线程在buy方法前排队等待,当第一个线程执行完buy释放锁,其他线程依次执行,然后线程休眠一秒
-
-
银行取钱:
-
package safe; //模拟两个人同时去银行取钱,对同一个账户进行操作 public class unSafeBank { public static void main(String[] args) { Account account=new Account(100,"10086"); BankDrawing you=new BankDrawing(account,50,"你"); BankDrawing me=new BankDrawing(account,100,"我"); you.start(); me.start(); } } //账户 class Account{ int moneyNum; String cardID; public Account(int moneyNum, String cardID) { this.moneyNum = moneyNum; this.cardID = cardID; } } //模拟银行取钱 class BankDrawing extends Thread{ Account account;//账户 //取了多少钱 int drawingMoney; //目前手里有多少钱 int nowMoney; public BankDrawing(Account account,int drawingMoney,String name){ super(name);//传给父类的构造方法 this.account=account; this.drawingMoney=drawingMoney; } @Override public void run() { synchronized (account) { if (account.moneyNum - drawingMoney < 0) { System.out.println("账户里的钱不够了"); return; } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } account.moneyNum = account.moneyNum - drawingMoney; nowMoney = nowMoney + drawingMoney; System.out.println("账户余额为:" + account.moneyNum); //this.getName()=Thread.currentThread().getName(),因为继承了Thread System.out.println(this.getName() + "目前手里有" + nowMoney + "钱"); } } } //此时应该锁住的是Account对象而不是BankDrawing对象(this)
-
-
arraylist
-
package safe; import java.util.ArrayList; public class unsafeList { public static void main(String[] args) { ArrayList<String> arrayList=new ArrayList<>(); for (int i = 0; i < 10000; i++) { new Thread(()->{ synchronized (arrayList) { arrayList.add(Thread.currentThread().getName()); System.out.println(Thread.currentThread().getName()); } }).start(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(arrayList.size()); } } //锁住arraylist对象就能保证arraylist是安全的
-
-
四、JUC编程
1.CopyOnWriteArrayList(并发工具包)
package safe;
import java.util.concurrent.CopyOnWriteArrayList;
//CopyOnWriteArrayList是一种线程安全的arraylist
public class JUCTest {
public static void main(String[] args) throws InterruptedException {
CopyOnWriteArrayList<String> list=new CopyOnWriteArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{list.add(Thread.currentThread().getName());}).start();
}
Thread.sleep(1000);
System.out.println(list.size());
}
}
//callable接口也是在java.util.concurrent包中
五、死锁
- 多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有“两个以上对象的锁”时,就可能发生“死锁”问题。
- 产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已获得的资源,在未使用之前,不能强行剥夺 。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
package safe;
//模拟死锁,死锁就是多个线程互相抱着对方需要的资源,形成僵持
public class DeadLock {
public static void main(String[] args) {
Makeup ping = new Makeup(0, "宋平");
Makeup ping1 = new Makeup(1, "sp");
ping.start();
ping1.start();
//两个线程已经分别拥有两个不同对象的锁,分别争抢对方拥有的锁,但是双方都不会释放锁,所以会导致死锁
}
}
//口红类
class LipStick{}
//镜子类
class Mirror{}
//定义一个类化妆
class Makeup extends Thread{
//定义一份资源
static LipStick lipStick=new LipStick();
static Mirror mirror=new Mirror();
int choice;
String girlName;
//定义构造方法
public Makeup(int choice, String girlName) {
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
try {
girlMakeUp();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//互相持有对方的锁,需要拿到对方的资源,形成死锁
private void girlMakeUp() throws InterruptedException {
if (choice==0){
//此时已经锁了一个lipStick
synchronized (lipStick){
System.out.println(this.girlName+"拿到了口红的锁");
Thread.sleep(1000);
//拿另一个锁
synchronized (mirror){
System.out.println(this.girlName+"拿到了镜子的锁");
}
}
}else{
//此时已经锁了一个lipStick
synchronized (mirror){
System.out.println(this.girlName+"拿到了镜子的锁");
Thread.sleep(2000);
synchronized (lipStick){
System.out.println(this.girlName+"拿到了口红的锁");
}
}
}
}
}
- 解决方案
package safe;
//模拟死锁,死锁就是多个线程互相抱着对方需要的资源,形成僵持
public class DeadLock {
public static void main(String[] args) {
Makeup ping = new Makeup(0, "宋平");
Makeup ping1 = new Makeup(1, "sp");
ping.start();
ping1.start();
//两个线程已经分别拥有两个不同对象的锁,执行完成synchonized代码块中的内容后,释放锁,另一个对象便可以使用锁,因此不会造成死锁
}
}
//口红类
class LipStick{}
//镜子类
class Mirror{}
//定义一个类化妆
class Makeup extends Thread{
//定义一份资源
static LipStick lipStick=new LipStick();
static Mirror mirror=new Mirror();
int choice;
String girlName;
//定义构造方法
public Makeup(int choice, String girlName) {
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
try {
girlMakeUp();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//互相持有对方的锁,需要拿到对方的资源,形成死锁
private void girlMakeUp() throws InterruptedException {
if (choice==0){
//此时已经锁了一个lipStick
synchronized (lipStick){
System.out.println(this.girlName+"拿到了口红的锁");
Thread.sleep(1000);
}
//拿另一个锁
synchronized (mirror){
System.out.println(this.girlName+"拿到了镜子的锁");
}
}else{
//此时已经锁了一个lipStick
synchronized (mirror){
System.out.println(this.girlName+"拿到了镜子的锁");
Thread.sleep(2000);
}
synchronized (lipStick){
System.out.println(this.girlName+"拿到了口红的锁");
}
}
}
}
六、Lock锁
- 从JDK1.5开始,java提供了更强大的线程同步机制----通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
- java.util.concurrent.locks.Lock(JUC包中)接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源前应先获得Lock对象。
- ReentrantLock(可重入锁)类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁
- ReentrantLock的加锁和释放锁一般写在try和finally代码块中
package safe;
import java.util.concurrent.locks.ReentrantLock;
//lock锁,显示调用释放锁,与synchornized效果差不多
public class ThreadLockTest {
public static void main(String[] args) {
LockTest lockTest = new LockTest();
new Thread(lockTest, "王运好").start();
new Thread(lockTest, "韩邦胜").start();
new Thread(lockTest, "李洪军").start();
}
}
//模拟多个线程买票
class LockTest implements Runnable {
private final ReentrantLock lock=new ReentrantLock();
int ticketNum = 1000;
boolean falg = true;
@Override
public void run() {
while (falg) {
buy();
}
}
private void buy() {
try{
lock.lock();
if (ticketNum > 0) {
System.out.println(Thread.currentThread().getName() + "--------" + ticketNum--);
}else {
falg=false;
}
}finally {
lock.unlock();
}
}
}
- synchronized和Lock的对比
- Lock是显式锁(手动开户和关闭锁)。synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。
- 优先使用顺序
- Lock->同步代码块(已经进入了方法体,分配了相应资源)->同步方法(在方法体之外)
本文来自博客园,作者:一只快乐的小67,转载请注明原文链接:https://www.cnblogs.com/sp520/p/16212262.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!