如何保证线程安全
什么是线程安全问题?
在多线程情况下,对共享内存中的变量做写操作时,就会容易出现线程安全问题。
举个栗子,我们现在要用两个线程实现两个窗口售卖5张票
class ThreadSaleTicket extends Thread{
public int ticket = 5;
@Override
public void run() {
while(ticket > 0){
sale();
}
}
public void sale(){
System.out.println(Thread.currentThread().getName() + "售出第: " + (5-ticket+1) + " 张票");
ticket--;
}
}
public class threadSecurityTest{
public static void main(String[] args) {
ThreadSaleTicket salethread1 = new ThreadSaleTicket();
Thread thread1 = new Thread(salethread1,"窗口1");
Thread thread2 = new Thread(salethread1,"窗口2");
thread1.start();
thread2.start();
}
}
output:
是不是,有问题了吧,卖出了6张票,完了,出现线程安全问题了!
如何解决线程安全问题?
1 使用局部变量或者定义为final类型
我们知道,JVM内存分为五个区域:
其中,PC寄存器、Java虚拟机栈和本地方发栈是线程私有的,方法区和Java对是线程共享的区域。因此最容易想到的保证线程安全的方式就是尽可能使用局部变量,局部变量存在方法体内,会存放在Java虚拟机栈的栈帧中,而这块区域每个线程私有,就不会存在线程安全问题。
来,咱们修改一下:
class ThreadSaleTicket extends Thread{
//public int ticket = 5;
@Override
public void run() {
int ticket = 5;
while(ticket > 0){
//sale();
System.out.println(Thread.currentThread().getName() + "售出第: " + (5-ticket+1) + " 张票");
ticket--;
}
}
// public void sale(){
// System.out.println(Thread.currentThread().getName() + "售出第: " + (5-ticket+1) + " 张票");
// ticket--;
// }
}
public class threadSecurityTest{
public static void main(String[] args) {
ThreadSaleTicket salethread1 = new ThreadSaleTicket();
Thread thread1 = new Thread(salethread1,"窗口1");
Thread thread2 = new Thread(salethread1,"窗口2");
thread1.start();
thread2.start();
}
}
output:
好像不得行,改成局部变量了之后,每个线程单独有了一份ticket,卖出了两倍的票数。。。
如果定义为final之后,那更完了,票数不能修改了,咋卖。。
2 上锁
Java提供关键字synchronized和Lock接口来实现排它锁,在可能会出现线程安全的代码上加上锁,防止某一时刻有多个线程进入修改同一共享变量出现线程安全问题,sychronized以时间换空间的方式,主要侧重点在于解决多个线程之间访问资源的同步
class ThreadSaleTicket extends Thread{
public int ticket = 5;
public Object oj = new Object();
@Override
public void run() {
int ticket = 5;
while(ticket > 0){
sale();
}
}
public void sale(){
synchronized (oj){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + "售出第: " + (5-ticket+1) + " 张票");
ticket--;
}
}
}
}
public class threadSecurityTest{
public static void main(String[] args) {
ThreadSaleTicket salethread1 = new ThreadSaleTicket();
Thread thread1 = new Thread(salethread1,"窗口1");
Thread thread2 = new Thread(salethread1,"窗口2");
thread1.start();
thread2.start();
}
}
output:
是不是就好了呢,哈哈哈,成功正确卖出5张票
但是呢,思考一下,我们使用多线程并发的初心是什么呢,原是为了提升CPU的利用率以及系统的响应速度,如果动不动在代码块中加锁,那么程序将会退化成并行执行,我们使用多线程的意义在哪里呢?因此,我们寻找更优秀的解决方式。
3 使用ThreadLocal
threadLocal为每个使用该变量的线程提供独立的副本,所以每个线程都可以独立的改变自己的副本,而不会影响其他线程所对应的的副本。它采用空间换时间的方式,让多线程之间每个线程之间的数据相互隔离。
ThreadLocal实现原理:
threadlocal类中有一个map,用于存储每一个线程的变量副本,map中元素的键为线程对象,而值对应线程的变量副本
class ThreadSaleTicket extends Thread{
public int ticket = 5;
public static ThreadLocal<Integer> value = new ThreadLocal<>(){};
@Override
public void run() {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
while(ticket > 0){
value.set(ticket--);
System.out.println(Thread.currentThread().getName() + "售出第: " + (5-value.get()+1) + " 张票");
}
}
}
public class threadSecurityTest{
public static void main(String[] args) {
ThreadSaleTicket salethread1 = new ThreadSaleTicket();
Thread thread1 = new Thread(salethread1,"窗口1");
Thread thread2 = new Thread(salethread1,"窗口2");
thread1.start();
thread2.start();
}
}
output:
4 使用juc包下面的一些线程安全类
juc包提供一些线程安全的容器或原子类来保证多线程环境下的线程安全,例如atomic包下面的运用了CAS的AtomicBoolean、AtomicInteger、AtomicReference等原子变量类,locks包下的AbstractQueuedSynchronizer(AQS)以及使用AQS的ReentantLock、ReentrantReadWriteLock等等。还有一些并发容器类例如ConcurrentHashMap、CopyOnWriteArrayList