• Welcome to the world's largest Chinese hacker forum

    Welcome to the world's largest Chinese hacker forum, our forum registration is open! You can now register for technical communication with us, this is a free and open to the world of the BBS, we founded the purpose for the study of network security, please don't release business of black/grey, or on the BBS posts, to seek help hacker if violations, we will permanently frozen your IP and account, thank you for your cooperation. Hacker attack and defense cracking or network Security

    business please click here: Creation Security  From CNHACKTEAM

Recommended Posts

前言

线程安全是非常基础的知识,但是基础不代表简单。一个人的基本功能往往决定了他能否写出高质量高性能的代码。关于什么是synchronized,Lock,volatile,相信大家都能略知一二,但是如果你知道所有的概念,你就会很困惑,一不小心就能写出死锁。

本文将基于生产者-消费者模型和具体案例,一步步讲解线程安全问题的诞生背景和解决方案,帮助你掌握synchronized的应用场景以及与Lock的区别。

1. 线程安全问题的诞生背景以及解决方式

1.1 为什么线程间需要通信?

线程是CPU执行的基本单位。为了提高CPU的利用率,模拟多个应用程序同时运行的场景,就衍生出了多线程的概念。

在JVM架构下,堆内存和方法区是可以被线程共享的,为什么要这样设计呢?

例如,简要描述:

现在做一个网络请求,请求响应后渲染到手机界面。Android为了提高用户体验,把主线程当做UI线程,只渲染界面。耗时的操作应该交给工作线程。如果UI线程执行耗时的操作,可能会被阻塞,最直观的感觉就是界面卡顿。网络请求属于IO操作,所以会被阻塞。如前所述,UI线程是不允许被阻塞的,所以网络请求必须扔给worker线程,但是如何在得到数据包后再交付给UI线程呢?最常见的方式是回调接口,将HTTP包解析成本地模型,然后通过接口将本地模型对应的堆内存地址值传递给UI线程。

一个线程把堆内存对象的地址值给UI线程的过程就是线程间的通信,也是JVM把堆内存设置为线程共享的原因。关于线程间通信的一句很好理解的话是:‘多个线程操作同一个资源’,位于堆内存或者方法区。

1.2 单生产单消费引发的安全问题

'多线程操作同一个资源'听起来很简单,但不知道你一不小心,就可能造成致命的问题。哟,这个怎么说?别急,听我解释。

案例

现有一家车辆公司,主要经营四轮车和两轮自行车。工人负责生产,业务员负责销售。

以上案例如何通过应用实现?想法如下:

定义一个车辆资源类,可以设置为汽车和自行车。

publicclassResource{

//一辆车对应一个id

privateintid

//汽车名称

privateStringname

//汽车的车轮数量

privateintwheelNumber

//标签(后面会用到)

privatebooleanflag=false

.

忽略setter和getter

.

@覆盖

publicStringtoString(){

return ' id=' id '-name=' name '-wheel number=' wheel number;

}

}

为生产者定义一个工人线程任务,专门用于生产四轮汽车和两轮自行车。

publiclassinputimplementsrunnable {

私人资源者;

publicInput(Resourcer){

this.r=r

}

publicvidrun(){

//无限量生产车辆

for(inti=0;i ){

if(i%2==0){

r . setid(I);//设置汽车的id

r . set name(' car ');

//设置车类型
                r.setWheelNumber(4);//设置车的轮子数
            }else{
                r.setId(i);//设置车的id
                r.setName("电动车");//设置车类型
                r.setWheelNumber(2);//设置车的轮子数
            }
        }
    }
}

定义一个销售员线程任务,专门用来销售车辆,为消费者

public class Output implements Runnable{
    private Resource r;
    public Output(Resource r){
        this.r = r;
    }
    public void run() {
        //无限消费车辆
        for(;;){
            //消费车辆
            System.out.println(r.toString());
        }
    }
}

开始生产、消费

//资源对象,对应车辆
Resource r = new Resource();
//生产者runnable,对应工人
Input in = new Input(r);
//消费者runnable,对应销售员
Output out = new Output(r);
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
//开启生产者线程
t1.start();
//开启消费者线程
t2.start();

打印结果:

...
id=51--- name=电动车--- wheelNumber=2
id=52--- name=小汽车--- wheelNumber=2
...

一切有条不紊的进行,老板数着钞票那叫一个开心。吃水不忘挖井人,正当老板准备给员工发奖金时,出现了一个严重问题 编号为52的小汽车少装了俩轮子!!!得,奖金不仅没了,还得连夜排查问题

导致原因:

tips:流程对应上面打印结果。下同

  • 生产者线程得到CPU执行权,将name和wheelNumber分别设置为电动车和2,随后CPU切换到了消费者线程。
  • 消费者线程得到CPU执行权,此时name和wheelNumber别为电动车和2,随后打印name=电动车--- wheelNumber=2,CPU切换到了生产者线程。
  • 生产者线程再次得到CPU执行权,将name设置为小汽车(未对wheelNumber进行设置),此时name和wheelNumber分别为小汽车和2,CPU切换到了消费者线程。
  • 消费者线程得到CPU执行权,此时name和wheelNumber别为小汽车和2,随后打印name=小汽车--- wheelNumber=2

工人:"生产到一半你销售员就拿去卖了,这锅我不背"

解决方案:

导致原因其实就是生产者对Resource的一次操作还未结束,消费者强行介入了。此时可以引入synchronized关键字,使得生产者一次工作结束前消费者不得介入

更改后的代码如下:

#Input
public void run() {
   //无限生产车辆
   for(int i =0;;i++){
       synchronized(r){
           if(i%2==0){
               r.setId(i);//设置车的id
               r.setName("小汽车");//设置车类型
               r.setWheelNumber(4);//设置车的轮子数
           }else{
               r.setId(i);//设置车的id
               r.setName("电动车");//设置车类型
               r.setWheelNumber(2);//设置车的轮子数
           }
       }
    }      
}
    
#Output
public void run() {
   for(;;){
       synchronized(r){
           //消费车辆
           System.out.println(r.toString());
       }
   }
}

生产者和消费者for循环中都加了一个synchronized,对应的锁是r,修改后重新执行。

...
id=79--- name=电动车--- wheelNumber=2
id=80--- name=小汽车--- wheelNumber=4
id=80--- name=小汽车--- wheelNumber=4
...

一切又恢复了正常。但又暴露出一个更严重的问题,编号为80的小汽车被消费(销售)了两次

也既销售员把一辆车卖给了两个客户,真乃商业奇才啊!!!

导致原因:
  • 生产者线程得到CPU执行权,将name和wheelNumber分别设置为小汽车和4,随后CPU执行权切换到了消费者线程。
  • 消费者线程得到CPU执行权,此时name和wheelNumber别为小汽车和4,随后打印name=小汽车--- wheelNumber=4,但消费后 CPU执行权并未切换到生产者线程,而是由消费者线程继续执行,于是就出现了编号为80的小汽车被打印(消费)了两次
解决方案:

产生问题的原因就是消费者把资源消费后未处于等待状态,而是继续消费。此时可以引入wait、notify机制,使得销售员售卖完一辆车后处于等待状态,当工人重新生产一辆新车后再通知销售员,销售员接收到工人消息后再进行售卖。

更改后的代码如下:

#Input
public void run() {
    //无限生产车辆
    for(int i =0;;i++){
         synchronized(r){
              //flag为true的时候代表已经生产过,此时将当前线程wait,等待消费者消费
              if(r.isFlag()){
                  try {
                      r.wait();
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
              if(i%2==0){
                  r.setId(i);//设置车的id
                  r.setName("小汽车");//设置车的型号
                  r.setWheel(4);//设置车的轮子数
              }else{
                  r.setId(i);//设置车的id
                  r.setName("电动车");//设置车的型号
                  r.setWheel(2);//设置车的轮子数
              }
              r.setFlag(true);
              //将线程池中的线程唤醒
              r.notify();
        }
    }
}
#Output
public void run() {
    //无限消费车辆
    for(;;){
        synchronized(r){
             //flag为false,代表当前生产的车已经被消费掉,
             //进入wait状态等待生产者生产
             if(!r.isFlag()){
                 try {
                     r.wait();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
             //消费车辆
             System.out.println(r.toString());
             r.setFlag(false);
             //将线程池中的线程唤醒
             r.notify();
        }
    }
}

打印结果:

...
id=129--- name=电动车--- wheelNumber=2
id=130--- name=小汽车--- wheelNumber=4
id=131--- name=电动车--- wheelNumber=2
...

这次真的没问题了,工人和销售员都如愿以偿的拿到了老板发的奖金

注意点1:

synchronized括号内传入的是一把锁,可以是任意类型的对象,生产者消费者必须使用同一把锁才能实现同步操作。这样设计的目的是为了更灵活使用同步代码块,否则整个进程那么多synchronized,锁谁不锁谁根本不明确。

注意点2:

wait、notify其实是object的方法,它们只能在synchronized代码块内由锁进行调用,否则就会抛异常。每一把锁对应线程池的一块区域,被wait的线程会被放入到锁对应的线程池区域,并且释放锁。notify会随机唤醒锁对应线程池区域的任意一个线程,线程被唤醒后会重新上锁,注意是随机唤醒任意一个线程

2. 由死锁问题看显示锁 Lock 的应用场景

2.1 何为死锁?

关于死锁,顾名思义应该是锁死了,它可以使线程处于假死状态但又没真死,卡在半道又无法被回收。

举个例子:

class Deadlock1 implements Runnable{
    private Object lock1;
    private Object lock2;
    public Deadlock1(Object obj1,Object obj2){
        this.lock1 = obj1;
        this.lock2 = obj2;
    }
    public void run() {
        while(true){
            synchronized(lock1){
                System.out.println("Deadlock1----lock1");
                synchronized(lock2){
                    System.out.println("Deadlock1----lock2");
                }
            }
        }
    }
}
class Deadlock2 implements Runnable{
    private Object lock1;
    private Object lock2;
    public Deadlock2(Object obj1,Object obj2){
        this.lock1 = obj1;
        this.lock2 = obj2;
    }
    public void run() {
        while(true){
            synchronized(lock2){
                System.out.println("Deadlock2----lock2");
                synchronized(lock1){
                    System.out.println("Deadlock2----lock1");
                }
            }
        }
    }
}
#运行
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
      Deadlock1 d1 = new Deadlock1(lock1,lock2);
      Deadlock2 d2 = new Deadlock2(lock1,lock2);
      Thread t1 = new Thread(d1);
      Thread t2 = new Thread(d2);
      t1.start();
      t2.start();
}

运行后打印结果:

Deadlock1----lock1
Deadlock2----lock2

run()方法中写的是无限循环,按理来说应该是无限打印。但程序运行后,在我没有终止控制台的情况下只打印了这两行数据。实际上这一过程引发了死锁,具体缘由如下:

  • 线程t1执行,判断了第一个同步代码块,此时锁lock1可用,于是持着锁lock1进入了第一个同步代码块,打印了:Deadlock1----lock1,随后线程切换到了线程t2
  • 线程t2执行,判断第一个同步代码块,此时锁lock2可用,于是持着锁lock2进入了第一个同步代码块,打印了:Deadlock2----lock2,接着向下执行,判断锁lock1不可用(因为锁lock1已经被线程t1所占用),于是线程t1进行等待.随后再次切换到线程t1
  • 线程t1执行,判断第二个同步代码块,此时锁lock2不可用(因为所lock2已经被线程t2所占用),线程t1也进入了等待状态

通过以上描述可知:线程t1持有线程t2需要的锁进行等待,线程t2持有线程t1所需要的锁进行等待,两个线程各自拿着对方需要的锁处于一种僵持现象,导致线程假死即死锁。

以上案例只是死锁的一种,死锁的标准就是判断线程是否处于假死状态

2.2 多生产多消费场景的死锁如何避免?

第一小节主要是在讲单生产单消费,为了进一步提升运行效率可以适当引入多生产多消费,既多个生产者多个消费者。继续引用第一小节案例,稍作改动:

//生产者任务
class Input implements Runnable{
    private Resource r;
    //将i写为成员变量而不是写在for循环中是为了方便讲解下面多生产多消费的内容,没必要纠结这点
    private int i = 0;
    public Input(Resource r){
        this.r = r;
    }
    public void run() {
        //无限生产车辆
        for(;;){
            synchronized(r){
                //flag为true的时候代表已经生产过,此时将当前线程wait,等待消费者消费
                if(r.isFlag()){
                    try {
                        r.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                if(i%2==0){
                    r.setId(i);//设置车的id
                    r.setName("小汽车");//设置车的型号
                    r.setWhell(4);//设置车的轮子数
                }else{
                    r.setId(i);//设置车的id
                    r.setName("电动车");//设置车的型号
                    r.setWhell(2);//设置车的轮子数
                }
                i++;
                r.setFlag(true);
                //将线程池中的线程唤醒
                r.notify();
            }
        }
    }
}

public static void main(String[] args) {
        Resource r = new Resource();
        Input in = new Input(r);
        Output out = new Output(r);
        Thread in1= new Thread(in);
        Thread in2 = new Thread(in);
        Thread out1 = new Thread(out);
        Thread out2 = new Thread(out);
        in1.start();//开启生产者1线程
        in2 .start();//开启生产者2线程
        out1 .start();//开启消费者1线程
        out2 .start();//开启消费者2线程
}

运行结果:

id=211--- name=自行车--- wheelNumber=2
id=220--- name=小汽车--- wheelNumber=4
id=220--- name=小汽车--- wheelNumber=4
id=220--- name=小汽车--- wheelNumber=4
...

安全问题又产生了,编号为211-220的车辆未被打印,也即生产了未被消费。同时编号为220的车辆被打印了三次。先别着急,我接着给大家分析:

  • 生产者线程in1得到执行权,生产了id为211的车辆,将flag置为true,循环回来再判断标记为true,此时执wait()方法进入等待状态
  • 生产者线程in2得到执行权,判断标记为true,执行wait()方法进入等待状态。
  • 消费者线程out1得到执行权,判断标记为true,不进行等待而是选择了消费id为211的车辆,消费完毕后将标记置为false并执行notify()将线程池中的任意一个线程给唤醒,假设唤醒的是in1
  • 生产者线程in1再次得到执行权,此时生产者线程in1被唤醒后不会判断标记而是选择生产一辆id为1的车辆,随后将标记置为true并执行notify()将线程池中任意一个线程给唤醒,假设唤醒的是in2
  • 生产者线程in2再次得到执行权,此时生产者线程in2被唤醒后不会判断标记而是直接生产了一辆id为212的车辆,随后唤醒in1生产id为213的车辆,再唤醒in2.....

以上即为编号211-220的车辆未被打印的原因,编号为220车辆重复打印同理。

如何解决?其实很简单,将生产者和消费者判断flag地方的if更改成while,被唤醒后重新再判断标记即可。代码就不重复贴了,运行结果如下:

id=0--- name=小汽车--- wheelNumber=4
id=1--- name=电动车--- wheelNumber=2
id=2--- name=小汽车--- wheelNumber=4
id=3--- name=电动车--- wheelNumber=2
id=4--- name=小汽车--- wheelNumber=4

看起来很正常,但在我没有关控制台的情况下打印到编号为4的车辆时停了,没错,死锁出现了,具体原因如下:

  • 线程in1开始执行,生产了一辆车将flag置为true,循环回来判断flag进入wait()状态,此时线程池中进行等待的线程有:in1
  • 线程in2开始执行,判断flag为true进入wait()状态,此时线程池中进行等待的线程有:in1,in2
  • 线程out1开始执行,判断flag为true,消费了一辆汽车将flag置为false并唤醒一个线程,我们假定唤醒的为in1(这里需要注意,被唤醒并不意味着会立刻执行,只是当前具备着执行资格但并不具备执行权),线程out1循环回来判读flag进入wait状态,此时线程池中的线程有in2,out1,随后out2得到执行权
  • 线程out2开始执行,判断标记为false,进入等待状态,此时线程池中的线程有in2,out1,out2
  • 线程in1开始执行,判断标记为false,生产了一辆汽车必将flag置为true并唤醒线程池中的一个线程,我们假定唤醒的是in2,随后in1循环判断flag进入wait()状态,此时线程池中的线程有in1,out1,out2
  • 线程int2得到执行权,判断标记为false,进入wait()状态,此时线程池中的线程有in1,in2,out1,out2

所有生产者消费者线程都被wait掉了,导致了死锁现象的产生。根本原因在于生产者wait后理应唤醒消费者,而不是唤醒生产者,object还有一个方法notifyAll(),它可以唤醒锁对应线程池区域的所有线程,所以将notify替换成notifyAll即可解决以上死锁问题。

2.3 通过 Lock 优雅的解决死锁问题

2.2提到的notifyAll是可以解决死锁问题,但不够优雅,因为notifyAll()会唤醒对应线程池所有线程,单其实只需要唤醒一个即可,多了就会造成线程反复被wait,进而会造成性能问题。所以后来Java在1.5版本引入了显示锁Lock的概念,它可以灵活的指定wait、notify的作用域,专门用来解决此类问题。

通过显示锁Lock对2.2死锁问题改进后代码如下:

#生产者
class Input implements Runnable{
    private Resource r;
    private int i = 0;
    private Lock lock;
    private Condition in_con;//生产者监视器
    private Condition out_con;//消费者监视器
    public Input(Resource r,Lock lock,Condition in_con,Condition out_con){
        this.r = r;
        this.lock = lock;
        this.in_con = in_con;
        this.out_con = out_con;
    }
    public void run() {
        //无限生产车辆
        for(;;){
            lock.lock();//获取锁
            //flag为true的时候代表已经生产过,此时将当前线程wait,等待消费者消费
            while(r.isFlag()){
                try {
                    in_con.await();//跟wait作用相同
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if(i%2==0){
                r.setId(i);//设置车的id
                r.setName("小汽车");//设置车的型号
                r.setWhell(4);//设置车的轮子数
            }else{
                r.setId(i);//设置车的id
                r.setName("电动车");//设置车的型号
                r.setWhell(2);//设置车的轮子数
            }
            i++;
            r.setFlag(true);
            //将线程池中的消费者线程唤醒
            out_con.signal();
            lock.unlock();//释放锁
        }
    }
}
//消费者
class Output implements Runnable{
    private Resource r;
    private Lock lock;
    private Condition in_con;//生产者监视器
    private Condition out_con;//消费者监视器
    public Output(Resource r,Lock lock,Condition in_con,Condition out_con){
        this.r = r;
        this.lock = lock;
        this.in_con = in_con;
        this.out_con = out_con;
    }
    public void run() {
        //无限消费车辆
        for(;;){
            lock.lock();//获取锁
            while(!r.isFlag()){
                try {
                    out_con.await();//将消费者线程wait
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(r.toString());
            r.setFlag(false);
            in_con.signal();//唤醒生产者线程
            lock.unlock();//释放锁
        }
    }
}
public static void main(String[] args) {
        Resource r = new Resource();
        Lock lock = new ReentrantLock();
        //生产者监视器
        Condition in_con = lock.newCondition();
        //消费者监视器
        Condition out_con = lock.newCondition();
        Input in = new Input(r,lock,in_con,out_con);
        Output out = new Output(r,lock,in_con,out_con);
        Thread t1 = new Thread(in);
        Thread t2 = new Thread(in);
        Thread t3 = new Thread(out);
        Thread t4 = new Thread(out);
        t1.start();//开启生产者线程
        t2.start();//开启生产者线程
        t3.start();//开启消费者线程
        t4.start();//开启消费者线程
    }

这次就真的没问题了。其中Lock对应synchronized,Condition为Lock下的监视器,每一个监视器对应一个wait、notify作用域,注释写的很清楚就不再赘述

综上所述

  • 多线程是用来提升CUP使用率的
  • 多个线程访问同一资源可能会引发安全问题
  • synchronized配合wait、notify可以解决线程安全问题
  • Lock可以解决synchronized下wait、notify的局限性
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now