JAVA并发编程

并发编程与多线程

1.1 基本概念

  1. 什么是并发编程

  2. 什么是线程:

    线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
    一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行

  3. 什么是多线程及一些例子:

1.2 Java多线程

线程生命周期一共有五种状态:创建、就绪、运行、阻塞、终止。

  1. 创建状态:new Thread(Runnable , "thread'name")
  2. 就绪状态:已创建好一个线程等待运行。
  3. 运行:run() 开始运行。
  4. 阻塞。
  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
/*Thread1.java*/
public class Thread1 {
synchronized void m1(){
System.out.println(Thread.currentThread().getName() + " is start!");
try {
Thread.sleep(5000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " is end..");
}

public /*synchronized*/ void m2() {
System.out.println(Thread.currentThread().getName() + " is start!!");
try {
Thread.sleep(100);

}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("I want to use t1");
m1();
System.out.println(Thread.currentThread().getName() + " is end");
}

public static void main(String[] arges){
Thread1 t = new Thread1();
new Thread(t :: m1 , "t1").start();
new Thread(t :: m2,"t2").start();
}

}

out:

1
2
3
4
5
6
7
t1 is start!
t2 is start!!
I want to use t1
t1 is end..
t2 is start!
t2 is end..
t2 is end

synchronized线程同步

这里同时启动了两个线程:t1和t2,并且这两个线程分别使用了m1和m2方法,在m1方法上我使用了synchronizedsynchronized 是给这个方法加锁:在t1线程使用m1方法时,其他线程无法在t1线程执行m1方法时再用m1方法,必须等t1线程结束后,释放掉锁,其他线程才可以使用加了锁的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
33
/*Thread1.java*/
public class Thread1 {
synchronized void m1(){
System.out.println(Thread.currentThread().getName() + " is start!");
try {
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
m2();
System.out.println(Thread.currentThread().getName() + " is end..");
}

public synchronized void m2() {
System.out.println(Thread.currentThread().getName() + " is start!!");
try {
Thread.sleep(100);

}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("I want to use t1");
m1();
System.out.println(Thread.currentThread().getName() + " is end");
}

public static void main(String[] arges){
Thread1 t = new Thread1();
new Thread(t :: m1 , "t1").start();
new Thread(t :: m2,"t2").start();
}

}

这段代码中,t1.m1方法在睡了两秒后开始调用t2线程加锁了的m2方法,由于锁的原因,t1进入阻塞状态等待m2锁被t2释放,但在t2线程的m2方法睡了一秒后开始调用已被t1加锁的m1方法,也进入阻塞等待m1锁被t1释放,于是就出现两个线程互相调用互相阻塞的情况,死锁。

当线程抛出异常时会自动释放锁

如果不希望释放锁但又不确定会不会抛出异常:

  1. 多跑几次试试
  2. try{}catch(){}

1.2.1

volatile共享变量

理解volatile需要点JMM的知识,volatile就是线程之间数据更新变化可检测性,也是多处理器共享数据的可见性,意思就是当一个线程修改了他们的共享数据时,另一个线程也可以读到修改的值。在JMM中存在一个主内存,而创建出的线程每个是从主线程中读取数据,每个线程会有自己的一块CPU和缓存区,如果没有volatile,线程会在运行时把从主线程读到的数据放到自己的缓存区然后执行自己的任务,这时,如果主线程的数据更改的话,线程内部自己的数据是不会改变的,但有volatile的话,就相当于所有(使用这个数据的)线程检测到数据需要updata然后就读到新值。volatile使一个变量在多线程中可见。

volatile并不能保证多个线程共同修改变量所带来不一致的问题,也就是说volatile不能代替synchronized

深入理解java内存模型和高并发

AtomicInteger原子操作

对于Java中的运算操作,例如自增或自减,若没有进行额外的同步操作,在多线程环境下就是线程不安全的。num++解析为num=num+1,明显,这个操作不具备原子性,多线程并发共享这个变量时必然会出现数据出错。

1
2
3
4
/*定义一个原子初始值为0*/
AtomicInteger count = new AtomicInteger(0);
/*自增操作,相当于++*/
count.incrementAndGet();

多线程并发共享同一数据的时候,保证数据在同一时刻只被一个线程使用。

在实现AtomXXX类时,如果有多个Atom方法不构成其原子性

比如:

1
2
3
if(count.get() < 1000){
count.incrementAndGet();
}

这时在两个AtomXXX方法代码中,就可能有其他线程进入来更改数据。

AtomXXX原子操作数据也和volatile一样多线程共享数据。

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
synchronized void m(){
try{
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace();
}
count++;
try{
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
/*上面这个方法实际只对count变量控制,但却给整个方法加锁,大大降低了执行效率,做以下优化*/
void m(){
try{
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace();
}
synchronized(this){
count++;
}
try{
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
/*采用细粒度的锁,可以使线程争用时间变短,从而提高效率*/

synchronized锁是锁在堆内存里的,不是锁在引用里的

不要以String常量作为锁定对象,比如有String a="Hello"; 还有String b="Hello"; 然后有两个方法分别对a和b加锁,但实际他们锁的是同一个对象,而且当引用某些类库的时候,在类库本身会出现对字符串加锁的情况,如果再锁有时候会出现很诡异的死锁阻塞。

1.2.2

线程交互

wait和notify

wait()方法会让当前调用该方法的线程进入等待状态,**并且释放锁,**notify()需要在其他线程中调用,作用是唤醒当前正在等待状态的线程,但是notify不会释放锁,如果另一个线程被唤醒了,没有拿到锁,那么还是会在阻塞状态。

notifyAll()作用是唤醒所有正在wait状态的线程。

门闩

countdownlatch/cyclicbarrier/semaphore

1
2
3
4
5
6
7
/*创建一个门闩,内部定为1,countdown就是往下数为0时就开了*/
CountDownLatch latch = new CountDownLatch(1);
/*等待方法,也可以指定时间*/
latch.await();
latch.await(2000);
/*开门*/
latch.countDown();

门闩不需要像synchronized会锁定任何东西。

参考:

volatile总结 (保证内存可见性:Lock前缀的指令、内存屏障禁止重排序)

synchronized总结 (保证内存可见性和操作原子性:互斥锁;锁优化)

0%