synchronized关键字需要注意的几点
脏读
对业务写方法加锁,对业务读方法不加锁,容易产生脏读。
public class Test1 {
private String name;
private double balance;
public synchronized void set(String name, double balance) {
this.name = name;
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.balance = balance;
}
public double getBalance() {
return this.balance;
}
public static void main(String[] args) throws InterruptedException {
Test1 test = new Test1();
//先是写方法,虽然加锁,但是对于读没有加锁,所以其并不会等待写的锁释放才能调用
new Thread(() -> {
test.set("zhangsna",100.0);
}).start();
//此时还没有设置完成,所以读取到的并不是正确的数据
Optional.of(test.getBalance()).ifPresent(System.out::println);
TimeUnit.SECONDS.sleep(2);
Optional.of(test.getBalance()).ifPresent(System.out::println);
}
}
synchronized是可重入锁
可重入锁:一个线程已经拥有某个对象的额锁,再次申请的时候仍然会得到该对象的锁
/**
Thread-0 m1 start
Thread-0 m2 start
Thread-0 m2 end
Thread-0 m1 end
可重入锁的机制是:每一个锁关联一个持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,
那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;
此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,
同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。
*/
public class Test2 {
public synchronized void m1() {
System.out.println(Thread.currentThread().getName() + " m1 start ");
try {
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){
}
/**
调用m2的时候,是不需要等待自己释放锁的,所以从这点上可以证明synchronized是可重入锁
*/
m2();
System.out.println(Thread.currentThread().getName() + " m1 end ");
}
public synchronized void m2() {
System.out.println(Thread.currentThread().getName() + " m2 start ");
try {
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){
}
System.out.println(Thread.currentThread().getName() + " m2 end ");
}
public static void main(String[] args) {
Test2 test = new Test2();
new Thread(() -> test.m1()).start();
}
}
synchronized对于异常处理
ynchronized 如果出现异常,默认情况锁会被释放。所以在并发处理过程中,有异常出现时需要小心,否则有可能会发现不一致的情况
public class Test3 {
int count = 0;
public synchronized void m1() {
System.out.println(Thread.currentThread().getName() + " start ");
while(true) {
count++;
System.out.println(Thread.currentThread().getName() + " count = " + count);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
if (count == 5) {
//此处抛出异常,锁就会被释放,要想锁不被释放,
//可以在这里catch,然后让循环继续
// try {
int i = 1/0;
//}catch (ArithmeticException e){
// System.out.println("抛出异常");
//}
}
}
}
public static void main(String[] args) throws InterruptedException {
Test3 test = new Test3();
new Thread(() -> test.m1(),"t1").start();
TimeUnit.SECONDS.sleep(3);
new Thread(() -> test.m1(),"m2").start();
}
}
锁对象的引用不能修改
public class Test4 {
/**
对于作为锁对象,对象的属性可以修改,但是对象的引用不能再被修改,
所以一般作为锁对象,都需要将其设置为final。
*/
Object o = new Object();
public void m1() {
synchronized (o) {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName());
}
}
}
public static void main(String[] args) throws InterruptedException {
Test4 test = new Test4();
new Thread(() -> test.m1(),"t1").start();
TimeUnit.SECONDS.sleep(2);
//修改变量指向新的对象
test.o = new Object();
//如果不修改o引用,t2线程将只要t1不释放锁,将无法获取锁
//但是将o指向另一个对象,使得t1的锁对象和t2的锁对象不是同一个
new Thread(() -> test.m1(),"t2").start();
}
}
所以也尽量不能使用字符串常量作为锁对象去同步代码。容易产生死锁
private static final String Lock = "LOCK";
public void m2() {
/**
因为不同包不同类中的值相同的字符串常量引用的是同一个字符串对象。
这就意味着外部任何的Class都可以包含指向同一个字符串对象的字符串常量,
因此就有可能出现死锁的情况!
一旦出现这种情况的死锁,是极难排查出来的。
*/
synchronized (Lock) {
}
}