synchronized和volatile详解

我们先来看一下两个关键词作用与区别

java的线程抽象内存模型
 
java的线程抽象内存模型中定义了每个线程都有一份自己的私有内存,里面存放自己私有的数据,其他线程不能直接访问,而一些共享数据则存在主内存中,供所有线程进行访问。 
上图中,如果线程A和线程B要进行通信,就要经过主内存,比如线程B要获取线程A修改后的共享变量的值,要经过下面两步: 
     (1)、线程A修改自己的共享变量副本,并刷新到了主内存中。 
     (2)、线程B读取主内存中被A更新过的共享变量的值,同步到自己的共享变量副本中。

java多线程中的原子性、可见性、有序性
     (1)、原子性:是指线程的多个操作是一个整体,不能被分割,要么就不执行,要么就全部执行完,中间不能被打断。 
     (2)、可见性:是指线程之间的可见性,就是一个线程修改后的结果,其他的线程能够立马知道。 
     (3)、有序性:为了提高执行效率,java中的编译器和处理器可以对指令进行重新排序,重新排序会影响多线程并发的正确性,有序性就是要保证不进行重新排序(保证线程操作的执行顺序)。

volatile关键字的作用
volatile关键字的作用就是保证了可见性和有序性(不保证原子性)

Volatile如何保证内存可见性:

1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
2.当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

Volatile如何保证内存有序性:
volatile能禁止指令重排,在指令重排序优化时,在volatile变量之前的指令不能在volatile之后执行,在volatile之后的指令也不能在volatile之前执行,所以它保证了有序性。

synchronized关键字的作用
synchronized提供了同步锁的概念,被synchronized修饰的代码段可以防止被多个线程同时执行,必须一个线程把synchronized修饰的代码段都执行完毕了,其他的线程才能开始执行这段代码。 
因为synchronized保证了在同一时刻,只能有一个线程执行同步代码块,所以执行同步代码块的时候相当于是单线程操作了,那么线程的可见性、原子性、有序性(线程之间的执行顺序)它都能保证了。

使用范围:

volatile只能修饰变量,作用范围比较小.

synchronized可以修饰变量,方法,类,代码块

修饰方法:

1,修饰实例方法

看代码:

public class SyncTest implements Runnable{
    //共享资源变量
    int count = 0;

    @Override
    public synchronized void run() {
        for (int i = 0; i < 5; i++) {
        	try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
            System.out.println(Thread.currentThread().getName()+":"+count++);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SyncTest syncTest1 = new SyncTest();
        //SyncTest syncTest2 = new SyncTest();
        Thread thread1 = new Thread(syncTest1,"thread1");
        Thread thread2 = new Thread(syncTest1, "thread2");
        thread1.start();
        thread2.start();
    }
}
运行结果:
thread1:0
thread1:1
thread1:2
thread1:3
thread1:4
thread2:5
thread2:6
thread2:7
thread2:8
thread2:9

我们两次new Thread()传进去的是同一对象(syncTest1),所以这里在run方法加上synchronized,相当于每次都是调用syncTest1的run()方法.我们说的获取一个锁,此处就是指获取syncTest1.run()的锁。

我们从输出结果看出:当一个线程正在访问一个对象synchronized实例方法时,别的线程是访问不了的。一个对象一把锁说的就是这个.

但是如果我们有分别属于两个对象的不同线程呢?

我们把上面代码的注释打开,同时创建第二个线程时,传进去syncTest2对象.

public class SyncTest implements Runnable{
    //共享资源变量
    int count = 0;

    @Override
    public synchronized void run() {
        for (int i = 0; i < 5; i++) {
        	try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
            System.out.println(Thread.currentThread().getName()+":"+count++);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SyncTest syncTest1 = new SyncTest();
        SyncTest syncTest2 = new SyncTest();
        Thread thread1 = new Thread(syncTest1,"thread1");
        Thread thread2 = new Thread(syncTest2, "thread2");
        thread1.start();
        thread2.start();
    }
}
运行结果:
thread1:0
thread2:0
thread2:1
thread1:1
thread1:2
thread2:2
thread2:3
thread1:3
thread1:4
thread2:4

如果是一个线程 A 需要访问实例对象 obj1 的 synchronized 方法 f1(当前对象锁是obj1),另一个线程 B 需要访问实例对象 obj2 的 synchronized 方法 f1(当前对象锁是obj2),这样是允许的,因为两个实例对象锁并不同相同,此时如果两个线程操作数据并非共享的,线程安全是有保障的,遗憾的是如果两个线程操作的是共享数据,那么线程安全就有可能无法保证了。thread1和thread2分别进入了syncTest1和syncTest2的实例锁,当然保证不了线程安全。

如果synchronized修饰的是静态方法呢?

看代码

public class SyncTest implements Runnable {
    //共享资源变量
    static int count = 0;

    @Override
    public synchronized void run() {
        increaseCount();
    }

    private synchronized static void increaseCount() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + count++);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SyncTest syncTest1 = new SyncTest();
        SyncTest syncTest2 = new SyncTest();
        Thread thread1 = new Thread(syncTest1, "thread1");
        Thread thread2 = new Thread(syncTest2, "thread2");
        thread1.start();
        thread2.start();
    }
}
运行结果:
thread1:0
thread1:1
thread1:2
thread1:3
thread1:4
thread2:5
thread2:6
thread2:7
thread2:8
thread2:9

同样是new了两个不同实例,却保持了线程同步。那是我们synchronizd修饰的是静态方法,run方法中调用这个静态方法,再说一次 静态方法不属于当前实例,而是属于类。所以这个方案其实是用的一个把锁,而这个锁就是这个类的class对象锁。
  需要注意的是如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁(结合开始两个现象)

synchronized修饰代码块

  首先这个使用时的场景是:在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了。所以他的作用范围为synchronizd(obj){}的这个大括号中

public class SyncTest implements Runnable {
    //共享资源变量
    static int count = 0;
    private byte[] mBytes = new byte[0];

    @Override
    public synchronized void run() {
        increaseCount();
    }

    private void increaseCount() {
        //假设省略了其他操作的代码。
        //……………………
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + count++);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SyncTest syncTest1 = new SyncTest();
        SyncTest syncTest2 = new SyncTest();
        Thread thread1 = new Thread(syncTest1, "thread1");
        Thread thread2 = new Thread(syncTest2, "thread2");
        thread1.start();
        thread2.start();
    }
    /**
     * 输出结果
     thread1:0
     thread2:0
     thread1:1
     thread2:2
     thread2:4
     thread1:3
     thread2:5
     thread1:5
     thread2:7
     thread1:6
     */
}

  从输出结果看出,这个demo并没有保证线程安全,因为我们指定锁为this,指的就是调用这个方法的实例对象。这里我们new了两个不同的实例对象syncTest1,syncTest2,所以有两个锁,thread1与thread2分别进入自己传入的对象锁的线程执行increaseCount方法,做成线程不安全。如果把这个demo的成员变量注释放开,并将mBytes传入synchronized后面的括号中,也是线程不安全的结果。这里之所以加上mBytes这个对象是为了说明synchronized后面的括号中是可以指定任意对象充当锁的,而零长度的byte数组对象创建起来将比任何对象都经济。当然,如果要使用这个经济实惠的锁并保证线程安全,那就不能new出多个不同实例对象出来啦。如果你非要想new两个不同对象出来,又想保证线程同步的话,那么synchronized后面的括号中可以填入SyncTest.class,表示这个类对象作为锁,自然就能保证线程同步啦。使用方法为:

synchronized(xxxx.class){
  //todo
}

总结

  1. 修饰普通方法 一个对象中的加锁方法只允许一个线程访问。但要注意这种情况下锁的是访问该方法的实例对象, 如果多个线程不同对象访问该方法,则无法保证同步。

  2. 修饰静态方法 由于静态方法是类方法, 所以这种情况下锁的是包含这个方法的类,也就是类对象;这样如果多个线程不同对象访问该静态方法,也是可以保证同步的。

  3. 修饰代码块 其中普通代码块 如Synchronized(obj) 这里的obj 可以为类中的一个属性、也可以是当前的对象,它的同步效果和修饰普通方法一样;Synchronized方法 (obj.class)静态代码块它的同步效果和修饰静态方法类似。

参考地址:Synchronized三种用法

 

volatile关键字和synchronized关键字的区别
     (1)、volatile只能作用于变量,使用范围较小。synchronized可以用在变量、方法、类、同步代码块等,使用范围比较广。 
     (2)、volatile只能保证可见性和有序性,不能保证原子性。而可见性、有序性、原子性synchronized都可以包证。 
     (3)、volatile不会造成线程阻塞。synchronized可能会造成线程阻塞。

 

面试题:

synchronized修饰变量,代码块,方法,类分别有什么作用?(腾讯)

修饰变量能够保证原子性,可见性,有序性.修饰代码块如果括号后面跟的是this或者一个属性,则此锁只作用在一个对象上,如果括号内是一个xxx.class,则作用在该类,即两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。修饰实例(普通)方法,则只作用在一个对象上,修饰静态方法,作用于该类.

阿里,完美等公司也问过相似问题

 

相关链接:

              基于JDK实现的锁:ReentrantLock以及重入锁实现方式

              JVM针对synchronized的锁优化:自旋锁,锁消除,锁粗化,轻量级锁,锁消除

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值