从JVM的内存模型上看,线程安全的问题是由于多个线程争夺共享内存的同一共享资源而产生的。
多个线程争夺共享内存的同一共享资源就一定会产生线程安全问题吗?
不一定,如果多条线程不修改共享资源,那么不会产生线程安全问题。
线程安全产生的两个原因
可见性
多个线程访问同一个共享变量时,其中一个线程对这个共享变量值的修改,其他线程能够立刻获得修改以后的值。
原子性
和数据库事务中的原子性一样,满足原子性特性的操作是不可中断的,要么全部执行成功要么全部执行失败
例子1
User类是一个实体类,提供了set方法修改密码。
UserServlet中提供了setPass的方法修改密码。
1 | class User { |
创建两个线程访问UserServlet:
1 | public static void main(String[] args) { |
这里的共享资源是什么?
误区1: 被抢占的资源是User
实际上,User对象是在每个线程的独立工作内存中创建的。它并不是共享资源。
这里的共享资源是UserServlet对象。两个线程共享着主内存区域创建的UserServlet对象。当每个线程调用setPass方法的时候,争夺UserServlet对象的使用权来修改自己独有内存区域的User对象的name和password。
这里是线程安全的吗?
线程安全!虽然两个线程共享着同一个UserServlet对象,但是线程只是调用UserServlet里的setPass方法来修改线程自己内存区域的对象,而不是对共享资源的修改,所以不存在线程安全的问题。
而且UserServlet不存在全局变量或静态变量被修改。
new出来的对象不是存放在堆中吗?而堆是共享内存,为什么不存在线程安全问题?
JVM的内存模型中,方法区和堆是共享内存区,而虚拟机栈、本地方法栈、程序技术器是每个线程独有的。
虽然new出来的对象保存在堆中,但是对象的引用保存在线程独有的虚拟机栈中。并不会被别的线程修改,所以不存在线程安全的问题。
运行结果
不管运行多少次,结果都一样。
1 | Thread[Thread-0,5,main]:李四:777777 |
总结
多条线程抢占共享区的对象使用权,调用对象的方法,并不会引起线程安全的问题。
例子1修改
这个例子与上面的不同的地方在于UserServlet中有一个默认User对象。提供给外部一个根据name和password修改默认User对象的方法。
1 | class User { |
1 | public static void main(String[] args) { |
这里的共享资源是什么?
这里的共享资源是UserServlet对象。更具体地说,两个线程共享着UserServlet对象中的User对象。
这里存在线程安全吗?
存在!两个线程共享一个User对象。当线程对User对象进行修改时,就会出现线程安全的问题。
运行结果
1 | Thread[Thread-0,5,main]:王五:777777 |
结果分析:
当线程1修改User对象的name后,线程1Sleep 5s,线程二开始修改User对象,此时,线程2会将线程1之前修改name=李四 覆盖为 name=王五,接着线程2Sleep 5s,线程1醒来,修改password=777777。线程2醒来,修改password=888888
为什么password不会被线程2覆盖为888888
因为线程1Sleep 5s后才会将password保存到主内存区的User对象上。而不是在修改name的时候就把password一起保存到主内存区。
如果去掉SetPass方法中的Thread,Sleep()方法,执行结果是怎样的?
1 | Thread[Thread-0,5,main]:李四:777777 |
如果去掉Thread,sleep方法,执行结果是正确的。事实上,正是因为线程1的Sleep方法,为了不浪费cpu,使得线程2有机会抢占执行。如果没有线程1的Sleep方法,线程2只能等线程1执行完成后才有机会执行。(不一定,看时间片分配???)
总结
多条线程抢占共享区域的对象,修改对象的成员变量,会引起线程安全的问题。
执行结果错误的本质原因
线程的执行不是原子性的。