synchronized关键字

synchronized的作用

互斥锁,对某个对象加锁,这里所说的并不是锁住栈中的一个引用,而是锁定的是堆中的对象,这一点很重要,如果是锁定的是对象的引用,那么引用可能在某个时刻之后指向的是其他的对象,这就有问题了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Synchronized01 {

private int count = 10;

private static int staticCount = 10;

public void printCount() {
synchronized (this) {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}

public synchronized static void printCount2() {
staticCount--;
}

}

​ printCount( )也可以直接在方法上加synchronized,因为该方法中的所有操作步骤都是在synchronized中的。printCount锁定的是当前对象,而printCount2( ) 锁定的是Synchronized01的Class对象,这两者是不一样的,当调用静态方法时是不需要创建对象的,那么自然也就没有对当前对象 this 上锁这么一说了。

同步方法和非同步方法是否可以同时调用?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class T{

public synchronized void m1() {
System.out.println("method m1() start");
try {
Thread.sleep(10000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("method m1() end");
}

public void m2() {
System.out.println("method m2() start");
}

public static void main(String[] args) {
T t = new T();
new Thread(() -> t.m1()).start();
new Thread(t::m2).start();
}

}

​ 如上的代码中,在m1( )方法被调用的时候,m2( ) 是否会执行?答案是肯定的,因为m2( )没有加锁,自然也就不需要等待m1( )执行完释放锁之后再执行了。这也说明了,在对业务写方法加锁,而对业务读方法没有加锁时,容易产生脏读的问题,因为在写过程中,状态的值还没有修改完,就已经被另外的线程读取了,而另外的线程读取到的并不是完全修改之后的值。

一个同步方法可以调用另外一个同步方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Synchronized02 {

synchronized void print1() {
System.out.println("method print1 ");
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
print2();

}

synchronized void print2() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("method print2");
}

public static void main(String[] args) {
Synchronized02 synchronized02 = new Synchronized02();
synchronized02.print1();
}
/** console output:
method print1
method print2
*/

}

​ 一个线程已经拥有了某个对象的锁,那么再次申请 的时候仍然可以获得该对象的锁。也就是说synchronized的锁是可重入的。

​ 对象在内存中的布局可以分为三部分:对象头信息、实例数据、对齐填充。

​ 而在对象头信息中又包含了两部分的信息,其中一部分就存储了GC分代年龄、hash码以及锁状态标志等的信息。

​ 在子类的同步中也同样是可以调用父类的同步方法的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class T{

public synchronized void m1() {
System.out.println("parent method start");
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("parent method end");
}

public static void main(String[] args) {
TT tt = new TT();
tt.m1();
}

}

class TT extends T{
@Override
public synchronized void m1() {
System.out.println("child method start");
super.m1();
System.out.println("child method end");
}
}
/**
console output:
child method start
parent method start
parent method end
child method end
*/

发生异常时,锁会被释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class Synchronized03 {

private int count = 0;

synchronized void m() {
System.out.println(Thread.currentThread().getName() + " start");
while (true) {
count++;
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count = " + count);

// 抛出异常,锁释放,其他线程可以争用
if (count == 3) {
int i = 1 / 0;// 抛出异常,锁被释放,如果不想释放,可以在这里catch,然后让循环继续。
}

}
}

public static void main(String[] args) {
Synchronized03 synchronized03 = new Synchronized03();
new Thread(() -> synchronized03.m(), "t1").start();

try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> synchronized03.m(), "t2").start();
}

/**
console output:
t1 start
count = 1
count = 2
count = 3
Exception in thread "t1" java.lang.ArithmeticException: / by zero
t2 start
at com.ethereal.thread.syncdemo.Synchronized03.m(Synchronized03.java:29)
at com.ethereal.thread.syncdemo.Synchronized03.lambda$main$0(Synchronized03.java:37)
at java.lang.Thread.run(Thread.java:748)
count = 4
count = 5
count = 6
*/

}

​ 如果线程t1在执行过程中抛出异常之后,锁没有释放,那么线程t2是永远不会执行的,显然与结果不符。如果想要锁不被释放,那么可以将异常捕获再进行相应的处理。

Volatile关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Volatile01 {

private volatile boolean running = true;

void m() {
System.out.println("method m start");
while (running) {
/*try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("adfsadf");*/
}
System.out.println("method m end");
}

public static void main(String[] args) {
Volatile01 t = new Volatile01();
new Thread(t::m, "t1").start();
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.running = false;
}

}

​ 如上的代码中,如果running属性没有使用volatile修饰的话,那么整个程序将陷入无限循环中,不会结束,而加了之后,则可以正常结束。这是因为,线程在执行的过程中,会将主内存(包括堆、栈内存等)中的数据读取到自己的缓冲区中,之后再使用的时候是从自己的缓冲区中读取该变量的值,而不是每次都从主内存中重新刷取一遍。所以,在main线程修改了running的值之后,会将修改之后的值刷同步到主内存中。但是其他线程如 t1 由于每次只从自己的缓冲区中读取数据,所以并不知道running的值已经被修改了,程序也就不会停止了。

​ 在使用了volatile修饰running后,当一个线程修改了对象的共享状态的值的时候,会通知其他线程再从主内存中将该变量的值写回到自己的缓冲区中,这样就解决了内存的可见性问题。注意,这里说的是通知其他线程重新获取,而不是每次都从主内存中获取变量的值。

​ 但是,如果在 while(running) 循环中,加入打印语句或者休眠一会,程序是有可能停止的。这是因为,当在执行这些操作的时候,CPU有可能有空闲的时间从主内存中,将自己缓冲区的数据刷新一遍。但这并不一定会发生,所以,该用volatile的时候就要用。它相对于synchronized轻的多的多。

​ 使用了volatile之后,性能会稍微下降一些,因为使用了volatile之后,jvm将不会进行重排序。另外,volatile并不能保证原子性也不具备“互斥性”。

AtomicXXX原子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class Atomic01 {

private AtomicInteger count = new AtomicInteger(0);

void m() {
for (int i = 0; i < 10000; i++) {
if (count.get() < 10) {
System.out.println(Thread.currentThread().getName() + ", count=" + count.get());
count.incrementAndGet();
}

}
}

public static void main(String[] args) {
Atomic01 atomic01 = new Atomic01();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10; i++) {
threads.add(new Thread(()-> atomic01.m(), "t" + i));
}
threads.forEach(o -> o.start());
threads.forEach(o -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(atomic01.count.get());
}

/**控制台输出:
t3, count=0
t3, count=1
t3, count=2
t3, count=3
t3, count=4
t0, count=0
t0, count=6
t0, count=7
t0, count=8
t0, count=9
t5, count=0
t2, count=0
t1, count=0
t4, count=0
t3, count=5
15
*/

}

​ 当去除上述代码中方法 m() 中for循环中的条件判断时,最后的打印结果永远是10,因为AtomicInteger的incrementAndGet() 操作是原子性的。但是如果加上条件判断之后,可以看到控制台最后的结果是15。这是因为,当t3线程进行条件判断完之后,执行累加操作之前,此时可能其他线程抢到了CPU的资源,导致 t3 线程进入等待状态,t3的程序计数器记录下该线程执行到哪一个位置,等到其他线程执行完之后,t3重新获得CPU资源,进入运行状态,开始从程序计数器记录的位置继续向下执行(这个时候 if 条件是在之前就已经完成的了,不会在进行一次判断),但是 t3 进行累加操作的时候,此时count的值已经是其他线程修改累加过的值了。所以出现了最终的结果比10大。

synchronized优化

缩小锁定的粒度

​ 1. 尽可能的减少需要加锁的代码块。

​ 2. 锁定一个对象obj,如果obj的属性发生改变,不会影响锁的使用,但是如果obj变成另外一个对象,那么锁定的对象也会发生改变。所以应该避免将锁定的应用变换成其他对象。这也说明了,是对堆中对象的实例加锁,而非对栈中对象的引用加锁

​ 3.不要锁定字符串常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Synchrnoized05 {

String s1 = "hello";
String s2 = "hello";

void m1() {
synchronized(s1) {

}
}

void m2() {
synchronized (s2) {

}
}

}

​ 如上的代码中,m1和m2分别锁定了s1和s2,这个时候会以为锁定的是两个对象,但其实是同一个对象,s1和s2都指向了常量池中的”hello”。要尽量的避免使用字符串常量的方式上锁,否则可能会造成难以排查的死锁阻塞的现象。