某个代码在单线程下执行没有任何问题,在多线程下执行出现bug。
把代码比如一个房间,每个线程就是要进入这个房间的人。如果没有任何保护机制,A进入之后,进行一些列隐私操作,然后B也可以进入房间,从而打断A,这样就是不具备原子性的。把线程A进入房间的一系列操作进行打包成一个整体进行上锁,其他线程就进不来,这样就保证了代码的原子性。
synchronized关键字,随便放Object对象都行,两个线程之间是否使用的是同一个对象,是同一个会产生竞争,反则是不会。进入代码块加锁,出代码块是解锁。synchronized 修饰普通方法,相当于给this加锁(锁对象是this),修饰静态方法,相当于给类对象加锁。
public class ThreadDemo19 {
private static int count = 0;
private static int count2 = 0;
public static void main(String[] args) throws InterruptedException {
// 随便创建个对象
Object locker = new Object(); //两个线程之间是否使用的是同一个对象,是同一个会产生竞争,反则是不会
Object locekr2 = new Object();
// 创建两个线程,每个线程都针对上述count变量循环5w次 循环自增的代码存在线程安全问题
int tmp = 0;
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
/*// 1)一下线程修改一个变量,没有影响
count++;*/
// 加锁 synchronized
synchronized(locker) { //进入大括号 就会加锁
count++;
} // 出大括号 就会解锁
// 2)多个线程读取同一个变量,不会影响
//System.out.println(count); //只是读取变量,变量的内容是固定不变的
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
//3)多个线程修改不同的变量,没有影响
//count2++;
//System.out.println(count);
//两个线程针对不同对象加锁,存在不了锁竞争,就会出现线程安全问题
/*synchronized (locekr2) {
count++;
}*/
synchronized (locker) {
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
//打印count的结果
System.out.println("count = "+ count);
}
}
synchronized 加锁的效果也称为 互斥性。
class Test {
public int count;
// synchronized 是加到static方法上,就等价于给 类对象加锁
/*synchronized public static void func() {
}
public static void func() { // 使用较少
synchronized (Test.class) {
}
}*/
// 等同于下面 synchronized (this)的代码
/*synchronized public void add() {
count++;
}*/
public void add() {
/*synchronized (this) {
count++;
}*/
// 获取Test类的对象 (在一个java进程中,一个类的类对象都是只有一个)
// 所以在这里第一线程和第二个线程拿到的类对象是同一个类对象,因此锁竞争仍然存在能保证线程安全
synchronized (Test.class) {
count++;
}
}
}
public class ThreadDemo20 {
public static void main(String[] args) throws InterruptedException {
Test test = new Test();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
test.add();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
test.add();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("count = "+test.count);
}
}
可重入锁:一共就只有一把锁,同一个线程,此时锁对象就知道第二次加锁的线程就是持有锁的线程,第二次进行加锁的发现加锁线程和持有锁线程是同一个线程,即能加锁。
判定当前加锁线程是否是加锁的线程,如果不是同一个线程,阻塞;如果是同一个线程,++计数器。
public class ThreadDemo21 {
public static void main(String[] args) {
Object locker = new Object();
Thread t = new Thread(() -> {
synchronized (locker) { //可重入锁:在最外层的{进行加锁 真正加锁,同时把计数器+1(初始是0,+1
//之后就变成了1,说明当前这个对象被该线程加锁一次 )同时记录线程是谁
//再加一个锁,当前由于是同一个线程,此时锁对象就知道了第二次加锁的线程,就是持有锁的线程
// 第二次操作,就可以直接放行通过,不会出现阻塞 ——》这个特性 称为“可重入”
synchronized (locker) { //第二次加锁的时候,发现加锁线程和持有锁线程是同一个线程,即能加锁
//成功,++计数器,如果不是同一个线程,阻塞
System.out.println("hello");
} // 把计数器-1,2-1=》1,不为0.不会真的解锁
} // 在最外层的}进行解锁 1-1=0》 进行解锁
});
t.start();
}
}
加锁是解决线程安全问题,但是加锁方式不当就可能产生死锁。
死锁三种典型场景:
public class ThreadDemo22 {
public static void main(String[] args) {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
//sleep一下,给t2时间,让t2也能拿到B
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//尝试获取B,并没有释放A
synchronized (B) {
System.out.println("t1拿到了两个线程");
}
}
});
/*Thread t2 = new Thread(() -> {
synchronized (B) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//尝试获取A,并没有释放B
synchronized (A) {
System.out.println("t2拿到了两个线程");
}
}
});*/
Thread t2 = new Thread(() -> {
synchronized (A) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 解决方案:先对A进行加锁,再对B进行加锁
synchronized (B) {
System.out.println("t2拿到了两个线程");
}
}
});
t1.start();
t2.start();
}
}
产生死锁的四个必要条件:
Java标准库中很多都是线程不安全的,这些类可能会涉及到多线程修改共享数据,又没有任何加锁措施。
还有一些是线程安全的,使用了一些锁机制来控制:
设计一个预期通过 t2 线程输入的整数,只要输入的不为0,就可以使t1线程结束。
原理:
解决方案:给判断的变量 添加volatile关键字,在写入的时候:
在读取volatile修饰的变量的时候:
import java.util.Scanner;
public class ThreadDemo23 {
// t2修改了内存,但是t1没有看到这个内存的变化,就称为 内存可见性 问题
//volatile关键字 核心功能:保证内存可见性; 另一个功能:禁止指令重排序
private volatile static int flag = 0;
public static void main(String[] args) {
// 预期通过t2线程输入的整数,只要输入的不为0,就可以使t1线程结束
Thread t1 = new Thread(() -> {
while (flag == 0) {
// 循环体里,没有内容
/*try {
// 不加sleep,一秒循环上百亿次,load操作的整体开销非常大,优化的迫切程度就更高
// 加了之后,一秒循环1000次,load整体开销就没这么大,优化的迫切程度就降低了
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
}
System.out.println("t1 线程结束");
});
Thread t2 = new Thread(() -> {
System.out.println("请输入flag的值");
Scanner scanner = new Scanner(System.in);
flag = scanner.nextInt();
});
t1.start();
t2.start();
}
}
volatile和synchronized区别:
会出现线程安全:
// 会出现线程安全,无法保证最后结果是 100000
static class Counter {
volatile public int count = 0;
void increase() {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
final Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
使用synchronized,加锁,去掉volatile,给t1的循环内部加锁,并借助counter对象加锁:
static class Counter {
public int flag = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (true) {
synchronized (counter) {
if (counter.flag != 0) {
break;
}
}
// do nothing
}
System.out.println("循环结束!");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输入一个整数:");
counter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
由于线程之间是抢占式执行的,因此线程之间执行的先后顺序是随机的。
wait做的事情:
wait() 要搭配synchronized来使用,脱离synchronized使用wait会直接抛出异常。即要先有锁,才能调用wait且对象必须是同一对象。
wait结束等待的条件:
wait 被唤醒后也要重新参与锁竞争。
public class ThreadDemo24 {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("wait 之前");
object.wait();
System.out.println("wait 之后");
}
}
}
使用的哪个对象就是唤醒哪个对象的wait,如果两个wait是同一个对象调用的,随机唤醒其中一个,而notifyAll 唤醒这个对象所有等待的线程。
下面代码执行的过程:
public class ThreadDemo25 {
public static void main(String[] args) {
Object locker = new Object();
Thread t1 = new Thread(() -> {
synchronized (locker) {
System.out.println("t1 wait之前");
try {
// 释放锁,阻塞等待
locker.wait(); // 死等
//locker.wait(100); 带有超时的等待,ms 如果这个时间内没有进行notify,就不等
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 wait之后");
}
});
Thread t2 = new Thread(() -> {
try {
// 如果sleep写到synchronized外面的话,由于t1和t2执行顺序不确定,就可能t2先拿到锁,
// t1 没执行到 wait t2就先notify
Thread.sleep(5000); // 让t1先拿到锁
// 由于locker.wait(),锁是释放的,t2就能拿到锁
synchronized (locker) {
System.out.println("t2 notify 之前");
// 唤醒t1,t1从WAITING 状态恢复过来
// 由于t2此时还没有释放锁,t1恢复之后尝试获取锁,就可能出现锁竞争从而导致阻塞
locker.notify();
System.out.println("t2 notify 之后");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
}
}