线程安全指的是多线程共享变量,导致访问数据出问题。
例:超卖问题
/**
* 模拟3个窗口卖100张票
*
*/
public class TestThread{
static int ticket = 100;
public static void main(String[] args){
Runnable runnable =()->{
while(true){
try{
Thread.sleep(1);
}catch(InteruptedException e){
e.printStackTrace();
}
if(ticket>0){
System.out.print(Thread.currentThread.getName()+"卖了第"+ticket--+"张票");
}else{
break;
}
}
}
Thread t1=new Thread(runnable,"窗口1");
Thread t2=new Thread(runnable,"窗口2");
Thread t3=new Thread(runnable,"窗口3");
t1.start();
t2.start();
t3.start();
}
}
运行结果:
可以看出窗口2已经卖出了第100张票,而窗口1又把第100张票卖出了,这里明显是存在bug的。
原因分析
java内存模型(Java Memory Model,简称JMM)
主线程的共享变量是所有线程共享的,而工作内存是线程私有的。当一个线程要修改共享变量时,首先从主内存中将共享变量复制到工作内存中,修改完成后再写回主内存。这就可能出现一个问题,就是当修改完成了 但还未写会主内存中,其他线程也开始对共享变量进行操作,此时读取的数据是主内存中的数据,产生了数据不一致问题。
并发问题的三大特性
-
原子性
一个操作或多个操作 ,要么全部执行且在执行的过程中不被打断,要么全部不执行。
-
可见性
当多个线程访问同一变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
-
有序性
JVM为了提高运行效率,会对代码进行优化,不保证程序中语句的先后顺序,但会保证程序最终运行结果,这就是指令重排序(happen-before)。指令重排序对单线程没有影响,对多线程就会产生影响。
解决办法1:volatile关键字
- 保证变量的可见性
- 屏蔽指令重排序
解决办法2:synchronized
保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的方法是可见的,可以代替volatile
底层原理:
当线程A要执行被synchronized加锁的代码块时,首先通过monitorenter来获得锁,此时若线程B也执行到了这个代码块,需要等待,当线程A运行结束后,通过monitorexit来释放锁,此时线程B才能获得这个代码块的锁。
解决办法3:lock锁
Lock接口提供了与synchronized关键字类似的功能,但需要手动获取锁与释放锁。
代码形式:
Lock lock=new ReentrantLock();
lock.lock();
try{
//需要加锁的代码块
}finally{
lock.unlock();
}
lock锁相比于synchronized提供了更加丰富的Api,例如trylock(),尝试获取锁,若获取失败可以执行其他操作,不会等待。