文章目录
- 一、多线程安全问题
- 1.1 线程安全问题的原因
- 1.2 如何解决线程安全问题
- 二、加锁
- 2.1 synchronized
- 2.2 synchronized的几种使用方式
- 2.3 synchronized的可重入性
- 三、死锁
- 3.1 死锁的必要条件
一、多线程安全问题
代码示例如下:
public class Demo20 {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count++;
}
});
Thread t2= new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count++;
}
});
//串行执行
// t1.start();
// t1.join();
// t2.start();
// t2.join();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count:"+count);
}
}
这段代码每次执行的结果都不一样,都没有达到预期的结果即100000。主要的原因在于:
(1)count++这个操作在cpu指令的角度上看其实是三个指令
- load:把内存中的数据加载到寄存器。
- add:把寄存器中的值+1。
- 把寄存器中的值写回内存。
(2)两个线程并发进行count++,多线程的执行是随即调度,抢占式的执行模式。
综合以上两点,实际并发执行时,两个线程的指令执行的相对顺序就存在多种可能,不同执行顺序下得到的结果就会有差异,例如:
光这一种情况,得到的结果就不可能时100000。
1.1 线程安全问题的原因
(1)线程在系统中是随机调度的,抢占式执行。
这个无法改变,是内核设计者的考虑。
(2)多个线程修改同一个变量。
一个线程修改一个变量=>没事
多个线程读取一个变量=>没事
多个线程修改不同的变量=>没事
(3)线程针对变量的修改操作,不是"原子"的。
(4)内存可见性。
(5)指令重排。
1.2 如何解决线程安全问题
从原因入手:
(1)原因1:无法干预。
(2)原因2:是一个切入点,但是在java中这种做法不是很普适,只针对一些特定场景是可以做到的。
(3)原因3:这是解决线程安全问题最普适的方案。可以通过一些操作,将非原子的操作打包成一个原子的操作。(加锁)
二、加锁
加锁就是针对原因3解决线程安全问题的操作。
锁的几个特点:
(1)锁涉及的几个操作:加锁和解锁。
(2)锁的主要特性:互斥。
一个线程获取到一个锁之后,如果其它线程也想要获取该锁,会进行阻塞等待。
(3)代码中可以创建多个锁。
只有多个线程竞争同一把锁才会互斥,针对不同的锁则不会。
2.1 synchronized
synchronized后面带上()里面写的就是"锁对象"。
注意:锁对象的用途有且仅有一个,就是用来区分两个线程是否对同一个对象加锁。如果是就会互斥,引起阻塞等待。如果不是,就不会出现锁竞争,也不会阻塞等待
synchronized后面还跟着{},进了代码块就是对()内的锁对象进行了加锁,出了代码块就是对()的锁对象进行了解锁。
用加锁解决线程安全问题代码示例如下:
public class Demo23 {
public static int count=0;
public static void main(String[] args) throws InterruptedException {
Object object=new Object();
Thread t1=new Thread(()->{
for (int i = 0; i < 50000; i++) {
synchronized (object) {
count++;
}
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 50000; i++) {
synchronized (object) {
count++;
}
}
});
t1.start();;
t2.start();;
t1.join();
t2.join();
System.out.println(count);
}
}
每次执行count++会因为锁竞争,从而强制变为串行执行,但是执行for循环的条件以及i++都是并发执行的。和join等待操作相比,join操作时是等一个线程执行完了 再让join返回,第二个线程才能继续执行。
上述代码t1释放锁之后,下一次拿到锁的是t1还是t2仍然是概率性问题。
2.2 synchronized的几种使用方式
在示例方法内加锁及给类方法加锁:
class Counter {
public int count = 0;
synchronized public void add() {
// synchronized (this) {
//
// }
count++;
}
synchronized public static void func() {
// synchronized (Counter.class) {
//
// }
}
public int get() {
return count;
}
}
public class Demo24 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Counter counter1 = new Counter();
Thread t1 = new Thread(() -> {
// synchronized (counter) {
// counter.add();
// }
for (int i = 0; i < 5000; i++) {
//counter.func();
counter.add();
}
});
Thread t2 = new Thread(() -> {
// synchronized (counter) {
// counter.add();
// }
//counter.add();
for (int i = 0; i < 5000; i++) {
//counter1.func();
counter.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.get());
}
}
2.3 synchronized的可重入性
class Counter1 {
private int count = 0;
synchronized public void add() {
count++;
}
public int get() {
return count;
}
}
public class Demo25 {
public static void main(String[] args) throws InterruptedException {
Counter1 counter=new Counter1();
Thread t1=new Thread(()->{
for (int i = 0; i < 50000; i++) {
//相当于
// synchronized (counter) {
// synchronized (counter) {
// count++;
// }
// }
//按理说这样写应该会阻塞,因为外括号已经在counter上加了锁,内括号再加就会堵塞
//内等待外执行完但是外又在等内执行完,于是进入死锁
//但是java中不会这样,因为如果在java中内外是一个锁对象则直接进入
//理论上上这是线程死锁的情况一
synchronized (counter) {
counter.add();
}
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.get());
}
}
上述代码中的t1按理应该进入死锁,但是因为java中的synchronized具有可重入性所以避免了死锁,但是在其它语言中都是会死锁的。
三、死锁
死锁的三种典型场景:
- 锁的不可重入性,一个线程获取了一个锁,如果再对这个锁加锁就会产生死锁。不过引入可重入锁就可以解决,并且java中的用来加锁的synchronized具有可重入性所以无需考虑这种情况。
- 两个线程两把锁。例如线程1加锁a,线程2加锁b,线程a又请求锁b,线程2又请求锁a此时就会死锁。
- 多个线程多把锁。例子就是经典的哲学家就餐问题。
3.1 死锁的必要条件
(1)锁具有互斥性,一个线程获取锁a,如果另一个线程也想获取锁a就得阻塞等待。
(2)锁具有不可剥夺性:一个线程获取锁a,除非它主动释放,其它线程无法夺取线程上的锁a。
(3)请求和保持:一个线程获取锁a,在不释放该锁的前提下,去获取其它的锁。
(4)循环等待:多个线程获取多个锁的过程中出现循环等待。
这四个必要条件缺一不可,任何死锁的情况都必须具备以上四点否则无法构成死锁。
如果想避免死锁,可以打破上述的四个必要条件。条件一二如果是自己实现的锁那么可以打破,但是在java的synchronized中这两条是无法打破的,但是可以从后面两条来进行打破。
打破条件三是要从代码结构上进行改变,因为条件三会造成锁的嵌套,只要让自己建立的锁不嵌套即可,当然有的时候会不得不去嵌套锁。
打破条件四只需要规定好加锁的顺序即可。
代码示例如下:
public class Demo26 {
public static void main(String[] args) throws InterruptedException {
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (locker1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker2) {
System.out.println("t1 获取了两把锁");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (locker1) {
synchronized (locker2) {
System.out.println("t2 获取了两把锁");
}
}
});
//以上代码会发生线程死锁的情况二
//可以通过修改代码结构来避免死锁
//避免循环等待:约定加锁的顺序
//Thread t1 = new Thread(() -> {
// synchronized (locker1) {
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
// synchronized (locker2) {
// System.out.println("t1 获取了两把锁");
// }
// }
//
// });
//
// Thread t2 = new Thread(() -> {
// synchronized (locker1) {
//
// synchronized (locker2) {
// System.out.println("t2 获取了两把锁");
// }
// }
// });
//避免请求和保持:
//Thread t1 = new Thread(() -> {
// synchronized (locker1) {
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
//
// }
// synchronized (locker2) {
// System.out.println("t1 获取了两把锁");
// }
// });
//
// Thread t2 = new Thread(() -> {
// synchronized (locker2) {
//
//
// }
// synchronized (locker1) {
// System.out.println("t2 获取了两把锁");
// }
// });
t1.start();
t2.start();
t1.join();
t2.join();
}
}