1.代码示例:
package thread3;
import java.util.Scanner;
public class Test2 {
public static Object object = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
while (true) {
synchronized (object) {
System.out.println("请输入一个整数:");
int a = scanner.nextInt();
System.out.println("a: " + a);
}
}
});
thread1.start();
Thread.sleep(2000);
Thread thread2 = new Thread(() -> {
synchronized (object) {
System.out.println("hello thread2");
}
});
thread2.start();
}
}
加锁的时候,如果针对不同的对象加锁,这就意味着俩个线程之间不会有任何的锁竞争.
2.synchronized 的特性
互斥
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.
- 进入 synchronized 修饰的代码块, 相当于 加锁
- 退出 synchronized 修饰的代码块, 相当于 解锁
可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 "锁定" 状态(类似于厕所的 "有人/无人").
如果当前是 "无人" 状态, 那么就可以使用, 使用时需要设为 "有人" 状态.
如果当前是 "有人" 状态, 那么其他人无法使用, 只能排队
一个线程先上了锁,其他线程只能等待这个线程释放!
理解 "阻塞等待"
针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝
试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的
线程, 再来获取到这个锁.
注意:
上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 "唤醒". 这
也就是操作系统线程调度的一部分工作.
假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B
和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能
获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.
synchronized的底层是使用操作系统的mutex lock实现的.
刷新内存
synchronized 的工作过程:
- 获得互斥锁
- 从主内存拷贝变量的最新副本到工作的内存
- 执行代码
- 将更改后的共享变量的值刷新到主内存
- 释放互斥锁
所以 synchronized 也能保证内存可见性. 具体代码参见后面 volatile 部分.
可重入
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题
package thread1;
class Counter {
public int count = 0;
synchronized public void increase() {
synchronized (this) {
count++;
}
}
}
public class Test10 {
private static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
}, "thread1");
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
}, "thread2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(counter.count);
}
}
1.当调用increase的时候,先进行加锁操作,针对this来加锁(此时this就是一个锁定的状态了,把this对象头中的标志位给设置上了)
2.继续执行到下面的代码块的时候,也会尝试再次加锁,由于此时this已经是锁定状态了,按照之前的理解,这里的加锁操作就会出现阻塞,这里的阻塞啥时候结束呢?等到之前的代码把锁给释放了。要执行完这个方法,锁才能释放,但是由于此处的阻塞,导致当前这个方法没法继续执行了(僵住了)。
连续针对一个同一个对象(此处是this,也就是counter对象),加锁俩次,并且如果锁不是可重入的话,就会出现死锁。
Java设计锁的大佬就考虑到了这个情况~于是就把当前这里的synchronized设计成可重入锁了.针对同一个锁,连续加锁多次,不会有负面效果~~
锁中持有了两个信息:
- 1.当前这个锁被哪个线程给持有了.
- 2.当前这个锁被加锁几次了~~
当线程t已经加锁成功之后
后续再次尝试加锁,就会自动的判定出,当前这把锁就是t持有的.第二次加锁不会真的"加锁",而只是进行一个修改计数.( 1 ->2)后续往下执行的时候,出了synchronized 代码块,就触发一次解锁.(也不是真的解锁,而是计数-1)在外层方法执行完了之后,再次解锁,再次计数-1,计数减成0了,才真正的进行解锁~
死锁出现的情况,不仅仅是上述这一种情况,(针对同一把锁,连续加俩次)
1.一个线程,一把锁
2.俩个线程,俩把锁
3.N个线程,M把锁
哲学家就餐问题
这里会单独出一篇博客,实现一下哲学家就餐问题,先介绍一下这个问题~
该问题描述的是五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五只筷子,他们的生活方式是交替的进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐完毕,放下筷子继续思考。
3.Java 标准库中的线程安全类
Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.
- ArrayList
- LinkedList
- HashMap
- TreeMap
- HashSet
- TreeSet
- StringBuilder
但是还有一些是线程安全的. 使用了一些锁机制来控制.
- Vector (不推荐使用)
- HashTable (不推荐使用)
- ConcurrentHashMap
- StringBuffer
还有的虽然没有加锁, 但是不涉及 "修改", 仍然是线程安全的
- String