1.synchronized的特性
1.1互斥
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到 同一个对象 synchronized 就会阻塞等待.
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁
synchronized用的锁是存在Java对象头里的。
可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 "锁定" 状态(类似于厕 所的 "有人/无人").
如果当前是 "无人" 状态, 那么就可以使用, 使用时需要设为 "有人" 状态.
如果当前是 "有人" 状态, 那么其他人无法使用, 只能排队
理解 "阻塞等待".
针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁.
注意:
上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 "唤醒". 这 也就是操作系统线程调度的一部分工作.
假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.(非公平锁)
synchronized的底层是使用操作系统的mutex lock实现的.
1.2刷新内存
synchronized 的工作过程:
1. 获得互斥锁
2. 从主内存拷贝变量的最新副本到工作的内存
3. 执行代码
4. 将更改后的共享变量的值刷新到主内存
5. 释放互斥锁
所以sychronized也可以保证内存可见性
1.3可重入
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
理解 "把自己锁死"
一个线程没有释放锁, 然后又尝试再次加锁.
按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无 法进行解锁操作. 这时候就会 死锁.
这样的锁称为不可重入锁
Java 中的 synchronized 是 可重入锁, 因此没有上面的问题.
代码示例
在下面的代码中, increase 和 increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 当前对象加锁的.
在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释放, 相当于连续加两次锁)
这个代码是完全没问题的. 因为 synchronized 是可重入锁
static class Counter {
public int count = 0;
synchronized void increase() {
count++;
}
synchronized void increase2() {
increase();
}
}
在可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息.
如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.
解锁的时候计数器递减为0的时候, 才真正释放锁. (才能被别的线程获取到)
2.synchronized使用示例
synchronized 本质上要修改指定对象的 "对象头". 从使用角度来看, synchronized 也势必要搭配一个具体的对象来使用.
2.1直接修饰普通方法:
锁的SynchronizedDemo对象
public class SynchronizedDemo{
public synchronized void method(){
}
}
2.2修饰静态方法
锁的 SynchronizedDemo 类的对象
public class SynchronizedDemo{
public synchronized static void method(){
}
}
2.3修饰代码块
明确指定锁哪个对象
锁当前对象:
public class SynchronizedDemo{
public synchronized void method(){
synchronized(this){
}
}
}
锁类对象
public class SynchronizedDemo {
public void method() {
synchronized (SynchronizedDemo .class) {
}
}
}
我们要重点理解,synchronized锁的是什么,两个线程竞争同一把锁,才会产生阻塞等待
两个线程分别尝试获取两把不同的锁,不会产生竞争
3.Java 标准库中的线程安全类
Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.
ArrayList、LinkedList、HashMap、TreeMap、HashSet、TreeSet、StringBuilder
但是还有一些是线程安全的. 使用了一些锁机制来控制.
Vector (不推荐使用)、HashTable (不推荐使用)、 ConcurrentHashMap、 StringBuffer
StringBuffer 的核心方法都带有synchronized
还有的虽然没有加锁, 但是不涉及 "修改", 仍然是线程安全的(string)
4.volatile关键字
4.1volatile能保证内存可见性
volatile 修饰的变量, 能够保证 " 内存可见性".
代码在写入 volatile 修饰的变量的时候:
改变线程工作内存中volatile变量副本的值,将改变后的副本的值从工作内存刷新到主内存
代码在读取 volatile 修饰的变量的时候:
从主内存中读取volatile变量的最新值到线程的工作内存中,从工作内存中读取volatile变量的副本
前面我们讨论内存可见性时说了, 直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度非常快, 但是可能出现数据不一致的情况.
加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了.
代码示例:
创建两个线程 t1 和 t2
t1 中包含一个循环, 这个循环以 flag == 0 为循环条件.、
t2 中从键盘读入一个整数, 并把这个整数赋值给 flag.
预期当用户输入非 0 的值的时候, t1 线程结束.
package thread;
import java.util.Scanner;
public class ThreadDemo9 {
// 内存可见性
private static int isQuit = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (isQuit == 0) {
;//do nothing
}
System.out.println("t1 执行结束. ");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入 isQuit 的值: ");
isQuit = scanner.nextInt();
});
t1.start();
t2.start();
}
}
执行效果:当用户输入非0值时,t1线程循环不会结束!(显然与预期不符)
程序在编译运行的时候,Java编译器和jvm可能会对代码做出一些“优化”
编译器优化本质上是靠代码,智能的对你写的代码进行分析判断,进行调整,这个调整在大多数情况都是没问题的,但是在多线程环境下有可能会出现差错!
isQuit==0 本质上是两个指令
1.load(读内存)读内存操作,速度非常慢
2.jcmp(比较并跳转) 寄存器操作,速度非常快
此时,编译器就发现这个逻辑中代码要反复的快速读取同一个内存的值,并且这个内存的值读出来每次都相同,此时编译器把load操作优化掉了!后续都不再执行load,直接拿寄存器中的数据进行比较!
编译器没想到在另一个线程中把isQuit的值改了,这就是内存可见性问题!
volatile关键字可以弥补上述缺口,把volatile用来修饰一个变量之后,编译器就明白这个变量是“易变的”,就不会按照上述方式优化,就可以保证t1在循环过程中,始终都能读取内存中的数据
volatile本质上是保证变量的内存可见性(禁止该变量的读操作被优化到寄存器中)
如果给flag加上volatile
当用户输入非0值时,t1线程循环能够立即结束
4.2volatile不保证原子性
volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性
代码示例
这个是最初的演示线程安全的代码.
给 increase 方法去掉 synchronized
给 count 加上 volatile 关键字.
package thread;
public class ThreadDemo8 {
static class Counter {
public volatile 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);
}
}
最终 count 的值仍然无法保证是 100000
1.synchronized 也能保证内存可见性
synchronized 既能保证原子性, 也能保证内存可见性.
对上面的代码进行调整:
掉 flag 的 volatile
给 t1 的循环内部加上 synchronized, 并借助 counter 对象加锁.
package thread;
import java.util.Scanner;
public class ThreadDemo9 {
// 内存可见性
// private volatile static int isQuit = 0;
private static int isQuit = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (isQuit == 0) {
synchronized (ThreadDemo9.class){
;
}
}
System.out.println("t1 执行结束. ");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入 isQuit 的值: ");
isQuit = scanner.nextInt();
});
t1.start();
t2.start();
}
}