我是按照《Java多线程编程技术》来学习的,所以这一篇是讲线程安全问题和synchronize关键字,下一篇是volatile关键字。
一、线程安全问题
1.线程安全和非线程安全
线程安全:指多线程操作同一个对象的某方法,修改该类的成员变量时,不会出现“脏读”现象,即实例变量的值是经过同步处理的。
非线程安全:指多线程操作同一个对象的某方法,修改该类的成员变量时,可能会出现“脏读”现象,即取到的数据其实是别更改过的。
非线程安全!=不安全,理由:(暂时空着)
2.为什么会出现非线程安全
在单线程编程里不会出现非线程安全问题,但是在多线程中,有可能会出现同时访问同一个资源的情况(即访问的是共享变量,也被称为临界资源),就会出现非线程安全问题了!!
共享变量(临界资源)有哪些?
在JVM里,方法区和堆是所有线程共享区域,所有方法区中的静态域静态变量,堆中的实例变量,数组等都是共享变量,而其他的区域就是非共享变量了,如栈中的局部变量,方法定义参数和异常处理器参数等,所有非共享变量不存在非线程安全,所得结果就是线程安全了。
总结:方法中的局部变量不存在非线程安全问题,这里方法内部的变量是私有特性造成的。
而多个线程同时修改某个外部传来的对象的成员变量,很容易就会出现错误,所有此时线程不安全,叫做非线程安全。
3.怎么解决非线程安全问题
基本上所有的并发模式在解决线程安全问题时,都采用“序列化访问临界资源”的方案,即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。
通常来说,是在访问临界资源的代码前面加上一个锁,当访问完临界资源后释放锁,让其他线程继续访问。
在Java中,提供了两种方式来实现同步互斥访问:synchronized和Lock。
本文主要讲述synchronized的使用方法,Lock的使用方法在下一次会详细讲。
4.脏读
二、synchronize
访问synchronize的方法是一定要排队的,只有共享变量的访问才需要同步,而不是访问共享变量没必要同步,并且多线程中(仅仅一个对象)如果A线程持有对象的Lock锁,B线程可以异步去访问非synchronize方法
锁重入:当一个线程得到一个对象锁后,再次请求此对象锁时是可以得到该对象的锁的,即在synchronize方法/块中内部调用其他synchronize方法是永远可以得到锁的(自己可以再次获得自己内部的锁)并且锁重入也支持父子继承环境
异常会释放锁
同步不具有继承性:也就是父类中的synchronize方法被子类重写并且子类没有加上关键字synchronize,此时子类的该方法不具有synchronize的同步性质。
- 在定义接口方法时不能使用synchronized关键字。
- 构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。
synchronized的几种用法:
synchronized方法,synchronized静态方法,synchronized(this),synchronized(类),synchronized(变量)。
一、锁住实例对象,对象锁
方法一:synchronized关键字加在普通实例方法上
public class MyObject { synchronized public void methodA() { //do something.... }}
方法二:synchronized关键字加在对象上
public class MyObject { public void methodA() { synchronized(this){ //do something.... } }
写法一修饰的是一个方法,写法二修饰的是一个代码块,二者是相同效果不同写法罢了,都是锁的是当前实例对象。
1.当两个并发线程访问同一个对象object中的这个synchronized同步方法时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个同步方法以后才能执行该同步方法。
class MyyObject { synchronized public void methodA(){ try { System.out.println(Thread.currentThread().getName()+" begin "+System.currentTimeMillis()); Thread.sleep(500); System.out.println(Thread.currentThread().getName()+" end "+System.currentTimeMillis()); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } }}class ThreadA extends Thread{ MyyObject object; public ThreadA(MyyObject object){ super(); this.object=object; } public void run(){ object.methodA();//调用相同方法A }}class ThreadB extends Thread{ MyyObject object; public ThreadB(MyyObject object){ super(); this.object=object; } public void run(){ object.methodA();//调用相同方法A }}public class MyObject { public static void main(String[] args){ MyyObject object=new MyyObject(); ThreadA a=new ThreadA(object); a.setName("A"); ThreadB b=new ThreadB(object); b.setName("B"); a.start(); b.start(); }}
输出
2.然而,当一个线程访问object的一个synchronized同步方法时,另一个线程仍然可以访问该object中的非synchronized同步方法。
class MyyObject { synchronized public void methodA(){ try { System.out.println(Thread.currentThread().getName()+" begin "+System.currentTimeMillis()); Thread.sleep(500); System.out.println(Thread.currentThread().getName()+" end "+System.currentTimeMillis()); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void methodB(){ try { System.out.println(Thread.currentThread().getName()+" begin "+System.currentTimeMillis()); Thread.sleep(1000); System.out.println(Thread.currentThread().getName()+" end "+System.currentTimeMillis()); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } }}class ThreadA extends Thread{ MyyObject object; public ThreadA(MyyObject object){ super(); this.object=object; } public void run(){ object.methodA();//调用synchronize修饰方法A }}class ThreadB extends Thread{ MyyObject object; public ThreadB(MyyObject object){ super(); this.object=object; } public void run(){ object.methodB();//调用普通实例方法B }}public class MyObject { public static void main(String[] args){ MyyObject object=new MyyObject(); ThreadA a=new ThreadA(object); a.setName("A"); ThreadB b=new ThreadB(object); b.setName("B"); a.start(); b.start(); }}
输出
3.尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
//把二中普通实例方法加上synchronizesynchronized public void methodB()
输出
4.第三个例子同样适用同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
//把同步方法B改成synchronized(this)代码块public void methodB(){ synchronized(this){ try { System.out.println(Thread.currentThread().getName()+" begin "+System.currentTimeMillis()); Thread.sleep(1000); System.out.println(Thread.currentThread().getName()+" end "+System.currentTimeMillis()); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
输出
5.如果两个线程分别访问两个对象,那么这两个线程是不会构成互斥同步的,因为此时的这两个线程对象锁是不一样的对象,有两把锁,多个对象多把锁。
//在三或者四基础上,多生成一个对象object2,用线程B去调用public class MyObject { public static void main(String[] args){ MyyObject object=new MyyObject(); MyyObject object2=new MyyObject();//两个对象 ThreadA a=new ThreadA(object); a.setName("A"); ThreadB b=new ThreadB(object2); b.setName("B"); a.start(); b.start(); }}
输出
二.锁住class对象,类锁
方法一:synchronized关键字加在静态方法上
class ClassName { synchronized public static void method() { } }
方法二:synchronized修饰类名.class
class ClassName { public void method() { synchronized(ClassName.class) { // todo } } }
方法二是把类加上锁(即类的所有对象都上了锁),而不再是由这个Class产生的某个具体对象了。
静态方法是属于类的而不属于对象的。同样的,synchronized修饰的静态方法锁定的是这个类的所有对象。
1.此时该类无论产生多少个对象,被多少个线程调用,它们都等同说公用一把锁
class MyyObject2 { synchronized public static void methodC(){ try { System.out.println(Thread.currentThread().getName()+" begin "+System.currentTimeMillis()); Thread.sleep(500); System.out.println(Thread.currentThread().getName()+" end "+System.currentTimeMillis()); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void methodD(){ synchronized (MyyObject2.class) { try { System.out.println(Thread.currentThread().getName()+" begin "+System.currentTimeMillis()); Thread.sleep(1000); System.out.println(Thread.currentThread().getName()+" end "+System.currentTimeMillis()); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }}class ThreadC extends Thread{ MyyObject2 object; public ThreadC(MyyObject2 object){ super(); this.object=object; } public void run(){ object.methodC(); }}class ThreadD extends Thread{ MyyObject2 object; public ThreadD(MyyObject2 object){ super(); this.object=object; } public void run(){ object.methodD(); }}public class MyObject2 { public static void main(String[] args){ MyyObject2 object=new MyyObject2(); MyyObject2 object2=new MyyObject2();//这里是产生两个对象 ThreadC c=new ThreadC(object); c.setName("C"); ThreadD d=new ThreadD(object2); d.setName("D"); c.start(); d.start(); }}
输出:
2.Class锁和对象锁是不一样的锁
如果一个类中定义了一个synchronized的static函数A,也定义了一个synchronized 的instance函数B,那么这个类的同一对象Obj
在多线程中分别访问A和B两个方法时,不会构成同步,因为它们的锁都不一样。A方法的锁是Obj所属的那个Class,而B的锁是Obj这个对象。
三、锁住变量,synchronized(变量)
这个不是很懂,目前了解是这个变量可以是普通成员或者局部变量,此时也是对象锁,看看下面例子吧
public void method3(SomeObject so){ synchronized(so) { //….. }}
这时,锁就是so这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。
当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁:
class Foo implements Runnable{ private byte[] lock = new byte[0]; // 特殊的instance变量 Public void methodA() { synchronized(lock) { //… } } //….}
注:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock= new Object()则需要7行操作码。
参考资料: