ThreadLocal在多线程中的作用和原理浅析(模拟学生入学报到场景)

声明:本人菜鸟,发布这个帖子旨在抛砖引玉,不当之处肯定不少,请多指教。

无意中看到ThreadLocal这个类,学习一番以后发现用它解决一些多线程问题真是一个很好的思路和方法。

首先模拟一个场景:

学生持通知书找老师报到,把通知书交给老师,老师检查通知书以后,完成报到。

由于学生比较多,所以继承Thread,学生必须持有一个老师和通知书的引用,老师检查完通知书以后,学生才能参与接下来的报到。

老师检查通知书时一个耗时的过程,但在这个过程可以检查多个通知书,这也是合理的,例如他要把通知书拿到教务处盖章,

而他的办公室和教务处离得很远,走路需要时间,而盖章的时间相对可以忽略。

假设只有一个老师,有一个通知书的引用,学生必须把通知书set给老师,老师检查通知书以后,学生可以调用老师的doProcedure方法办理入学手续。

学生类:Freshman.java

public class Freshman extends Thread {

    private Teacher teacher;
    private Admission admission;
    
    public Freshman(Teacher teacher, Admission admission) {
        this.teacher = teacher;
        this.admission = admission;
    }

 

    public void run() {
        teacher.setAdmission(admission);
        teacher.doProcedure();
    }
   
   
}

老师类:Teacher.java

public class Teacher {

    private Admission admission;

    public Admission getAdmission() {
        return admission;
    }

    public void setAdmission(Admission admission) {
        this.admission = admission;
        checkAdmission();
    }

    public void doProcedure() {
        System.out.println("Student " + Thread.currentThread().getId()
                + " do procedure with admission " + admission);
    }

    private void checkAdmission() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

通知书类:Admission.java

public class Admission {

    private String id;
   
    public Admission(String id) {
        this.id = id;
    }
   
    @Override
    public String toString() {
        return id;
    }
   
}

运行类:Main.java

public class Main {

    public static void main(String[] args) {
        Teacher teacher = new Teacher();
        for (int i = 0; i < 10; i++) {
            Admission admission = new Admission("adminision-" + i);
            Freshman fm = new Freshman(teacher, admission);
            fm.start();
        }
    }
   
}

运行结果发现出了问题:

Student 10 do procedure with admission adminision-8
Student 14 do procedure with admission adminision-8
Student 11 do procedure with admission adminision-8
Student 8 do procedure with admission adminision-8

从上面代码可以看出,学生8在调用setAdmission(admission)后,老师检查证书是一个耗时的操作,而在这个操作过程中,

其他的学生也可以调用教师的setAdmission(admission),导致教师持有的admission应用发生了改变,所以输出的结果不正确。

解决这个问题的常见方法是用synchronized关键字,在一个学生取得这个老师的引用以后,其他学生不能再和教师交流,

等这个学生完成入学手续以后,其他学生才能把admission set给这个老师。

学生代码修改如下:

public class Freshman extends Thread {

    private Teacher teacher;
    private Admission admission;
   
   
   
    public Freshman(Teacher teacher, Admission admission) {
        this.teacher = teacher;
        this.admission = admission;
    }

 

    public void run() {
        synchronized(teacher) {
            teacher.setAdmission(admission);
            teacher.doProcedure();
        }
    }
   
   
}

运行结果:

Student 8 do procedure with admission adminision-0
Student 17 do procedure with admission adminision-9
Student 16 do procedure with admission adminision-8
Student 15 do procedure with admission adminision-7
Student 14 do procedure with admission adminision-6
Student 13 do procedure with admission adminision-5
Student 12 do procedure with admission adminision-4
Student 11 do procedure with admission adminision-3
Student 10 do procedure with admission adminision-2
Student 9 do procedure with admission adminision-1

这回的输出结果是符合预期的,但等了好长时间才执行完,当然主要不合理的原因是用了synchronized导致老师同一时间只能处理一个学生的手续,其他学生不能把admission set给老师,那假如学生1把admission set给老师以后就去睡觉了,不调用老师的doProcedure();那么老师和其他学生只能等他睡醒了。

修改学生代码为:

public class Freshman extends Thread {

    private Teacher teacher;
    private Admission admission;
   
   
   
    public Freshman(Teacher teacher, Admission admission) {
        this.teacher = teacher;
        this.admission = admission;
    }

 

    public void run() {
        synchronized(teacher) {
            teacher.setAdmission(admission);
            if (getId() == 16) {
                try {
                    sleep(1000 * 10);
                    System.out.println("I sleeped for a while");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            teacher.doProcedure();
        }
    }
   
   
}

 

输出结果是:

Student 8 do procedure with admission adminision-0
I sleeped for a while
Student 16 do procedure with admission adminision-8
Student 17 do procedure with admission adminision-9
Student 15 do procedure with admission adminision-7
Student 14 do procedure with admission adminision-6
Student 13 do procedure with admission adminision-5
Student 12 do procedure with admission adminision-4
Student 11 do procedure with admission adminision-3
Student 10 do procedure with admission adminision-2
Student 9 do procedure with admission adminision-1

不合理的情况出现了,学生16在sleep的时候,程序不能执行下去了,直到学生16醒过来。

所以用synchronized确实解决了问题,但使得整个过程效率低,执行慢。

接下来把student类改成最开始的样子,用ThreadLocal模拟这个过程:

将teacher类改成:

public class Teacher {

   
    private ThreadLocal<Admission> locals = new ThreadLocal<Admission>();


    public void setAdmission(Admission admission) {
        locals.set(admission);
        checkAdmission();
    }

    public void doProcedure() {
        System.out.println("Student " + Thread.currentThread().getId()
                + " do procedure with admission " + locals.get());
    }

    private void checkAdmission() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

Student 9 do procedure with admission adminision-1
Student 13 do procedure with admission adminision-5
Student 10 do procedure with admission adminision-2
Student 8 do procedure with admission adminision-0
Student 14 do procedure with admission adminision-6
Student 17 do procedure with admission adminision-9
Student 11 do procedure with admission adminision-3
Student 12 do procedure with admission adminision-4
Student 16 do procedure with admission adminision-8
Student 15 do procedure with admission adminision-7

可见,结果是符合预期的,和使用synchronized的时候的结果一致,但耗时明显比使用synchronized少。

阅读ThreadLocal的代码可以发现,它将当前的线程和需要被多个线程修改的变量作为一个 key-value对保存到map中了,当调用set的时候,ThreadLocal将当前的Thread和参数保存起来,当调用get的时候,ThreadLocal会以当前的Thread为key去取得它对应的value,也就是当有多个学生给老师set admission的时候,ThreadLocal将保存一份学生的Admission的副本,并和当前学生建立对应关系,例如填了一个花名册,然后在学生调用doProcedure()的时候,老师 根据这个花名册为每个学生办理手续。

由于肤浅的理解和笨拙的表达能力,我也不能说清楚ThreadLocal的工作原理,接下来我自己实现一个简单的ThreadLocal,将Teacher类修改为:

public class Teacher {

   
    private MyThreadLocal<Admission> locals = new MyThreadLocal<Admission>();


    public void setAdmission(Admission admission) {
        locals.set(admission);
        checkAdmission();
    }

    public void doProcedure() {
        System.out.println("Student " + Thread.currentThread().getId()
                + " do procedure with admission " + locals.get());
    }

    private void checkAdmission() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
   
    class MyThreadLocal<T> {
        Map<Thread, T> locals = new HashMap<Thread, T>();
       
        public void set(T t) {
            locals.put(Thread.currentThread(), t);
        }
       
        public T get() {
            return locals.get(Thread.currentThread());
        }
    }
   
}

 

输出结果是:

Student 9 do procedure with admission adminision-1
Student 11 do procedure with admission adminision-3
Student 10 do procedure with admission adminision-2
Student 8 do procedure with admission adminision-0
Student 15 do procedure with admission adminision-7
Student 12 do procedure with admission adminision-4
Student 14 do procedure with admission adminision-6
Student 16 do procedure with admission adminision-8
Student 13 do procedure with admission adminision-5
Student 17 do procedure with admission adminision-9

效果和使用java.lang.ThreadLocal是一样的,结果也是符合预期的。

 

谢谢阅读,请批评指教。

 

 

 

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值