Java 多线程(四)——线程同步(synchronized、ReentrantLock)

心已赠人 2022-01-11 08:47 536阅读 0赞

同步,是指协同步调,按预定的先后次序进行运行。而不是从字面上理解的“一起工作”。

1 一个线程安全问题

  银行取钱问题:使用两个线程来模拟两个人对同一账户取钱操作。

  1. package thread;/**
  2. * Created by Zen9 on 2016/3/9.
  3. */public class DrawTest {
  4. public static void main(String[] args) { //创建一个账户
  5. Account account = new Account(1000); //模拟两个线程对同一个账户取钱
  6. new DrawThread("A",account,800).start(); new DrawThread("B",account,800).start();
  7. }
  8. }//账户class Account{ //余额
  9. private double balance; public Account(double balance){ this.balance = balance;
  10. } public double getBalance() { return balance;
  11. } public void setBalance(double balance) { this.balance = balance;
  12. } public void draw(double drawAmount){ if (balance >= drawAmount){
  13. System.out.println(Thread.currentThread().getName() + "取钱成功!取出金额为:" + drawAmount); try{
  14. Thread.sleep(1);
  15. }catch (InterruptedException ex){
  16. ex.printStackTrace();
  17. }
  18. balance -= drawAmount;
  19. System.out.println("余额为:" + balance);
  20. }else {
  21. System.out.println(Thread.currentThread().getName() + "余额不足!");
  22. }
  23. }
  24. }//取钱线程class DrawThread extends Thread{ private Account account; private double drawAmount; public DrawThread(String name,Account account,double drawAmount){ super(name); this.account = account; this.drawAmount = drawAmount;
  25. } @Override
  26. public void run() {
  27. account.draw(drawAmount);
  28. }
  29. }1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162

运行上面程序,有可能出现如下结果,这是不希望出现的错误:
这里写图片描述

2 synchronized关键字实现同步

  使用对象互斥锁来保证共享数据操作的完整性。每个对象都对应一个可称为“互斥锁”的标记,这个标记保证在任一时刻,只有一个线程访问该对象。使用synchronized关键字与对象互斥锁联系。

2.1 同步代码块

  Java多线程引入同步监视器来解决多线程并发访问问题:

  1. synchronized(obj)
  2. {
  3.   //同步代码块  ...
  4. }12345

  其中,obj就是同步监视器。线程开始执行同步代码之前,必须先获得对同步监视器的锁定,即获得对象的互斥锁。
  同步监视器的目的是阻止多个线程对统一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。

  改写上面的DrawThread类的run()方法为同步代码块:

  1. //取钱线程class DrawThread extends Thread{
  2. private Account account; private double drawAmount; public DrawThread(String name,Account account,double drawAmount){ super(name); this.account = account; this.drawAmount = drawAmount;
  3. }
  4. @Override public void run() { //使用account作为同步监视器,任何线程进入同步代码之前
  5. //必须先获得account对象的锁定,其他线程无法获得锁
  6. // 逻辑:“加锁、修改、释放锁”
  7. synchronized (account) { //修改之处///
  8. account.draw(drawAmount);
  9. }
  10. }
  11. }1234567891011121314151617181920

这里写图片描述

2.2 同步方法

  synchronized关键字还可以放到方法声明中,表示整个方法为同步方法,例如:

  1. //账户class Account{ //余额
  2. private double balance; public Account(double balance){ this.balance = balance;
  3. } public double getBalance() { return balance;
  4. } public void setBalance(double balance) { this.balance = balance;
  5. } //synchronized修饰方法
  6. synchronized public void draw(double drawAmount){ //修改之处///
  7. if (balance >= drawAmount){
  8. System.out.println(Thread.currentThread().getName() + "取钱成功!取出金额为:" + drawAmount); try{
  9. Thread.sleep(1);
  10. }catch (InterruptedException ex){
  11. ex.printStackTrace();
  12. }
  13. balance -= drawAmount;
  14. System.out.println("余额为:" + balance);
  15. }else {
  16. System.out.println(Thread.currentThread().getName() + "余额不足!");
  17. }
  18. }
  19. }//取钱线程class DrawThread extends Thread{ private Account account; private double drawAmount; public DrawThread(String name,Account account,double drawAmount){ super(name); this.account = account; this.drawAmount = drawAmount;
  20. } @Override
  21. public void run() {
  22. account.draw(drawAmount);
  23. }
  24. }123456789101112131415161718192021222324252627282930313233343536373839404142434445464748

  运行结果与synchronized修饰代码块一样。

  对于synchronized修饰的实例方法(非static方法),无须显示指定同步监视器,同步方法的同步监视器是this,即调用该方法的对象。

2.3 释放监视器的锁定

  线程进入同步代码块(方法)之前,会对同步监视器加锁。程序无法显示释放对同步监视器的锁定,当出现如下情况时会释放:

  • 同步方法(代码块)执行结束时
  • 在同步方法(代码块)中遇到break、return等终止语句
  • 在同步方法(代码块)中出现了未处理的Error或Exception,导致异常结束
  • 在同步方法(代码块)中执行了同步监视器对象的wait()方法,当前线程暂停,释放同步监视器

线程不会释放同步监视器的情况:

  • 线程执行同步方法(代码块)时,程序调用了Thread.sleep()、Thread.yield()方法来暂停当前线程的执行
  • 线程执行同步方法(代码块)时,其他线程调用了该线程的suspend()方法将该线程挂起

3 同步锁(Lock对象)实现同步

  同步锁由Lock对象充当,通过显示定义同步锁对象来实现同步。
  Lock是控制多个线程对共享资源进行访问的工具,比较常用的是ReentrantLock(可重入锁),该Lock对象可以显式地加锁、释放锁。
  ReentrantLock锁具有可重入性,即一个线程可以对已被加锁的ReentrantLock锁再次加锁。

改写上面Account类:

  1. //账户class Account{ private double balance; public Account(double balance){ this.balance = balance;
  2. } public double getBalance() { return balance;
  3. } public void setBalance(double balance) { this.balance = balance;
  4. } //定义锁对象
  5. private final ReentrantLock lock = new ReentrantLock(); //修改之处///
  6. public void draw(double drawAmount){ //加锁
  7. lock.lock(); //修改之处
  8. try{ if (balance >= drawAmount){
  9. System.out.println(Thread.currentThread().getName() + "取钱成功!取出金额为:" + drawAmount); try{
  10. Thread.sleep(1);
  11. }catch (InterruptedException ex){
  12. ex.printStackTrace();
  13. }
  14. balance -= drawAmount;
  15. System.out.println("余额为:" + balance);
  16. }else {
  17. System.out.println(Thread.currentThread().getName() + "余额不足!");
  18. }
  19. }finally { //释放锁
  20. lock.unlock(); //修改之处///
  21. }
  22. }
  23. }1234567891011121314151617181920212223242526272829303132333435363738394041

  使用Lock与使用synchronized同步代码块相似,同样符合“加锁、修改、解锁”的逻辑。只是Lock是显式使用Lock对象作为同步锁,而synchronized同步代码块是使用当前对象作为同步监视器。

4 死锁

  当两(多)个线程互相等待对方释放资源(同步监视器)时就会发生死锁。一旦发生死锁,整个程序既不会发生任何异常,也不会出现任务提示,只是所有线程都处于阻塞状态,无法继续。

例如:

  1. package thread;/**
  2. * Created by Zen9 on 2016/3/9.
  3. */public class DeadLockTest {
  4. public static void main(String[] args) {
  5. DeadLock deadLock = new DeadLock(); new Thread(deadLock).start();
  6. deadLock.init();
  7. }
  8. }
  9. class DeadLock implements Runnable{
  10. A a = new A();
  11. B b = new B(); public void init(){
  12. Thread.currentThread().setName("主线程");
  13. a.Amethod(b);
  14. System.out.println("进入主线程之后");
  15. } @Override
  16. public void run() {
  17. Thread.currentThread().setName("副线程");
  18. b.Bmethod(a);
  19. System.out.println("进入副线程后");
  20. }
  21. }
  22. class A{ //进入该方法前,对A实例加锁
  23. synchronized public void Amethod(B b){
  24. System.out.println(Thread.currentThread().getName() + " 进入A实例的Amethod方法"); try {
  25. Thread.sleep(200);
  26. }catch (InterruptedException ex){
  27. ex.printStackTrace();
  28. }
  29. System.out.println(Thread.currentThread().getName() + " 企图调用B的B_another_method方法"); //视图调用B实例的方法,但B实例已被锁定
  30. b.B_another_method();
  31. } synchronized public void A_another_method(){
  32. System.out.println("进入A类的A_another_method方法");
  33. }
  34. }
  35. class B{ //进入该方法时,对B实例加锁
  36. synchronized public void Bmethod(A a){
  37. System.out.println(Thread.currentThread().getName() + " 进入B实例的Bmethod方法"); try {
  38. Thread.sleep(200);
  39. }catch (InterruptedException ex){
  40. ex.printStackTrace();
  41. }
  42. System.out.println(Thread.currentThread().getName() + " 企图调用A的A_another_method方法"); //视图调用A实例的方法,但A实例已被锁定
  43. a.A_another_method();
  44. } synchronized public void B_another_method(){
  45. System.out.println("进入B类的B_another_method方法");
  46. }
  47. }

转载于:https://blog.51cto.com/11221198/1980990

发表评论

表情:
评论列表 (有 0 条评论,536人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Java线-线同步简述

    为什么要线程同步 由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问机制上的冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制(sy

    相关 线-线同步

    线程同步:协调多个线程间的并发操作,以获得符合预期的,确定的执行结果,消除多线程应用程序的不确定性. 使用线程的同步:可以保护资源同时只能由一个线程访问,一般采取的措施是获取

    相关 Java线线同步

    线程安全问题 在单线程中不会出现线程安全问题,而多线程编程中,如果多个线程同时操作同一个资源,这种资源可以是各种类型的的资源:一个变量、一个对象、一个文件、一个数据库表等