在Java语言中写出线程安全的程序
- synchronized对象监视器为Object
- synchronized对象监视器为Class
- volatile作用
- synchronized和volatile区别及使用
synchronized同步方法
保障预案自信、可见性和有序性
- 方法体内部的私有变量不存在线程安全问题
- 全局实例变量会存在线程安全问题
全局实例变量存在线程安全问题
如果多个线程同时操作同一个对象内部的实例变量,则有可能出现线程安全问题。
- 解决办法
1.多个线程访问的对象都不相同则不会有问题
2.在需要调用的方法上加上synchronized,此时会锁住该方法所属的对象,而不是锁该方法
也就是说如果有多个对象实例,则synchronized锁住的是多个对象,每个对象之间不会冲突,无需等待。
而如果多个线程操作同一个对象内的synchronized方法时,则需要等待
同步synchronized原理
其在字节码文件中会加入flag访问修饰符ACC_SYNCHRONIZED来控制访问权限,告知此方法是同步方法,再使用monitorenter进入和monitorexit退出指令来进行同步处理,具体字节码文件可在我的博客地址查看
http://jtao.work/archives/lei-wen-jian-jie-gou
synchronized总结
一个对象有A、B两个方法,都为非static。2个线程分别调用A、B方法
- 如果A加锁、B未加锁,则可以异步同时调用
- 如果A、B都加锁,则另一个线程只能等待先得到对象锁的线程释放锁之后才能调用,即锁等待
- synchronized并不是锁方法,而是锁对象。哪个线程拿到这个锁,就可以访问这个锁对象中的synchronized方法
脏读
一个对象里面的set方法加了synchronized,但get方法没有加锁。那么多线程时,线程A使用get,线程B使用set。有可能get得到的值是set之前的,真实的值已经改变。解决的办法是在get方法上也加上synchronized锁住。
synchronized锁重入
即在使用synchronized时,一个线程拥有了对象锁之后,再次请求此对象锁调用其他方法是可以的,即synchronized方法内部调用本类的其他synchronized方法是永远可以得到对象锁的,synchronized是可重入锁
- 这种情况在子类和父类也存在,可以理解为继承
- 子类如果继承父类的方法时重写没有加入synchronized关键字则不会加锁
出现异常时,锁会自动释放
当出现异常时,对象的锁会自动释放,让其他线程进入。
注意:Thread类中的suspend()方法和sleep()方法被调用后并不会释放锁
判断对象是否被上锁
public static native boolean holdsLock(Object obj);
synchronized同步代码块,一半异步,一半同步
有时候因为加了synchronized锁,所以当多个线程调用同一个方法时那么必须等待该方法执行完成,或许这很耗时,没有达到多线程的目的。此时可以根据具体的业务逻辑区分方法体内哪些必须串行执行,哪些可以异步执行
//同步代码块
/**
* @author jtao
*/
class E {
void doLongTimeTask() {
for (int i = 0; i < 100; i++) {
System.out.println("未加synchronized锁 ThreadName=" + Thread.currentThread().getName() + " i=" + i);
}
System.out.println(" ");
synchronized (this) {
for (int i = 0; i < 100; i++) {
System.out.println("加synchronized锁 ThreadName=" + Thread.currentThread().getName() + i);
}
}
}
public static void main(String[] args) {
new Thread(() -> new E().doLongTimeTask()).start();
new Thread(() -> new E().doLongTimeTask()).start();
}
}
此时没有在方法上面加上synchronized,而是在方法体内的代码块中加入synchronized
未加锁的代码块交替执行
加synchronized锁的代码必然不能交替执行
- 注意其synchronized锁的还是对象,当加锁时,另一个线程调用同一个对象的其他synchronized方法或者代码块不能够被使用
println()方法也是synchronized锁this对象
这样多线程情况下不会因为PrintStream对象被修改后导致打印内容混乱
synchronized锁任意对象
synchronized除了锁this对象外还可以锁对象内的实例变量和其他对象,此时和锁this的对象锁是不同的锁,可以异步执行,不需要同步
- 原因
因为synchronized(this)在代码块和方法上锁的是类的对象,如果多个线程同时争抢锁会导致效率低下。如果此时不锁住类的对象而是锁某一个局部对象,其与需要对象锁的方法或代码块是异步的,大大提高运行效率
synchronized锁非this对象时结论
synchronized(x){}此x非this对象时
- 其他线程执行该代码块时呈同步效果
- 执行x对象中的synchronized同步方法时呈同步效果
- 执行x对象中的synchronized(this){}代码块时呈同步效果
synchronized锁static方法和Class
//synchronized锁class和static
class F {
synchronized static void a() {
System.out.println("A1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("A2");
}
void b() {
synchronized (F.class) {
System.out.println("B1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B2");
}
}
synchronized void c() {
System.out.println("C1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("C2");
}
synchronized static void d() {
System.out.println("D1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("D2");
}
public static void main(String[] args) {
new Thread(() -> F.a()).start();
new Thread(() -> F.a()).start();
new Thread(() -> new F().b()).start();
new Thread(() -> new F().c()).start();
}
}
上图演示的锁住F.class对象和static方法和普通的方法锁时可以看出static和Class对象锁的都是类,因为Class是单例的,且static是和类绑定在一起的所以就算调用实例对象来调用static方法也是并行的
String常量池直接引用内存地址
//直接引用
class G {
private Integer a = 0;
void setA(int a) {
synchronized (this.a) {
this.a = a;
}
}
int getA() {
return this.a;
}
public static void main(String[] args) {
G g = new G();
new Thread(() -> {
System.out.println(g.getA() + Thread.currentThread().getName());
g.setA(2);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(g.getA() + Thread.currentThread().getName());
}).start();
new Thread(() -> {
System.out.println(g.getA() + Thread.currentThread().getName());
g.setA(1);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(g.getA() + Thread.currentThread().getName());
}).start();
}
}
上述代码的结果如下,因为锁的对象是this.a而且没有用到a所以失效
但是如果是锁入参数String类型常量池、或者Integer类型由于自动拆装箱缘故,所以直接需要锁等待
class G {
private String a ="";
void setA(Integer a) {
synchronized (a) {
System.out.println( Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
String getA() {
return this.a;
}
public static void main(String[] args) {
G g = new G();
new Thread(() -> {
g.setA(2);
}).start();
new Thread(() -> {
g.setA(2);
}).start();
}
}
此时通过关键字new生成新的内存地址,那么就判定2个对象不同,锁不生效
synchronized锁无限等待解决方案
- 背景
当synchronized在方法上,即锁this时间过长,其他synchronized方法无法调用 - 方案
可以使用synchronized锁住方法入参或者this.x的其他对象
在方法体内调用synchronized(x){}代码块
多线程死锁
不同的线程在相互等待不被释放的对象锁,导致所有的任务都无法继续完成。
死锁时必须避免的,会造成程序假死,是程序设计的BUG,必须要避免双方互相持有对方的锁
//死锁
class H {
private Object object1=new Object();
private Object object2=new Object();
public void a() {
synchronized (object1) {
System.out.println(object1);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object2) {
System.out.println(object2);
}
}
}
public void b() {
synchronized (object2) {
System.out.println(object2);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object1) {
System.out.println(object2);
}
}
}
public static void main(String[] args) {
H h = new H();
new Thread(()->h.a()).start();
new Thread(()->h.b()).start();
}
}
如下图所示打印2个Object内存地址后就一直等待
可使用jps、jstack查看死锁情况
内部类和静态内部类的synchronized锁
和普通类使用点相同,不做解释
synchronized锁对象改变导致异步执行
synchronized(x)在锁代码块内部改变时,此时如果有其他线程访问的synchronized代码块时因为x已经改变了,如果其内存指向改变的话,那么锁的对象是不同的也就是说可以异步并发执行,而不是同步执行。注意如果是基本类型是没有引用的,直接在方法区和常量池中
synchronized锁对象不改变。
synchronized(x){}代码块在不同的Class类中定义,但在执行多线程时,只要入参x是同一个,那么还是会锁住同步串行执行
synchronized大致使用锁对象总结
class I {
synchronized public static void a() {
}
public void b() {
synchronized (I.class) {
}
}
synchronized public void c() {
}
public void d() {
synchronized (this) {
}
}
public void e() {
synchronized ("abc") {
}
}
}
在上述中:
- a和b锁的是同一个I.Class,其同步,与其他异步
- c和d锁的是同一个I对象,其同步,与其他异步
- d锁的是字符串“abc”,与其他异步
volatile关键字
特性:
- 可见性
B线程能看到A线程更改后的数据 - 原子性
1.32位系统中,未使用volatitle声明的数据类型没有原子性
2.X86的64位JDK中,写double、long是原子性的
3.volatile声明的int类型的变量i进行i++操作也是非原子的 - 禁止冲排序
可见性
Java多线程运行时,对象存在于私有队线程中
- 加入volatile关键字后当线程访问该变量时,强制从公共堆栈中进行取值
- synchronized代码块也具有增加可见性的作用
原子性
- volatile修饰变量并不能让其操作变为原子性,只能增加其可见性
- 使用Atomic原子类可以实现i++的原子性效果
禁止重排序
- 在Java程序运行时,JIT(Just-In-Time Complier,即时编译器)可以动态的改变程序代码运行的顺序
A代码-重耗时
B代码-轻耗时
C代码-重耗时
D代码-轻耗时
JIT可能加工如下
B代码-轻耗时
D代码-轻耗时
A代码-重耗时
C代码-重耗时
- 加入了volatile关键字之后则可以限制冲排序
A变量的操作
B变量的操作
volatile z变量的操作
C变量的操作
D变量的操作
此时变量z是一个屏障,AB和CD不可能跨越z重新排序
- synchronized也具有该特性
总结
synchronized
保证同一时刻,只有一个线程可以执行某一个方法,或代码块,synchronized可以修饰方法或代码块
- 可见性
- 原子性
实现了同步就实现了原子性 - 禁止重排序
volatile
让其他线程可以看到最新的值,只能修饰变量
- 可见性
- 原子性
1.32位系统中未声明未volatile的数据类型没有实现原子性,如果想实现则添加volatile
2.64位操作系统是根据具体架构实现的。声明未volatile也不一定具有原子性
3.对volatile声明的变量i进行i++是不具备原子性的 - 禁止重排序
使用场景
1.想实现一个变量的值被更改时,其他线程可以获取到最新的值时,可以对变量使用volatile
2.多个线程对同一个对象中的一个实例变量进行操作时,为了避免线程安全问题可以使用synchronized关键字
Comments | 0 条评论