java——同步机制(synchronized, volatile)
1. java的线程间通信是由java的内存模型(JMM)来控制的。
JMM(java memory management) 定义了线程和主内存之间的抽象关系,一个是主内存(多线程之间来进行共享),一个是每个线程自己的私有内存
2. 为什么需要同步机制?
(1) 同步机制一般发生在多线程中,当需要跨线程维护程序的正确性,当需要多个线程之间共享非final变量时,就必须使用同步机制来保证一个线程的操作对于另一个线程是可见的
(2) 同步机制保证了多个线程之间的可靠通信,保证了多个线程之间对共享变量的互斥访问,同步常用的关键字是synchronized和volatile
3. 同步和异步
同步:同步数据时,服务器不能干其他的事。例如客户端提起一个同步请求后,服务器在同步数据期间不能执行其他的操作
异步:客户端提起一个异步请求后,服务器在异步同步数据时依然可以执行其他操作
而java中的同步通常指同步共享数据,或者某个对象或者同步某个方法
4. volatile关键字
(1) 成员变量加上volatile后,线程每次访问成员变量时,都需要首先从共享内存中获取该对象,如果成员变量发生变化,还必须写回内存
(2) 也就是说volatile保证了成员变量的读写互斥,相当于一个读写锁
主意:volatile并不能保证多线程操作的原子性,只能保持每次从内存拿到的数据是当前最新的
例如:线程A,线程B同时从内存拿到一个 public volatile static int count=5 的成员变量,A,B同时操作count = count+1后,A将count=6写进去,B将count=6写进去,最后内存变量中只会是count=6。
也就是说volatile只是在线程和内存之间加了一个锁,比较线程执行前后成员变量是否改变,而不会控制线程之间的互斥操作。
public class VolatileTest { private volatile int count = 0; public void test1(){ try { Thread.sleep(5000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } count = count+5; System.out.println("test1 out: "+count); } public void test2(){ count = count+10; System.out.println("test2 out: "+count); } public void out(){ System.out.println("count now: "+count); } public static void main(String[] args){ VolatileTest vt = new VolatileTest(); new Thread(new Runnable(){ @Override public void run() { // TODO Auto-generated method stub vt.test1(); } }).start(); vt.out(); new Thread(new Runnable(){ @Override public void run() { // TODO Auto-generated method stub vt.test2(); } }).start(); vt.out(); } }
这里有一个例子,开辟了两个线程去操作一个volatile修饰的变量,然后每次输出这个变量,看看他的值,输出的结果是这样的
count now: 0 count now: 0 test2 out: 10 test1 out: 15
也就是说线程之间虽然相当于读写锁了,但并不能保证更改后的数据已经写进去了,拿出来的数据还是可能是错的,也就是没有实现原子性操作。
即使换成了原子性操作,如下面的代码,和上面的volatile是一样的。也就是说,线程没有执行完之前都是会拿到错误的答案,这个是今后需要特别注意的。
import java.util.concurrent.atomic.AtomicInteger; public class VolatileTest { private AtomicInteger count = new AtomicInteger(0); public void test1(){ try { Thread.sleep(5000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } count.addAndGet(5); System.out.println("test1 out: "+count.get()); } public void test2(){ count.addAndGet(10); System.out.println("test2 out: "+count.get()); } public void out(){ System.out.println("count now: "+count.get()); } public static void main(String[] args){ VolatileTest vt = new VolatileTest(); new Thread(new Runnable(){ @Override public void run() { // TODO Auto-generated method stub vt.test1(); } }).start(); vt.out(); new Thread(new Runnable(){ @Override public void run() { // TODO Auto-generated method stub vt.test2(); } }).start(); vt.out(); } }
5. synchronized关键字:保证同一时刻只有一个线程在执行synchronized修饰的内容
(1) synchronized 方法(使用了类java的内置锁,锁住的方法所属对象本身)
public class ThreadTest { public synchronized void test1(){ for(int i=0;i<100;i++){ System.out.println("test1 out: "+i); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } public synchronized void test2(){ for(int i=0;i<100;i++){ System.out.println("test2 out: "+i); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } public static void main(String[] args){ ThreadTest tt = new ThreadTest(); new Thread(new Runnable(){ public void run(){ tt.test1(); } }).start(); new Thread(new Runnable(){ public void run(){ tt.test2(); } }).start(); } }
例如上面所示的例子,即使是不同的线程访问不同的sychronized函数,仍然是串行执行的,而不是同时执行的,即使有多个CPU也是无用的,输出结果如下,执行完test1,才会执行test2。
test1 out: 96 test1 out: 97 test1 out: 98 test1 out: 99 test2 out: 0 test2 out: 1 test2 out: 2 test2 out: 3
(2) synchronized 块
public void test(){ synchronized(obj){ //代码块,每次只有一个线程可以访问 } }
上面的示例稍微修改一下,如果用代码块来定义两个锁
public class ThreadTest { private Object lock1 = new Object(); private Object lock2 = new Object(); public void test1(){ synchronized(lock1){ for(int i=0;i<100;i++){ System.out.println("test1 out: "+i); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } public void test2(){ synchronized(lock2){ for(int i=0;i<100;i++){ System.out.println("test2 out: "+i); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } public static void main(String[] args){ ThreadTest tt = new ThreadTest(); new Thread(new Runnable(){ public void run(){ tt.test1(); } }).start(); new Thread(new Runnable(){ public void run(){ tt.test2(); } }).start(); } }
输出的结果就变成了下面所示,两个线程就可以同步执行了
test1 out: 0 test2 out: 0 test2 out: 1 test1 out: 1 test1 out: 2 test2 out: 2 test2 out: 3 test1 out: 3 test2 out: 4 test1 out: 4
若是test2也用lock1哪,这就变成了互斥锁
public void test2(){ synchronized(lock1){ for(int i=0;i<100;i++){ System.out.println("test2 out: "+i); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
和第一种synchronized方法是输出同样的效果的。
如果再换一种方式
public class ThreadTest { private Object lock1 = new Object(); private Object lock2 = new Object(); public synchronized void test1(){ // synchronized(lock1){ for(int i=0;i<100;i++){ System.out.println("test1 out: "+i); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } // } } public void test3(){ for(int i=0;i<100;i++){ System.out.println("test3 out: "+i); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } public static void main(String[] args){ ThreadTest tt = new ThreadTest(); new Thread(new Runnable(){ public void run(){ tt.test1(); } }).start(); new Thread(new Runnable(){ public void run(){ tt.test3(); } }).start(); } }
test1 out: 0 test3 out: 0 test3 out: 1 test1 out: 1 test1 out: 2 test3 out: 2 test1 out: 3 test3 out: 3 test3 out: 4 test1 out: 4 test3 out: 5
可见其他非同步的方法还是可以同步访问的
虽然一般同步都会介绍synchronized和volatile,但个人感觉volatile有些鸡肋,能用volatile的可以用synchronized代替。而且volatile很容易误导大家想成原子性的