1. 前言
临界资源:一次仅允许一个进程使用的资源就称为临界资源
临界区:访问临界资源的代码块
竞态条件:多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。
为了避免临界区的竞态条件的发生,我们可以采用阻塞式的解决方案和非阻塞式的解决方案。阻塞式的方案就是使用synchronized。synchronized:对象锁,保证了临界区内代码的原子性,采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其它线程获取这个对象锁时会阻塞,保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
synchronized的锁对象理论上可以是任意的唯一对象,并且 synchronized是可重入的,不公平的重量级锁。
2. 使用
关键字所在位置
使用的三种格式:锁对象,同步静态方法,同步普通方法
synchronized(锁对象){
// 访问共享资源的核心代码
}
//同步方法
修饰符 synchronized 返回值类型 方法名(方法参数) {
方法体;
}
//同步静态方法
修饰符 static synchronized 返回值类型 方法名(方法参数) {
方法体;
}
同步方法的底层也是类似于锁对象的:
//如果方法是实例方法:同步方法默认用 this 作为的锁对象
public synchronized void test() {}
//等价于
public void test() {
synchronized(this) {}
}
//如果方法是静态方法:同步方法默认用类名 .class 作为的锁对象
class Test{
public synchronized static void test() {}
}
//等价于
class Test{
public void test() {
synchronized(Test.class) {}
}
}
举例:
public class Demo {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
room.increment();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
room.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(room.get());
}
}
class Room {
int value = 0;
public synchronized void increment() {
value++;
}
public synchronized void decrement() {
value--;
}
public synchronized int get() {
return value;
}
}
3. 锁分类
当在代码块中使用时,锁是代码块中传入的对象,比方说我们自己创建的对象或者是this。
当在普通方法上使用,锁是当前实例对象。
被
synchronized
修饰函数我们简称同步函数,线程执行称同步函数前,需要先获取监视器锁,简称锁,获取锁成功才能执行同步函数,同步函数执行完后,线程会释放锁并通知唤醒其他线程获取锁,获取锁失败「则阻塞并等待通知唤醒该线程重新获取锁」,同步函数会以this
作为锁,即当前对象。
当再静态方法上使用,锁是当前类的对象。
Java
的静态资源可以直接通过类名调用,静态资源不属于任何实例对象,它只属于Class
对象,每个Class
在J V M
中只有唯一的一个Class
对象,所以同步静态函数会以Class
对象作为锁,后续获取锁、释放锁流程都一致。Java类可能会有很多个对象,但是Class对象只有一个。不过Class对象其实也是存放在堆中的实例对象,只不过比new出来的对象特殊一点,是jvm加载类时所创建的。所以不管这个对象new多少个新实例,class对象只有一个,作为锁的对象,线程安全。
4. 线程八锁问题
可能看名字比较晦涩难懂,但是其实就是看synchronized锁住的是哪个对象,根据锁住的对象对代码的执行进行分析。
锁住类对象:所有类的实例方法都是安全的
锁住this对象:只有当前实例对象的线程内是安全的,如果存在多个实例就可能导致不安全
来看几个例子:
例一:线程不安全,线程1调用a方法,锁住类对象;而线程2调用b方法,锁住实例对象,即n2。两个线程锁住的不是同一个对象,相当于锁并没有起作用。
class Number{
public static synchronized void a(){
Thread.sleep(1000);
System.out.println("1");
}
public synchronized void b() {
System.out.println("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}
例二:synchronized标注在静态方法上,则说明a和b两个方法都是对类对象进行加锁。当线程1调用a方法,线程2调用b方法时,两个线程对同一对象进行加锁,则是线程安全的。
class Number{
public static synchronized void a(){
Thread.sleep(1000);
System.out.println("1");
}
public static synchronized void b() {
System.out.println("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}