星球作业(第一期)synchronized关键字理解

synchronized关键字的作用

多线程访问共享变量时,多个线程对同一个变量进行操作时,有可能会发生数据的脏读。比如:
线程不安全-脏读

输出结果小于200000的原因是,两个线程同时对index进行增加时,当线程1对读取了index的值,对index进行了自增,还未返回时,线程2也读取了index的值。线程2返回的index值会将之前的值覆盖掉,就出现了与预计不同的结果。

synchronized关键字的作用就是在多线程情况下,对共享数据的访问进行同步,能够保证在同一时间下只有一个线程会对共享数据进行操作。实现这种同步的方式就是使用synchronized关键字进行加锁。


synchronized关键字的使用

synchronized关键字同步的方式有三种:

·修饰实例方法:相当于对当前对象进行加锁
·修饰静态方法:相当于对当前对象的类对象进行加锁
·修饰代码块 :指定加锁对象,相当于指定对象加锁

修饰实例方法

public class SynchronizedTest01 implements Runnable {
    private static int index = 0;

    private static SynchronizedTest01 instance = new SynchronizedTest01();

    //实例方法同步
    private synchronized void increase() {
        index ++;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100000; i ++)
            increase();
    }

    public static void main(String[] args) throws InterruptedException {
        //对同一对象进行操作
        Thread thread1 = new Thread(instance);
        Thread thread2 = new Thread(instance);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("index = " + index);
    }
}

/**
 * index = 200000
 * Process finished with exit code 0
*/

Thread1,2调用同一个Runnable对象的run方法,而run方法中的increase函数是实例同步,在同一时间内只有一个线程获取到instance实例的锁,调用increase函数;而另一个线程会在等待队列中尝试获取锁。

如果线程1,2调用的是不同的Runnable对象,index的值不能保证为200000:因为两个线程调用的同步方法,获取的是各自实例对象的锁,并不冲突,运行效果与最上面的截图类似。


修饰静态方法

public class SynchronizedTest01 implements Runnable {
    private static int index = 0;

    private static SynchronizedTest01 instance = new SynchronizedTest01();

    //静态同步方法
    private synchronized static void increase() {
        index ++;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100000; i ++)
            increase();
    }

    public static void main(String[] args) throws InterruptedException {
        //传入不同的Runnable对象
        Thread thread1 = new Thread(new SynchronizedTest01());
        Thread thread2 = new Thread(new SynchronizedTest01());
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("index = " + index);
    }
}

/**
 * index = 200000
 * Process finished with exit code 0
*/

线程1,2传入不同的Runnable对象,increase方法修改为静态同步方法,index结果为200000。因为当synchronized关键字修饰静态方法时,线程尝试获取的锁是当前实例对象对应的类对象的锁(这句话有些绕:class对象的锁,不是实例对象的锁)。而静态方法同步时获取的就是类对象的锁,相同类的不同对象,获取到的也是同一个锁,能保证线程安全。


修饰代码块

用synchronized关键字修饰方法时,是对函数整体的访问添加限制。当一个函数体结构很大,而需要同步的执行语句只有一小部分时,这时对函数整体添加同步,会降低代码的效率。这时我们可以用synchronized关键字直接修饰代码块,指明获取锁的对象,如下:

public class SynchronizedTest01 implements Runnable {
    private static int index = 0;

    private static SynchronizedTest01 instance = new SynchronizedTest01();

    private void increase() {
        index ++;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100000; i ++) {
            //修饰代码块
            //修饰实例对象
            synchronized (this) {
                increase();
            }
            //修饰类对象
            //synchronized (SynchronizedTest01.class) {
            //    increase();
            //}
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(instance);
        Thread thread2 = new Thread(instance);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("index = " + index);
    }
}

实例对象的锁 -> synchronized(instance)
类对象的锁 -> synchronized(TestSynchronized.class)


synchronized关键字和volatile关键字

共同点:
用于多线程访问中,对共享数据的同步,避免出现对线程访问时修改造成的数据异常。

不同点:
synchronized关键字如上所述,可用于修饰方法和代码块,通过获取不同对象的锁,来保证代码运行时的同步。这里再说一点:synchronized在代码运行之前就获取了锁,默认有其他线程与其争夺资源,是一种悲观锁;而CAS操作是在比较是否不同后再进行不同的操作,属于一种乐观锁。
volatile关键字用于修饰实例对象,通过jvm实现的规则来保证多线程访问数据的一致性:写操作优先于读操作;数据改变时会通知所有工作线程的本地副本失效,从内存中重新获取数据。volatile只能保证原子性操作的同步,如 i ++ 这类非原子性操作,即使用volatile修饰也无法保证同步,这时就需要用到synchronized关键字来修饰代码块了。


synchronized的内部优化

java锁的状态有四种:无锁状态,偏向锁,轻量级锁,重量级锁。锁的状态会随着竞争而升级,但无法降级。偏向锁和轻量级锁是java 1.6时引入的,目的是减少获得锁和释放锁带来的性能消耗。下面让我们一起看一看:

偏向锁

偏向锁使用了一种等到竞争出现时才会释放的机制,当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

为什么要使用偏向锁呢?我们来想一下:当一个对象的方法设计为支持多线程调用时,使用synchronized关键字对代码块进行同步。如果对象目前只被线程A调用,此时如果线程A每次访问都需要获得对象的锁,执行完再释放锁,并没有实际的作用,浪费了性能。
所以此时,线程A会持有对象的偏向锁。当没有其他线程竞争时,线程A会一直持有该锁,不去释放;当线程A再次访问对象时,由于已经持有了偏向锁,进入和退出同步块时不需要进行任何同步操作和CAS操作。

轻量级锁

轻量级锁加锁:线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

书接上文,当线程B开始争夺对象的锁时,发现偏向锁已被持有,这时会通过CAS来替换Mark Word,从而获取轻量级锁;而线程A持有的偏向锁也会被收回,与B一同竞争轻量级锁。获取到锁开始执行同步体,为获取到锁则进行自旋,等待锁释放。如果自旋的时间过长(自旋消耗cpu),或是在自旋的过程中出现了第三个线程来进行争夺,轻量级锁就会膨胀为重量级锁。重量级锁使除了持有锁之外的线程都阻塞,防止轻量级锁时等待过程中的CPU空转(自旋)。


参考资料

1.(推荐) zejian_ - 深入理解Java并发之synchronized实现原理
2.存在mornning -【Java多线程 锁优化】锁的三种状态切换
3.[深入理解java虚拟机]
4.[java并发编程艺术]

猜你喜欢

转载自blog.csdn.net/asd501823206/article/details/81534513
今日推荐