脏读
对写业务加锁,对读业务不加锁。在写的过程中去读取,容易产生脏读问题。
重入锁
一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁,也就是说synchronized获得的锁是可重入的。
synchronized方法调用synchronized方法,前提是他们持有同一个对象的锁。只要锁定的是同一个对象。
同一个类中
synchronized void m1() {
// ….
m2();
}synchronized void m2() {
System.out.println(“m2”);
}继承中的情形,子类调用父类的同步方法
public class T {
synchronized void m() {
System.out.println("m");
}
public static void main(String[] args) {
new TT().m();
}
}
class TT extends T {
@Override
synchronized void m() {
System.out.println("child m");
super.m();
}
}
异常情况
程序在执行过程中,如果出现异常,默认情况锁会被释放
在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。
因此要非常小心的处理同步业务逻辑中的异常
Java 内存模型(JMM) 虚拟机 volatile
volatile 使一个变量在多个线程间可见,保证线程间变量的可见性。使用synchronized效率比较低
每个线程可能有自己的缓存,缓存数据从主内存读取。使用volatile后,主内存的值改变后,会通知各个线程更新数据
A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道
使用volatile关键字,会让所有线程都会读到变量的修改值
public class T {
/*volatile*/ boolean running = true;
void m() {
System.out.println("m start");
while(running) {
}
System.out.println("m end!");
}
public static void main(String[] args) {
T t = new T();
new Thread(t::m, "t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.running = false;
}
}
volatile 和 synchrozied
volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
volatile只保证可见性
synchrozied既保证可见性,又保证原子行
简单的同步的地方使用 AtomicXXX 变量,更高效。比如++/–
AtomXXX类本身方法都是原子性的,但不能保证多个方法连续调用是原子性的
/*volatile*/ //int count = 0;
AtomicInteger count = new AtomicInteger(0);
/*synchronized*/ void m() {
for (int i = 0; i < 10000; i++) {
//if(count.get() < 1000)
//比如这里中间就不保证原子性
count.incrementAndGet(); //count++
}
}
synchronized优化
void m2() {
//do sth need not sync
//业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁
//采用细粒度的锁,可以使线程争用时间变短,从而提高效率
synchronized(this) {
count ++;
//do sth need not sync
}
不要以字符串常量作为锁定对象
在下面的例子中,m1和m2其实锁定的是同一个对象
String s1 = "Hello";
String s2 = "Hello";
void m1() {
synchronized(s1) {
}
}
void m2() {
synchronized(s2) {
}
}