Java多线程

一.基本概念

  1. 程序、进程、线程
  • 程序(program)可以理解为静态的代码
  • 进程(process)可以理解为执行中的程序
  • 线程(thread)可以理解为进程的进一步细分, 程序的一条执行路径
  1. 何时需要多线程
  • 程序需要同时执行两个或多个任务
  • 程序需要实现一些等待的任务时, 如: 用户输入、文件读写、网络操作顿、搜索等
  • 需要一些后台运行的任务时

二.创建多线程

通过java.lang.Thread类实现

方式一:继承Thread类

  1. 继承Thread类,并重写run()方法,run()方法内实现此子线程想要实现的功能
  2. 在主线程内, 创建一个子线程的对象.
  3. 调用子线程的start()方法, 启动此线程; 调用相应线程的run()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class PrintNum extends Thread{
public void run(){
//子线程要执行的代码
for(int i = 1;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public PrintNum(String name){
super(name);
}
}
public class TestThread {
public static void main(String[] args) {
PrintNum p1 = new PrintNum("线程1");
PrintNum p2 = new PrintNum("线程2");
p1.setPriority(Thread.MAX_PRIORITY);//10
p2.setPriority(Thread.MIN_PRIORITY);//1
p1.start();//start()会调用run()方法
p2.start();
}
}

注意: ①一个线程只能start()一次; ②不能用run()启动线程

Thread类的常用方法:

One More Thing 👇
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Thread类的常用方法:
* 1.start():启动线程并调用相应的run()方法
* 2.run():子线程要执行的代码
* 3.currentThread():静态方法, 调取当前线程
* 4.getName():获取此线程的名字
* 5.setName():设置此线程的名字
* 6.yield():调用此方法的线程强制释放当前CPU的执行权
* 7.join():在A线程中调用B线程的join()方法.表示当A线程执行到此方法时,停止
* 执行A线程,直到B线程执行完毕,A线程再接着B.join()之后的代码执行
* 8.isAlive()判断此线程是否存活
* 9.sleep(long time)显示的让当前线程睡眠time毫秒
* 10.线程通信有关的: wait() notify() notifyAll()//在Object类中
* 设置线程的优先级MAX_PRIORITY=10;NORM_PRIORITY=5;MIN_PRIORITY=1
* 改变抢占CPU资源的概率
* setPriority(int newPriority)
* getPriority()
*/

例子: 三个售票窗口售卖100张票(一共100张)

继承Thread类的方式实现:

注意: 此程序存在隐患(线程安全问题)

One More Thing 👇
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
//继承Thread类的方式实现
class Window extends Thread{
static int ticket = 100;
public void run() {
while(true) {
if(ticket>0) {//开启注释放大错误
// try {
// Thread.currentThread().sleep(10);//阻塞10ms
// } catch (InterruptedException e) {
// // TODO 自动生成的 catch 块
// e.printStackTrace();
// }
System.out.println(Thread.currentThread().getName() + "售票, 票号为:"+ticket--);
}
else break;
}
}
}
public class TestWindow {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("窗口一:");
w2.setName("窗口二:");
w3.setName("窗口三:");
w1.start();
w2.start();
w3.start();
}
}

方式二:实现Runnable接口

  1. 创建一个实现Runnable接口的类
  2. 实现接口的抽象run()方法
  3. 创建一个实现Runnable接口实现类的对象
  4. 将此对象作为形参传给Thread类的构造器, 创建Thread类的对象, 此对象即为一个线程
  5. 调用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
//1.创建一个实现Runnable接口的类
class SubThread implements Runnable{
//2.实现接口的抽象run()方法
public void run(){
//子线程执行的代码
for(int i = 1;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class TestThread{
public static void main(String[] args){
//3.创建一个实现Runnable接口实现类的对象
SubThread s = new SubThread();
//想要启动一个线程必须调用start()方法
//4.将此对象作为形参传给Thread类的构造器, 创建Thread类的对象, 此对象即为一个线程
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
//5.调用start()启动线程
t1.setName("线程1");
t2.setName("线程2");
t1.start();//启动线程: 执行Thread对象生成时构造器形参的run()方法.
t2.start();
}
}

两种方式的对比: 联系:class Thread implements Runnable, 两种方式实际上都与Runnable接口发生了关系

哪个比较好?

  • 实现的方式(方式二)较好:
  • ①解决了单继承的局限性
  • ②如果多个线程有共享数据的话, 建议使用实现方式, 同时, 共享数据所在的类可以作为Runnable接口的实现类.

实现Runnable接口的方式实现(上述售票例题):

注意: 此程序存在隐患(线程安全问题)

One More Thing 👇
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
//有共享数据的问题(一共100张票)
class Window1 implements Runnable{
int ticket = 100;
public void run(){
while(true) {
if(ticket>0) {//开启注释放大错误
// try {
// Thread.currentThread().sleep(10);//阻塞10ms
// } catch (InterruptedException e) {
// // TODO 自动生成的 catch 块
// e.printStackTrace();
// }
System.out.println(Thread.currentThread().getName() + "售票, 票号为:"+ticket--);
}
else break;
}
}
}
public class TestThread{
public static void main(String[] args){
Window1 w = new Window1();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);

t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}

三.Java的多线程

1.多线程的优点

  • 提高应用程序的响应
  • 提高计算机系统的CPU利用率
  • 改善程序接口

2.Java线程的分类

  • 守护线程VS用户线程
  • 若JVM中都是守护线程, 当前JVM将退出

3.线程的生命周期

新建、就绪、运行、阻塞、死亡

线程的生命周期

四.线程同步

上述售票的例子:

程序存在线程安全问题: 打印车票时, 会出现重票、错票

  1. 线程安全问题存在的原因?

由于一个线程在操作共享数据的过程中, 未执行完的情况下, 另外一个线程参与进来, 导致共享数据存在了安全问题.

  1. 如何解决该问题?

必须让一个线程操作共享数据完毕以后, 其他线程才有机会进入参与共享数据的操作.

  1. Java如何实现线程的安全: 线程的同步机制

方式一: 同步代码块:

1
2
3
synchronized(同步监视器){
//操作共享数据的代码
}

①同步监视器: 俗称锁, 任何一个类的对象都可以才充当锁. 要想保证线程的安全, 必须要求所有的线程共用同一把锁!
②共享数据: 多个线程需要共同操作的变量. 明确哪部分是操作共享数据的代码.

③使用实现Runnable接口的方式创建多线程的话, 同步代码块中的锁, 可以考虑是this. 如果使用继承Thread类的方式, 慎用this!

修改原来的售票代码:

One More Thing 👇
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
//有共享数据的问题(一共100张票)
class Window1 implements Runnable{
int ticket = 100;
Object obj = new Object();
public void run() {
while(true) {
synchronized (this) {//同步锁可以由任何对象充当, 但是同步的线程要共用一把锁
//this表示当前对象, 本程序中为w对象, this可以改为obj
//具体问题具体分析, 有时候不能用this ,用继承Thread方式实现的多线程不能用this
if(ticket>0) {
System.out.println(Thread.currentThread().getName() + "售票, 票号为:"+ticket--);
}
else break;
}
}
}
}
public class TestThread{
public static void main(String[] args){
Window1 w = new Window1();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}

方式二: 同步方法:

  • 将需要共享的数据的操作放到一个方法里, 并给该方法添加 synchronized 修饰
  • 同步方法也有锁, 即为当前对象 this
  • 如果使用在继承的方式实现多线程的话, 慎用同步方法!因为它们的this锁不一样
One More Thing 👇
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//这个类是用实现Runnable接口实现的多线程
class Window implements Runnable{
int ticket = 100;
@Override
public void run() {
while(true) {
this.show();
}
}
public synchronized void show() {
if(ticket>0) {
System.out.println(Thread.currentThread().getName() + "售票, 票号为:"+ticket--);
}
}
}

懒汉式单例模式的线程安全

  • 使用线程同步机制解决问题
  • 对于一般的方法内, 使用同步代码块的方式, 可以考虑用this当锁
  • 对于静态方法而言, 使用当前类本身充当锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//懒汉式
class Singleton{
private Singleton(){}
private static Singleton instance = null;
public static Singleton getInstance(){
if(instance == null){//提高效率
synchronized(Singleton.class){//后面会说, Singleton.class对象
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}

线程同步的弊端: 由于同一时间只能有一个线程访问共享数据, 效率变低了

何时释放锁?

  1. 当前线程的同步方法、同步代码块执行结束时
  2. 当前线程的同步方法、同步代码块中出现了未处理的Error或Exception, 导致异常结束
  3. 当前线程的同步方法、同步代码块中执行了线程对象的wait()方法, 当前线程暂停并释放锁

线程的死锁:

不同的线程分别占用对方需要的同步资源不放弃, 都在等待对方放弃自己需要的同步资源, 就形成了线程的死锁.
死锁是我们在使用同步时, 需要避免的问题!

线程的通信

涉及到三个方法 wait() notify() notifyAll() 在java.lang.Object类中

  • wait(): 令当前线程挂起, 放弃CPU、同步资源…
  • notify(): 唤醒正在排队等待同步资源的线程中优先级最高的线程
  • notifyAll(): 唤醒正在排队等待同步资源的所有线程

注意: 这三个方法只能在同步方法或同步代码块中使用, 否则会报异常

例, 用两个线程交替打印1-100:

One More Thing 👇
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
class PrintNum implements Runnable{
int num = 1;
@Override
public void run() {
while(true) {
synchronized(this) {
notify();
if(num<100) {
System.out.println(Thread.currentThread().getName() + ":" + num);
num++;
}else break;
try {
wait();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
}
}
public class TestCommunication {
public static void main(String[] args) {
PrintNum p = new PrintNum();
Thread t1 = new Thread(p);
Thread t2 = new Thread(p);
t1.setName("甲");
t2.setName("乙");
t1.start();
t2.start();
}
}

经典例题: 生产者/消费者问题

One More Thing 👇
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/*
* 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,
* 店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫
* 生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员
* 会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
*/
class Producer implements Runnable {
Clerk clerk;
Producer(Clerk clerk) {
this.clerk = clerk;
}
public void run() {
System.out.println("生产者开始生产产品");
while (true) {
try {
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
clerk.addProduct();
}
}
}
class Consumer implements Runnable {
Clerk clerk;
Consumer(Clerk clerk) {
this.clerk = clerk;
}
public void run() {
System.out.println("消费者开始消费产品");
while (true) {
try {
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
clerk.subProduct();
}
}
}
class Clerk {
int product;
public synchronized void addProduct() {
if (product >= 20) {
try {
wait();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
} else {
product++;
notifyAll();
System.out.println(Thread.currentThread().getName() + "生产了第" + this.product + "个产品");
}
}
public synchronized void subProduct() {
if (product > 0) {
System.out.println(Thread.currentThread().getName() + "消费了第" + this.product + "个产品");
product--;
notifyAll();
} else {
try {
wait();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
}
public class Test {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
Consumer c1 = new Consumer(clerk);
Thread t1 = new Thread(c1);
Thread t2 = new Thread(p1);
Thread t3 = new Thread(p1);
t1.setName("消费者1");
t2.setName("生产者1");
t3.setName("生产者2");
t2.start();
t1.start();
t3.start();
}
}

评论