Java 学习笔记(12)——多线程

Java内部提供了针对多线程的支持,线程是CPU执行的最小单位,在多核CPU中使用多线程,能够做到多个任务并行执行,提高效率。

使用多线程的方法

  1. 创建Thread类的子类,并重写run方法,在需要启动线程的时候调用类的start() 方法,每一个子类对象只能调用一次start()方法,如果需要启动多个线程执行同一个任务就需要创建多个线程对象
  2. 实现Runnable 接口并重写run 方法,传入这个接口的实现类构造一个Thread类,然后调用Thread类的start方法
  3. 实现Callable 接口并重写call方法,然后使用Future 来包装 Callable 对象,使用 Future 对象构造一个Thread对象,并调用Thread 类的start方法启动线程

平时在使用上第一个方式用的很少,一般根据情况使用第2中或者第3中,与第一种方式相比,它们具有的优势如下:

  1. 降低了程序的耦合性,它将设置线程任务和开启线程进行了分离
  2. 避免了单继承的局限性,一旦继承了Thread 类,那么他就不能继承其他类。使用重写接口的方式可以再继承一个别的类

第3中方式相比第二种方式来说,它提供了一个获取线程返回值的方式。我们在call函数中返回值,通过 FutureTask 对象的get方法来获取返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ThreadTask extends Thread{
@Override
public void run(){
System.out.println("当前线程:" + getName() + "正在运行");
}
}

public class ThreadDemo{
public static void main(String[] args){
new ThreadTask().start();
new ThreadTask().start();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ThreadDemo{
public static void main(String[] args){
//使用匿名内部类的方式
Runnable thread1 = new Runnable(){
@Override
public void run(){
System.out.println("当前线程:" + Thread.currentThread().getName() + "正在运行");
}
}

new Thread(thread1).start();
new Thread(thread1).start();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ThreadDemo implements Callable<Integer>{
public static void main(String[] args){
FutureTask<Integer> ft = new new FutureTask<>(new ThreadDemo());
new Thread(ft).start();
System.out.println("线程返回值:" + ft.get());
}

@Override
public Integer call() throws Exception
{
System.out.println("当前线程:" + Thread.currentThread().getName() + "正在运行");
return 1;
}
}

thread 状态

在操作系统原理中讲到,线程有这么几种状态:新建、运行、阻塞、结束。而Java中将线程状态进行了进一步的细分,根据阻塞原因将阻塞状态又分为:等待(调用等待函数主动让出CPU执行权), 阻塞(线程的时间片到达,操作系统进行线程切换)
它们之间的状态如下:
线程状态图

等待唤醒

入上图所示,可以使用wait/sleep方法让线程处于等待状态。在另一个线程中使用wait线程对象的notify方法可以唤醒wait线程。

wait/notify 方法定义于 Object 方法,也就是说所以的对象都可以有wait/notify 方法。

1
2
3
4
void wait()  ;调用该函数,使线程无限等待,直到有另外的线程将其唤醒
void wait(long timeout);调用该函数,使线程进行等待,直到另外有线程将其唤醒或者等待时间已过
void notify(); 唤醒正在等待对象监视器的单个线程
void notifyAll(); 唤醒正在等待对象监视器的所有线程。

上面说过这些方法都是在 Object 类中实现的,也就是说所有的对象都可以调用。上面的等待监视器就是随意一个调用了wait 的对象。这个对象会阻塞它所在的线程。

线程同步

我们知道在访问多个线程共享的资源时可能会发生数据的安全性问题。因此这个时候需要做线程的同步

Java中同步的方法有: 同步代码块、同步方法和Lock锁的机制

同步代码块

同步代码块是使用synchronized来修饰需要进行同步的代码块

同步代码块需要提供一个锁对象,当一个线程执行到这个代码块时,该线程获得锁对象。当另外的线程也执行到同一个锁对象的同步代码块时,由于无法获取到锁对象因此会陷入等待。直到获得锁对象的线程执行完同步代码块,并释放锁。这里获取、释放锁由Java虚拟机自己完成。

例如

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
public static void synchronizedCode(){
Runnable thread = new Runnable(){
private int ticket = 100;
@Override
public void run(){
synchronized (this){
while(ticket > 0){
//这里休眠10s,用来表示提交订单后的付款等操作
try{
Thread.sleep(10);
}catch(Exception e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket);
ticket--;
}

System.out.println(Thread.currentThread().getName() + "票已售罄");
}
}
};

new Thread(thread).start();
new Thread(thread).start();
new Thread(thread).start();
}

同步方法

同步方法是使用 synchronized 关键字修饰的方法。它与同步代码块的原理相同,保证了多个线程只有一个处于同步代码块中。同步方法中也有一个锁对象,这个锁对象是this这个对象,静态方法的锁对象是本类的class文件对象。

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
public static void synchronizedMethod(){
Runnable thread = new Runnable(){
private int ticket = 100;
@Override
public void run(){
payTicket();
}

public synchronized void payTicket(){
while(ticket > 0){
//这里休眠10s,用来表示提交订单后的付款等操作
try{
Thread.sleep(10);
}catch(Exception e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket);
ticket--;
}

System.out.println(Thread.currentThread().getName() + "票已售罄");
}
};

new Thread(thread).start();
new Thread(thread).start();
new Thread(thread).start();
}

Lock 锁机制

除了上述方法,可以使用lock锁来进行同步,在执行代码前先调用 lock方法获得锁,执行完成之后使用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
public static void lockMethod(){
Runnable thread = new Runnable(){
private int ticket = 100;
private Lock lock = new ReentrantLock();
@Override
public void run(){
try{
lock.lock();
while(ticket > 0){
//这里休眠10s,用来表示提交订单后的付款等操作
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket);
ticket--;
}
}catch(Exception e){
e.printStackTrace();
}finally{
lock.unlock();
}

System.out.println(Thread.currentThread().getName() + "票已售罄");
}
};

new Thread(thread).start();
new Thread(thread).start();
new Thread(thread).start();
}