Java多线程(二)---线程安全/线程同步

1 线程同步的概念

(1)线程同步:在单线程程序中,每次只能做一件事情。后面的事情需要等待前面的事情完成后才可以进行,但是如果使用多线程程序,就会发生两个或多个线程抢占资源的问题,如两个人同时说话,两个人同时过同一个独木桥。所以在多线程编程中需要防止这些资源访问的冲突。Java提供了线程同步机制来防止资源访问的冲突。

(2)线程安全:实际开发中,使用多线程程序的情况会很多,如车站售票系统,医院挂号系统。这种多线程的程序通常会发生问题,以车站售票系统为例,当代码中判断当前票数是否大于0,如果大于0则执行将该票出售给乘客的功能,但当两个线程同时访问这段代码时(假如这时只剩下一张票),第一个线程将票售出,与此同时第二个线程也已经执行完成判断是否有票的操作,并得出票数大于0的结论,于是他也执行售出操作,这样就会产出负数。所以,在编写多线程程序时,应该考虑到线程安全问题。实质上线程安全问题来源于两个线程同时存取单一对象的数据。

(3)★安全问题出现的条件:☆是多线程环境 ☆有共享数据 ☆有多条语句操作共享数据

如何解决多线程安全问题:基本思想就是让程序没有安全问题的环境。采用线程同步机制,把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可。

(4)同步的好处与弊端

☆好处:解决了多线程数据安全问题。

☆弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。

2 线程同步机制

2.0 卖票---暴露出安全问题

public class Demo01 implements Runnable{
   private static int tickets =100;
   private Object o =new Object();
    @Override
    public void run() {
        while (true) {
                if (tickets <= 0) {
                    break;
                } else {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    tickets--;
                    System.out.println(Thread.currentThread().getName() + ":" + tickets + "张票");
                }
        }
    }
}

public class Test01 {
    public static void main(String[] args) {
        Demo01 d =new Demo01();
        Thread t1 =new Thread(d,"窗口1");
        Thread t2 =new Thread(d,"窗口2");
        Thread t3 =new Thread(d,"窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

出现的问题:有相同的票出现。同时也出现了负数票。

问题产生的原因:发生了线程安全问题,是线程执行的随机性导致的,可能在买票过程中丢失cpu的执行权,导致出现问题。


2.1同步代码块解决数据安全问题

synchronized(任意对象){

      多条语句操作共享数据的代码

}

public class Demo01 implements Runnable{
   private static int tickets =100;
   private Object o =new Object();
    @Override
    public void run() {
        while (true) {
            synchronized (o) {
                if (tickets <= 0) {
                    break;
                } else {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    tickets--;
                    System.out.println(Thread.currentThread().getName() + ":" + tickets + "张票");
                }

            }
        }
    }
}

public class Test01 {
    public static void main(String[] args) {
        Demo01 d =new Demo01();
        Thread t1 =new Thread(d,"窗口1");
        Thread t2 =new Thread(d,"窗口2");
        Thread t3 =new Thread(d,"窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

分析一些问题

public class Demo01 implements Runnable{
   private static int tickets =100;
    @Override
    public void run() {
        while (true) {
            synchronized ( new Object()) {
                if (tickets <= 0) {
                    break;
                } else {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    tickets--;
                    System.out.println(Thread.currentThread().getName() + ":" + tickets + "张票");
                }

            }
        }
    }
}
这样也会出现,打印出的结果有重复的,出现负数,原因是每次循环结束的时候synchronized都会换新的对象,这样就相当于换新的锁,根本监视不到对象,所以把object o放在了成员变量,这样就一直是一把锁,利用这种原理可以试出来后面的同步方法的对象是this和静态同步方法的对象是 类名.class

2.2同步方法解决数据安全问题

同步方法:就是把synchronized关键字加到方法上。

修饰符 synchronized 返回值类型 方法名(方法参数){

方法体

}

同步静态方法:在同步方法的基础上加上static关键字

修饰符 static synchronized 返回值类型 方法名(方法参数){

方法体

}

同步方法的锁对象:this

同步静态方法的锁对象:类名.class

该代码用来解释为什么同步方法的锁对象是this,同理证明同步静态方法的锁对象。
public class Demo02 implements Runnable{
    private static int ticket =100;
    private Object o =new Object();
    private int x=0;
    @Override
    public void run() {
       l: while (true) {
            if(x%2==0) {
              synchronized (o) {   用o的时候,发现会有安全问题把o改成this 发现没有问题
                    if (ticket <= 0) {             证明了同步方法的锁对象是this
                        break;
                    } else {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + ":" + ticket);
                        ticket--;
                    }
                }
            }
            else{
                if (ticket <= 0) {
                break ;
              }
                method();
            }
            x++;
        }
    }
   private synchronized void method(){
           try {
                   Thread.sleep(100);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println(Thread.currentThread().getName() + ":" + ticket);
               ticket--;
    }
}

 同步方法的综合举例



public class Demo03 implements Runnable{
    private static int ticket=100;
    @Override
    public void run() {
    while(true){
        if("窗口一".equals(Thread.currentThread().getName())){
            boolean  result = Method();
             if(result){
                break;
            }
        }
        if("窗口二".equals(Thread.currentThread().getName())){
            synchronized (Demo03.class){
                if(ticket<=0){
                    break;
                }
                else{
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":"+ticket);
                    ticket--;
                }
            }

        }

    }
    }
          private static synchronized boolean Method()  {
              if (ticket <= 0) {
                  return true;
              } else {
                  try {
                      Thread.sleep(100);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println(Thread.currentThread().getName() + ":" + ticket);
                  ticket--;
                  return false;
              }
          }
}

public class Test03 {
    public static void main(String[] args) {
        Demo03 d =new Demo03();
        Thread t1 =new Thread(d,"窗口一");
        Thread t2 =new Thread(d,"窗口二");
        t1.start();
        t2.start();
    }
}

3 Lock锁去解决数据安全问题

3.1 Lock锁

(1)介绍:虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达在哪里加上了锁,在哪里释放了锁,JDK5以后提供了一个新的锁对象Lock

(2)Lock是一个接口(Public interface Lock)不能直接实例化,我们这里是采取它的实现类ReentrantLock来实例化的。

(3)Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。

(4)Lock提供的方法

Void lock() 获得锁

Void unLock( ) 释放锁

public class Demo04 implements Runnable{
    private int ticket=100;
    Lock lock =new ReentrantLock();//多态的形式。
    @Override
    public void run() {
        while(true) {
           try{ lock.lock();
            if(ticket <= 0) {
                break;
            }
        else{
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+":"+ticket);
                ticket--;
            } }
         finally {
               lock.unlock();}
        }
    }
}

public class Test04 {
    public static void main(String[] args) {
        Demo04 d =new Demo04();
        Thread t1 =new Thread(d,"窗口1");
        Thread t2 =new Thread(d,"窗口2");
        t1.start();
        t2.start();
    }
}

我们不用下面这种方式写lock的代码,一般采用上面的try finally代码块的形式写,虽然下面的也能运行,但会在一定的条件下会出错,导致无法释放锁,以及一系列问题。

public class Demo04 implements Runnable{
    private int ticket=100;
    Lock lock =new ReentrantLock();//多态的形式。
    @Override
    public void run() {
        while(true) {
            lock.lock();
            if(ticket <= 0) {
                break;
            }
        else{
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+":"+ticket);
                ticket--;
            }
         lock.unlock();
        }
    }
}

3.2 死锁

概念:编程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。

public class Test0 {
    public static void main(String[] args) {
        Object a =new Object();
        Object b =new Object();
        new Thread(()->{
           synchronized (a){
               System.out.println("i get a");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println("i want b");
           synchronized (b){
               System.out.println("i get b");
           }
           }
        }).start();

        new Thread(()->{
            synchronized (b){
                System.out.println("i get b");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("i want a");
                synchronized (a){
                    System.out.println("i get a");
                }
            }
        }).start();
        
    }
}
运行结果  
i get a
i get b
i want a
i want b
程序就僵在这了

由于第一个线程,占用了a锁想要b锁,而第二个线程占用了b锁想要a锁。故僵在这了,形成死锁。

你可能感兴趣的:(java,安全,开发语言)