线程安全定义:如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
用主线程来执行增加减少操作,这是安全线程
static class Counter {
// 定义的私有变量
private int num = 0;
// 任务执行次数
private final int maxSize = 100000;
//num++;
public void incrment() {
for (int i = 0; i < maxSize; i++) {
num++;
}
}
//num--
public void decrment() {
for (int i = 0; i < maxSize; i++) {
num--;
}
}
public int getNum() {
return num;
}
}
public static void main(String[] args) {
Counter counter = new Counter();
counter.incrment();
counter.decrment();
System.out.println("最终的执行结果:" + counter.getNum());
}
最后输出0
线程不安全定义:多线程执行中,程序的执行结果和预期不相符就叫作线程不安全
用两个线程分别执行增加,减少操作,这是不安全线程
static class Counter {
// 定义的私有变量
private int num = 0;
// 任务执行次数
private final int maxSize = 100000;
//num++;
public void incrment() {
for (int i = 0; i < maxSize; i++) {
num++;
}
}
//num--
public void decrment() {
for (int i = 0; i < maxSize; i++) {
num--;
}
}
public int getNum() {
return num;
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
counter.incrment();
});
t1.start();
Thread t2 = new Thread(() -> {
counter.decrment();
});
t2.start();
t1.join();
t2.join();
System.out.println("最终的执行结果:" + counter.getNum());
}
每次结果都不同,不为0
线程执行操作会分为3步:
我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的
编译器优化再单线程下没问题,可以提升程序的执行效率,单在多线程下就会出现混乱,从而导致线程不安全的问题
所以不难看出,连个线程并行执行时,每次都用的一个存储空间的值,每次保存都会覆盖这个count值,所以结果自然也就千奇百怪,这就是线程不安全的原因。
这就内存的不可见性,可能会导致线程的不安全
我们来用代码举例理解:
private static boolean flag = false;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (!flag) {
}
System.out.println("终止执行");
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("设置 flag 为 true");
flag = true;
}
});
t2.start();
}
执行结果
可以看到明明将flag改为false了,但是代码并不会终止,一直在运行
这是由于线程的不可见性导致线程不安全出现的情况。
本来两个线程都会从主内存中取到flag变量,但是由于线程 t1 中的while循环体中没有任何代码,cpu这时候进行优化,每次调用的就是t1工作区中的数据,这个数据是没有改变的,t2只改变了主内存中的数据,所以导致了t1并没有接收到flag更改后的数据,这也就是线程安全中的可见性的问题。
解决方案:
(一)如果我们把 t1.join()
移动到 t2.start()
之前,这样就相当于 t1 执行完后 t2 再执行,相当于串行操作,就不会有这种线程不安全的情况了
(二)让两个线程修改他们各自的变量
static class Counter {
// 任务执行次数
private final int maxSize = 100000;
//num++;
public int incrment() {
int num1 = 0;
for (int i = 0; i < maxSize; i++) {
num1++;
}
return num1;
}
//num--
public int decrment() {
int num2 = 0;
for (int i = 0; i < maxSize; i++) {
num2--;
}
return num2;
}
}
private static int num1 = 0;
private static int num2 = 0;
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
num1 = counter.incrment();
});
t1.start();
Thread t2 = new Thread(() -> {
num2 = counter.decrment();
});
t2.start();
t1.join();
t2.join();
System.out.println("最终的执行结果:" + (num1 + num2));
}
我们只需要将全局变量flag中加上volatile关键字就可以啦
private static volatile boolean flag = false;
volatile作用:
注意事项:volatile不能解决原子性问题
锁操作的关键步骤:
【JVM 层面的解决方案,自动帮我们进行加锁和释放锁】
操作层面:
JVM层面:
JAVA层面:
synchronized同步快对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
synchronized (lock) {
synchronized (lock) {
number--;
}
}
因为有可重入的性值,当获取到lock这把锁后,无论加几层这把锁,都可以进入
同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入
synchronized 在JDK 6 之前使用重量级锁实现的,性能非常低,所以用的并不多
JDK6 对synchronized 做了一个优化(锁升级):
那我们用synchronized来解决之前线程不安全的问题
//全局变量
private static int number = 0;
//循环次数
private static final int maxSize = 100000;
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
//+10w
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < maxSize; i++) {
synchronized (lock) {
number++;
}
}
}
});
t1.start();
//-10w
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < maxSize; i++) {
synchronized (lock) {
number--;
}
}
}
});
t2.start();
//等t1,t2线程执行完
t1.join();
t2.join();
System.out.println("运行结果为:" + number);
}
注意事项:在进行加锁操作的时候,同一组业业务一定是同一个锁对象
Object lock = new Object();
synchronized (lock) {
number++;
}
public static synchronized void increment() {
for (int i = 0; i < maxSize; i++) {
number++;
}
}
public synchronized void increment() {
for (int i = 0; i < maxSize; i++) {
number++;
}
}
而 Lock 只能用来修饰代码块
【程序员自己加锁和释放锁】
在 java.util.concurrent.locks
这个包里面 简称为 JUC
我们把之前报错的代码用 Lock 改进一下
//全局变量
private static int number = 0;
//循环次数
private static final int maxSize = 100000;
public static void main(String[] args) throws InterruptedException {
//1.创建手动锁
Lock lock = new ReentrantLock();
//+10w
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < maxSize; i++) {
//2.加锁
lock.lock();
try {
number++;
}finally {
//3.释放锁
lock.unlock();
}
}
}
});
t1.start();
//-10w
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < maxSize; i++) {
lock.lock();
try {
number--;
}finally {
lock.unlock();
}
}
}
});
t2.start();
//等t1,t2线程执行完
t1.join();
t2.join();
System.out.println("运行结果为:" + number);
}
注意事项:一定要把 lock()放在 try 外面
公平锁可以按顺序进行执行,而非公平锁执行的效率更高
在 java 中所有的锁默认的策略都是非公平锁(synchronized锁机制就是非公平锁)
Lock 默认的锁策略也是非公平锁,但是 Lock 可以显式地声明为公平锁
Lock lock = new ReentrantLock(true);
只需要在参数设置为 true 就可以设置为公平锁
public static void main(String[] args) throws InterruptedException {
//声明一个公平锁
Lock lock = new ReentrantLock(true);
Runnable runnable = new Runnable() {
@Override
public void run() {
for(char item : "ABCD".toCharArray()) {
lock.lock();
try {
System.out.print(item);
}finally {
lock.unlock();
}
}
}
};
Thread t1 = new Thread(runnable,"t1");
Thread t2 = new Thread(runnable,"t2");
Thread.sleep(10);
t1.start();
t2.start();
}