0%

Java基础—多线程

这一部分主要记录了Java中多线程的基础操作,文章结构分为「多线程的实现方法、多线程的基本操作、线程安全」三个部分以及一个「生产者与消费者模型」的实例。

多线程的实现方法

方法一:继承Thread类

需要:线程类 + 主类

表格描述

操作位置 操作方式 方法名/类名 说明
线程类(MyThread) 继承 Thread 定义一个类MyThread继承Thread类
重写 void run() 在MyThread类中重写run()方法
主类 new 线程类 创建MyThread类的对象
对象.方法 void start() 【启动线程】执行线程类重写的方法

代码描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//MyThread类
public class MyThread extends Thread{
@Override
public void run() {
for(int i = 0; i < 1000000; i++){
System.out.println(i);
}
}
}


//测试类
public class MyThreadDemo {
public static void main(String[] args) {
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();

//使用 start() 方法启动线程,调 run() 方法
my1.start();
my2.start();
}
}

方法二:实现Runnable接口

主要:线程类 + 主类

表格描述

操作位置 操作方式 方法名/类名/接口名 传参 说明 备注
线程类(MyRunnable) 实现 Runnable
重写 void run() *需要获取Thread.currentThread()才能对类操作
主类 new 线程类(MyRunnable) 创建MyRunnable类的对象
new Thread类 线程类对象 创建Thread类的对象,
把MyRunnable对象作为构造方法的参数
*多种构造方法
对象.方法 void start()

Thread类构造方法

方法名 说明
Thread(Runnable target) 分配一个新的Thread对象
Thread(Runnable target, String name) 分配一个新的Thread对象
并给线程命名

代码描述

MyRunnable类:

1
2
3
4
5
6
7
8
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i); //要先获取当前线程类的引用【Thread.currentThread()】后才能用
}
}
}

主类:

1
2
3
4
5
6
7
8
9
10
public class MyRunnableDemo {
public static void main(String[] args) {
//创建MyRunnable类的对象
MyRunnable my = new MyRunnable();

//创建Thread类的对象,把MyRunnable对象作为构造方法的参数
Thread th1 = new Thread(my);
Thread th2 = new Thread(my);
}
}

好处

  • 避免了Java单继承的局限性(Runnable 可以再继承其他类)
  • 适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码、数据有效分离,较好的体现了面向对象的设计思想

对线程的操作

主要包括:「获取/设置线程名称、线程调度、线程控制」以及特殊方法「获取当前线程对象的引用」

A:获取当前线程对象的引用

因为对线程的操作需要对“Thread类的引用”操作。所以,在【Runnable类】【主类】要对当前类操作时,需要用此方法

方法名 说明 使用方法
Thread currentThread() 返回对当前正在执行的线程对象的引用 Thread.currentThread().xxx

例如:在【Runnable类】【主类】中要获得线程名称,使用Thread.currentThread().getName()

B:设置/获取线程名称

方法一:使用Thread类方法

方法列表:

方法名 说明
void setName(String name) 【设置线程名字】为name
String getName() 【返回线程名称】

方法二:使用带参构造函数命名

多线程实现方法 构造函数 使用示例
继承Thread类 Thread(String name) MyThread my = new MyThread(“小狗”);
实现Runnable方法 Thread(Runnable target, String name) Thread th = new Thread(myRunnable, “小猫”);

C:线程调度

优先级

主要靠优先级来实现,关于优先级有如下说明:

  • 默认优先级:Thread.NORM_PRIORTY (=5)
  • 优先级范围:Thread.MIN_PRIORTY (=1) ~ Thread.MAX_PRIORITY (=10)
  • 线程优先级高代表获取线程优先级高,不保证先运行

优先级操作方法:

方法名 说明 示例
final int getPriority() 获取优先级 tp1.getPriority()
final void setPriority(int newPriority) 设置优先级 tp1.setPriority(5);

D:线程控制

控制方法

方法名 说明 示例
static void sleep(long millis) 使线程停留指定的毫秒数 Thread.sleep(1000);
*异常处理
void join() 等待此线程死亡后才能执行其他 tj1.join();
*异常处理
void setDaemon(boolean on) 标记守护线程※ td1.setDaemon(true);

控制意义

  • sleep():让这个线程停下来,给别的线程抢资源。让线程们大致均匀的抢资源
  • join():让这个线程死亡再执行其他,阻止其他线程抢资源
  • setDaemon(boolean on):如果只剩下守护线程,那么 虚拟机就退出

线程安全——同步 synchronized

判断多线程是否会出现数据安全问题的标准

  • 是否多线程环境
  • 是否有共享数据
  • 是否有多条语句操作共享数据(含比较、更改、输出等各种操作)

关于锁

锁是通过对象来判定的,对象是同一个,就是同一个锁,会同时锁住。

A:同步代码块(给代码块加锁)

使用位置

锁住“操作共享数据的多条语句”

只要同步代码块锁中传入的对象是同一个,就是同一个锁,会同时锁住

使用格式

1
2
3
4
5
private Object obj = new Object();//一定要在外界创建对象,一个对象就是一把锁

synchronized(任意对象-obj){
多条语句操作共享数据
}

B:同步(成员)方法(给方法加锁)

使用格式

分类同步方法和同步静态方法,区别就是是否有static

1
2
3
private <static> synchronized void sellTicket(){
多次操作的共享数据
}

锁的对象

  • 非静态方法:this
  • 静态方法:类名.class(此方法在反射中,可以获得类)

C:线程安全的类【理解即可】

线程安全数据类型:

StringBuffer(线程安全可变的字符序列)

  • 线程安全,可变的字符序列
  • 从JDK 5开始,被StringBuilder 替代。 通常应该使用StringBuilder类,因为它支持所有相同的操作,但它更快,因为它不执行同步

Vector(线程安全列表List)

  • 从Java 2平台v1.2开始,该类改进了List接口,使其成为Java Collections Framework的成员。 与新的集合实现不同, Vector被同步。
  • 如果不需要线程安全的实现,建议使用ArrayList代替Vector

Hashtable(线程安全Hash表)

  • 该类实现了一个哈希表,它将键映射到值。 任何非null对象都可以用作键或者值
  • 从Java 2平台v1.2开始,该类进行了改进,实现了Map接口,使其成为Java Collections Framework的成员。 与新的集合实现不同, Hashtable被同步。
  • 如果不需要线程安全的实现,建议使用HashMap代替Hashtable

使非线程安全的数据类型变成线程安全(Collections)

  • static List synchronizedList ( List list);
  • static List synchronizedSet ( Set list);
  • static List synchronizedMap ( Map list);

示例:

1
List<String> list = Collections.synchronizedList(new ArrayList<String>());

线程安全——锁 Lock接口

使用方法请关注「代码描述」

Lock说明

  • 是接口,采用它的实现类ReentrantLock来实例化
  • synchronized更广放的锁定操作
  • 使用于线程类中
  • Lock 加锁解锁 要用 try{…}finally{…} 环绕,防止程序出错不能解锁

构造方法

方法名 说明
ReentrantLock() 创建一个ReentrantLock的实例

加解锁方法

方法名 说明
void lock() 加锁
void unlock() 解锁

※Lock 加锁解锁 要用 try{…}finally{…} 环绕,防止程序出错不能解锁

代码描述

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 SellTicket implements Runnable {
private int tickets = 100;
//利用 Lock 的实现类对象 创建 Lock:
private Lock lock = new ReentrantLock();

@Override
public void run() {
while (true) {

//Lock 加锁解锁 要用 try{...}finally{...} 环绕,防止程序出错不能解锁
try {
//加锁
lock.lock();

if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--;
}

} finally {

//解锁
lock.unlock();
}
}
}
}

生产者消费者模型

模型概述

是一个经典的多线程模型,提供两个线程和一个数据共享区。数据共享区的存在解耦了生产者和消费者关系。

此程序关键在于数据共享区提供的操作

名称 说明
生产者线程 生产数据
消费者线程 使用数据
共享数据区域 生产者产生的数据放入共享区域,不关心消费者使用
消费者从共享区域域获取数据,不关心数据生产

示意图:

img

等待与唤醒

使用注意

  • wite()notify()方法,必须在同步(锁)内部使用;
  • wite()之后必须要notify()唤醒方可继续执行;

方法
来自 Object类

方法名 说明
void wait() 当前线程等待,直到另一个线程调用该对象唤醒( notify()方法或notifyAll()方法)
void notify() 唤醒正在等待对象监视器的单个线程
void notifyAll() 唤醒正在等待对象监视器的所有线程

实例【生产者与消费者】

实例指南

生产者消费者案例中包含的类:

  1. 奶箱类(Box):定义一个成员变量,表示第x瓶奶,提供存储牛奶和获取牛奶的操作
  2. 生产者类(Producer):实现Runnable接口,重写run()方法,调用存储牛奶的操作
  3. 消费者类(Customer):实现Runnable接口,重写run()方法,调用获取牛奶的操作
  4. 测试类(BoxDemo):里面有main方法,main方法中的代码步骤如下
     A:创建奶箱对象,这是共享数据区域
     B:创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作
     C:创建消费者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用获取牛奶的操作
     D:创建2个线程对象,分别把生产者对象和消费者对象作为构造方法参数传递
     E:启动线程
    

代码实现

Box类(重点)

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
public class Box {
//定义一个成员变量,表示第x瓶奶,提供存储牛奶和获取牛奶的操作
private int milk;

//定义一个成员变量,表示Box的状态
private boolean state = false;

//提供存储牛奶和获取牛奶的操作
public synchronized void put(int milk){
//如果有奶,就等待消费
if(state){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

//如果没有牛奶,就生产牛奶
this.milk = milk;
System.out.println("送奶工送入第" + this.milk + "瓶奶");

//生产完毕之后,修改Box状态,唤醒其他等待的线程
state = true;
notifyAll();
}

public synchronized void get(){
//如果没奶,就等待生产
if(!state){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

//如果有奶,就消费牛奶
System.out.println("用户拿到第" + this.milk + "瓶奶");

//消费牛奶之后,就修改Box状态,唤醒其他等待的线程
state = false;
notifyAll();
}
}

Producer类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Producter implements Runnable{
private Box b;

public Producter(Box b) {
this.b = b;
}

@Override
public void run() {
for(int i = 1; i <= 5; i++){
b.put(i);
}
}
}

Customer类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Customer implements Runnable{

private Box b;

public Customer(Box b) {
this.b = b;
}

@Override
public void run() {
while(true) {
b.get();
}
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class BoxDemo {
public static void main(String[] args) {

//创建奶箱对象,这是共享数据区域
Box b = new Box();

//创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作
Producter producter = new Producter(b);

//创建消费者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用获取牛奶的操作
Customer customer = new Customer(b);

//创建2个线程对象,分别把生产者对象和消费者对象作为构造方法参数传递
Thread t1 = new Thread(producter);
Thread t2 = new Thread(customer);

//启动线程
t1.start();
t2.start();
}
}