如果你这样回答“什么是线程安全”,面试官都会对你刮目相看 不是线程的安全面试官问“什么是线程安全”如果你不能很好的回答那就请往下看吧。论语中有句话叫“学而优则仕”相信很多人都觉得是“学习好了可以做官”。然而这样理解却是错的。切记望文生义。同理“线程安全”也不是指线程的安全而是指内存的安全。为什么如此说呢这和操作系统有关。目前主流操作系统都是多任务的即多个进程同时运行。为了保证安全每个进程只能访问分配给自己的内存空间而不能访问别的进程的这是由操作系统保障的。在每个进程的内存空间中都会有一块特殊的公共区域通常称为堆内存。进程内的所有线程都可以访问到该区域这就是造成问题的潜在原因。假设某个线程把数据处理到一半觉得很累就去休息了一会回来准备接着处理却发现数据已经被修改了不是自己离开时的样子了。可能被其它线程修改了。比如把你住的小区看作一个进程小区里的道路/绿化等就属于公共区域。你拿1万块钱往地上一扔就回家睡觉去了。睡醒后你打算去把它捡回来发现钱已经不见了。可能被别人拿走了。因为公共区域人来人往你放的东西在没有看管措施时一定是不安全的。内存中的情况亦然如此。所以线程安全指的是在堆内存中的数据由于可以被任何线程访问到在没有限制的情况下存在被意外修改的风险。即堆内存空间在没有保护机制的情况下对多线程来说是不安全的地方因为你放进去的数据可能被别的线程“破坏”。那我们该怎么办呢解决问题的过程其实就是一个取舍的过程不同的解决方案有不同的侧重点。私有的东西就不该让别人知道现实中很多人都会把1万块钱藏着掖着不让无关的人知道所以根本不可能扔到大马路上。因为这钱是你的私有物品。在程序中也是这样的所以操作系统会为每个线程分配属于它自己的内存空间通常称为栈内存其它线程无权访问。这也是由操作系统保障的。如果一些数据只有某个线程会使用其它线程不能操作也不需要操作这些数据就可以放入线程的栈内存中。较为常见的就是局部变量。double avgScore(double[] scores) { double sum 0; for (double score : scores) { sum score; } int count scores.length; double avg sum / count; return avg; }这里的变量sumcountavg都是局部变量它们都会被分配在线程栈内存中。假如现在A线程来执行这个方法这些变量会在A的栈内存分配。与此同时B线程也来执行这个方法这些变量也会在B的栈内存中分配。也就是说这些局部变量会在每个线程的栈内存中都分配一份。由于线程的栈内存只能自己访问所以栈内存中的变量只属于自己其它线程根本就不知道。就像每个人的家只属于自己其他人不能进来。所以你把1万块钱放到家里其他人是不会知道的。且一般还会放到某个房间里而不是仍在客厅的桌子上。所以把自己的东西放到自己的私人地盘是安全的因为其他人无法知道。而且越隐私的地方越好。大家不要抢人人有份相信聪明的你已经发现上面的解决方案是基于“位置”的。因为你放东西的“位置”只有你自己知道或能到达所以东西是安全的因此这份安全是由“位置”来保障的。在程序里就对应于方法的局部变量。局部变量之所以是安全的就是因为定义它的“位置”是在方法里。这样一来安全是达到了但是它的使用范围也就被限制在这个方法里了其它方法想用也不用了啦。现实中往往会有一个变量需要多个方法都能够使用的情况此时定义这个变量的“位置”就不能在方法里面了而应该在方法外面。即从方法的局部变量变为类的成员变量其实就是“位置”发生了变化。那么按照主流编程语言的规定类的成员变量不能再分配在线程的栈内存中而应该分配在公共的堆内存中。其实也就是变量在内存中的“位置”发生了变化由一个私有区域来到了公共区域。因此潜在的安全风险也随之而来。那怎么保证在公共区域的东西安全呢答案就是大家不要抢人人有份。设想你在街头免费发放矿泉水来了1万人你却只有1千瓶水结果可想而知一拥而上场面失守。但如果你有10万瓶水大家一看水多着呢不用着急一个个排着队来因为肯定会领到。东西多了自然就不值钱了从另一个角度来说也就安全了。大街上的共享单车现在都很安全因为太多了到处都是都长得一样所以连搞破坏的人都放弃了。因此要让一个东西安全就疯狂的copy它吧。回到程序里要让公共区域堆内存中的数据对于每个线程都是安全的那就每个线程都拷贝它一份每个线程只处理自己的这一份拷贝而不去影响别的线程的这不就安全了嘛。相信你已经猜到了我要表达的就是ThreadLocal类了。class StudentAssistant { ThreadLocalString realName new ThreadLocal(); ThreadLocalDouble totalScore new ThreadLocal(); String determineDegree() { double score totalScore.get(); if (score 90) { return A; } if (score 80) { return B; } if (score 70) { return C; } if (score 60) { return D; } return E; } double determineOptionalcourseScore() { double score totalScore.get(); if (score 90) { return 10; } if (score 80) { return 20; } if (score 70) { return 30; } if (score 60) { return 40; } return 60; } }这个学生助手类有两个成员变量realName和totalScore都是ThreadLocal类型的。每个线程在运行时都会拷贝一份存储到自己的本地。A线程运行的是“张三”和“90”那么这两个数据“张三”和“90”是存储到A线程对象Thread类的实例对象的成员变量里去了。假设此时B线程也在运行是“李四”和“85”那么“李四”和“85”这两个数据是存储到了B线程对象Thread类的实例对象的成员变量里去了。线程类Thread有一个成员变量类似于Map类型的专门用于存储ThreadLocal类型的数据。从逻辑从属关系来讲这些ThreadLocal数据是属于Thread类的成员变量级别的。从所在“位置”的角度来讲这些ThreadLocal数据是分配在公共区域的堆内存中的。说的直白一些就是把堆内存中的一个数据复制N份每个线程认领1份同时规定好每个线程只能玩自己的那份不准影响别人的。需要说明的是这N份数据都还是存储在公共区域堆内存里的经常听到的“线程本地”是从逻辑从属关系上来讲的这些数据和线程一一对应仿佛成了线程自己“领地”的东西了。其实从数据所在“位置”的角度来讲它们都位于公共的堆内存中只不过被线程认领了而已。这一点我要特地强调一下。其实就像大街上的共享单车。原来只有1辆大家抢着骑老出问题。现在从这1辆复制出N辆每人1辆各骑各的问题得解。共享单车就是数据你就是线程。骑行期间这辆单车从逻辑上来讲是属于你的从所在位置上来讲还是在大街上这个公共区域的因为你发现每个小区大门口都贴着“共享单车禁止入门”。哈哈哈哈。共享单车是不是和ThreadLocal很像呀。再重申一遍ThreadLocal就是把一个数据复制N份每个线程认领一份各玩各的互不影响。只能看不能摸放在公共区域的东西只是存在潜在的安全风险并不是说一定就不安全。有些东西虽然也在公共区域放着但也是十分安全的。比如你在大街上放一个上百吨的石头雕像就非常安全因为大家都弄不动它。再比如你去旅游时经常发现一些珍贵的东西会被用铁栅栏围起来上面挂一个牌子写着“只能看不能摸”。当然可以国际化一点“only lookdont touch”。这也是很安全的因为光看几眼是不可能看坏的。回到程序里这种情况就属于只能读取不能修改。其实就是常量或只读变量它们对于多线程是安全的想改也改不了。class StudentAssistant { final double passScore 60; }比如把及格分数设定为60分在前面加上一个final这样所有线程都动不了它了。这就很安全了。小节一下以上三种解决方案其实都是在“耍花招”。第一种找个只有自己知道的地方藏起来当然安全了。第二种每人复制1份各玩各的互不影响当然也安全了。第三种更狠了直接规定只能读取禁止修改当然也安全了。是不是都在“避重就轻”呀。如果这三种方法都解决不了该怎么办呢Dont worryjust continue reading。没有规则那就先入为主前面给出的三种方案有点“理想化”了。现实中的情况其实是非常混乱嘈杂的没有规则的。比如在中午高峰期你去饭店吃饭进门后发现只剩一个空桌子了你心想先去点餐吧回来就坐这里吧。当你点完餐回来后发现已经被别人捷足先登了。因为桌子是属于公共区域的物品任何人都可以坐那就只能谁先抢到谁坐。虽然你在人群中曾多看了它一眼但它并不会记住你容颜。解决方法就不用我说了吧让一个人在那儿看着座位其它人去点餐。这样当别人再来的时候你就可以理直气壮的说“不好意思这个座位我已经占了”。我再次相信聪明的你已经猜到了我要说的东西了没错就是互斥锁。回到程序里如果公共区域堆内存的数据要被多个线程操作时为了确保数据的安全或一致性需要在数据旁边放一把锁要想操作数据先获取锁再说吧。假设一个线程来到数据跟前一看发现锁是空闲的没有人持有。于是它就拿到了这把锁然后开始操作数据干了一会活累了就去休息了。这时又来了一个线程发现锁被别人持有着按照规定它不能操作数据因为它无法得到这把锁。当然它可以选择等待或放弃转而去干别的。第一个线程之所以敢大胆的去睡觉就是因为它手里拿着锁呢其它线程是不可能操作数据的。当它回来后继续把数据操作完就可以把锁给释放了。锁再次回到空闲状态其它线程就可以来抢这把锁了。还是谁先抢到锁谁操作数据。class ClassAssistant { double totalScore 60; final Lock lock new Lock(); void addScore(double score) { lock.obtain(); totalScore score; lock.release(); } void subScore(double score) { lock.obtain(); totalScore - score; lock.release(); } }假定一个班级的初始分数是60分这个班级抽出10名学生来同时参加10个不同的答题节目每个学生答对一次为班级加上5分答错一次减去5分。因为10个学生一起进行所以这一定是一个并发情形。