非线程安全会在多个线程对同一个对象中的实例变量进行并发访问时发生。产生的后果是脏读,也就是取到的数据其实是被更改过的。
线程安全就是获得的实例变量的值是经过同步处理的,不会出现脏读的现象。
非线程安全问题存在于实例变量中,如果是方法内部的私有变量,则不存在非现场安全问题,所得的结果也就是线程安全的了。
synchronized同步方法
关键字synchronized
取得的都是对象锁,而不是把一段代码和方法当做锁。 当多个线程访问同一个对象时,哪个线程先执行带synchronized
关键字的方法,哪个线程就持有该方法所属对象的锁Lock
。
如果多个线程访问多个对象,则JVM会创建多个锁。
调用关键字synchronized
声明的方法一定是排队运行的。
既然使用了synchronized
,那么肯定是对“共享”资源的读写访问。
如何查看Lock锁对象的效果呢?
public class MyObject {
synchronized public void methodA(){
try {
System.out.println("begin 执行methodA, 线程为:"+Thread.currentThread().getName());
Thread.sleep(2000);
System.out.println("end 执行methodA, 线程为:"+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void methodB(){
try {
System.out.println("begin 执行methodB, 线程为:"+Thread.currentThread().getName());
Thread.sleep(2000);
System.out.println("end 执行methodB, 线程为:"+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Run {
public static void main(String[] args){
//同一个实例对象
MyObject object = new MyObject();
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
object.methodA();
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
object.methodB();
}
});
//启动两个线程,分别执行methodA和methodB方法
threadA.start();
threadB.start();
}
}
/**
begin 执行methodA, 线程为:Thread-0
begin 执行methodB, 线程为:Thread-1
end 执行methodA, 线程为:Thread-0
end 执行methodB, 线程为:Thread-1
*/
结论:
threadA
线程调用了synchronized
的methodA
,持有了对象锁。但是threadB
完全可以异步调用非synchronized
的methodB
。说明没有满足上述所说的排队执行。
改进:
对methodB
增加synchronized
关键字
public class MyObject {
synchronized public void methodA(){
try {
System.out.println("begin 执行methodA, 线程为:"+Thread.currentThread().getName());
Thread.sleep(2000);
System.out.println("end 执行methodA, 线程为:"+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized public void methodB(){
try {
System.out.println("begin 执行methodB, 线程为:"+Thread.currentThread().getName());
Thread.sleep(2000);
System.out.println("end 执行methodB, 线程为:"+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
begin 执行methodA, 线程为:Thread-0
end 执行methodA, 线程为:Thread-0
begin 执行methodB, 线程为:Thread-1
end 执行methodB, 线程为:Thread-1
*/
结论:
threadA
线程调用了synchronized
的methodA
,持有了对象锁。前者执行完后,threadB
才开始执行methodB
。满足了上述所说的排队执行。
当
A线程
调用anyObject对象
加入synchronized
关键字的X方法
时,A线程
就获得了X方法锁
,更准确地讲,是获得了对象的锁
,所以其他线程必须等A线程
执行完毕才可以调用X方法
,但B线程
可以随意调用其他的非synchronized同步方法
。当
A线程
调用anyObject对象
加入synchronized
关键字的X方法
时,A线程
就获得了X方法
所在对象的锁,所以其他线程必须等A线程
执行完毕才可以调用X方法
,而B线程
如果调用声明了synchronized
关键字的非X方法
时,必须等A线程
将X方法
执行完,也就是释放对象锁后才可以调用。
synchronized锁重入
关键字synchronized
拥有锁重入的功能,也就是在使用synchronized
时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。这也证明在一个synchronized
方法/块的内部调用本类的其他synchronized
方法/块时,是永远可以得到锁的。
“可重入锁”的概念是:自己可以再次获取自己的内部锁。
比如有1条线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。
public class Service {
public synchronized void service1(){
System.out.println("service1");
service2();
}
public synchronized void service2(){
System.out.println("service2");
service3();
}
public synchronized void service3(){
System.out.println("service3");
}
}
public class Run {
public static void main(String[] args){
Service service = new Service();
new Thread(new Runnable() {
@Override
public void run() {
service.service1();
}
}).start();
}
}
/**
service1
service2
service3
*/
可重入锁也支持在父子类继承的环境中,子类的同步方法可以调用父类的同步方法。
synchronized同步语句块
使用synchronized同步代码块,一方面缩小了需要同步的范围,提高了程序的执行效率。另一方面,可以指定锁对象。
使用格式:synchronized(this){ …. }
public class Task {
private String getData1;
private String getData2;
public void doLongTimeTask(){
try {
System.out.println("begin task");
Thread.sleep(3000);
String privateGetData1 = "长时间处理任务后从远程返回的值1 threadName="
+ Thread.currentThread().getName();
String privateGetData2 = "长时间处理任务后从远程返回的值2 threadName="
+ Thread.currentThread().getName();
synchronized (this) {
getData1 = privateGetData1;
getData2 = privateGetData2;
}
System.out.println(getData1);
System.out.println(getData2);
System.out.println("end task");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
如果使用同步方法,则每个线程都需要排队执行,导致每个线程最少执行3秒。而真正需要同步的地方只有getData1和getData2的赋值操作。耗时的操作没必要同步执行。
任意对象作为对象监视器
使用格式:synchronized(非this对象){ …. }
优点:
如果在一个类中有很多个synchronized
方法,这时虽然能实现同步,但会受到阻塞,所以影响运行效率;但如果使用同步代码块锁非this对象
,则synchronized(非this)
代码块中的程序与同步方法是异步的,不与其他锁this
同步方法争抢this锁
,则可大大提高运行效率。
对象监视器不同,运行结果就是异步调用了。
public class MyObject {
synchronized public void methodC(){
try {
System.out.println("methodC ____getLock time="
+ System.currentTimeMillis() + " run ThreadName="
+ Thread.currentThread().getName());
Thread.sleep(2000);
System.out.println("methodC releaseLock time="
+ System.currentTimeMillis() + " run ThreadName="
+ Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Service {
public void testMethod1(MyObject object) {
synchronized (object) {
try {
System.out.println("testMethod1 ____getLock time="
+ System.currentTimeMillis() + " run ThreadName="
+ Thread.currentThread().getName());
Thread.sleep(5000);
System.out.println("testMethod1 releaseLock time="
+ System.currentTimeMillis() + " run ThreadName="
+ Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Run {
public static void main(String[] args){
MyObject object = new MyObject();
Service service = new Service();
// 同步方法和同步代码块使用的是同一个对象锁object,结果同步打印
new Thread(new Runnable() {
@Override
public void run() {
object.methodC();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
service.testMethod1(object);
}
}).start();
}
}
/**
methodC ____getLock time=1528446201224 run ThreadName=Thread-0
methodC releaseLock time=1528446203224 run ThreadName=Thread-0
testMethod1 ____getLock time=1528446203224 run ThreadName=Thread-1
testMethod1 releaseLock time=1528446208225 run ThreadName=Thread-1
*/
静态同步synchronized方法与synchronized(class)代码块
synchronized
关键字加到static静态方法上是给Class类
上锁,而synchronized
关键字加到非static静态方法上是给对象
上锁。
证明不是同一个锁:
public class Service {
public synchronized void testMethod1() {
try {
System.out.println("testMethod1 ____getLock time="
+ System.currentTimeMillis() + " run ThreadName="
+ Thread.currentThread().getName());
Thread.sleep(5000);
System.out.println("testMethod1 releaseLock time="
+ System.currentTimeMillis() + " run ThreadName="
+ Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized public static void printA() {
try {
System.out.println("线程名称为:" + Thread.currentThread().getName()
+ "在" + System.currentTimeMillis() + "进入printA");
Thread.sleep(3000);
System.out.println("线程名称为:" + Thread.currentThread().getName()
+ "在" + System.currentTimeMillis() + "离开printA");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Run {
public static void main(String[] args){
//异步执行
Service service = new Service();
new Thread(new Runnable() {
@Override
public void run() {
service.testMethod1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
service.printA();
}
}).start();
}
}
/**
testMethod1 ____getLock time=1528448721070 run ThreadName=Thread-0
线程名称为:Thread-1在1528448721070进入printA
线程名称为:Thread-1在1528448724070离开printA
testMethod1 releaseLock time=1528448726070 run ThreadName=Thread-0
*/
volatile关键字
作用:
- 使变量在多个线程间可见(只修饰于变量)
- 强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值
比较(与synchronized
不同)
- 关键字
volatile
是线程同步的轻量级实现,所以volatile
性能肯定比synchronized
要好,并且volatile
只能修饰于变量,而synchronized
可以修饰方法,以及代码块。随着JDK
新版本的发布,synchronized关键字在执行效率上得到很大提升,在开发中使用synchronized
关键字的比率还是比较大的。 - 多线程访问
volatile
不会发生阻塞,而synchronized
会出现阻塞。 volatile
能保证数据的可见性,但不能保证原子性;而synchronized
可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步。- 再次重申一下,关键字
volatile
解决的是变量在多个线程之间的可见性;而synchronized
关键字解决的是多个线程之间访问资源的同步性。
线程安全包含原子性和可见性两个方面,Java的同步机制都是围绕这两个方面来确保线程安全的。
应用场景
应用场景是在多个线程中可以感知实例变量被更改了,并且可以获得最新的值使用,也就是用多线程读取共享变量时可以获得最新值使用。(仅仅是读取最新的值)
关键字volatile
提示线程每次从共享内存中读取变量,而不是从私有内存中读取,这样就保证了同步数据的可见性。但在这里需要注意的是:如果修改实例变量中的数据,比如i++,也就是i=i+1,则这样的操作其实并不是一个原子操作,也就是非线程安全的。表达式i++的操作步骤分解如下:
1)从内存中取出i的值;
2)计算i的值;
3)将i的值写到内存中。
假如在第2步计算值的时候,另外一个线程也修改i的值,那么这个时候就会出现脏数据。解决的办法其实就是使用synchronized
关键字,所以说volatile
本身并不处理数据的原子性,而是强制对数据的读写及时影响到主内存的。