多个线程对同一个类的普通成员进行非原子性操作就会造成线程共享的安全问题。示例如下,我们使用一个类实现Runnable接口,在这个类中定义了计数值count,当count的值小于最大值maxCount时,计数值就会加1。我们用测试类分别启动四个线程操作这个类。
实现类:
/**
* @Author lsc
* @Description <p> </p>
* @Date 2019/10/22 20:44
* @Version 1.0
*/
public class SysYouku1 implements Runnable {
int count = 0;
int maxCount = 200;
@Override
public void run() {
while (count<maxCount){
System.out.println(Thread.currentThread().getName()+" - "+count++);
}
}
}
测试类:
/*
* @Author lsc
* @Description <p> 多线程安全问题</p>
* @Date 2019/10/22 20:53
* @Param []
* @return void
**/
@Test
public void syncronizedTest1(){
SysYouku1 sysYouku1 = new SysYouku1();
Thread t1 = new Thread(sysYouku1, "youku1");
Thread t2 = new Thread(sysYouku1, "youku2");
Thread t3 = new Thread(sysYouku1, "youku3");
Thread t4 = new Thread(sysYouku1, "youku4");
t1.start();
t2.start();
t3.start();
t4.start();
}
输出结果:
youku4 - 132
youku4 - 196
youku4 - 197
youku4 - 198
youku4 - 199
youku2 - 195
youku3 - 184
youku1 - 177
发现这些数据无序,并且会造成缺省等情况,这是由于我们在类中共享变量,cpu调度轮询引起的。在这个过程中会发生三种问题,情况如下。
当两个线程都进入run方法并且依次取到count的值(同一个值),thread 1将count数值+1然后cpu将控制权交给thread2,thread将count的值也加1。thread 1 和 thread2 ,将改变的count值 赋值,然后依次输出,造成了数据重复问题。
thread1 和 thread2 都进入run方法,thread1 操作了 count 加1 并且将操作后的数值赋值,thread2 此时拿到的count 是 thread1 操作后的值,再次基础上 thread再次将值 加1,造成超值问题。
thread1 , thread2 同时进入run 方法,thread 1 对 count 进行操作数值加1 输出,thread2此时拿到count的值操作加1停顿,thread1再次进入在thread2操作的基础上对count加1,输出。
线程安全问题的产生是在多线程对这个类的进行访问时,这个类表现出了不正确的行为。大量的线程安全问题都是对变量的操作非原子性和数据共享造成的,如上的例子,对count这个成员变量进行了非原子性操作,在时序上出现了混乱,我们称这种情况是竞争条件(race condition
)。
当然字段也可以使用ava.util.concurrent.atomic包中的关键字修饰解决,比如AtomicInteger,本文主要讲解synchronized同步机制,具体如下文。
happens-before
关系。这保证了所有线程能够对这个对象的状态改变都是可见的。happens-before
关系。官网描述如下,意指当一个线程调用这个同步方法,这个线程会自动获得拥有这个方法的对象的内部锁,并且在方法返回的时候释放锁;即使在返回的时候出现了未捕获的异常任然会释放这个锁。
示例:
public synchronized String sync(){
// do nothing
return null;
}
官网描述如下,意指 静态同步方法修饰的锁是与类相关联的不是这个对象;访问这个类的静态字段是被这个锁所控制,这个锁与任何类的实例的锁都不相同。
示例:
public static synchronized String syncS(){
// do nothing
return null;
}
官网描述如下,意指同步代码块必须要有一个具体的对象提供其内部锁。
示例:
public String syncy(){
// do nothing
synchronized (this){
}
return null;
}
含有同步方法的类:
/**
* @Author lsc
* @Description <p> </p>
* @Date 2019/10/23 22:08
* @Version 1.0
*/
public class Monitor {
private int count = 0;
public int sync(){
synchronized(this){
count++;
try {
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return count;
}
}
主类使用Monitor 对象启动两个线程:
public static void main(String[] args) {
Thread t1 = new Thread(new Monitor()::sync, "youku1");
Thread t2 = new Thread(new Monitor()::sync, "youku2");
t1.start();
t2.start();
}
可以证明上述中一个对象只有一个Monitor(监视器),在同一个时刻,只能有一个线程获得lock进入monitor,当释放lock时出monitor(也就是方法返回的时候)
可以看见当youku2线程进入睡眠时候,已经上锁了,此时其他线程是无法进入的。