高并发专题

一、基本概念:

  线程同步:
  线程安全:当多个线程访问某个类时,这个类都始终能表现出正确的行为,那么就称这个类是线程安全的。
  线程与进程:

二、线程创建

  三种方式

三、线程状态

  五种状态

四、线程控制基本方法

  阻塞线程的方法:
    join():合并线程,当前线程会等待调用该方法的线程执行完毕后才会执行
    sleep():使线程进入阻塞状态,但是不会交出锁。由于线程进入了阻塞态,即使没有其他等待执行的线程,该线程也不会获得执行。
    yield():使线程进入就绪态,该方法也不会交出锁。但由于线程是进入了就绪态,如果当前没有其他执行的线程,则该线程会获得执行。
wait():
notify():

一、synchronized

  1.synchronized锁的是对象。
    对于代码块,synchronized(o){}锁的是括号中的对象,可以使用this锁住当前对象
    对于普通方法上加synchronized关键字,锁的是该方法所在的对象,也即相当于使用synchronized(this){}
    对于静态方法上加synchronized关键字,锁的是类对象。
细粒度、粗粒度???
  2.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
public class Syn1 implements Runnable{
int count = 0;
@Override
public void run() {
for(int i = 0;i < 1000;i++) {
count++;
}
System.out.println(Thread.currentThread().getName()+"线程执行结束");
}
public static void main(String[] args) {
Syn1 s1 = new Syn1();
new Thread(s1).start();
new Thread(s1).start();
new Thread(s1).start();
new Thread(s1).start();
new Thread(s1).start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(s1.count);
}
}

    执行结果:

    分析原因:
      从Java的内存模型角度分析,各个线程拥有自己的工作区,对象存在于主存。线程工作时需要从主存中拷贝对象数据的副本,修改完成后再写入主存。
    如果代码中一个线程拿到了主存中的数据100,但还没有修改,此时再发生线程切换,另一个线程也拿到了主存中的数据100,一个线程修改完成后将101
    写入主存,另一个线程修改完成后同样是101,写入主存。如此一来主存中本应该变成102的数据确变成了101,数据出现了错误。即count++并不是一个原子操作。
    
    加入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
public class Syn1 implements Runnable{
int count = 0;
@Override
public void run() {
for(int i = 0;i < 1000;i++) {
synchronized (this) {
count++;
}
}
System.out.println(Thread.currentThread().getName()+"线程执行结束");
}
public static void main(String[] args) {
Syn1 s1 = new Syn1();
new Thread(s1).start();
new Thread(s1).start();
new Thread(s1).start();
new Thread(s1).start();
new Thread(s1).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(s1.count);
}
}

  3.synchronized使用常见问题
  1)同步和非同步方法可以同时调用
    由于在执行非同步方法时,并不需要获得锁,故可以在对象被锁住的情况下执行。
    代码示例:

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
public class Syn2 {
public synchronized void m1() {
System.out.println(Thread.currentThread().getName() + "m1 start");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "m1 end");
}
public void m2() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "m2");
}
public static void main(String[] args) {
Syn2 s = new Syn2();
new Thread(()->s.m1(),"t1").start();
new Thread(()->s.m2(),"t2").start();
}
}

    运行结果:

1
2
3
t1m1 start
t2m2
t1m1 end

    可见m1获得锁之后,m2仍然能正常执行。

  2)如果只在写方法上加锁,在读方法上不加锁,则容易产生脏读的现象(读取未提交数据)
    如果在写的数据提交前后各读取一次,则可能读取的数据
  
  3)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
public class Syn4 {
public synchronized void m1() {
System.out.println("m1 start");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
m2();
}
public synchronized void m2() {
System.out.println("m2 start");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("m2 end");
}
public static void main(String[] args) {
new Syn4().m1();
}
}

  4)程序执行过程中,如果同步代码块中出现异常,锁会被释放

  5)synchronized粒度问题
    在保证线程安全的前提下,应尽量缩小synchronized代码块的范围,这样可以提高并发。

  

  7)锁锁定某对象o,如果o的属性发生了改变,不会影响锁的使用,如果对象o变成了另外一个对象,则锁状态改变,原本抢到的锁作废,线程会去抢新锁。
  如下列代码,执行s.o = new Object();后,t1仍然锁住的是旧对象,而t2则抢到了新对象的锁,因此两个线程都获得了各种的锁,因而都可以执行了。

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
public class Syn5 {
Object o = new Object();
public void m1() {
synchronized (o) {
while(true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
}
public static void main(String[] args) {
Syn5 s = new Syn5();
Thread t1 = new Thread(()->s.m1(),"t1");
t1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Thread t2 = new Thread(()->s.m1(),"t2");
s.o = new Object();
t2.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

二、volatile

  1.volatile的可见性
    在多线程的环境中,当一个线程修改了主存中的某个数据的值,另一个线程不一定会马上知道,也就是并不一定马上会从主存中更新该数据的值(特别是
在改线程所在的cpu特别忙的情况下)。而对变量使用volatile关键字则会保证所有线程都会读到变量的修改值。
    例如在以下程序中如果不加入volatile关键字,程序会一直执行下去,即使在主线程中running变量被修改为了false,但此时另一个线程并不会更新
running变量。而加了volatile关键字后,线程中的running变量会强制更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Volatile1 {
volatile boolean running = true;
public void m1() {
System.out.println("m1 start");
while(running) {}
System.out.println("m2 end");
}
public static void main(String[] args) {
Volatile1 v = new Volatile1();
new Thread(()->v.m1()).start();;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
v.running = false;
}
}

  2.volatile不可保证原子性
    对应count++问题,volatile并不能保证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
public class Volatile2 implements Runnable{
volatile int count = 0;
public void run() {
for(int i = 0;i < 1000;i++) {
count++;
}
}
public static void main(String[] args) {
Volatile2 v = new Volatile2();
Thread t1 = new Thread(v);
Thread t2 = new Thread(v);
Thread t3 = new Thread(v);
Thread t4 = new Thread(v);
Thread t5 = new Thread(v);
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
try {
t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(v.count);
}
}

  synchronized与volatile对比

三、AtomXXX类

  AtomXXX类是一类在java.util.concurrent下封装好的类,其方法本身就具有原子性,且其效率比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
public class Atom1 implements Runnable{
AtomicInteger count = new AtomicInteger(0);
@Override
public void run() {
for(int i = 0;i < 1000;i++) {
count.incrementAndGet();
}

}
public static void main(String[] args) {
Atom1 a = new Atom1();
List<Thread> list = new ArrayList<>();
for(int i = 0;i < 5;i++) {
list.add(new Thread(a));
}
list.forEach((o)->o.start());
list.forEach((o)->{
try {
o.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
System.out.println(a.count);
}
}

四、死锁

  当多个线程各自锁定一个资源等待对方锁定的资源时会发生死锁,典型的有哲学家进餐问题。
1.死锁实例

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class DeadLock {
public static String obj1 = "obj1";
public static String obj2 = "obj2";
public static void main(String[] args) {
MyThread1 thread1 = new MyThread1();
MyThread2 thread2 = new MyThread2();
new Thread(thread1).start();
new Thread(thread2).start();
}
}
class MyThread1 implements Runnable{

@Override
public void run() {
System.out.println(new Date().toString()+"thread1开始执行");
while(true) {
synchronized(DeadLock.obj1) {
System.out.println("t1获得obj1");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized(DeadLock.obj2) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("t1获得obj2");
}
}
}
}

}
class MyThread2 implements Runnable{

@Override
public void run() {
System.out.println(new Date().toString()+"thread2开始执行");
while(true) {
synchronized(DeadLock.obj2) {
System.out.println("t2获得obj2");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized(DeadLock.obj1) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("t2获得obj1");
}
}
}
}

}

2.死锁避免:

  • 保持获取锁的顺序一致
  • 尽量使用开放调用
  • 使用定时锁

3.死锁诊断
  可通过JVM的线程转储信息来分析死锁。

4.其他活跃性危险
  饥饿:当线程由于无法访问它所需要的资源而不能继续执行时,就发生了饥饿。常见的有线程优先级使用不当,优先级低的线程始终得不到CPU的执行时会导致该线程饥饿。我们应该尽量保持各线程优先级相同。

  思考角度:
    内存角度
    线程重入
    延迟角度
    线程调度异步性