线程安全
通常来说,一个线程如果达到了我们预期的效果我们就说这个线程是安全的,如果没有达到我们的预期那么这个线程就是不安全的。
我们从以下的代码分析:
class Count{
public int count=0;
public void add(){
count++;
}
}
public class Main {
public static void main(String[] args) {
Count count =new Count();
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
count.add();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
count.add();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("count:"+count.count);
}
}
这个代码定义了一个变量count,让count在两个线程中分别自增5w次,在我们预期中count最后应该是10w。
运行结果如下:
很显然,这个运行结果没有达到我们的预期,那就说明这个线程不安全。
count++操作本质可以分为load,add,save这三步
1.load:先把内存中的值,读取到CPU寄存器中。
2.add:把CPU寄存器中的数值+1运算。
3.save:把得到的结果会写到内存中。
多线程不安全的原因
1.抢占式执行,随机调度。(这是线程不安全的根本原因)
2.多线程同时修改同一个变量。
3.修改操作不是原子性。
4.内存可见性问题。
5.指令重问题。
多线程同时修改同一个变量就会引起多线程不安全,如果是以下情况呢?
1.一个线程修改一个变量
2.多个线程读取同一个变量
3.多个线程修改多个变量
没错!那就是都没事。
修改操作不是原子性
在化学中,原子是化学元素的最小单元,是化学反应不可再分的最小微粒。
在上面代码中可以知道变量count在两个线程中分别自增5w次,count++这里可以分为load,add,save三个操作。说明count++操作并不是原子性,而load,add,save这三个操作是无法进一步进行拆分了,所以load,add,save这三个操作是原子性的。
内存可见性问题
可见性指:一个线程对共享变量值的修改,能够及时地被其他线程看到。一个线程读,一个线程改,可能出现此处读的结果不符合预期,这样就出现了线程不安全问题。
指令重排序
指令重排序本质就是编译器优化出bug了。编译器优化就是编译器自作主张将我们的代码在保持逻辑不变的情况下调整代码的执行顺序,从而加快程序的执行效率。
解决之前的线程不安全问题
class Count{
public int count=0;
synchronized public void add(){
count++;
}
}
public class Main {
public static void main(String[] args) {
Count count =new Count();
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
count.add();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
count.add();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("count:"+count.count);
}
}
输出的结果:
很显然我们在add方法前加了一个关键字synchronized(加锁)解决了之前的线程不安全问题。
synchronized 关键字 - 监视器锁monitor lock
"synchronized"是Java中的一个关键字,用于控制多线程访问资源的同步性。当一个线程访问一个synchronized方法或者代码块时,其他试图访问这个方法或者代码块的线程将被阻塞,直到第一个线程执行完毕。这样可以避免多个线程同时修改同一个资源而引起的并发问题。
synchronized
1.修饰方法(普通方法,静态方法)
修饰普通方法将锁加到this对象上,修饰静态方法将锁加到类对象中
2.修饰代码块
手动指定加到哪个对象上
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁
关于锁对象的规则
1.如果两个线程针对同一个对象进行加锁,就会出现锁竞争/锁冲突,一个线程能够获取到锁则另一个线程就会阻塞等待,等到上一个线程解锁,他才能获取锁成功,否则就不会。
2.如果两个线程针对不同对象加锁,此时不会发生锁竞争/锁冲突。因为这两个线程都能获取到各自的锁,就不会有阻塞等待了。
synchronized的特性
1)互斥
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到 同一个对象 synchronized 就会阻塞等待。
理解 "阻塞等待":
针对每一把锁, 操作系统内部都维护了一个等待队列。当这个锁被某个线程占有的时候, 其他线程尝 试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的 线程, 再来获取到这个锁。
举个例子,传闻学长有一个喜欢很久的女神,但是呢传闻学长的女神被他的舍友李哥给截胡了,也就是传闻学长的女神和李哥好上了。这相当于李哥给女神"加锁",传闻学长只能阻塞等待,只有等李哥"解锁"后才能继续追求女神。
2)可重入
一个线程可以多次获取同一个synchronized锁而不会产生死锁,那就说明这个是可重入的。如果一个线程可以多次获取同一个synchronized锁而会产生死锁,那就说明这个是不可重入的。
死锁
死锁是指两个或者两个以上的进程(线程)在执行的过程中,由于竞争资源而造成的阻塞问题,若无外力的作用下会无法继续推进,此时系统称之为死锁状态。简单来说,就是多个进程(线程)相互等待对方释放资源,导致都停滞不前。
死锁的三个情况
1.一个线程,一把锁,连续加锁两次。如果这个锁是不可重入的,就会发生死锁。
2.两个线程两把锁,线程1和线程2各自先针对锁A和锁B加锁,再尝试获取对方的锁。
3.多个线程多把锁,类似2的一般情况(哲学家问题)。
死锁的四个必要条件
1.互斥使用:线程1拿到了锁,线程2就得等待(锁的基本特性)
2.不可抢占:线程1拿到锁之后,必须是线程1主动释放,不能说是线程2就把锁给强行获得了
3.请求和保持:线程1拿到锁A之后,再尝试获取锁B,A这把锁还是保持的,不会因为获取锁B就把A给释放了
4.循环等待:线程1尝试获取锁A和锁B,线程2尝试获取锁B和锁A。线程1在获取B的时候等待线程2释放B,同时线程2在获取A的时候等待线程1释放A。
如何破除死锁
针对两个线程两把锁,t1与t2各自先针对锁A和锁B加锁,再尝试获取对方的锁 案例:
福州人吃饺子蘸番茄酱,江西人吃饺子蘸醋。
李哥:你把醋给我,我用完给你
传闻学长:你把酱油给我,我用完给你
此时两个人互不相让
用代码表示:
public class ThreadDemo14 {
public static void main(String[] args) {
Object fanqiejiang = new Object();//锁对象1
Object cu = new Object();//锁对象2
Thread t1 =new Thread(()->{
synchronized(fanqiejiang) {//李哥把番茄酱已经加锁了
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//再尝试获取对方的锁
synchronized (cu){
System.out.println("李哥把番茄酱和醋都拿到了");
}
}
});
Thread t2 =new Thread(()->{
synchronized (cu) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (jiangyou){
System.out.println("传闻学长把酱油和醋都拿到了");
}
}
});
t1.start();
t2.start();
}
}
结果很明显,李哥和传闻学长僵持住了。就发生了"死锁"情况。
如何破除死锁呢?打破必要条件即可。没错!突破口就是循环等待。
办法:
给锁编号,然后指定一个固定的顺序(比如从小到大)来加锁。任意线程加多把锁的时候,都会让线程遵守上述顺序,此时循环等待自然就会破除了。
假设fanqiejiang是1号,cu是2号。我们约定先拿小号的后拿大号的,这样就突破循环等待了。
public class ThreadDemo14 {
public static void main(String[] args) {
// 假设 fanqiejiang 是 1 号, cu 是 2 号, 约定先拿小的, 后拿大的.
Object fanqiejiang = new Object();//锁对象1
Object cu = new Object();//锁对象2
Thread t1 =new Thread(()->{
synchronized(fanqiejiang) {//李哥把番茄酱已经加锁了
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//再尝试获取对方的锁
synchronized (cu){
System.out.println("李哥把番茄酱和醋都拿到了");
}
}
});
Thread t2 =new Thread(()->{
synchronized (fanqiejiang) {//原本是cu
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (cu){//原本是fanqiejiang
System.out.println("传闻学长把番茄酱和醋都拿到了");
}
}
});
t1.start();
t2.start();
}
}
这样死锁也就破解了