目录
synchronized关键字(监视器锁-monitor lock)
观察多线程下的风险
class TestClass {
public int sum;
public void add(){
sum ++;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
TestClass test = new TestClass();
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("the final result: " + test.sum);
}
}
我们使用两个线程, 分别对TestClass类下的test实例调用50000次add操作, 两个例子的操作都对test实例中的sum字段进行了50000次自增, 按理来说最后执行的结果应该是100000, 但是我们最终执行了3次, 分别得到了不同的结果, 如下:
发现: 预期结果是10w, 但是缺和实际上的不符, 三次运行的结果是个随机值, 结果都不确定, 实际结果和预期结果不一样, 这就是bug, 这也是多线程引起的bug之一,
三次的执行结果都不样, 这是为什么呢?
其本质上是因为线程之间的调度是不确定的,
此处的sum++操作在本质上被大致分成了3个CPU指令:
- load 读取操作, 将内存中的数据读取到CPU寄存器当中
- add 自增操作, 将sum的值自增+1
- save 保存操作, 将寄存器中sum的自增结果保存到内存当中去
但是两个线程调度顺序时随机的. 不确定的, 实际上的sum++操作就有很多种指令排序的可能.
这里简单的举个例子, 如下:
这种情况, 两个线程按顺序调度, 那么就不会产生问题, 但是如果两个线程按照不规则顺序调度, 那么就会产生多线程问题:
经过上面的讨论, 我们对线程安全的概念做出一些总结
线程安全的概念
程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
线程不安全的原因
修改共享数据
原子性
- 读取内存的数据
- 修改数据
- 将结果数据保存到内存中去
如果不保证关键语句的原子性, 那么在多线程的情况下, 势必在操作一个变量的时候, 会有另外一个线程插入到其中, 来影响最终结果.
可见性
![](https://i-blog.csdnimg.cn/blog_migrate/b36587c0adad271283cf33a83969d1d3.png)
- 线程之间的共享变量存储在 主内存 (Main Memory).
- 每一个线程都有自己的 "工作内存" (Working Memory) .
- 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
- 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.
Java中线程安全的类
- ArrayList
- LinkedList
- HashMap
- TreeMap
- HashSet
- TreeSet
- StringBuilder
- Vector
- HashTable
- ConcurrentHashMap
- StringBuffer
StringBuffer的核心方法都带有synchronized, 此外, 有些类虽然没有加锁, 但是被设定了无法修改, 仍然是线程安全的,例如 String类
锁
对于上述问题, 我们能否把这个sum++操作变成原子的呢? 这就要介绍java的锁机制了, 我们将sum++ 的多个指令集合捆绑在一起,让他能够在一次线程调度的时候全部执行, 这样子就解决这种线程随机调度所引起的问题,.
锁, 可以保证java语句的原子性. 锁有两个核心操作:
- 加锁
- 解锁
一旦某个进程加了锁之后, 其他线程也想加锁, 就不能直接加上, 必须先阻塞等待, 知道拿到锁的线程释放了锁为止.
由于其随机调度性, 如果有三个线程, 让线程1解锁之后, 线程2和线程3谁能拿到所是不确定的.
java中如何进行加锁, 这就要谈到synchronized关键字
synchronized关键字(监视器锁-monitor lock)
例如, 我们给上面的sum++进行加锁操作:
public void add(){
synchronized (this) {
sum++;
}
}
此处使用代买块的方式来表示: 进入synchronized修饰的代码块的时候就会触发加锁操作, 除了代码块就会触发解锁操作.
其中的this为锁所指向的对象. 如果两个线程针对同一个对象加锁, 此时就会出现"锁竞争"(一file:///C:/Users/L/Desktop/JavaEE初阶/java107_0316_多线程.png个线程拿到了锁, 另外一个线程就需要阻塞等待).
如果两个线程针对不同的对象进行加锁, 此时就不会存在锁竞争.
这个里面的()里的对象, 可以是任意一个Object对象(除了内置类型), 此处写了this就相当于给test实例为锁对象:
对于之前的例子, 我们对其进行加锁操作,并运行:
class TestClass {
public int sum;
public void add(){
synchronized (this) {
sum++;
}
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
TestClass test = new TestClass();
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("the final result: " + test.sum);
}
}
此时才是我们想要的结果.
![](https://i-blog.csdnimg.cn/blog_migrate/117001f461836b5acca0d54cd6a7780d.png)
例如这种情况, thread1已经先拿到了锁, 如果这个时候thread2再尝试进行加锁, 此时就会出现阻塞等待的情况, thread2就会等待thread1完成指令集并解锁. 这个本质上是把这个并发sum++操作变成了串行操作.
此外, 直接给方法加synchronized:
synchronized public void add(){
sum++;
}
此时就相当于以this为所对象. 如果synchronized修饰静态方法, 此时就不是给this加锁, 而是给类对象加锁, 例如test.class.
特性:
file:///C:/Users/L/Desktop/JavaEE初阶/java107_0316_多线程.png互斥
- 进入 synchronized 修饰的代码块, 相当于 加锁
- 退出 synchronized 修饰的代码块, 相当于 解锁
synchronized (this) {
sum++;
}
synchronized用的锁是存在于java的对象里头的, 每个对象在存储的时候, 都有一块用来表示当前锁的状态的内存. 如果是无锁状态, 就可以对其进行加锁, 加锁后需要标识已经加了锁, 其他线程要使用, 如果发现已经加锁, 那么就只能阻塞等待.
刷新内存
- 获取互斥锁
- 从内存中拷贝数据的副本到工作内存中去
- 执行代码
- 将执行结果返回存储到主内存当中去
- 释放锁
可重入
// 第一次加锁, 加锁成功
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待.
lock();
按照之前所说的, 加了锁的线程, 再次加锁就会进入阻塞等待, 直到第一次的锁被释放, 但是释放锁的过程也是由这个线程来执行的, 这就产生了矛盾, 也就是无法进行解锁操作, 这个时候就被称之为"死锁"
class TestClass {
public int sum;
synchronized public void add(){
doadd();
}
synchronized public void doadd() {
sum++;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
TestClass test = new TestClass();
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("the final result: " + test.sum);
}
}
synchronized的使用
synchronized本质上要修改指定对象的"锁标识", 所以在使用的角度来说也必须要搭配一个对象.
public class Test{
public synchronized void methond() {
}
}
2.修饰静态方法: 锁的Test类的对象
public class Test{
public synchronized static void method() {
}
}
3.修饰代码块: 明确指出锁哪个对象
public class Test {
public void method() {
synchronized (this) {
// 代码块
}
}
}
volatile 关键字
先来看一个多线程bug:
public class Main {
public static int flag = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(()-> {
while(flag == 0) {
}
System.out.println("Thread1结束!!");
});
Thread thread2 = new Thread(()->{
Scanner in = new Scanner(System.in);
System.out.println("输入一个整数");
flag = in.nextInt();
});
thread1.start();
thread2.start();
}
}
上面的例子中, 使用全局变量flag作为线程1结束的标志判断, 然后再从线程2中去改变这个标志, 让线程1结束,, 但是我们在输入非0数字后, 线程并没有立马结束:
我们使用java的jdk.jconsole工具来查看这个线程1是否继续在运行:
可以发现这个Thread-0, 也就是我们的线程1, 并没有结束, 线程1 感受不到线程2对flag进行的修改,
public static volatile int flag = 0;
public class Main {
public static volatile int flag = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(()-> {
while(flag == 0) {
}
System.out.println("Thread1结束!!");
});
Thread thread2 = new Thread(()->{
Scanner in = new Scanner(System.in);
System.out.println("输入一个整数");
flag = in.nextInt();
});
thread1.start();
thread2.start();
}
}
为什么会产生上面这种问题?
我们来看这个while循环
load操作从内存读取数据到寄存器, 然后进行compare操作, 此处的cmp操作, load操作的时间开销是远远超过cmp的.
但是此时的编译器就发现, load的开销很大, 同时每次load的结果都是一样, 于是编译器就把这个load操作给又花掉了, 这样子就只执行了第一次load, 后续就只进行cmp操作.
volatile不保证原子性
class TestClass {
volatile public int sum;
public void add() {
sum++;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
TestClass test = new TestClass();
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("the final result: " + test.sum);
}
}
volatile不能保证其原子性
synchronized也能保证内存的可见性:
import java.util.Scanner;
class TestClass {
public int sum;
public void add() {
sum++;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
TestClass test = new TestClass();
Thread thread1 = new Thread(()-> {
while(true) {
synchronized (test) {
if (test.sum != 0) {
break;
}
}
}
});
Thread thread2 = new Thread(()-> {
Scanner in = new Scanner(System.in);
test.sum = in.nextInt();
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("the final result: " + test.sum);
}
}
wait和notify
wait()
- 使执行到wait代码的线程进入等待, (把线程放到等待队列中去)
- 释放当前的锁
- 满足一定条件的时候被唤醒, 重新尝试获取这个锁
注意: wait要搭配synchronized来使用, 脱离synchronized使用wait会直接抛出异常
wait 结束等待的条件
-
其他线程调用该对象的 notify 方法 .
-
wait等待时间超时, wait方法有一个指定参数的方法, 来制定等待时间
-
其他线程调用该线程的interrupted方法, 导致wait抛出异常
import java.util.Scanner;
public class Main {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
System.out.println("wait之前!");
object.wait();
System.out.println("wait之后 !!");
}
}
运行发现抛出异常(无效锁状态异常):
我们需要配合synchronized使用:
import java.util.Scanner;
public class Main {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
System.out.println("wait之前!");
synchronized (object) {
object.wait();
}
System.out.println("wait之后 !!");
}
}
结果才是正确的:
加锁的对象必须和wait的对象是同一个, 同时notify也要放在synchronized中使用.
但是我们也不能让他一直这样等待下去, 我们应该在需要唤醒他的时候来唤醒它.
wait(Long time)
wait还有一个传入参数版本的, 可以指定等待的时候, 如果时间过了就自动结束等待:
import java.util.Scanner;
public class Main {
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread thread1 = new Thread(()->{
while (true) {
System.out.println("wait start!");
synchronized (locker) {
try {
locker.wait(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("wait ended !");
}
});
thread1.start();
}
}
notify() / notifyAll()
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
- 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行 完,也就是退出同步代码块之后才会释放对象锁。
import java.util.Scanner;
public class Main {
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread thread1 = new Thread(()->{
while (true) {
System.out.println("wait start!");
synchronized (locker) {
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("wait ended !");
}
});
thread1.start();
Thread.sleep(1000);
// 必须要先wait, 才能notify才有效果, 如果还没有wait就notify, 此时wait就唤不醒,但是不会出现异常
Thread thread2 = new Thread(()->{
synchronized (locker) {
System.out.println("this is notify start!!");
locker.notify();
System.out.println("this is notify ended !");
}
});
thread2.start();
}
}
运行结果:
注意: 如果此时有三个线程thread1, thread2, thread3中都调用了object.wait, 此时如果在main方法中调用一个object.notify(), 会随机唤醒这三个线程中的一个, 另外两个仍然是wait状态, 如果调用了object.notifyAll, 此时就会把三个线程都唤醒. 然后这三个线程就会同时竞争锁,然后随机调度.
wait和sleep的对比
wait带有一个有时间参数版本的, 可以自动唤醒, 这个时候就感觉和sleep差不多.
但是他们最大的区别在于根本的用法, 或者是说设计这个东西是用来干嘛的, 是不一样的.
- wait是解决线程之间的控制顺序, 而sleep是单纯的让线程休眠一会!
- 实现上也是有区别的: wait需要搭配锁来使用, 必须拿到锁之后才能wait, 而sleep不需要