1. 相关概念
-
程序:指令的集合,是一段静态的代码。
-
进程:正在运行的程序,是一个动态的过程;有自身的生命周期,是资源分配的单位(系统为每个进程分配不同的内存区域)。
-
线程:进程的进一步细化,是程序内部的一条执行路径。
2. 线程的创建
2.1 继承Thread
1、声明一个类继承Thread类,并重写run()方法
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
}
}
2、创建该类的实例对象并调用start()方法
public class Test {
public static void main(String[] args) {
MyThread t1 = new MyThread();
// 如果调用run()则只是普通的方法调用,不会创建新线程
t1.start();
}
}
2.2 实现Runnable
1、声明一个类实现Runnable接口,并重写run()方法
public class MyThread2 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
}
}
2、创建该类的实例对象,并把该对象作为参数传入Thread的构造方法中,再调用Thread对象的start()方法
public class Test2 {
public static void main(String[] args) {
MyThread2 t2 = new MyThread2();
new Thread(t2).start();
}
}
2.3 实现Callable
1、声明一个类实现Callable接口,并重写call()方法
public class MyThread3 implements Callable {
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
sum += i;
}
}
return sum;
}
}
2、创建该类的实例对象,把它作为参数传入FutureTask的构造器中,再把FutureTask对象传入Thread的构造器中,调用Thread对象的start()。如果需要获取call()的返回值,则调用FutureTask对象的get()方法
public class Test3 {
public static void main(String[] args) {
MyThread3 t = new MyThread3();
FutureTask ft = new FutureTask(t);
new Thread(ft).start();
try {
// get()的返回值是FutureTask构造器中,参数Callable实例对象的call()的返回值
Object sum = ft.get();
System.out.println(sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
2.4 使用线程池
1、声明一个类,实现Runnable或Callable接口,如上面的MyThread2或MyThread3
2、创建ExecutorService对象,并调用它的execute()方法或submit()方法
public class Test4 {
public static void main(String[] args) {
// 创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);
// 设置线程池的各种属性
// ThreadPoolExecutor newService = (ThreadPoolExecutor) service;
// newService.setXXX();
service.execute(new MyThread2()); // 适合实现Runnable的对象
// service.submit(new MyThread3()); 适合实现Callable的对象
// 关闭线程池
service.shutdown();
}
}
3. Thread的常用方法
| 方法 | 作用 |
|---|---|
| void start() | 启动线程,调用run()方法 |
| void run() | 如果该线程是使用独立的Runnable运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回 |
| Thread currentThread() | 获取当前正在执行的线程 |
| String getName() | 获取当前线程的名字 |
| void setName(String name) | 设置当前线程的名字 |
| void yield() | 暂停当前正在执行的线程对象,并执行其他线程 |
| void join() | 等待当前线程执行完成。比如:在线程a中调用线程b的join()方法,那么a就会进入阻塞状态,直到线程b执行完,a才继续执行 |
| void stop() | (已过时)强制结束当前线程 |
| void sleep(long millis) | 让线程休眠millis毫秒 |
| boolean isAlive() | 判断线程是否处于活动状态 |
4. 线程优先级
4.1 三个基本优先级
1.MAX_PRIORITY:10
2.NORM_PRIORITY:5
3.MIN_PRIORITY:1
高优先级线程会抢占低优先级线程的CPU执行权,但只是概率上的。并不代表只有当高优先级的线程执行完后,低优先级线程才执行。
4.2 相关方法
| 方法 | 作用 |
|---|---|
| int getPriority() | 获取线程的优先级 |
| void setPriority(int newPriority) | 更改线程优先级 |
5. 线程的生命周期
线程的生命周期大致如下图所示:
6. 线程同步
6.1 线程安全问题
当多个线程操作某个共享资源时,就会出现线程不安全问题。例子如下。
6.2 线程不安全例子
模仿售票站卖票的情况。
1、创建一个售票站,共10张票
public class Station implements Runnable {
private int ticket = 10;
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + " : " + ticket);
ticket--;
} else {
break;
}
}
}
}
2、创建三个窗口卖票,这10张票就是共享资源
public class Test {
public static void main(String[] args) {
Station s = new Station();
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
Thread t3 = new Thread(s);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
3、分析结果
输出结果如下所示
窗口1 : 10
窗口2 : 10
窗口1 : 9
窗口2 : 8
窗口1 : 7
窗口2 : 6
窗口1 : 5
窗口1 : 3
窗口1 : 2
窗口1 : 1
窗口3 : 4
窗口2 : 4
虽然每次运行结果不同,但是大概率会出现多个窗口出售同一张票的情况,这显然是不合理的,是线程不安全的。
6.3 解决线程不安全
6.3.1 同步代码块
1、synchronized代码块的编写
synchronized (mutex) {
// 操作共享资源的代码
}
// mutex:同步监视器(锁),任何对象都可以充当同步监视器,但这多个线程必须共享同一个监视器。
2、修改上述例子的Station类
public class Station implements Runnable{
private int ticket = 10;
Object mutex = new Object();
@Override
public void run() {
while (true) {
synchronized (mutex) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + " : " + ticket);
ticket--;
} else {
break;
}
}
}
}
}
3、修改后的运行结果如下
窗口2 : 10
窗口2 : 9
窗口2 : 8
窗口2 : 7
窗口2 : 6
窗口2 : 5
窗口2 : 4
窗口3 : 3
窗口3 : 2
窗口3 : 1
无论运行多少次,都不会出现多个窗口出售同一张票的情况。
6.3.2 同步方法
1、把操作共享资源的代码封装到一个方法中,再用synchronized修饰这个方法
public class Station implements Runnable{
private int ticket = 10;
@Override
public void run() {
while (true) {
boolean flag = sell();
if (!flag) {
break;
}
}
}
private synchronized boolean sell() { // 此时同步监视器是this关键字
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + " : " + ticket);
ticket--;
return true;
}
return false;
}
}
2、修改后的运行结果也是合理安全的
3、注意事项
- 非静态同步方法的同步监视器是this
- 静态同步方法的同步监视器是所属类本身,即XXX.class
6.3.3 Lock锁
使用Lock接口的实现类ReentrantLock也可以解决线程不安全问题。代码如下
public class Station implements Runnable{
private int ticket = 10;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
// 上锁
lock.lock();
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + " : " + ticket);
ticket--;
} else {
break;
}
} finally {
// 解锁
lock.unlock();
}
}
}
}
(注:使用Lock锁需要手动上锁和解锁)
7. 线程通信
本文的线程通信可以理解为线程之间有规律地访问共享资源。在上述的线程安全例子中,各线程总是随机获取CPU执行权,导致每次运行结果不一。那么我们可以通过几个方法来让线程之间形成某种规律。比如,有这样的需求:让两个线程轮流输出1至100,于是可以这么做:
7.1 编写Number类
public class Number implements Runnable {
private int num = 1;
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
obj.notify(); // 唤醒进入了wait()的线程,若有多个,则按优先级来
if (num <= 100) {
System.out.println(Thread.currentThread().getName() + " : " + num);
num++;
try {
obj.wait(); // 当前线程进入阻塞状态,直到其他线程调用监视器的notify()方法
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
7.2 编写测试类
public class Test02 {
public static void main(String[] args) {
Number n = new Number();
Thread t1 = new Thread(n);
Thread t2 = new Thread(n);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
7.3 输出结果
线程1 : 1
线程2 : 2
线程1 : 3
线程2 : 4
线程1 : 5
线程2 : 6
线程1 : 7
线程2 : 8
线程1 : 9
线程2 : 10
......
7.4 相关方法说明
| 方法 | 作用 |
|---|---|
| void wait() | 当前线程进入阻塞状态,直到其他线程调用监视器的notify()方法 |
| void notify() | 唤醒进入wait()的线程,若有多个,则按优先级来 |
| void notifyAll() | 唤醒全部进入wait()的线程 |
注意事项
- 这三个方法要使用在同步代码块或同步方法中
- 这三个方法的调用者必须是同步监视器。在上述例子中,obj对象是监视器,所以由它来调用
- 由于任何对象都可以是同步监视器,且这三个方法的调用者得是监视器,所以这三个方法其实是在Object类中声明的
7.5 sleep()和wait()的比较
相同点:
- 都可以使当前线程进入阻塞状态
不同点:
- 声明的位置不同,sleep()是声明在Thread类中,而wait()是声明在Object类中
- 调用的场景不同,sleep()可以在任何场景下用,而wait()要用在同步代码块或同步方法中
- sleep()不会释放同步监视器(锁),而wait()会释放同步监视器(锁)
8. 生产者/消费者问题
需求如下:某产品最多为20个,当产品个数大于0时,消费者才可以消费;当产品个数小于20时,生产者才可以生产。
1、编写产品类
public class Product {
private int num = 0;
public synchronized void addProduct() {
if (num < 20) {
System.out.println(Thread.currentThread().getName() + "生产产品");
num++;
System.out.println("现有产品" + num + "个");
System.out.println("-----------------------------------");
notify();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void delProduct() {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "消费产品");
num--;
System.out.println("现有产品" + num + "个");
System.out.println("-----------------------------------");
notify();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2、编写生产者类
public class Producer extends Thread {
private Product product;
@Override
public void run() {
while (true) {
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
product.addProduct();
}
}
public Producer(Product product) {
super();
this.product = product;
}
}
3、编写消费者类
public class Consumer extends Thread {
private Product product;
@Override
public void run() {
while (true) {
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
product.delProduct();
}
}
public Consumer(Product product) {
super();
this.product = product;
}
}
4、编写测试类
public class Test {
public static void main(String[] args) {
Product p = new Product();
Producer producer = new Producer(p);
Consumer consumer = new Consumer(p);
producer.setName("生产者");
consumer.setName("消费者");
producer.start();
consumer.start();
}
}
5、运行结果如下
生产者生产产品
现有产品1个
-----------------------------------
消费者消费产品
现有产品0个
-----------------------------------
生产者生产产品
现有产品1个
-----------------------------------
消费者消费产品
现有产品0个
-----------------------------------
......