文章目录
线程安全与非线程安全
非线程安全:多个线程对同一个对象中的同一个实例变量进行操作时会出现值被更改、值不同步的情况,影响程序的执行流程(出现脏读)。
“非线程安全”的问题存在于“实例变量”中,如果是方法内部的私有变量,则不存在非线程安全问题。即在方法内部声明一个变量时,是不存在“非线程安全”问题的。因为方法内部的变量是私有的。
实现线程安全大致有三种方法:
- 多实例,也就是不用单例模式了。
- 使用java.util.concurrent(JUC)包下的类库
- 使用锁机制synchronized、lock方式
synchronized
synchronized是Java语言的关键字,当它用来修饰一个方法或者一个代码块时,能够保证同一时刻最多只有一个线程执行该段代码。相当于加了一把锁。
synchronized取得的锁都是对象锁或类锁,而不是把一段代码或方法当做锁(锁的是堆中的对象)。调用用关键字synchronized声明的方法一定是排队运行的。它用于解决多线程之间的数据共享问题,具有同步功能,是一种互斥锁,锁的是类的单个对象或所有对象。
1. 同步监视器
一个监视器对象就相当于一扇门,里面锁着的是共享的资源,每次只能有一个人能进入,并且只能容纳一个人,也就是说只有一个线程能获得这个锁。Java程序允许使用任何对象作为同步监视器,但通常推荐使用可能被并发访问的共享资源充当同步监视器。
public Integer ai = 100;
public void methodB() {
synchronized(ai) {
}
}
public int ao=200;
public void methodC() {
//编译的时候报错int is not a valid type's argument for the synchronized statement
//出错的原因:基本类型不能做同步监视器
synchronized(ao) {
}
}
2. synchronized
synchronized可以修饰代码块、方法(普通方法、静态方法),下面分别讲修饰方法和代码块时的特性和区别。
2.1 synchronized修饰方法
关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法当做锁(锁住的不是代码块或方法,而是对象或类(堆区))。调用用关键字synchronized声明的方法一定是排队运行的。即同一时刻只有一个线程能够执行synchronized修饰的代码块、方法中的代码
- synchronized修饰实例方法(非static方法) 时,获取的是对象锁(即类的实例对象),作用范围是整个方法,作用的对象是调用该方法的单个对象。
public synchronized void methodB(){
System.out.println(Thread.currentThread().getName());
}
- 静态方法是属于类的而不属于对象的,所以synchronized修饰类方法(static方法) 时,取的是类锁(即Class本身,注意:不是实例),作用的对象是这个类的所有对象。
public synchronized static void methodD(){
System.out.println(Thread.currentThread().getName());
}
结论:
- synchronized修饰static静态方法是给Class类(所有对象)上锁,而synchronized加到非static静态方法上是给对象上锁。
- 如果A线程先持有Object对象的Lock锁,B线程可以以异步的方式调用Object对象中的
非synchronized类型
的方法 - 如果A线程先持有Object对象的Lock锁,B线程在这时调用Object对象中的
synchronized类型
的方法则需等待,即同步。 - synchronized拥有锁重入的功能,即当一个线程得到锁后,再次请求此对象锁时是可以调用本类的其他synchronized方法/块时,是可以再次得到锁的。即可以再次获得自己的内部锁。
- 当存在父子类继承关系时,子类是完全可以通过“可重入锁”调用父类的同步方法的。
- 出现异常,锁自动释放:当一个线程执行的代码出现异常时,它持有的锁就会自动释放。
synchronized方法的缺点: 运行时间较长,解决方法是使用synchronized代码块
2.2 synchronized(this)修饰代码块
和synchronized普通方法
一样,synchronized(this)代码块
是锁住当前对象的。
public void methodA(){
synchronized (this){
System.out.println(Thread.currentThread().getName());
}
}
上面的代码和下面的代码功能是一样的:
public synchronized void methodB(){
System.out.println(Thread.currentThread().getName());
}
2.3 synchronized(非this对象x)代码块
锁非this对象具有一定的优点:如果一个类中有很多synchronized方法,这时虽然能实现同步,但会受到阻塞,所以影响效率。但如果使用synchronized同步代码块锁非this对象,则synchronized(非this)代码块中的程序与同步方法是异步的,不与其他this同步方法争夺this锁(当前对象的锁),可大大提高运行效率。
String anything = new String();
synchronized(anything ){
...//同步代码块
}
2.4 synchronized类
synchronized修饰类时,锁住的是类的所有对象。
class ClassName {
public static void methodC(){
synchronized (ClassName.class){
System.out.println(Thread.currentThread().getName());
}
}
}
上面的写法和下面的写法是等价的,锁住的都是类的所有对象;
public synchronized static void methodD(){
System.out.println(Thread.currentThread().getName());
}
3.synchronized使用时需要注意的问题
- synchronized使用时,同步代码块中的代码越少越好;即锁的粒度越细越好;
- synchronized使用时不要使用字符串对象作为锁的对象(容易造成冲突,引起死锁)
4. 释放锁与不释放锁的情况
释放锁的情况:
- 当前线程的同步方法、同步代码块执行结束,当前线程即释放锁
- 当前线程出现了未处理的
Error
或Exception
,导致该代码块或方法异常结束,当前线程就会释放锁- 在代码块、方法中遇到
break
、return
终止了该代码块,当前线程会释放锁- 程序执行了同步监视器的
wait()
方法,则当前线程暂停,释放同步监视器
不释放锁的情况:
- 程序调用
Thread.sleep()
、Thread.yield()
方法来暂停当前线程的执行,当前线程不会释放对同步监视器的锁定。- 其他线程调用了当前线程的
suspend()
方法将当前线程挂起,当前线程不会释放同步监视器的锁。
4. synchronized底层原理
Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException
的异常的原因。
参考链接:深入synchronized
5. Java对synchronized的底层优化
参考链接:Java对synchronized的优化