Java 并发编程梳理一:迈进大门

文章目录
  1. 1. 简要介绍
  2. 2. 实现多线程
    1. 2.1. Thread
    2. 2.2. Runnable
  3. 3. 参考文献
  4. 4. 更多内容

Every day is a good day, some are better than others.

“勿以浮沙筑高台”!最近,业余在看并发编程,即多线程编程相关的,于是打算将并发编程这块由浅入深,全面地作一次梳理,系列博客持续更新。

简要介绍

开发中,大部分时候都是顺序编程,即程序中的所有事物,在任意时刻都是一步步的,只能执行一个步骤。但是,对于某些问题,若能并行地执行程序中的多个部分,程序执行速度得到极大提高的同时,此种处理方式也显得十分必要。

首先,明确一些基础知识。

线程:程序内的顺序控制流,使用分配给程序的资源和环境,只能用于程序中。

额外插一句所谓的进程:执行中的程序,一个进程可以包含一个或多个线程,并且,一个进程至少要包含一个线程。

单线程:程序中只有一个线程,程序启动运行时,自动产生一个线程,主方法 main 在这个主线程上运行。

多线程:共享一块内存空间和一组系统资源,单个程序中,同时运行多个不同的线程,执行不同的任务。通俗点说,CPU 会随机地抽出时间,以使得我们的程序一会儿执行这件任务,一会儿又可以执行另外的任务。

其次,从宏观角度把握,来看一张线程状态图:

状态转换的详细如下:

  • 新建:线程对象被创建。如 Thread t = new Thread()

  • 可运行:创建线程对象后,另一个线程调用了该对象的 start() 方法 来启动该线程,如 t.start(),等待被线程调度选中,获得 CPU 的使用权

  • 运行:线程获得 CPU 时间片,执行代码。注意,线程只能从可运行状态进入到运行状态

  • 阻塞:线程因为某种原因放弃 CPU 使用权,即让出 CPU 时间片,暂停运行,直到下一次进入可运行状态,获得 CPU 的使用权,转到运行状态

    1.等待阻塞:调用运行线程的 wait() 方法,等待某个工作完成

    2.同步阻塞:运行的线程获取对象的 synchronized 同步锁失败,如锁被其他线程占用

    3.其他阻塞:运行的线程调用 Thread.sleep() 或 t.join() 或发出 I/O 请求,会阻塞住;sleep() 状态超时、join() 等待线程终止或超时、或处理完 I/O,线程会重新进入可运行状态

  • 死亡:线程 run()、main() 结束,或异常退出,则线程的生命周期结束

以上还有锁池等待队列,synchronized 是锁池,wait()、notify()、notifyAll() 是等待队列。等待队列的对象是不会竞争锁的,notifyAll() 后,等待队列中的线程会被唤醒,然后进入到该线程的锁池中,重新竞争对象锁。

实现多线程

先介绍两种常见的实现方式,即 Thread 和 Runnable。

Thread

一个类,其本身即实现了 Runnable 接口,源码中:

1
2
3
public class Thread implements Runnable {

}

示例:

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 ThreadDemo {

static class MyThread extends Thread {
private int ball = 10;

@Override
public void run() {
super.run();
for (int i = 0; i < 20; i++) {
if (ball > 0) {
System.out.println(getName() + " 投球:ball " + ball--);
}
}
}
}

public static void main(String[] args) {
MyThread mt1 = new MyThread();
MyThread mt2 = new MyThread();
MyThread mt3 = new MyThread();
mt1.start();
mt2.start();
mt3.start();
}
}

运行结果为:

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
Thread-1 投球:ball 10
Thread-1 投球:ball 9
Thread-2 投球:ball 10
Thread-2 投球:ball 9
Thread-2 投球:ball 8
Thread-2 投球:ball 7
Thread-2 投球:ball 6
Thread-2 投球:ball 5
Thread-2 投球:ball 4
Thread-2 投球:ball 3
Thread-2 投球:ball 2
Thread-0 投球:ball 10
Thread-2 投球:ball 1
Thread-1 投球:ball 8
Thread-1 投球:ball 7
Thread-1 投球:ball 6
Thread-0 投球:ball 9
Thread-1 投球:ball 5
Thread-0 投球:ball 8
Thread-1 投球:ball 4
Thread-0 投球:ball 7
Thread-1 投球:ball 3
Thread-0 投球:ball 6
Thread-1 投球:ball 2
Thread-0 投球:ball 5
Thread-1 投球:ball 1
Thread-0 投球:ball 4
Thread-0 投球:ball 3
Thread-0 投球:ball 2
Thread-0 投球:ball 1

主线程 main 创建并启动 3 个自定义的 MyThread 子线程,每个子线程各自投了 10 个球。

注意 start() 和 run() 的简单区别:

start():启动一个新线程,执行相应的 run() 方法,start() 没法被重复调用。

run():可以被重复调用。独自调用 run() 方法,会在当前线程中执行 run(),不会启动新线程

Runnable

一个接口,其中只包含了一个 run() 方法,源码中:

1
2
3
public interface Runnable {
public abstract void run();
}

由于一个类只能有一个父类,但是却能实现多个接口,故而,Runnable 具有更好的扩展性。其也可以用在资源的共享上,多个线程可以基于某一个 Runnable 对象建立,共享 Runnable 对象上的资源。相比较而言,更推荐通过 Runnable 方式实现多线程

示例:

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 RunnableDemo {

static class MyRunnable implements Runnable {
private int ball = 10;

@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (ball > 0) {
System.out.println(Thread.currentThread().getName() + " 投球:ball " + ball--);
}
}
}
}

public static void main(String[] args) {
MyRunnable mr = new MyRunnable();

// 启动 3 个线程,共用 1 个 Runnable 对象
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
Thread t3 = new Thread(mr);
t1.start();
t2.start();
t3.start();
}
}

运行结果为:

1
2
3
4
5
6
7
8
9
10
Thread-1 投球:ball 10
Thread-0 投球:ball 9
Thread-2 投球:ball 7
Thread-1 投球:ball 8
Thread-2 投球:ball 5
Thread-0 投球:ball 6
Thread-0 投球:ball 3
Thread-0 投球:ball 1
Thread-1 投球:ball 4
Thread-2 投球:ball 2

这里实现的是 Runnable 接口,主线程 main 创建并启动了 3 个子线程,基于 Runnable 对象创建的,3 个子线程共投了 10 个球,即它们共享了 MyRunnable 接口。

注意!Runnable 的例子可能会多出 10 个球!

原因:t1、t2、t3 共用一个任务,同时对 ball 进行操作,可能导致并发问题。比如,t1 和 t2 开始读取到的 ball 值为 10,t1 投了一个球后,ball 为 9;正在此时,t2 开始投球,其之前读取到的球数为 10,那么,t2 投完之后也是 9 个球。问题来了!原来就 10 个球,t1 和 t2 各投了 1 个球,竟然还剩 9 个球,因此投出的可能会多于 10 个球!

为了避免球数多于 10 的情况,可以加一个 synchronized 同步。代码段换为:

1
2
3
4
5
synchronized (this) {
if (ball > 0) {
System.out.println(Thread.currentThread().getName() + " 投球:ball " + ball--);
}
}

不过运行程序只有 thread-0 在工作,是因为 thread-0 启动之后,一直占用着同步锁,一个对象有且一个同步锁。

至此,关于 Java 并发编程梳理一完毕。

本人才疏学浅,如有疏漏错误之处,望读者中有识之士不吝赐教,谢谢。

1
Email: [email protected] / WeChat: Wolverine623

您也可以关注我个人的微信公众号码农六哥第一时间获得博客的更新通知,或后台留言与我交流

参考文献

1.http://jiangzhengjun.iteye.com/blog/521821

2.http://www.cnblogs.com/skywang12345/p/3479063.html

更多内容

Java 并发编程梳理二:浅析 synchronized

Java 并发编程梳理三:浅谈 volatile