(1)确保线程互斥的访问同步代码
(2)保证共享变量的修改能够及时可见
原因:被synchronized修饰的代码,在开始执行的时候会加锁,为了保证可见性,有一条规则是这样的,
在对变量解锁的时候,必须先把变量同步回主内存中,保证了可见性。
(3)有效解决重排序问题
(注:synchronized是无法阻止指令重排的,但是由于加了synchronized之后,同一时间只能被
同一线程访问,也就是单线程执行的,所以可以保证有序性)
(1)修饰普通方法
(2)修饰静态方法
(3)修饰代码块
public synchronized void method1(){
System.out.println("method1 start");
try {
System.out.println("method1 execute");
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("method1 end");
}
public synchronized void method2(){
System.out.println("method2 start");
try {
System.out.println("method2 execute");
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("method2 end");
}
public static void main(String[] args) {
final SynchronizedTest test = new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
test.method1();;
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
test.method2();
}
}).start();
}
}
public static synchronized void method1(){
System.out.println("method1 start");
try {
System.out.println("method1 execute");
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("method1 end");
}
public void method3(){
System.out.println("method3 start");
try {
synchronized (this) {
System.out.println("method3 execute");
Thread.sleep(3000);
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("method3 end");
}
public void method4(){
System.out.println("method4 start");
try {
synchronized (this) {
System.out.println("method4 execute");
Thread.sleep(1000);
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("method4 end");
}
public static void main(String[] args) {
final SynchronizedTest test = new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
test.method3();;
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
test.method4();
}
}).start();
}
输出结果
method3 start
method3 execute
method4 start
method3 end
method4 execute
method4 end
虽然两个线程都正常的开始了,但是线程2在进入同步块之前,还是要等待线程1中同步块执行完毕。
Synchronized底层是通过一个monitor对象实现的,每个对象有一个监视器锁monitor。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时
尝试获取monitor的所有权。
1.如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2.如果线程已经占用该monitor,只是重新进入,则进入monitor的进入数加1
3.如果其他线程占用了该monitor,则线程进入阻塞状态,直到monitor的进入数变为0,
再重新尝试获取monitor的所有权。
Synchronized方法相对应普通方法,其常量池中多了ACC_SYNCHRONIZED标识符。JVM就是根据该标识符来实现方法的同步的,当方法调用时,调用指令将会检查
方法的ACC_SYNCHRONIZED方法是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完之后再释放monitor。在方法执行
期间,其他线程都不能同时获取同一个monitor对象。
首先理解一下
1.原子性
即一个操作或者多个操作,要么全部被执行,要么就都不执行。
在单线程环境中,我们可以认为整个步骤都是原子性操作,但是在多线程环境下则不一样,Java只保证了基本数据类型的变量和赋值操作才是原子性的。
要想在多线程下保证原子性操作,可以通过锁来实现,volatile无法保证复合操作的原子性。
2.可见性
指当多线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值。
当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后,它会立即更新到主内存中,当其他线程读取共享变量时,会直接从内存中读取。
3.有序性
即程序执行的顺序按照代码的先后顺序执行。
在java内存模型中,为了效率是允许编译器和处理器对指令进行重排序的,当然重排序不会影响单线程的操作,但是对多线程有影响。volatile可以保证有序性。
volatile的第一条语义是要保证线程之间变量的可见性,要符合两个规则:
Java为了保证其平台性,使Java应用程序与操作系统内存模型分开,需要定义自己的内存模型。在Java内存模型中,内存分为主内存和工作内存两部分,其中主内存
是所有线程共享的,而工作内存则是每个线程分配一份,各线程之间的内存独立,互不可见。在线程启动之后,虚拟机为每块内存分配一块工作内存,不仅包含线程内部定义
的局部变量,也包含线程之间共享的变量的副本,即为了提高执行效率,读取副本比直接读取内存更快。工作内存和主内存之间的图入下:
对于普通共享变量来说,如果再工作内存中发生了变化,必须要写回工作内存。但对于volatile变量来说,要求工作内存中发生变化之后要马上写回内存,而读取的时候要
从内存中读取,而不是读取本地工作内存中的副本。
比如执行i++操作,线程A和线程B同时执行,i++分为 读取i的值,对i进行操作,把结果赋值给i三部分操作,比如线程在执行i+1操作,还没有进行赋值操作,此时线程B
读取i的值,依然为初始值0,线程A执行完操作之后,把i赋值为1,而线程B也赋值为1,最终内存中i的值为1,而不是预期的2。
指令重排 是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能的提高并行度。编译器,处理器也遵循这个目标,单线程指令重排序没有问题,
但是多线程指令重排序就会带来问题。
参考网址:http://www.importnew.com/23535.html
(1)多线程时,如果线程A指令重排
线程A: context = loadInit();
flag = true;
线程B: while(!flag){
sleep(1000)
}
doSomething(context);
如果线程A指令重排,可能导致context还没有初始化,而线程B就已经使用,从而报错。
(2)指令重排导致单例模式失效
public class Singleton {
private static Singleton singleton = null;
private Singleton(){};
public static Singleton getInstance(){
if(singleton==null){
synchronized (Singleton.class) {
singleton = new Singleton(); //非原子操作
}
}
return singleton;
}
}
由于singleton = new Singleton()非原子操作,可以分为三部分
memory = allocate(); //1.分配对象的内存空间
ctorInstance(memory); //2.初始化对象
instance = memory; //3.设置instance指向刚分配的内存
这3步操作,2依赖于1,但是3并不依赖于2,多线程操作下,指令重排可能导致2和3互换,instance指向了刚分配的内存,此时对象还未初始化,
导致下一个线程进来发现instance不为空,直接返回,导致出错。如果再instance关键字上加volatile修饰就可防止指令重排。
volatile通过内存屏障来防止指令重排:
内存屏障:用来控制和规范CPU对内存操作顺序的CPU指令。
内存屏障列表:
1.loadload: 确保”前者数据装载“先于”后者装载指令“,屏障之前的执行完,屏障后面的才能执行
2.storestore:确保“前者数据”先于“后者数据”刷入系统内存,且“前者刷入系统内存的数据”对后者是可见的
3.loadstore: 确保“前者装载数据”先于“后者刷新数据到内存系统”,必须先从内存中获取数据,
然后再修改完push回内存,主要防止后面文件修改前面修改的值。
4.storeload: 确保“前者刷入系统内存”的数据对“后者加载数据”是可见的。
volatile的内存语义的实现策略:
1.在每个volatile写操作前,插入一个storestore屏障
2.在每个volatile写操作后,插入一个storeload屏障
3.在每个volatile读操作后,插入一个loadload屏障
4.在每个volatile读操作后,插入一个loadstore屏障