Java高并发编程 | 学习笔记(二)

1.7 写方法和读方法的加锁选择问题

下面代码中:存在一个 Account 对象(属性有名称和余额),下面代码中存在的注释暂时忽略不计:set() 方法是同步的,getBalance() 方法不是同步的。

main方法启动的时候,先启动了个线程,设置账户名和余额,主线程会两次读取账户余额,可能看到的设置余额和读取余额是一致的,但是当 Account 类的 run() 方法中增加线程睡眠,以增大线程阻塞问题,就会出现设置账户名称之后,还没设置账户余额的时候,其他线程就读取到了账户余额,因此导致脏读的问题,在金融业务中,脏读很有必要避免的。

避免线程脏读问题就需要对读操作也进行加锁。

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
/**
* 对业务写方法加锁
* 对业务读方法不加锁
* 容易产生脏读问题(dirtyRead)
*/

import java.util.concurrent.TimeUnit;

public class Account {
String name;
double balance;

public synchronized void set(String name, double balance) {
this.name = name;
/*
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
*/

this.balance = balance;
}

public /*synchronized*/ double getBalance(String name) {
return this.balance;
}


public static void main(String[] args) {
Account a = new Account();
new Thread(()->a.set("zhangsan", 100.0)).start();

try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(a.getBalance("zhangsan"));

try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(a.getBalance("zhangsan"));
}
}
1.8 synchronized 是重入锁

下面代码中:m1() 和 m2() 方法都加了 synchronized 锁,两个锁的对象都是this,那么当一个线程已经运行 m1() 方法之时,在 m1() 方法中调用了 m2() 方法,且这个 m2() 方法也是加了锁的,是否可以调用这个 m2() 方法运行,答案是可以的,synchronized 会有计数器进行计数,表示重入了几次。

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
/**
* 一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁.
* 也就是说synchronized获得的锁是可重入的
*
*/

import java.util.concurrent.TimeUnit;

public class T {
synchronized void m1() {
System.out.println("m1 start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
m2();
}

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

在子类和父类之间,也是可以 synchronized 重入。

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
/**
* 一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁.
* 也就是说synchronized获得的锁是可重入的
* 这里是继承中有可能发生的情形,子类调用父类的同步方法
*
*/
package yxxy.c_010;

import java.util.concurrent.TimeUnit;

public class T {
synchronized void m() {
System.out.println("m start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m end");
}

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");
}
}
1.9 synchronized遇到抛出异常的时候会释放锁

程序在执行过程中,如果出现异常,默认情况锁会被释放,所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。代码中,当 count 子增到 5 的时候会因为算术计算异常导致锁被释放,那么当前线程只执行了一半操作,另一半未操作的资源会被其他线程抢去操作,造成线程同步问题。

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
/**
* 程序在执行过程中,如果出现异常,默认情况锁会被释放
* 所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。
* 比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,
* 在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。
* 因此要非常小心的处理同步业务逻辑中的异常
*
*/
public class T {
int count = 0;
synchronized void m() {
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) {
e.printStackTrace();
}

if(count == 5) {
int i = 1/0; //此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch,然后让循环继续
}
}
}

public static void main(String[] args) {
T t = new T();
Runnable r = new Runnable() {

@Override
public void run() {
t.m();
}

};
new Thread(r, "t1").start();

try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}

new Thread(r, "t2").start();
}
}
1.10 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* volatile 关键字,使一个变量在多个线程间可见
* A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道
* 使用volatile关键字,会让所有线程都会读到变量的修改值
*
* 在下面的代码中,running是存在于堆内存的t对象中
* 当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去
* 读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行
*
* 使用volatile,将会强制所有线程都去堆内存中读取running的值
*
* 可以阅读这篇文章进行更深入的理解
* http://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html
*
* volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
*
*/
import java.util.concurrent.TimeUnit;

public class T {
/*volatile*/ boolean running = true; //对比一下有无volatile的情况下,整个程序运行结果的区别
void m() {
System.out.println("m start");
while(running) {
/*
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
}
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 并不能保证原子性

十个线程在自增的时候,当自增完了就会刷新到主内存,当其他线程要拿的时候会更新,但是拿完之后,刚要自增操作的时候,CPU 资源被其他线程抢占,因此别的线程都自增成很大的数值,而此线程再抢到 CPU 资源执行自增的时候,还是之前的小数值自增,因此下面代码中的每个线程自增一万,十个线程执行之后,最终的 count 应该是十万,当时结果很差强人意。

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
/**
* volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
* 运行下面的程序,并分析结果
*
*/
import java.util.ArrayList;
import java.util.List;

public class T {
volatile int count = 0;
void m() {
for(int i=0; i<10000; i++) count++;
}

public static void main(String[] args) {
T t = new T();

List<Thread> threads = new ArrayList<Thread>();

for(int i=0; i<10; i++) {
threads.add(new Thread(t::m, "thread-"+i));
}

threads.forEach((o)->o.start());

threads.forEach((o)->{
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});

System.out.println(t.count);
}
}

使用带有原子性操作的对象,下面代码中的 AtomicInteger在自增时候就是原子操作,因此可以保证一致性问题。

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
/**
* 解决同样的问题的更高效的方法,使用AtomXXX类
* AtomXXX类本身方法都是原子性的,但不能保证多个方法连续调用是原子性的
*
*/
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;


public class T {
/*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++
}

public static void main(String[] args) {
T t = new T();

List<Thread> threads = new ArrayList<Thread>();

for (int i = 0; i < 10; i++) {
threads.add(new Thread(t::m, "thread-" + i));
}

threads.forEach((o) -> o.start());

threads.forEach((o) -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});

System.out.println(t.count);
}
}
1.11 synchronized 优化(粗细锁)

下面代码中,由于 synchronized 锁的范围不同,会导致程序执行效率的不同,越精细的锁越高效。

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
/**
* synchronized优化
* 同步代码块中的语句越少越好
* 比较m1和m2
*
*/
import java.util.concurrent.TimeUnit;

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();
}
}
}
1.12 到底锁的是什么(锁定堆中的对象)

下面代码中,m() 方法是一个死循环,当第一个线程拿到锁的时候,按理来说,一定是不会释放锁的,因为这个线程就是个无限死循环执行代码,而当锁对象发生改变,会造成锁定要锁的对象被释放掉了。

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
/**
* 锁定某对象o,如果o的属性发生改变,不影响锁的使用
* 但是如果o变成另外一个对象,则锁定的对象发生改变
* 应该避免将锁定对象的引用变成另外的对象
*
*/

import java.util.concurrent.TimeUnit;

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();
}
}
updated updated 2024-01-01 2024-01-01
本文结束感谢阅读

本文标题:Java高并发编程 | 学习笔记(二)

本文作者:woodwhales

原始链接:https://woodwhales.cn/2019/02/14/023/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%