Java学习之synchronized锁


前言

本文章主要对synchronized锁机制进行讲述

本人第一次写博客,如有不足,还请大家包含,谢谢~~

那么接下来就开始吧~


引入

当多个线程同时进行时,线程的调度是由操作系统决定的,也就是说各个线程执行的先后顺序是无法确定的

如果多个线程同时读写共享变量,读取的数据就会不一致:

public class Main {
    public static void main(String[] args) throws Exception {
        AddThread add = new AddThread();
        DevThread dev = new DevThread();
        add.start();
        dev.start();
        add.join();
        dev.join();
        System.out.println("count = " + Counter.count);
    }
}

class Counter{
    public static int count = 0;
}

class AddThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            Counter.count++;
        }
    }
}

class DevThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            Counter.count--;
        }
    }
}

当然,这里的count从逻辑上来讲应该是0 的

但多次运行之后可以发现,有时结果会是0,也会出现-259,-1228,422 … 之类很奇怪的数

要使结果正确,必须保证是原子操作


原子操作与可见性

原子操作是指不能被中断的一个或一系列的操作

举个例子:

i = i + 1;

这样一个简单的语句其实对应3 个操作:

  • 读取变量i的值
  • 进行加1 操作
  • 将新的值赋值给变量i

例如上述程序执行时可能会像这样:

Java学习之synchronized锁_第1张图片

这样count的值会因为线程执行时被操作系统中断而使得最终结果发生变化

同时还需要保证可见性:

可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到被修改的值


synchronized锁

通过加锁和解锁的操作,可以保证某一线程执行时,其他线程处于等待状态:

Java学习之synchronized锁_第2张图片

使用synchronized关键字对一个对象进行加锁,即可保证代码块在任意时刻最多只有一个线程能执行:

synchronized(lock) {
    i = i + 1;
}

在使用synchronized的时候,在代码块结束的时候都会正确释放锁,不必担心是否会出现异常

当然,这里的lock 可以是客户端锁(随便用一个对象作为锁,不推荐 --> 代码逻辑容易混乱,也不利于封装):

public static final Object lock = new Object();
...
synchronized(lock) {
	...
}

也可以是内置锁(this即当前实例,每一个对象都有一个内置锁):

synchronized(this) {
	...
}

如果锁住的是this实例时,等同于用synchronized修饰这个方法(这个方法就是同步方法,表示整个方法都必须用this加锁):

public synchronized void test() { // 锁住this
    ...
} // 解锁

那如果这个方法是静态方法呢?静态方法是没有this实例的

这时其实锁住的是该类的class实例(类锁),相当于:

public class Student {
    public static void test() {
        synchronized(Student.class) {
            ...
        }
    }
}

于是,通过使用synchronized,最开始遇到的问题就可以得到解决了:

class AddThread extends Thread{
    @Override
    public synchronized void run() {
        for (int i = 0; i < 10000; i++) {
            Counter.count++;
        }
    }
}

class DevThread extends Thread{
    @Override
    public synchronized void run() {
        for (int i = 0; i < 10000; i++) {
            Counter.count--;
        }
    }
}

synchronized去修饰着两个方法,结果就会正确了~

注意

使用synchronized的时候,获取到的是哪个锁很重要,锁对象如果不对,代码逻辑就不对

另外还需要注意一些不需要synchronized的操作

JVM 规范定义了几种原子操作:

  • 基本类型(longdouble除外)赋值(多行赋值语句必须保证是同步操作)
  • 引用类型赋值

这里longdouble是64 位数据,但在x64 平台的JVM 是把longdouble的赋值作为原子操作实现的,所以这个问题一般忽略


使用synchronized解决了多线程同步访问共享变量的问题

但因为synchronized代码块无法并发执行,加锁和解锁需要消耗一定的时间,它也带来了性能的下降

如果文章有什么不足,欢迎大家评论指出;如果本文对大家有什么帮助的话,也希望能点个赞,谢谢大家~

你可能感兴趣的:(java学习,java)