什么叫做线程安全性:当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么称这个类是线程安全的。
那么什么叫做正确的行为?所见即所知We know it when we see it。
原子性
什么叫原子性?举个例子,假如现在有一个类中有一个计数器方法如下:
public class TestCount {
private long count = 0;
public long getCount() {
return count ;
}
public void addCount() {
count++;
}
}
每次执行addCount方法,就给私有的count字段+1,请问addCount具有原子性吗?答案是否定的。因为看似简单的一行代码count++,其实包含了三个动作:
1、取出count值;
2、count值+1;
3、写入count值。
显然这是三个有先后顺序的复合操作,在单线程下没有问题,但是多线程下就不一样了。运行以下代码,会发现经过100个用户并发对count+1后,最终打印的结果并不一定是100。造成这种情况的原因是竞态条件的存在:当某个计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件。
public static void main(String[] args) {
//List<User> users = accountWalletService.initUsers();
TestCount testCount = new TestCount();
List<Integer> users = initUsers();
//模拟并发
users.parallelStream().forEach(b -> {
testCount.addCount();
//System.out.println(testCount.getCount());
// accountWalletService.process(b);
});
System.out.println("sum:" + testCount.getCount());
System.out.println("-------------");
}
private static List<Integer> initUsers() {
List<Integer> res = new ArrayList<>();
for (int i=0;i<100;i++) {
res.add(i);
}
return res;
}
如何确保单个变量的原子性?java原生提供了Atomic类型:
public class TestCount {
private AtomicLong count = new AtomicLong(0);
//private long count = 0;
public AtomicLong getCount() {
return count;
}
// public long getCount() {
// return count;
// }
public void addCount() {
count.incrementAndGet();
//count++;
}
}
再次运行代码,发现结果正确。
加锁机制
当需要确保一个以上状态变量的线程安全性时,就需要用到锁。java提供了一种内置锁机制:同步代码块。以下是一个未加锁的类,简单的将私有变量ticket减一来模拟抢票。
package com.example.demo.impl;
/**
* @ClassName
* @Description TODO
* @Author 重装机器
* @Date 2022/2/27 15:10
**/
public class MyRunnable implements Runnable {
private int ticket = 100;
private Object object = new Object();
public void run() {
while (true) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":出售第" + (101 - ticket) + "张票");
ticket -= 1;
} else {
break;
}
}
}
}
主函数如下。总共起了3个线程,模拟3个窗口同时抢票。
public class testMain {
public static void main(String[] args) {
//初始化一个对象
MyRunnable myRunnable = new MyRunnable();
//启动三个线程跑该对象的run方法
Thread t1 = new Thread(myRunnable,"窗口1");
Thread t2 = new Thread(myRunnable,"窗口2");
Thread t3 = new Thread(myRunnable,"窗口3");
t1.start();
t2.start();
t3.start();
}
}
运行起来看看。当不加锁时,三个窗口出现了抢到同一张票的情况,这显然不符合实际。
我们加了同步代码块后。注意代码用synchronized包裹了起来。
public class MyRunnable implements Runnable {
private int ticket = 100;
private Object object = new Object();
public void run() {
while (true) {
//加同步代码块
synchronized (object) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":出售第" + (101 - ticket) + "张票");
ticket -= 1;
} else {
break;
}
}
}
}
}
再次运行程序,发现结果正确: