(1)什么是线程安全?
当一个类被多个线程访问的时,这个类始终能表现出正确的行为,那么就称这个类是线程安全的。
(2)什么是有状态对象,什么是无状态对象呢?无状态对象一定是线程安全的。
有状态对象,就是有数据存储功能的对象。有状态对象,就是有示例变量的对象,可以保存数据,是非线程安全的。
无状态对象,就是不能保存数据。就是没有成员变量的对象,不能保存数据,是线程安全的。
(3)安全与不安全
不安全就是存在的数据是多个线程共享的才不安全,而每个线程自己独有的局部变量,则是安全的。所以可以总结如下:
1)常量始终是线程安全的,因为只存在只读操作。
2)每次调用方法前都新建一个实例是线程安全的,因为不会访问共享资源(共享堆资源)。
3)局部变量是安全的。因为每执行一个方法,都会独立的空间创建局部变量,它不是共享资源。局部变量包括方法的参数变量和方法的内部变量。
3.1)举个栗子
线程安全的调用方式,每次都new一个新的对象,每个对象在自己的堆空间中,多个线程不共享。
public classSafeThreadClient {
publicstatic voidmain(String[]args) {
for(inti&#61;0;i<10000;i&#43;&#43;){
UnsafeSequenceunsafeSequence &#61;newUnsafeSequence();
Threadthread &#61;newThread(unsafeSequence);
thread.start();
}
}
}
线程安全不安全调用&#xff0c;如下&#xff0c;new一次&#xff0c;多个线程共享堆中的这一个对象的成员变量。
public classUnsafeThreadClient {
publicstatic voidmain(String[]args) {
UnsafeSequenceunsafeSequence &#61;newUnsafeSequence();
for(inti&#61;0;i<10000;i&#43;&#43;){
Threadthread &#61;newThread(unsafeSequence);
thread.start();
}
}
}
public classUnsafeSequenceimplementsRunnable{
privateintvalue;
publicintgetNext(){
returnvalue&#43;&#43;;
}
publicvoidrun(){
System.out.println(getNext());
}
}
3.2)上述代码内存分析&#xff1a;
非线程安全&#xff1a;
线程安全&#xff1a;
就像上面的例子&#xff0c;value每次都要加1&#xff0c;但是可能会出现这样的问题&#xff0c;当两个线程同时到达&#xff08;读取--修改--写入&#xff09;&#xff0c;同时读取value&#xff0c;同时对value&#43;1&#xff0c;然后写入到内存中&#xff0c;假如刚才的操作value为4&#xff0c;然后两个线程同时操作&#xff0c;value的值应该加两次&#xff0c;但可能两个线程在读value值的时候都读到的是4&#xff0c;都会加1&#xff0c;写入内存都会认为是5。 而实际上两个线程对value应该加了两次&#xff0c;此时的value为6才对&#xff0c;这就是在多线程情况下&#xff0c;这个类表现的某些错误行为&#xff0c;我们就说这个类是线程不安全的。
而究其结果是因为&#xff0c;在 读出--修改--写入这个过程中&#xff0c;其中的一个状态都依赖于上一个状态。
最终会导致数据的不完整性&#xff0c;或不确定性&#xff0c;就是在不恰当的执行顺序&#xff0c;导致不正确的结果&#xff0c;有一个名字就是线程交替执行的时候&#xff0c;就会发生竞争条件。
(4)竞争条件&#xff1f;
当某个计算的正确性取决于多个线程的交替时序时&#xff0c;那么就会发生竞争条件。
(5)复合操作
要避免竞争条件问题&#xff0c;就必须在某个线程修改该变量的时候&#xff0c;通过某个方式阻止其他线程使用这个变量&#xff0c;从而确保其他线程只能在修改操作完成之前或者之后读取和修改状态&#xff0c;而不是在修改状态中。
加入A B两个线程&#xff0c;从A执行角度来看&#xff0c;B执行的时候&#xff0c;要么B全部执行完&#xff0c;要么完全不执行B。那么A和B的执行来说是原子的。原子操作是&#xff0c;对于访问同一个状态的所有操作&#xff08;包括操作本身&#xff09;这个操作是以原子方式执行的操作。
(5.1)利用java线程安全原子类AtomicLong
利用线程安全类来确保vaue在读取--修改--写入的一致性。修改代码如下所示&#xff1a;
package unsafeThread;
importjava.util.concurrent.atomic.AtomicLong;
/**
* Created by fang on 2017/11/18.
*/
public classSafeSequenceimplementsRunnable{
privatefinalAtomicLongvalue&#61;newAtomicLong(0);
publicintgetNext(){
value.incrementAndGet();
return(int)value.get();
}
publicvoidrun(){
System.out.println(getNext());
}
}
packageunsafeThread;
/**
* Created by fang on 2017/11/18.
*/
public classSafeThreadClient {
public static voidmain(String[]args) {
//利用AtomicLongjava.util.corrurnent包中的,线程安全类计数器.
SafeSequence safeSequence &#61; new SafeSequence();
for(inti&#61;0;i<10000;i&#43;&#43;){
Threadthread &#61;newThread(safeSequence);
thread.start();
}
}
}
(5.2)内存分析&#xff1a;
java.util.concurrent.atomic是java封装好的原子性的类包&#xff0c;在这里我们用线程安全的类AtomicLong来管理对象的属性变量&#xff0c;所以这个类是安全的。&#xff08;看jdk8的源码可以看到AtomicLong实现线程安全的基本原理是CAS-compare and swap&#xff0c;具体就不在这里写了&#xff0c;后续文章详细讲解jdk源码&#xff09;
(5.3)利用java内置锁
java提供了一种内置机制来支持原子性:同步代码块&#xff0c;代码如下所示。
public classUnsafeSequenceimplementsRunnable{
privateintvalue;
publicsynchronized intgetNext(){
returnvalue&#43;&#43;;
}
publicvoidrun(){
System.out.println(getNext());
}
}
我们在方法上添加上synchronized&#xff0c;被synchronized锁定的块就会以原子性的方式执行。
(6)重入
尽管synchronized锁住的不是同一个方法块,但是做到一个线程同时调这两个方法。
例如如下代码&#xff1a;
packagesafeThread;
/**
* Created by fang on 2017/11/19.
*/
public classWidget {
public synchronized voiddoSomething(){
System.out.println("i&#39;m father, in execute...");
}
}
packagesafeThread;
/**
* Created by fang on 2017/11/19.
*/
public classLoggingWidget extends Widget {
public synchronized voiddoSomething(){
System.out.println("child " &#43; toString() &#43; ": callingdoSomething");
super.doSomething();
}
}
packagesafeThread;
/**
* Created by fang on 2017/11/19.
* Synchronized可重复锁,举例.
*/
public classReentrantSynchronized {
/**
* main主线程.
* &#64;param args
*/
public static voidmain(String[] args) {
LoggingWidget loggingWidget &#61; new LoggingWidget();
loggingWidget.doSomething();
}
}
内存分析
就像家里的大门有一把锁,屋子里也有锁,对于屋子里的锁来说&#xff0c;我是同一个“线程”&#xff0c;既然有进入大门的权限&#xff0c;就一定有进入屋子的权限。
(7)活跃性与性能
一些保证线程安全的方法&#xff0c;线程安全保证了&#xff0c;但是程序的执行效率可能降低了。因为&#xff0c;当访问量很大的时候&#xff0c;如果是多cpu或者cpu多核的时候&#xff0c;请求只能一个一个的被执行&#xff0c;需要等待前一个完全执行才可以执行&#xff0c;可能会影响程序效率。
当使用锁的时候&#xff0c;我们应该清楚代码块中的实现功能&#xff0c;以及在执行该代码块时是否需要很长时间。无论是执行密度操作还是执行某个可能的阻塞操作&#xff0c;如果持有锁的时间过长&#xff0c;那么就可能带来活跃性或者性能问题。
(8)总结
了解到了线程安全和非安全线程&#xff0c;对象和人一样都要有“安全感”&#xff0c;下一篇对象的共享。