1.使用注意(6点)
1)synchronized 加在静态方法(static)时锁的是类,比如 synchronized (A.class)
2)synchronized 的锁粒度应该尽量小,保证原子性即可
public class T {
int count = 0;
synchronized void m1() {
// do sth need not sync
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁
count ++;
// do sth need not sync
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
void m2() {
// do sth need not sync
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁
// 采用细粒度的锁,可以使线程争用时间变短,从而提高效率
synchronized(this) {
count ++;
}
// do sth need not sync
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3)synchronized 遇到异常时会自动释放锁,需要在catch块中做处理
程序在执行过程中,如果出现异常,默认情况锁会被释放
- 所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。
- 比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。
- 因此要非常小心的处理同步业务逻辑中的异常
public class T {
int count;
synchronized void m1(){
while (true){
System.out.println(Thread.currentThread().getName()+" count="+count++);
if(count == 5){
try {
int i = 1/0; // 出现异常
System.out.println(i);
}catch (Exception e){
throw new RuntimeException("!!!");
}
}
}
}
public static void main(String[] args) {
new Thread(()->new T().m1(),"t1").start();
new Thread(()->new T().m1(),"t2").start();
}
}
4)synchronized 只能锁住堆,不要锁不要以字符串常量作为锁定对象(字符串常量池)
在下面的例子中,m1和m2其实锁定的是同一个对象
public class T {
String s1 = "abcd";
String s2 = "abcd";
void m1() {
synchronized(s1) {
}
}
void m2() {
synchronized(s2) {
}
}
}
这种情况还会发生比较诡异的现象,比如你用到了一个类库,在该类库中代码锁定了字符串“Hello”,但是你读不到源码,所以你在自己的代码中也锁定了"Hello",这时候就有可能发生非常诡异的死锁阻塞,因为你的程序和你用到的类库不经意间使用了同一把锁
5)避免将锁定对象的引用变成另外的对象
锁定某对象o,如果o的属性发生改变,不影响锁的使用;但是如果o变成另外一个对象,则锁定的对象发生改变,
public class T {
Object o = new Object();
void m() {
synchronized(o) {
while(true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
}
public static void main(String[] args) {
T t = new T();
// 启动第一个线程
new Thread(t::m, "t1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 创建第二个线程
Thread t2 = new Thread(t::m, "t2");
t.o = new Object(); // 锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永远得不到执行机会
t2.start();
}
}
6)synchronized 是可重入锁,在 synchronized 块中调用 synchronized 方法自动获得锁
一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁,也就是说 synchronized 获得的锁是可重入的。
这里是继承中有可能发生的情形,子类调用父类的同步方法
public class T {
synchronized void m(){
System.out.println("m start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new TT().m();
}
}
class TT extends T{
@Override
synchronized void m() {
System.out.println("child m start");
super.m();
System.out.println("child m end");
}
}
2.死锁示例
死锁就是,线程1 拿了 A 锁等 B 锁, 与此同时,线程2 拿了 B 锁等 A 锁…
public class DeadLcok {
public static void main(String[] args) {
// 共享变量1
Object A = new Object();
// 共享变量2
Object B = new Object();
new Thread(() -> {
System.out.println("t1启动。。");
synchronized (A) {
System.out.println("t1 拿到了 A 锁。");
}
System.out.println("t1 想拿 B 锁!");
try {
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
new Thread(()->{
System.out.println("t2启动。。");
synchronized (B) {
System.out.println("t2 拿到了 B 的锁。");
}
System.out.println("t2想拿a1的锁!");
try {
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t2").start();
}
}