多线程的同步和死锁

多线程同步和死锁

在多线程编程中,有可能会出现同时访问同一个资源的情况,这种资源可以是各种类型的的资源:一个变量、一个对象、一个文件、一个数据库表等,而当多个线程同时访问同一个资源的时候,就会存在一个问题:由于每个线程执行的过程是不可控的,所以很可能导致最终的结果与实际上的愿望相违背或者直接导致程序出错。

在售票员的案例中,多个线程访问的时候就会出现数据出错的情况

售票系统有两个个渠道, 网络购票,现场购票,下面模拟购票流程,然后我们启动两个线程代表网络和线程购票
我们定义两个个线程一起执行,
则会有可能出现数据错误, 如下图
这是因为当一个线程在还有一张票的时候进入到购票系统,但是在这儿cpu的时间段内没有走完程序,即票数没有-1, 然后cpu执行另一个线程,此时票数还是1,所以还会进入到购票流程,当这个线程执行完毕以后票数会变成0,cpu会调用第一次堵塞的线程, 此时票的数量为 0-1=-1 所以此时数据出错.

线程同步

可以用线程同步的方式解决上面的数据异常方法,有三种方法,分别为同步代码块,同步方法 ,Lock

当线程遇到同步代码块或者同步方法的时候,会先判断同步锁(一个对象)是否存在,如果存在,则会将同步锁加到这个线程上,执行程序,(如果程序没有执行完同步代码块的方法则这个同步锁不会被释放)  ,当另一个线程想要进入这个方法的时候会先判断一下同步锁是否存在,如果有,则进入执行,如果没有,则等待同步锁被释放,即保证了这个程序在某一时刻只能有一个线程去访问.

对象锁(同步锁)    :     任意对象,如果多个线程需要对某一个对象保持同步,则这些线程的对象锁要相同,锁住的不是变量,而是操作变量的方法,一个对象只拥有一个锁.类本身也有锁.

同步代码块

            synchronized(对象锁){
              线程要操作的共享数据
            }

            synchronized(obj){
            //对票数判断,大于0,可以出售,变量--操作
                if( ticket > 0){
                    try{
                       Thread.sleep(10);
                    }catch(Exception ex){}
                    System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);
                }
            }

同步方法

同步方法的对象锁是当前对象  即 this     

静态方法的对象锁为当前类.class
        public  synchronized void payTicket(){  
            if( ticket > 0){
                try{
                   Thread.sleep(10);
                }catch(Exception ex){}
                System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);
            }
        
     }

lock锁

lock所更灵活,但是需要自己手动释放锁

示例

        Lock ck = new ReentrantLock();
        public void run(){
            while(true){
            ck.lock();
            if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
                }
                ck.unlock();
            }
        }

等待与唤醒(wait()、notify()、notifyAll())

这三个方法是 java.lang.Object 的 final native 方法,任何继承 java.lang.Object 的类都有这三个方法。它们是Java语言提供的实现线程间阻塞和控制进程内调度的底层机制.

三个方法的解释:

- wait() :等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。

- notify():唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。

-  notifyAll(): 唤醒全部:可以将线程池中的所有wait() 线程都唤醒。

其实,所谓唤醒的意思就是让 线程池中的线程具备执行资格。必须注意的是,这些方法都是在 同步中才有效。同时这些方法在使用时必须标明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程。   

通过输入输出来演示等待和唤醒

有两个线程 input线程的作用是输入数据到对象,output作用是从对象中输出数据, 然后要求一次输入一次输出

  **基本过程**
  - 输入:赋值后,执行方法wait()等待,并且改变Resource的Tag值,唤醒输出

  - 输出:被唤醒后,判断Tag,如果不进入wait() 然后输出,改变Tag的值 , 输出完毕以后自己wait()  唤醒输入 notify()

   **注意** :1. 要给输入和输出同一个锁才能起到同步的作用  2. wait()和notify()需要用锁对象来调用,这样才知道唤醒或者休眠那个锁中的线程

**程序**

```
    main:
         public class ThreadDemo{
           public static void main(String[] args) {
             Resource r = new Resource();

             Input in = new Input(r);   // 要给输入和输出同一个锁才能起到同步的作用
             Output out = new Output(r);
             
             Thread tin = new Thread(in);
             Thread tout = new Thread(out);
             
             tin.start();
             tout.start();
           }
         }
    
 public class Resource {
      public String name;
      public String sex;
      public boolean flag = false;
     }

     /*
      *  输入的线程,对资源对象Resource中成员变量赋值
      *  一次赋值 张三,男
      *  下一次赋值 lisi,nv
      */
     public class Input implements Runnable {
      private Resource r ;
      
      public Input(Resource r){
        this.r = r;
      }
      
      public void run() {
        int i = 0 ;
        while(true){
          synchronized(r){
            //标记是true,等待
              if(r.flag){
                try{r.wait();}catch(Exception ex){}
              }
            
            if(i%2==0){
              r.name = "张三";
              r.sex = "男";
            }else{
              r.name = "lisi";
              r.sex = "nv";
            }
            //将对方线程唤醒,标记改为true
            r.flag = true;
            r.notify();   // wait()和notify()需要用锁对象来调用,这样才知道唤醒或者休眠那个锁中的线程
          }
          i++;
        }
      }

     }
     
     /*
      *  输出线程,对资源对象Resource中成员变量,输出值
      */
     public class Output implements Runnable {
      private Resource r ;
      
      public Output(Resource r){
        this.r = r;
      }
      public void run() {
        while(true){
          synchronized(r){  
            //判断标记,是false,等待
          if(!r.flag){
            try{r.wait();}catch(Exception ex){}
            }
          System.out.println(r.name+".."+r.sex);
          //标记改成false,唤醒对方线程
          r.flag = false;
          r.notify();
          }
        }
      }

     }
   

```

通过

死锁

当线程任务中出现了多个同步(多个锁)  时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。其中同步锁要是唯一锁(即整个程序中只有一个这种锁)

两个线程互相持有对象在等待的东西

死锁的四个必要条件

  • 互斥条件:一个资源每次只能被一个进程使用。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 程序演示死锁
 public class DeadLock implements Runnable{
            private int i = 0;
            public void run(){
              while(true){
                if(i%2==0){
                  //先进入A同步,再进入B同步
                  synchronized(LockA.locka){
                    System.out.println("if...locka");
                    synchronized(LockB.lockb){
                      System.out.println("if...lockb");
                    }
                  }
                }else{
                  //先进入B同步,再进入A同步
                  synchronized(LockB.lockb){
                    System.out.println("else...lockb");
                    synchronized(LockA.locka){
                      System.out.println("else...locka");
                    }
                  }
                }
                i++;
              }
            }
           }
        
          public class DeadLockDemo {
            public static void main(String[] args) {
              DeadLock dead = new DeadLock();
              Thread t0 = new Thread(dead);
              Thread t1 = new Thread(dead);
              t0.start();
              t1.start();
            }
          }


          public class LockA {
            private LockA(){}
            
            public  static final LockA locka = new LockA();
          }

          
          public class LockB {
            private LockB(){}
            
            public static final LockB lockb = new LockB();
          }

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏小勇DW3

Java多线程面试题整理 1) 什么是线程?

线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速。比...

772
来自专栏流柯技术学院

Python多线程学习

1、  函数式:调用thread模块中的start_new_thread()函数来产生新线程。如下例:

721
来自专栏Linyb极客之路

并发编程之queue

一、什么是queue 队列是一种特殊的线性表,它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。进行插入操作的端称为队尾,进行...

3337
来自专栏博客园迁移

Java并发问题--乐观锁与悲观锁以及乐观锁的一种实现方式-CAS

  悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库...

752
来自专栏微信公众号:Java团长

Java多线程编程

1)设计更复杂 虽然有一些多线程应用程序比单线程的应用程序要简单,但其他的一般都更复杂。在多线程访问共享数据的时候,这部分代码需要特别的注意。线程之间的交互往往...

1292
来自专栏java一日一条

Java并发问题--乐观锁与悲观锁以及乐观锁的一种实现方式-CAS

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边...

432
来自专栏我是攻城师

理解另类的并发安全实现CopyOnWriteArrayList

在Java的并发包java.util.concurrent里面有一个比较有意思现象,针对Map和LinkList都有对应的高效的+线程安全的并发实现类:

853
来自专栏开发之途

重拾Java(4)-线程

1304
来自专栏CSDN技术头条

Java 并发编程之美-线程相关的基础知识

借用 Java 并发编程实践中的话:编写正确的程序并不容易,而编写正常的并发程序就更难了;相比于顺序执行的情况,多线程的线程安全问题是微妙而且出乎意料的,因为在...

1253
来自专栏Java编程技术

高并发编程必备基础(上)

借用Java并发编程实践中的话"编写正确的程序并不容易,而编写正常的并发程序就更难了",相比于顺序执行的情况,多线程的线程安全问题是微妙而且出乎意料的,因为在没...

772

扫码关注云+社区