这里先记录一些基础概念和说明
一、构建线程安全类方式
如果多个线程访问同一个可变的状态变量时,没有使用合适的同步,那就会出现不可预知的错误,修复方式如下:
1、不在线程之间共性这个变量(线程封闭)
2、将变量修改为不可变(final)
3、在访问变量时使用同步(同步代码块)
二、无状态对象
无状态对象一定是线程安全的,无状态对象定义如下:
/**
* 有状态bean:包含实例变量对象,如这里的user
*/
public class StatefulBean {
public int state;
// 由于多线程环境下,user是引用对象,是非线程安全的
public User user;
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
/**
* 无状态bean:没有实例变量对象。
*/
public class StatelessBeanService {
public int state;
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
}
延伸到spring的bean就是,只包含简单字段(没有类类型的属性)的实体是线程安全的无状态对象,包含类类型属性的实体则相反(如在mybatis中可能会在Article实体内添加User user这样的类类型的属性,以便一次查询到作者)
在spring中默认mvc各层都是单例的,因为service和dao层一般都是无状态的(没有类类型属性,以及类类型属性的get,set方法)。这样多个线程使用同一个实例也是安全的。
三、竞态条件
竞态条件:当某个计算正确性,取决于多个线程的交替执行时序所引发,例如:
private int count=0;
++count;
System.out.println(count);
++count不是一个原子操作,是三个独立的操作:读取count值,给值加1,将结果写入count变量;是读取-修改-写入的操作。
多线程下在线程A写入时,线程B可能对count做了第二次的修改,结果就会偏差1,这种操作方式引发的现象就是竞态条件。
同理先检查-后执行(延迟初始化)的操作也会引发竞态条件,如A条件下执行a方法,在检查后和执行a方法前,其他线程操作将A条件改为了B,这时仍执行a方法得到的结果就可能是不正确的。常用的if else,判断条件如果是共享变量就可能引起竞态条件
四、复合操作
复合操作:如上例,将读取-修改-写入的操作,和先检查-后执行的操作,统称为复合操作:包含了一组必须已原子方式执行才能保证线程安全的操作。
五、综上简单案例如下:
package gcc.thread.test;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Created by gcc on 2018/3/29.
* thread.start(),内部会调用start0(),而这个方法是本地(native)方法,能启动线程,同时也创建了一个线程。
* thread.run(),只是调用一个java方法,不是真正启动多线程。
*/
public class Test1 implements Runnable {
private int count=0;
// private final AtomicInteger count = new AtomicInteger();
public static void main(String[] args) {
Test1 test1 = new Test1();
Thread thread1 = new Thread(test1);
Thread thread2 = new Thread(test1);
Thread thread3 = new Thread(test1);
thread1.start();
thread2.start();
thread3.start();
}
public void run() {
for (int i = 0; i <30 ; i++) {
// System.out.println("结果:"+Thread.currentThread().getName()+"--:"+count.incrementAndGet());
System.out.println("结果:"+Thread.currentThread().getName()+"---:"+count++);
//线程休眠时间0.1-0.5秒
try {
Thread.sleep(100*(new Random().nextInt(5)+1));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
如上,Atomic开头的类是java自带的原子操作类,可以将类似count++的复合操作改为同作用的原子操作。案例默认的是复合操作,会引发的错误结果。如下:
结果:Thread-1---:11
结果:Thread-0---:12
结果:Thread-2---:12
结果:Thread-1---:13
结果:Thread-0---:14
结果:Thread-2---:15
六、使用synchronized方法,如下:
public synchronized void run() {
for (int i = 0; i <50 ; i++) {
++count;
System.out.println("结果:"+Thread.currentThread().getName()+"----"+count);
//线程休眠时间0.1-0.3秒
try {
Thread.sleep(10*(new Random().nextInt(3)+1));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
其它代码与(五)相同,这里增加synchronized,这是java内置锁,加在方法上则在同一时间只有一个线程能执行这个方法,解决了多线程不安全问题,但效率大大降低,不推荐使用,测试结果如下:
结果:Thread-0----47
结果:Thread-0----48
结果:Thread-0----49
结果:Thread-0----50
结果:Thread-2----51
结果:Thread-2----52
结果:Thread-2----53
七、使用synchronized代码块,如下:
public void run() {
for (int i = 0; i <50 ; i++) {
synchronized (this){
++count;
System.out.println("结果:"+Thread.currentThread().getName()+"----"+count);
}
//线程休眠时间0.1-0.3秒
try {
Thread.sleep(10*(new Random().nextInt(3)+1));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
其它代码与(五)相同,这里增加synchronized(this){},添加代码块,效率高于synchronized方法,通过平衡性能、安全和编码的简单性来确定同步代码块的大小,相对于(六)此方式更优,测试结果如下:
结果:Thread-0----1
结果:Thread-2----2
结果:Thread-1----3
结果:Thread-1----4
结果:Thread-0----5
结果:Thread-2----6
结果:Thread-0----7
注意:
同步代码块防止了一个对象在被使用时,同时被另一个线程修改。
还有要知道的一点是,同步机制保证了可见性。
可见性:读操作和写操作在两个线程时,不能保证读操作可以适时的看到其他线程写入的值,为了保证写入可见,使用同步机制。