Java多线程编程

在网络编程的程序中,网络通讯常常会花费大量时间,而如果我们使用单线程编程的话,这段时间无法被利用起来,这也就导致程序效率低下的问题,而多线程恰巧能让利用起这部分时间,提高效率。

并发与并行

并发:指两个或多个事件在同一时间段内发生
并行:指两个或多个事件在同一时刻发生

test

在操作系统中,并发指的是在一段时间内宏观上看有多个程序同时运行,这在单CPU系统中,每一时刻只有一道程序执行,即微观上这些程序是交替执行的,只不过交替的时间非常短.

线程与进程

进程: 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程是系统运行程序的基本单位;系统运行程序即是一个进程从创建、运行到消亡的过程.
线程: 线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程.一个进程中可以有多个线程,这个应用程序可以称为多线程程序.

单核处理器的计算机肯定是不能并行处理多个任务的,只是多个任务在CPU上并发运行.线程也一样,微观上看单核处理器中线程是串行运行的,线程会以某种顺序执行多个线程,这种情况称之为线程调度

线程调度:
1.分时调度
所有线程轮流使用CPU,平均分配每个线程占用CPU的时间
2.抢占式调度
抢先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个线程,Java使用的就是抢占式调度

创建线程类

Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例.每个线程的作用是完成一定的任务.Java使用线程执行来代表这段程序流.
Java创建并启动多线程步骤:
1.定义Thread类的子类,并重写该类的run()方法,该run()方法就代表了线程需要完成的任务,run()方法因此也称线程执行体,
2.创建Thread子类的实例,即创建线程对象
3.调用线程对象的start()方法启动线程

测试类:

1
2
3
4
5
6
7
8
public class Demo{
public static void main(String[] args) {
MyThread thread1 = new MyThread("线程一");
MyThread thread2 = new MyThread("线程二");
thread1.run();
thread2.run();
}
}

自定义线程类:

1
2
3
4
5
6
7
8
9
10
11
12
public class MyThread extends Thread {
public MyThread(String name){
super(name);
}

@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(getName()+":正在执行!"+i);
}
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
线程一:正在执行!0
线程一:正在执行!1
线程一:正在执行!2
线程一:正在执行!3
线程一:正在执行!4
线程一:正在执行!5
线程一:正在执行!6
线程一:正在执行!7
线程一:正在执行!8
线程一:正在执行!9
线程二:正在执行!0
线程二:正在执行!1
线程二:正在执行!2
线程二:正在执行!3
线程二:正在执行!4
线程二:正在执行!5
线程二:正在执行!6
线程二:正在执行!7
线程二:正在执行!8
线程二:正在执行!9

原理分析

程序启动运行main时候,Java虚拟机启动一个进程–主进程.随着调用thread1、thread2的对象的start方法,另外两个新进程也就启动了,整个应用就在多进程下运行.
多线程运行时,在栈内存中,每一个执行线程都有一片自己的栈内存空间来进行方法的压栈和弹栈.
当执行线程的任务结束,线程自动在栈内存中释放.当所有线程都结束,进程也就结束.

创建线程方式二

除了继承Thread类以实现多进程,我们还可以实现Runnable接口.
步骤如下:
1.定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体.
2.创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象.
3.调用线程对象的start()方法来启动线程

测试类

1
2
3
4
5
6
7
8
9
10
public class Demo{
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
Thread th1 = new Thread(thread1,"线程一");
Thread th2 = new Thread(thread2,"线程二");
th1.start();
th2.start();
}
}

自定义实现Runnable接口类

1
2
3
4
5
6
7
8
public class MyThread implements Runnable{
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+":正在执行!"+i);
}
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
线程一:正在执行!0
线程一:正在执行!1
线程一:正在执行!2
线程一:正在执行!3
线程一:正在执行!4
线程一:正在执行!5
线程一:正在执行!6
线程二:正在执行!0
线程二:正在执行!1
线程一:正在执行!7
线程二:正在执行!2
线程二:正在执行!3
线程二:正在执行!4
线程二:正在执行!5
线程二:正在执行!6
线程二:正在执行!7
线程二:正在执行!8
线程二:正在执行!9
线程一:正在执行!8
线程一:正在执行!9

Thread和Runnable的区别

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享.

实现Runnable接口比继承Thread类所具有的优势:
1.适合多个相同的程序代码的线程去共享一个资源
2.可以避免java中的单继承的局限性。

3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
4. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

扩充:在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM就是在操作系统中启动了一个进程。

匿名内部类方式实现线程创建

使用线程的匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作.
使用匿名内部类的方式实现Runnable接口,重新Runnable接口的run方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Demo{
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+":正在执行!"+i);
}
}
};
new Thread(r,"线程一").start();
new Thread(r,"线程二").start();
}
}

线程安全

如果多个进程同时运行,而这些进程可能会同时运行这段代码.程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全.

这里举一个例子:

模拟电影院买票过程,假设总票数100张,我们开辟了三个窗口来售票.
转化为程序语言也就是三条线程同时对票数进行减法操作.

先用之前讲述构建方法写出代码

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 Demo {
public static void main(String[] args) {
Runnable r = new Runnable() {
int ticketCount = 100;

@Override
public void run() {
//窗口 永远开启
while (true) {
if (ticketCount > 0) {//有票
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticketCount--);
}
}
}
};

new Thread(r, "窗口一").start();
new Thread(r, "窗口二").start();
new Thread(r, "窗口三").start();
}
}

多运行几次后发现,程序会出现这样的结果

1
2
3
4
5
窗口二正在卖:3
窗口三正在卖:2
窗口一正在卖:1
窗口三正在卖:0
窗口二正在卖:-1

很显然出现逻辑错误了.这是因为三个线程当票数为1同时进入了判断,然后依次打印了输出,导致出现这样的错误.

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

线程同步

要解决上述多线程并发访问一个资源的安全性问题,Java提供了同步机制(synchronized)来解决.
有三种方式可以完成同步操作
1.同步代码块
2.同步方法
3.锁机制

同步代码块

同步代码块synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:

1
2
3
synchronized(同步锁){
//同步代码
}

同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
1.锁对象可以是任意类型
2.多个线程对象要使用同一把锁.

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

使用同步代码块解决上述问题:

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
public class Demo {
public static void main(String[] args) {
Runnable r = new Runnable() {
int ticketCount = 100;
Object lock = new Object();

@Override
public void run() {
//窗口 永远开启
while (true) {
synchronized (lock) {
if (ticketCount > 0) {//有票
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticketCount--);
}
}
}
}
};

new Thread(r, "窗口一").start();
new Thread(r, "窗口二").start();
new Thread(r, "窗口三").start();
}
}

同步方法

同步方法:使用synchronized修饰的方法,叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等待.

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
public class Demo {
public static void main(String[] args) {
Runnable r = new Runnable() {
int ticketCount = 100;
Object lock = new Object();

@Override
public void run() {
//窗口 永远开启
while (true) {
sellTicket();
}
}

synchronized public void sellTicket() {
if (ticketCount > 0) {//有票
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticketCount--);
}
}

};

new Thread(r, "窗口一").start();
new Thread(r, "窗口二").start();
new Thread(r, "窗口三").start();
}
}

锁机制

java.util.concurrent.locks.Lock提供了比synchronized代码块synchronized方法更广泛的锁定操作.
Lock锁也称同步锁,加锁和释放锁方法化了,如下:
1.public void lock():加同步锁
2.public void unlock():释放同步锁

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
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Demo {
public static void main(String[] args) {
Runnable r = new Runnable() {
int ticketCount = 100;
Lock lock = new ReentrantLock();

@Override
public void run() {
//窗口 永远开启
while (true) {
lock.lock();
if (ticketCount > 0) {//有票
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticketCount--);
}
lock.unlock();
}
}
};

new Thread(r, "窗口一").start();
new Thread(r, "窗口二").start();
new Thread(r, "窗口三").start();
}
}

线程状态

线程状态概述

当线程被创建并启动后,它会处于一些状态上。APIjava.lang.Thread.State中给出了六种线程状态:

线程状态 导致状态发生条件
NEW(新建) 线程刚被创建,但并未启动
RUNNABLE 线程可以在JVM中运行
BLOCKED 当一个线程试图获取一个对象锁,而该对象锁被其他线程持有,则该线程进入BLOCKED状态;当线程持有锁时,该线程会变为RUNNABLE状态
WAITING 一个线程等待另一个现车给执行一个唤醒动作时,该进程进入WAITING状态.进入该状态后是不能自动唤醒的,必须等待另一个线程来调用notify或者notifyAll方法唤醒
TIMED_WAITING 同WAITING状态,有几个方法有超时参数,调用他们将进入TIMED_WAITING状态.这一状态将一直保持到超时期满或者接收到唤醒通知.
TEMINATED 因为run()方法正常结束而死亡,或者因为没有捕获的异常终止了run方法而死亡.

Timed Waiting(计时等待)

Timed Waiting在API中描述为:一个正在计时等待另一个线程执行一个(唤醒)动作的线程处于这一状态.

之前我们已经接触过这个状态了,在我们写卖票的案例中,为了避免线程执行过快(出现不了线程安全问题),我们使用了sleep语句,这样就强制当前正在执行的线程休眠,以”减慢线程”.

当我们调用了sleep方法之后,当前执行的线程就进入到“休眠状态”,就是所谓的Timed Waiting(计时等待),我们通过一个案例加深对该状态的一个理解。

实现一个计数器,计数到100,在每个数字之间暂停1秒,每隔10个数字输出一个字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyThread extends Thread{
@Override
public void run() {
for(int i=0;i<100;i++){
if(i%10==0)
System.out.println("-------------");
System.out.println(i);
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
new MyThread().start();
}
}
  1. 进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协作关系。
  2. 为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程中会睡眠
  3. sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态。

sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就开始立刻执行。

BLOCKED(锁阻塞)

Blocked状态在API中的描述为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。
我们已经学完同步机制,那么这个状态是非常好理解的了。比如,线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。
这是由Runnable状态进入Blocked状态。除此Waiting以及Time Waiting状态也会在某种情况下进入阻塞状态.

Waiting(无限等待)

Wating状态在API中的描述为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。

用代码来举例讲解一下

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
public class main{
public static Object object = new Object();
public static void main(String[] args) {

//演示waiting
new Thread(new Runnable() {
@Override
public void run() {
while (true){
synchronized (object){
try{
System.out.println(Thread.currentThread().getName()+"-----获取到锁对象,调用wait方法,进入wait状态");
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"-----解除wait状态,继续执行");
}
}
},"等待线程").start();

//唤醒waiting线程
new Thread(new Runnable() {
@Override
public void run() {
while(true){
try{
System.out.println(Thread.currentThread().getName()+"-----等待3秒钟");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}

synchronized (object){
System.out.println(Thread.currentThread().getName()+"-----获取到锁对象,调用notify方法,唤醒一个wait线程");
object.notify();
}
}
}
},"唤醒线程").start();
}
}

通过上述例子我们可以发现,一个调用了某个对象的Object.wait方法的线程会等待另一个线程调用此对象的Object.notify或Object.notifyAll方法

其实waiting状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系.