栈内存:线程是私有的,也就是说局部变量和方法是不可共享的。
堆内存:对象和数组是在堆内存中创建的,所有线程都可以访问,包括成员变量、静态变量和数组元素是可共享的;
原子性操作:一个或某几个操作只能在一个线程执行完之后,另一个线程才能开始执行该操作,也就是说这些操作是不可分割的,线程不能在这些操作上交替执行。
i++为什么不是原子性操作?
它相当于三个原子性操作:
1.读取变量i的值;
2.将变量i的值加1;
3.将结果写入变量i中。
此图的意思就是:
1.线程1执行自增方法时先读取i的值,发现是5,此时切换到线程2执行自增方法时,读取到变量i的值也是5,
2.线程1执行将变量i的值加1的操作,线程2也执行此操作,
3.线程1将结果赋给变量i,线程2也将结果赋给变量i。
这两个线程都执行了一次自增方法之后,最后的结果都是i从5变到了6,而不是我们想要的7....
由于CPU的速度非常快,这种交叉执行在执行次数较低的时候体现的并不明显,但是在执行次数非常多的时候,就十分明显了。
如何解决此类问题?
提供两种解决方案:
1.尽量使用局部变量解决问题.
因为方法中的局部变量(包括方法参数和方法体中创建的变量)是线程私有的,所以无论多少线程调用某个不涉及共享变量的方法都是安全的。
2.使用ThreadLocal类
不同线程操作同一个ThreadLocal对象执行各种操作而不会影响其他线程里的值。
对一个网络程序,通常每一个请求都分配一个线程去处理,可以在ThreadLocal里记录一下这个请求对应的用户信息,比如用户名、登录失效时间什么的,这样就很有用了。
尽管ThreadLocal很有用,但是它作为一种线程级别特别高的全局变量,如果某些代码依赖它的话,会造成耦合,从而影响了代码的可重用性。
从可变性解决
通过让对象不可变的方式保证线程安全,那就把变量声明为final。
加锁解决
1.同步代码块
任何一个对象都可以作为一个锁,称为内置锁;
加锁的语法:
如果一个线程获得一个锁之后,其他线程处于一种阻塞状态,直到已经获取锁的线程把该锁释放掉,某个线程就可以获得锁了。这样线程们按照获取锁的顺序执行的方式也叫同步执行,这个被锁保护的代码叫同步代码块,换言之这段代码被锁保护。由于如果线程没有获得锁就会阻塞在同步代码块这,所以我们要格外注意的是,在同步代码块中要尽量的短,不要把不需要的同步代码块也加到同步代码中,在同步代码块中千万不要执行特别耗时或者可能发生阻塞的一些操作,比如I/O操作啥的。
为什么一个对象就可以当作一个锁呢?我们知道一个对象会占据一些内存,这些内存地址可是唯一的,也就是说两个对象不能占用相同的内存。真实的对象在内存中的表示其实有对象头和数据区组成的,数据区就是我们声明的各种字段占用的内存部分,而对象头里存储了一系列的有用信息,其中就有几个位代表锁信息,也就是这个对象没有作为某个线程的锁的信息。
2.锁的重入
只要一个线程持有了某个锁,那么它就可以进入任何一个被这个锁保护的代码块。看代码
3.同步方法:
对于成员方法来说,我们可以直接用this作为锁。
对于静态方法来说,我们可以直接用Class对象作为锁(Class对象可以直接在任何地方访问)。
整个方法的操作都需要被同步,而且使用this作为锁的成员方法,使用Class对象作为锁的静态方法。
通用格式:
上述的两种方法也称同步方法,也就是说整个方法都需要被同步执行,而且使用的锁是this对象或者Class对象。
总结:
共享、可变的变量形成了并发编程的三大杀手:安全性
、活跃性
、性能
,本章详细讨论安全性
问题。
本文中的共享变量指的是在堆内存上创建的对象或者数组,包括成员变量、静态变量和数组元素。
安全性
问题包括三个方面,原子性操作
、内存可见性
和指令重排序
,本篇文章主要对原子性操作
进行详细讨论。
原子性操作就是一个或某几个操作只能在一个线程执行完之后,另一个线程才能开始执行该操作,也就是说这些操作是不可分割的,线程不能在这些操作上交替执行。
为了保证某些操作的原子性,提出了下边几种解决方案:
尽量使用局部变量解决问题
使用ThreadLocal类解决问题
从共享性解决,在编程时,最好使用下边这两种方案解决问题:
从可变性解决,最好让某个变量在程序运行过程中不可变,把它使用final
修饰。
加锁解决
任何一个对象都可以作为一个锁,也称为内置锁
。某个线程在进入某个同步代码块
的时候去获取一个锁,在退出该代码块的时候把锁给释放掉。
锁的重入是指只要一个线程持有了某个锁,那么它就可以进入任何被这个锁保护的代码块。
同步方法是一种比较特殊的同步代码块,对于成员方法来讲,使用this
作为锁对象,对于静态方法来说,使用Class对象
作为锁对象。