java如何实现线程同步
一、什么是线程的同步
线程有自己的私有数据,比如栈和寄存器,同时与其它线程共享相同的虚拟内存和全局变量等资源。 在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。但是当多个线程同时读写同一份共享资源的时候,会引起冲突,例如在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就要使线程同步。这时候就需要引入线程同步机制使各个线程排队一个一个的对共享资源进行操作,而不是同时进行。
简单的说就是,在多线程编程里面,一些数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。
二、为什么需要线程间的通信
1. 多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
2.当然如果我们没有使用线程通信来使用多线程共同操作同一份数据的话,虽然可以实现,但是在很大程度会造成多线程之间对同一共享变量的争夺,那样的话势必为造成很多错误和损失!
3.所以,我们才引出了线程之间的通信,多线程之间的通信能够避免对同一共享变量的争夺。
三、实现线程同步的方式
本篇博客暂时讲解两种方法通过synchronized关键字和lock锁
四、线程间的通信方式
Object类中相关的方法有notify方法和wait方法。因为wait和notify方法定义在Object类中,因此会被所有的类所继承。这些方法都是**final**的,即它们都是不能被重写的,不能通过子类覆写去改变它们的行为。
1.wait()
①wait()方法:** 让当前线程进入等待,并释放锁。
②wait(long)方法:** 让当前线程进入等待,并释放锁,不过等待时间为long,超过这个时间没有对当前线程进行唤醒,将**自动唤醒**。
2.notify()
③notify()方法:** 让当前线程通知那些处于等待状态的线程,当前线程执行完毕后释放锁,并从其他线程中唤醒其中一个继续执行。
④notifyAll()方法:** 让当前线程通知那些处于等待状态的线程,当前线程执行完毕后释放锁,将唤醒所有等待状态的线程。
3.wait()与sleep()比较
当线程调用了wait()方法时,它会释放掉对象的锁。
Thread.sleep(),它会导致线程睡眠指定的毫秒数,但线程在睡眠的过程中是不会释放掉对象的锁的。
五、synchronized简单介绍
synchronized 是java语言关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
1.实现原理
jvm基于进入和退出Monitor对象来实现方法同步和代码块同步。
方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。
代码块的同步是利用monitorenter和monitorexit这两个字节码指令。它们分别位于同步代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。
六、synchronized 关键字使用
synchronized 关键字,主要有两种用法,分别是同步方法和同步代码块。也就是synchronized 方法和 synchronized 块。
1.同步方法
pubilc synchronized void test() {
System.out.println("方法运行");
}
修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
2.同步代码块
public void test() {
synchronized(obj) {
System.out.println("===");
}
}
```
- 被`synchronized`修饰的代码块及方法,在同一时间,只能被单个线程访问。
修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
六、synchronized实现
需求是,模拟银行的存钱,取钱操作,多个线程存,多个线程取
1.synchronized同步方法
银行账户account对象
package s1; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * @ClassName: Account * @Author: tanp * @Description: 账户 * @Date: 2020/9/10 10:21 */ public class Account { private String name; private int money; private boolean isAwait; public Account(String name, int money, boolean isAwait) { this.money = money; this.name = name; this.isAwait = isAwait; } public String getName() { return name; } /** * @Description 存钱 * @Date 2020/9/10 10:30 * @Author tanp */ public synchronized void saveMoney(int value, int threadId, String name) { System.out.println("在线程" + threadId + "运行存钱方法时" + name + "账户有" + money + "元"); if (value > 0) { money = money + value; } //如果当前用户上的余额高于0元,说明可以取钱了,通知那些等待在this对象上的线程如取钱线程,可以醒过来了 if (isAwait) { this.notify(); System.out.println(name + "账户余额充足,线程" + threadId + "调用notify方法"); isAwait = false; } try { //休息一秒钟 System.out.println("线程" + threadId + "存钱" + value + "元到" + name + "账户,现有余额" + money + "元"); Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } } /** * @Description 取钱 * @Date 2020/9/10 10:36 * @Author tanp */ public synchronized void fechMoney(int value, int threadId, String name) { try { System.out.println("在线程" + threadId + "运行取钱方法时" + name + "账户有" + money + "元"); //如果当前所要取的钱,大于银行卡余额,让占有this的取钱线程,暂时释放对this的占有,并等待 while (value > money) { System.out.println(name + "账户余额不足,线程" + threadId + "调用wait方法"); isAwait = true; this.wait(); } money = money - value; System.out.println("线程" + threadId + "取钱" + value + "元," + name + "账户现有余额" + money + "元"); //休眠一秒钟 Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(15, 30, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); Account account = new Account("王先生", 23, false); for (int i = 0; i < 5; i++) { SaveThread saveThread = new SaveThread(account, i); threadPoolExecutor.execute(saveThread); FetchThread fetchThread = new FetchThread(account, i); threadPoolExecutor.execute(fetchThread); } Account account1 = new Account("李先生", 48, false); for (int i = 0; i < 2; i++) { SaveThread saveThread = new SaveThread(account1, i); threadPoolExecutor.execute(saveThread); FetchThread fetchThread = new FetchThread(account1, i); threadPoolExecutor.execute(fetchThread); } } }
取钱线程
package 菜鸟教程.多线程编程.线程同步; import java.util.Random; public class FetchThread extends Thread { private Account account; private int threadId; private Random r = new Random(); public FetchThread(Account account,int threadId) { this.threadId = threadId; this.account = account; } @Override public void run() { while (true) { account.fechMoney( r.nextInt(50),threadId,account.getName()); } } }
存钱线程
package 菜鸟教程.多线程编程.线程同步; import java.util.Random; public class SaveThread extends Thread{ private Account account; private int threadId; private Random r = new Random(); public SaveThread(Account account,int threadId) { this.account = account; this.threadId = threadId; } @Override public void run() { while (true) { account.saveMoney(r.nextInt(50),threadId,account.getName()); } } }
2.synchronized同步代码块
account对象
package 菜鸟教程.多线程编程.线程同步.s2; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * @ClassName: Account * @Author: tanp * @Description: 账户 * @Date: 2020/9/10 10:21 */ public class Account { private String name; private int money; private boolean isAwait; public Account(String name, int money, boolean isAwait) { this.money = money; this.name = name; this.isAwait = isAwait; } public String getName() { return name; } /** * @Description 存钱 * @Date 2020/9/10 10:30 * @Author tanp */ public void saveMoney(int value, int threadId, String name) { System.out.println("在线程" + threadId + "运行存钱方法时" + name + "账户有" + money + "元"); if (value > 0) { money = money + value; } //如果当前用户上的余额高于0元,说明可以取钱了,通知那些等待在this对象上的线程如取钱线程,可以醒过来了 if (isAwait) { this.notify(); System.out.println(name + "账户余额充足,线程" + threadId + "调用notify方法"); isAwait = false; } try { //休息一秒钟 System.out.println("线程" + threadId + "存钱" + value + "元到" + name + "账户,现有余额" + money + "元"); Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } } /** * @Description 取钱 * @Date 2020/9/10 10:36 * @Author tanp */ public void fechMoney(int value, int threadId, String name) { try { System.out.println("在线程" + threadId + "运行取钱方法时" + name + "账户有" + money + "元"); //如果当前所要取的钱,大于银行卡余额,让占有this的取钱线程,暂时释放对this的占有,并等待 while (value > money) { System.out.println(name + "账户余额不足,线程" + threadId + "调用wait方法"); isAwait = true; this.wait(); } money = money - value; System.out.println("线程" + threadId + "取钱" + value + "元," + name + "账户现有余额" + money + "元"); //休眠一秒钟 Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(15, 30, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); Account account = new Account("王先生", 23, false); for (int i = 0; i < 5; i++) { SaveThread saveThread = new SaveThread(account, i); threadPoolExecutor.execute(saveThread); FetchThread fetchThread = new FetchThread(account, i); threadPoolExecutor.execute(fetchThread); } Account account1 = new Account("李先生", 48, false); for (int i = 0; i < 2; i++) { SaveThread saveThread = new SaveThread(account1, i); threadPoolExecutor.execute(saveThread); FetchThread fetchThread = new FetchThread(account1, i); threadPoolExecutor.execute(fetchThread); } } }
与前面同步方法的account对象相比,就是去掉了方法上的synchronized关键字
取钱线程
package 菜鸟教程.多线程编程.线程同步.s2; import java.util.Random; /** * @ClassName: FetchThread * @Author: tanp * @Description: 取钱线程 * @Date: 2020/9/10 10:56 */ public class FetchThread extends Thread { private Account account; private int threadId; private Random r = new Random(); public FetchThread(Account account, int threadId) { this.threadId = threadId; this.account = account; } @Override public void run() { while (true) { synchronized (account) { account.fechMoney( r.nextInt(50),threadId,account.getName()); } } } }
与前面的取钱线程相比就是在线程run方法里加了synchronized (account)代码
存钱线程
package 菜鸟教程.多线程编程.线程同步.s2; import java.util.Random; /** * @Package: s2 * @ClassName: SaveThread * @Author: tanp * @Description: 存钱线程 * @Date: 2020/9/10 11:54 */ public class SaveThread extends Thread { private Account account; private int threadId; private Random r = new Random(); public SaveThread(Account account, int threadId) { this.account = account; this.threadId = threadId; } @Override public void run() { while (true) { synchronized (account) { account.saveMoney(r.nextInt(50), threadId, account.getName()); } } } }
与前面的存钱线程相比就是在线程run方法里加了synchronized (account)代码
七、lock锁实现
需求跟前面的一样,也就是代码也也就是更改了一点
1.使用ReentrantLock实现同步
lock()方法:上锁
unlock()方法:释放锁
trylock():synchronized 是不占用到手不罢休的,会一直试图占用下去。与 synchronized 的钻牛角尖不一样,Lock接口还提供了一个trylock方法。
2.使用Condition实现等待/通知
使用synchronized方式进行线程交互,用到的是同步对象的wait,notify和notifyAll方法
Lock也提供了类似的解决办法,首先通过lock对象得到一个Condition对象,然后分别调用这个Condition对象的:await, signal,signalAll 方法
3.代码案例
account对象
package 菜鸟教程.多线程编程.线程同步.r; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @Package: r * @ClassName: Account * @Author: tanp * @Description: ${description} * @Date: 2020/9/10 15:12 */ public class Account { private String name; private int money; boolean isAwait; public Account(String name, int money, boolean isAwait) { this.money = money; this.name = name; this.isAwait = isAwait; } public String getName() { return name; } /** * @Description 存钱 * @Date 2020/9/10 10:30 * @Author tanp */ public void saveMoney(int value, int threadId, String name,Condition condition) { System.out.println("在线程" + threadId + "运行存钱方法时" + name + "账户有" + money + "元"); if (value > 0) { money = money + value; } //如果当前有线程在等待,存钱之后唤醒等的线程 if (isAwait) { condition.signal(); System.out.println(name+"账户余额充足,线程"+threadId+"调用signal()方法"); //线程唤醒之后,将是否有等待线程的标志设置为false isAwait = false; } try { //休息一秒钟 System.out.println("线程" + threadId + "存钱" + value + "元到" + name + "账户,现有余额" + money + "元"); Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } } /** * @Description 取钱 * @Date 2020/9/10 10:36 * @Author tanp */ public void fechMoney(int value, int threadId, String name,Condition condition ) { try { System.out.println("在线程" + threadId + "运行取钱方法时" + name + "账户有" + money + "元"); //如果当前所要取的钱,大于银行卡余额,让释放当前取钱线程的占有,并等待 while (value > money) { System.out.println(name+"账户余额不足,线程"+threadId+"调用await方法"); //将是否有等待线程标志设置为true isAwait = true; condition.await(); } money = money - value; System.out.println("线程" + threadId + "取钱" + value + "元," + name + "账户现有余额" + money + "元"); //休眠一秒钟 Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(15, 30, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); Account account = new Account("王先生", 3,false); for (int i = 0; i < 5; i++) { SaveThread saveThread = new SaveThread(account, i, lock,condition); threadPoolExecutor.execute(saveThread); FetchThread fetchThread = new FetchThread(account, i, lock,condition); threadPoolExecutor.execute(fetchThread); } Account account1 = new Account("李先生", 48,false); for (int i = 0; i < 2; i++) { SaveThread saveThread = new SaveThread(account1, i, lock,condition); threadPoolExecutor.execute(saveThread); FetchThread fetchThread = new FetchThread(account1, i, lock,condition); threadPoolExecutor.execute(fetchThread); } } }
取钱线程
package 菜鸟教程.多线程编程.线程同步.r; import java.util.Random; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; /** * @Package: r * @ClassName: FetchThread * @Author: tanp * @Description: ${description} * @Date: 2020/9/10 15:40 */ public class FetchThread extends Thread { private Account account; private int threadId; private Lock lock; private Condition condition; private Random r = new Random(); public FetchThread(Account account, int threadId, Lock lock, Condition condition) { this.threadId = threadId; this.account = account; this.lock = lock; this.condition = condition; } @Override public void run() { while (true) { try { lock.lock(); account.fechMoney(r.nextInt(50), threadId, account.getName(),condition); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } try { Thread.sleep(1000L); } catch (InterruptedException e) { System.out.println("休眠失败"); } } } }
存钱线程
package 菜鸟教程.多线程编程.线程同步.r; import java.util.Random; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; /** * @Package: r * @ClassName: SaveThread * @Author: tanp * @Description: ${description} * @Date: 2020/9/10 15:36 */ public class SaveThread extends Thread { private Account account; private int threadId; private Lock lock; private Condition condition; private Random r = new Random(); public SaveThread(Account account, int threadId, Lock lock, Condition condition) { this.account = account; this.threadId = threadId; this.lock = lock; this.condition = condition; } @Override public void run() { while (true) { try { lock.lock(); account.saveMoney(r.nextInt(5), threadId, account.getName(),condition); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } try { //休眠让其他线程可以抢占到锁 Thread.sleep(1000L); } catch (InterruptedException e) { System.out.println("休眠失败"); } } } }
4.何时用ReentrantLock
- 适用场景:
- 时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票
- 在确实需要一些synchronized所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票。
- 如何选用
- ReentrantLock 还具有可伸缩性的好处,应当在高度争用的情况下使用它,但是请记住,大多数 synchronized 块几乎从来没有出现过争用,所以可以把高度争用放在一边。我建议用 synchronized 开发,直到确实证明 synchronized 不合适,而不要仅仅是假设如果使用 ReentrantLock “性能会更好”。请记住,这些是供高级用户使用的高级工具。(而且,真正的高级用户喜欢选择能够找到的最简单工具,直到他们认为简单的工具不适用为止。)。一如既往,首先要把事情做好,然后再考虑是不是有必要做得更快。
八、ReentrantLock和synchronized使用分析
- ReentrantLock是Lock的实现类,是一个互斥的同步器,在多线程高竞争条件下,ReentrantLock比synchronized有更加优异的性能表现。
- 1 用法比较
- Lock使用起来比较灵活,但是必须有释放锁的配合动作
- Lock必须手动获取与释放锁,而synchronized不需要手动释放和开启锁
- Lock只适用于代码块锁,而synchronized可用于修饰方法、代码块等
- 2 特性比较
- ReentrantLock的优势体现在:
- 具备尝试非阻塞地获取锁的特性:当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁
- 能被中断地获取锁的特性:与synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
- 超时获取锁的特性:在指定的时间范围内获取锁;如果截止时间到了仍然无法获取锁,则返回
- 3 注意事项
- 在使用ReentrantLock类的时,一定要注意三点:
- 在finally中释放锁,目的是保证在获取锁之后,最终能够被释放
- 不要将获取锁的过程写在try块内,因为如果在获取锁时发生了异常,异常抛出的同时,也会导致锁无故被释放。
- ReentrantLock提供了一个newCondition的方法,以便用户在同一锁的情况下可以根据不同的情况执行等待或唤醒的动作。
存钱线程
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!