Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
Java程序默认是多线程的, 程序启动后默认会存在两条线程:
- 主线程
- 垃圾回收线程
多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。
这里定义和线程相关的另一个术语 – 进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
创建线程
Java提供了三种创建线程的方法:
- 通过继承 Thread 类本身;
- 通过实现 Runnable 接口;
- 通过 Callable 和 Future 创建线程。
继承Thread类
继承Thread类创建线程的步骤如下:
- 编写一个类继承Thread
- 重写run方法
- 将线程任务代码写在run方法中
- 创建线程对象
- 调用start方法开启线程
但需要注意的是,调用start方法开启线程, 会自动的调用run方法执行。
另外,只有调用了start方法, 才是开启了新的线程,所有线程并发执行;如果只是调用run方法,仅仅是调用方法,不会并发执行,只会顺序执行。
示例:
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i <=200; i++){
System.out.println("线程任务执行了" + i);
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();
}
}
显然,两个线程是并发执行的。
实现 Runnable 接口
实现Runnable接口创建线程的步骤如下:
- 编写一个类实现Runnable接口
- 重写run方法
- 将线程任务代码写在run方法中
- 创建线程任务资源
- 创建线程对象, 将资源传入
- 使用线程对象调用start方法, 开启线程
public class ThreadDemo3 {
public static void main(String[] args) {
// 4. 创建线程任务资源
MyRunnable mr = new MyRunnable();
// 5. 创建线程对象, 将资源传入
Thread t = new Thread(mr);
// 6. 使用线程对象调用start方法, 开启线程
t.start();
for (int i = 1; i <= 2000; i++) {
System.out.println("main线程执行了");
}
}
}
// 1. 编写一个类实现Runnable接口
class MyRunnable implements Runnable {
// 2. 重写run方法
@Override
public void run() {
// 3. 将线程任务代码写在run方法中
for (int i = 1; i <= 200; i++) {
System.out.println("线程任务执行了" + i);
}
}
}
这里发现,不仅创建的线程 t 执行了,下方的for循环语句也执行了,这是因为Java程序默认是多线程的。
另外,线程任务资源 Runnable 可以传入线程对象 Thread,是因为 Thread底层源码中存在:
通过 Callable 和 Future 创建线程
通过 Callable 和 Future 创建线程的步骤如下:
- 编写一个类实现Callable接口
- 重写call方法
- 将线程任务代码写在call方法中
- 创建线程任务资源对象
- 创建线程任务对象, 封装线程资源
- 创建线程对象, 传入线程任务
- 使用线程对象调用start开启线程
public class ThreadDemo4 {
public static void main(String[] args) throws Exception {
// 创建线程任务资源对象
MyCallable mc = new MyCallable();
// 创建线程任务对象, 封装线程资源
FutureTask<Integer> task1 = new FutureTask<>(mc);
FutureTask<Integer> task2 = new FutureTask<>(mc);
// 创建线程对象, 传入线程任务
Thread t1 = new Thread(task1);
Thread t2 = new Thread(task2);
// 使用线程对象调用start开启线程
t1.start();
t2.start();
Integer result1 = task1.get();
Integer result2 = task2.get();
System.out.println("task1获取到的结果为:" + result1);
System.out.println("task2获取到的结果为:" + result2);
}
}
// 1. 编写一个类实现Callable接口
class MyCallable implements Callable<Integer> {
// 2. 重写call方法
@Override
public Integer call() throws Exception {
// 3. 将线程任务代码写在call方法中
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
}
}
这里来看一下线程任务对象 FutureTask 为什么可以传入线程对象 Thread 中,FutureTask 的底层源码为:
可以发现,FutureTask 实际上继承了 Runnable ,自然可以传入线程对象 Thread 中。
线程相关方法
此处仅讲解几个重点方法。
设置线程优先级
线程的调度方式分为抢占式调度(随机)和非抢占式调度(轮流使用)
设置线程优先级是抢占式调度,线程优先级分为1-10,默认是5,优先级数字越大,优先级越大
设置守护线程
在Java中,守护线程(Daemon Thread)是一种特殊类型的线程,它是为其他线程提供服务的后台线程。当所有非守护线程结束时,守护线程也会随之结束。
创建守护线程的方式与创建普通线程相同,只需要将线程的setDaemon(true)方法设置为true即可。守护线程通常用于执行后台任务,例如垃圾回收、自动保存或其他不需要与用户交互的任务。
public class ThreadMethodDemo3 {
/*
public final void setDaemon(boolean on) : 设置为守护线程
*/
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 1; i <= 20; i++){
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}, "线程A: ");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 1; i <= 200; i++){
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}, "线程B: ");
t2.setDaemon(true);
t1.start();
t2.start();
}
}
在上面的代码中,创建了两个线程 t1 和 t2 ,将 t2 设置为守护线程后,当 t1 线程结束后,t2 自动结束(不会瞬间结束)。
线程安全与同步
安全问题出现的条件:
- 多线程环境
- 有共享数据
- 有多条语句操作共享数据
同步技术:将多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程可以执行
- 同步代码块
- 同步方法
- Lock 锁
同步代码块
synchronized(锁对象) {
多条语句操作共享数据的代码
}
注意事项:
- 锁对象可以是任意对象,但是需要保证多条线程的锁对象,是同一把锁
- 同步可以解决多线程的数据安全问题,但是也会降低程序的运行效率
给出一个示例代码:
public class ThreadTest2 {
/*
同步代码块:
synchronized(锁对象) {
多条语句操作共享数据的代码
}
*/
public static void main(String[] args) {
// 这里实际上是3个锁对象
TicketTask2 t1 = new TicketTask2();
TicketTask2 t2 = new TicketTask2();
TicketTask2 t3 = new TicketTask2();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
// 继承Thread类
class TicketTask2 extends Thread {
// static关键字修饰的变量在静态方法中,属于类变量,所有对象共享,保证线程锁一致
private static int tickets = 2000;
private static Object o = new Object();
@Override
public void run() {
while (true) {
synchronized (o) {
if (tickets == 0) {
break;
}
// 作为Thread子类没有重写getName方法,所以可以省略super关键字
System.out.println(getName() + "卖出了第" + tickets + "号票");
tickets--;
}
}
}
}
线程继承类的代码也可写为:
class TicketTask2 extends Thread {
private static int tickets = 2000;
@Override
public void run() {
while (true) {
// 类的字节码对象唯一,非常适合作为锁对象
synchronized (TicketTask2.class) {
if (tickets == 0) {
break;
}
System.out.println(getName() + "卖出了第" + tickets + "号票");
tickets--;
}
}
}
}
同步方法
- 在方法的返回值类型前面加入 synchronized 关键字
public synchronized void method() {
多条语句操作共享数据的代码
}
注意事项:
- 方法分为静态和非静态
- 静态方法的锁对象是字节码对象,非静态方法的锁对象是 this
非静态同步方法
public class ThreadTest4 {
public static void main(String[] args) {
TicketTask4 task = new TicketTask4();
Thread t1 = new Thread(task, "窗口1");
Thread t2 = new Thread(task, "窗口2");
Thread t3 = new Thread(task, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
class TicketTask4 implements Runnable {
private int tickets = 100;
@Override
public void run() {
while (true) {
String name = Thread.currentThread().getName();
if ("窗口1".equals(name)){
if (method()) break;
} else if ("窗口2".equals(name)) {
// 非静态方法的锁对象是this
synchronized (this) {
if (tickets == 0) {
break;
}
System.out.println(name + "卖出了第" + tickets + "号票");
tickets--;
}
}
}
}
// 非静态的同步方法,内部锁对象是this
private synchronized boolean method() {
if (tickets == 0) {
return true;
}
System.out.println(Thread.currentThread().getName() + "卖出了第" + tickets + "号票");
tickets--;
return false;
}
}
静态同步方法
public class ThreadTest5 {
public static void main(String[] args) {
TicketTask5 task = new TicketTask5();
Thread t1 = new Thread(task, "窗口1");
Thread t2 = new Thread(task, "窗口2");
t1.start();
t2.start();
}
}
class TicketTask5 implements Runnable {
private static int tickets = 100;
@Override
public void run() {
while (true) {
String name = Thread.currentThread().getName();
if ("窗口1".equals(name)) {
if (method()) {
break;
}
} else if ("窗口2".equals(name)) {
synchronized (TicketTask5.class) {
if (tickets == 0) {
break;
}
System.out.println(Thread.currentThread().getName() + "卖出了第" + tickets + "号票");
tickets--;
}
}
}
}
// 静态同步方法 内部的锁是类的字节码对象
private static synchronized boolean method() {
if (tickets == 0) {
return true;
}
System.out.println(Thread.currentThread().getName() + "卖出了第" + tickets + "号票");
tickets--;
return false;
}
}
Lock锁
使用 Lock 锁,我们可以更清晰的看到哪里加了锁,哪里释放了锁
lock.lock();
...
lock.unlock();
Lock 是接口,无法直接创建创造对象
示例代码:
public class ThreadTest6 {
/*
互斥锁 ReentrantLock()
*/
public static void main(String[] args) {
TicketTask6 task = new TicketTask6();
Thread t1 = new Thread(task, "窗口1");
Thread t2 = new Thread(task, "窗口2");
t1.start();
t2.start();
}
}
class TicketTask6 implements Runnable {
private int tickets = 100;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 使用try/catch保证出现异常也能使锁结束
try {
lock.lock();
if (tickets == 0) {
break;
}
System.out.println(Thread.currentThread().getName() + "卖出了第" + tickets + "号票");
tickets--;
} finally {
lock.unlock();
}
}
}
}
线程通信
线程通信能确保线程能够按照预定的顺序执行,并且能够安全地访问共享资源。
比如两个线程抢占资源,可能出现一个线程一直抢占资源,而另一个线程分配不到资源的情况。
等待唤醒机制
需要注意的是,这些方法需要使用锁对象调用。另外,wait() 方法在等待的使用会释放锁对象。
给出一个双线程通信的示例代码
public class CorrespondenceDemo1 {
/*
两条线程通信
*/
public static void main(String[] args) {
Printer1 p = new Printer1();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (Printer1.class) {
try {
p.print1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (Printer1.class) {
try {
p.print2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}).start();
}
}
class Printer1 {
int flag = 1;
public void print1() throws InterruptedException {
if(flag != 1){
Printer1.class.wait();
}
System.out.print("传");
System.out.print("智");
System.out.print("教");
System.out.print("育");
System.out.println();
flag = 2;
Printer1.class.notify();
}
public void print2() throws InterruptedException {
if(flag != 2){
Printer1.class.wait();
}
System.out.print("黑");
System.out.print("马");
System.out.print("程");
System.out.print("序");
System.out.print("员");
System.out.println();
flag = 1;
Printer1.class.notify();
}
}
双线程通信的代码中,锁对象使用了Printer1的字节码对象(字节码对象唯一,非常适合作为锁对象),同时使用 flag 信号量判断线程的等待与唤醒状态。
notifyAll 方法会唤醒所有等待的线程,对于多线程通信非常有效。
给出一个三线程通信的示例代码:
public class CorrespondenceDemo2 {
/*
三条线程通信
问题: sleep方法和wait方法的区别?
回答:
sleep方法是线程休眠, 时间到了自动醒来, sleep方法在休眠的时候, 不会释放锁.
wait方法是线程等待, 需要由其它线程进行notify唤醒, wait方法在等待期间, 会释放锁.
*/
public static void main(String[] args) {
Printer2 p = new Printer2();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (Printer2.class) {
try {
p.print1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (Printer2.class) {
try {
p.print2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (Printer2.class) {
try {
p.print3();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}).start();
}
}
class Printer2 {
int flag = 1;
public void print1() throws InterruptedException {
while (flag != 1) {
// 线程1等待
Printer2.class.wait();
}
System.out.print("传");
System.out.print("智");
System.out.print("教");
System.out.print("育");
System.out.println();
flag = 2;
Printer2.class.notifyAll();
}
public void print2() throws InterruptedException {
while (flag != 2) {
// 线程2等待
Printer2.class.wait();
}
System.out.print("黑");
System.out.print("马");
System.out.print("程");
System.out.print("序");
System.out.print("员");
System.out.println();
flag = 3;
Printer2.class.notifyAll();
}
public void print3() throws InterruptedException {
while (flag != 3) {
Printer2.class.wait();
}
System.out.print("传");
System.out.print("智");
System.out.print("大");
System.out.print("学");
System.out.println();
flag = 1;
Printer2.class.notifyAll();
}
}
三线程通信的代码对比双线程通信,在信号量判断线程状态时将 if 改为了 while ,确保不会出现线程死锁。
也可以使用使用 ReentrantLock 实现同步,并获取 Condition 对象
在三线程通信的代码上使用 Lock 锁进行优化:
public class CorrespondenceDemo3 {
/*
三条线程通信 - 优化
*/
public static void main(String[] args) {
Printer3 p = new Printer3();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
p.print1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
p.print2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
p.print3();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
class Printer3 {
ReentrantLock lock = new ReentrantLock();
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();
Condition c3 = lock.newCondition();
int flag = 1;
public void print1() throws InterruptedException {
lock.lock();
if (flag != 1) {
// 线程1等待, c1绑定线程1
c1.await();
}
System.out.print("传");
System.out.print("智");
System.out.print("教");
System.out.print("育");
System.out.println();
flag = 2;
// 若未绑定,则随机唤醒线程
c2.signal();
lock.unlock();
}
public void print2() throws InterruptedException {
lock.lock();
if (flag != 2) {
// 线程2等待, c2绑定线程2
c2.await();
}
System.out.print("黑");
System.out.print("马");
System.out.print("程");
System.out.print("序");
System.out.print("员");
System.out.println();
flag = 3;
c3.signal();
lock.unlock();
}
public void print3() throws InterruptedException {
lock.lock();
if (flag != 3) {
c3.await();
}
System.out.print("传");
System.out.print("智");
System.out.print("大");
System.out.print("学");
System.out.println();
flag = 1;
c1.signal();
lock.unlock();
}
}
线程生命周期
线程启动后的各种状态构成了线程的生命周期,线程被创建并启动以后,它并不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期有不同的状态。
Java 中的线程状态被定义在了 java.lang.Thread.State 枚举类中。
线程池
系统创建一个线程的成本是比较高的,因为它涉及到与操作系统交互,当程序中需要创建大量生存期很短暂的线程时,频繁的创建和销毁线程,就会严重浪费系统资源,线程池则应运而生。
将线程对象交给线程池维护,可以降低系统成本,从而提升程序的性能。
自定义线程池
使用 ThreadPoolExecutor 类,其构造方法:
关于拒绝策略,最常用的是 AbortPolicy ,具体如下: