我们先来看一下两个关键词作用与区别
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 }
总结
修饰普通方法 一个对象中的加锁方法只允许一个线程访问。但要注意这种情况下锁的是访问该方法的实例对象, 如果多个线程不同对象访问该方法,则无法保证同步。
修饰静态方法 由于静态方法是类方法, 所以这种情况下锁的是包含这个方法的类,也就是类对象;这样如果多个线程不同对象访问该静态方法,也是可以保证同步的。
修饰代码块 其中普通代码块 如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的锁优化:自旋锁,锁消除,锁粗化,轻量级锁,锁消除