synchronized同步方法
非线程安全:多个线程堆同一个对象的实例变量进行并发访问时,产生“脏读”,取到的数据是被更改过的。
线程安全:获取实例变量时,经过同步处理。
“非线程安全”的问题在于“实例变量”中,方法内部的私有变量是不会有“非线程安全”问题的。
多个线程共同访问1个实例变量,则可能出现“非线程安全”的问题。处理方式是在方法前加synchronized,让方法变成同步的方法,两个线程访问同一个对象中的同步方法时,一定是线程安全的。
synchronized在方法上加锁,取得的锁是对象锁,而不是一段代码或者方法当做锁。如果多个线程访问同一个类的synchronized的方法,但是不是同一个对象实例,则会创建多个锁。
调用关键字synchronized声明的方法一定是排队运行的,且共享资源的读写访问才是需要同步化的。
A线程持有object对象的Lock锁,B线程可以继续访问object对象的非synchronized类型的方法,不受影响。
A线程持有object对象的Lock锁,B线程如果调用object对象中的任何一个synchronized类型的方法,则需要等待,也就是同步。
解决“读脏”问题
a=1;
b=1
synchronized public void set(){
this.a=2;
Thread.sleep(10000); //(1)
this.b=2;
}
public void get(){
system.out.println(a+b);
}
同步类有以上两个方法,当一个线程在执行到(1)处是,另一个线程执行get方法,由于get方法没有同步,所有可以不用锁,直接调用,则会出现a已经赋值,但是b未赋值的情况(a=2,b=1),也就是出现了“读脏”,解决方法就是get方法加synchronized。
synchronized锁重入
synchronized的锁有重入功能,当一个线程得到了一个对象的锁后,如果再次请求对象锁是可以获取的。也就是说,synchronized方法/块的内部调用本类的其他synchronized方法/块时,永远可以得到锁。(自己可以再次获取自己的内部锁)
当存在父子类继承关系时,子类是完全可以通过“可重入锁”调用父类的同步方法的。
出现异常,自动释放锁。
重写父类方法时,如果父类为同步方法,子类方法为不同步,则调用子类方法时不同步的。需要在重写的子类方法也加上同步。
synchronized同步代码块
synchronized声明方法有一定的弊端,即如果A执行同步方法需要很长时间,则B方法也需要等待很长时间才能执行。
使用使用同步代码块,只把需要同步的代码包起来,可以提高一些效率
synchronized(this)获取的也是当前对象的锁与synchronized加在方法上的是同一个。这个()中的叫做“对象监视器”
public void set(){
Thread.sleep(1000);
synchronized(this){
System.out.println("hello");
}
Thread.sleep(1000);
}
非this对象锁
- 在多线程持有“对象监视器”为同一个对象的前提下,同一时间只要一个线程可以执行synchronized(非this对象x)同步代码块
- 当持有“对象监视器”为同一个对象的前提下,同一时间只要一个线程可以执行synchronized(非this对象x)同步代码块
String str=new String();
synchronized( str){
}
锁非this对象有一定优点:如果一个类中有很多synchronized方法,这时候虽然能实现同步,但是会受到阻隔,影响效率,如果使用非this对象锁,则可以和其他锁对象一起运行。
静态同步synchronized方法和synchronized(class)代码块
关键字synchronized加在static静态方法上或者synchronized(Service.class),则是对当前的Class类进行加锁,所有的对象都会上锁。
//使用参数作为锁对象,如果两个线程传入参数都为"AA",由于String的常量池原因,则会持有同一个锁,所以一般情况,String不作为锁对象。通常直接new一个Object()作为锁
public static void set(String str){
synchronized(str){
system.out.println(str);
}
}
死锁
如下情况则会出现死锁,set1等待set2的B锁,但是set2又在等待set1的A锁。
public static void set1(){
synchronized(A){
synchronized(B){
}
}
}
public static void set2(){
synchronized(B){
synchronized(A){
}
}
}
查看死锁
1.jsp查到id号
2.jstack -l 3244查看详情。
锁对象改变
AB同时访问同步代码块。A拿到123锁,B尝试123锁没拿到,等待。B不管lock是否改变,都回去尝试123锁,也就是尝试锁是基于第一次的。
只要对象不变,即使对象的属性改变,运行的结果也是同步的。
String lock="123";
synchronized(lock){
Thread.sleep(10000);
lock="456"
Thread.sleep(10000);
}
volatile
使变量在多个线程间可见,强制从公共堆栈取得变量,而不是从线程私有数据栈中取得变量
如果线程中有如下代码,在main线程把isContinuePrint方法置为false,print()可能不会停止。因为启动运行Print的线程时,变量isContinuePrint=true存放在公共堆栈和线程私有堆栈中。在main主线程中执行isContinuePrint=false时,更新的是公共堆栈的值,运行Print的线程私有变量没变,所以死循环。
解决办法 volatile boolean isContinuePrint=true;
public class Print implements Runable{
boolean isContinuePrint=true;
void setcontinuePrint{
...
}
public void print(){
while(isContinuePrint){
System.out.println("hello");
}
}
}
volatile每次读取共享内存中的变量,而不是从私有内存中读取,保证了数据的可见性。但是如果堆实例变量做了修改,比如i++,则并不是原子操作,是非线程安全的,分解步骤
- 从内存读取i
- 计算i
写会内存i
如果第二步计算的时候,另一个线程修改了i的值,则出现了脏数据。volatile只保证多个线程读取的时候是最新的值,解决办法是加synchronized,或者使用原子类
原子类
原子类是原子操作的,没有其他线程可以中断或者检查正在原子操作的变量,他可以在没有锁的情况下做到线程安全
AtomicInteger count=new AtomicInteger(0);
count.incrementAndGet();
synchronized和volatile比较
- volatile性能高于synchronized。volatile只能修饰变量,synchronized修饰方法,代码块。
- volatile不会发生阻塞,synchronized会出现阻塞
- volatile能保证数据的可见性,但不保证原子性;synchronized可以保证原子性,间接保证可见性,因为它会将私有内存和公共内存中的数据做同步。
- volatile解决的是变量在多线程之间的可见性,synchronized解决的是多线程之间访问资源的同步性。
怎么理解synchronized的可见性
这个是因为Java内存模型对synchronized语义做了以下的保证,
即当ThreadA释放锁M时,它所写过的变量(比如,x和y,存在它工作内存中的)都会同步到主存中,而当ThreadB在申请同一个锁M时,ThreadB的工作内存会被设置为无效,然后ThreadB会重新从主存中加载它要访问的变量到它的工作内存中(这时x=1,y=1,是ThreadA中修改过的最新的值)。通过这样的方式来实现ThreadA到ThreadB的线程间的通信。
线程安全包含原子性和可见性两部分。