1.线程简介
任务、进程、线程、多线程
程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。而进程是执行程序的一次执行过程,它是一个动态的概念,是系统资源分配的单位。
通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程就是独立的执行路径
线程是CPU调度和执行的单位。
2.线程创建
四种方式:继承Thread类、实现Runnable接口、实现Callable接口、使用线程池
(1)Thead类
步骤:
- 自定义线程类继承Thread类
- 重写run()方法,编写线程执行体
- 创建线程对象,调用start()方法启动线程
public class MyThread extends Thread {
@Override
public void run() {
// run方法线程体
for (int i = 0; i < 20; i++) {
System.out.println("run线程:" + i);
}
}
public static void main(String[] args) {
// 创建一个线程对象
MyThread thread = new MyThread();
// 调用start()方法开启线程
thread.start();
// 主线程
for (int i = 0; i < 20; i++) {
System.out.println("主线程:" + i);
}
}
}
线程不一定立即执行,CPU安排调度!
多线程下载网络图片
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
/**
* @author Jeremy Li
* @data 2020/12/26 - 0:03
* 练习Thread,实现多线程同步下载图片
*/
public class TestThread extends Thread{
private String url; // 网络图片地址
private String name; // 保存的文件名
public TestThread(){}
public TestThread(String url, String name){
this.url = url;
this.name = name;
}
// 线程执行体
@Override
public void run() {
WebDownLoader downLoader = new WebDownLoader();
downLoader.downLoader(url, name);
System.out.println("下载了文件名为:" + name);
}
public static void main(String[] args) {
TestThread t1 = new TestThread("https://bkimg.cdn.bcebos.com/pic/77c6a7efce1b9d16ff80d1b1fddeb48f8d54649e?x-bce-process=image/watermark,image_d2F0ZXIvYmFpa2UxMTY=,g_7,xp_5,yp_5", "1.jpg");
TestThread t2 = new TestThread("https://bkimg.cdn.bcebos.com/pic/d52a2834349b033bfe89646316ce36d3d539bd02?x-bce-process=image/watermark,image_d2F0ZXIvYmFpa2U4MA==,g_7,xp_5,yp_5", "2.jpg");
TestThread t3 = new TestThread("https://bkimg.cdn.bcebos.com/pic/71cf3bc79f3df8dc8037cb0dcd11728b47102827?x-bce-process=image/watermark,image_d2F0ZXIvYmFpa2U4MA==,g_7,xp_5,yp_5", "3.jpg");
t1.start();
t2.start();
t3.start();
}
}
// 下载器
class WebDownLoader{
// 下载方法
public void downLoader(String url, String name){
try {
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,downLoader方法出现异常");
}
}
}
运行结果:
(2)实现Runnable接口
推荐使用Runnable接口,因为Java单继承的局限性
步骤:
- 定义某个类实现Runnable接口
- 实现run()方法,编写线程执行体
- 创建线程对象,调用start()方法启动线程
public class TestRunnable implements Runnable {
@Override
public void run() {
// run方法线程体
for (int i = 0; i < 20; i++) {
System.out.println("run线程:" + i);
}
}
public static void main(String[] args) {
// 创建一个实现runnable接口的实现类对象
TestRunnable runnable = new TestRunnable();
// 通过Thread(runnable)调用start()方法开启线程
new Thread(runnable).start();
// 主线程
for (int i = 0; i < 20; i++) {
System.out.println("主线程:" + i);
}
}
}
总结:
- 继承Thread类:由于OOP单继承局限性
- 实现Runnable接口:避免了单继承性,灵活方便,方便同一个对象被多个线程使用
- 这里用到了代理模式(静态代理),Thread类实现了也Runnable接口,自定义类也实现Runnable接口,可以通过代理对象(Thread)来调用run方法
线程不安全:
/*
存在问题:线程冲突,拿到同一张票、出现负数
*/
public class TestThread1 implements Runnable{
private int ticketNums = 10;
@Override
public void run() {
while (true) {
if (ticketNums <= 0) {
break;
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--> 拿到了第" + ticketNums-- + "票");
}
}
public static void main(String[] args) {
TestThread1 ticket = new TestThread1();
new Thread(ticket, "小明").start();
new Thread(ticket, "小花").start();
new Thread(ticket, "小雨").start();
}
}
(3)实现Callable接口
步骤:
- 实现Callable接口,需要返回值类型
- 重写call方法,需要抛出异常
- 创建目标对象
- 创建执行服务:ExecutorService service = Executors.newFixedThreadPool(1);
- 提交执行:Future<Boolean> result = service.submit(t1);
- 获取结果:boolean r1 = result.get();
- 关闭服务:service.shutdownNow()
public class TestCallable implements Callable<Boolean> {
private String url; // 网络图片地址
private String name; // 保存的文件名
public TestCallable(){}
public TestCallable(String url, String name){
this.url = url;
this.name = name;
}
// 线程执行体
@Override
public Boolean call() {
WebDownLoader downLoader = new WebDownLoader();
downLoader.downLoader(url, name);
System.out.println("下载了文件名为:" + name);
return true;
}
public static void main(String[] args) throws Exception{
TestCallable t1 = new TestCallable("https://bkimg.cdn.bcebos.com/pic/77c6a7efce1b9d16ff80d1b1fddeb48f8d54649e?x-bce-process=image/watermark,image_d2F0ZXIvYmFpa2UxMTY=,g_7,xp_5,yp_5", "1.jpg");
TestCallable t2 = new TestCallable("https://bkimg.cdn.bcebos.com/pic/d52a2834349b033bfe89646316ce36d3d539bd02?x-bce-process=image/watermark,image_d2F0ZXIvYmFpa2U4MA==,g_7,xp_5,yp_5", "2.jpg");
TestCallable t3 = new TestCallable("https://bkimg.cdn.bcebos.com/pic/71cf3bc79f3df8dc8037cb0dcd11728b47102827?x-bce-process=image/watermark,image_d2F0ZXIvYmFpa2U4MA==,g_7,xp_5,yp_5", "3.jpg");
// 创建执行服务
ExecutorService service = Executors.newFixedThreadPool(3);
// 提交执行
Future<Boolean> result1 = service.submit(t1);
Future<Boolean> result2 = service.submit(t2);
Future<Boolean> result3 = service.submit(t3);
// 获取结果
Boolean r1 = result1.get();
Boolean r2 = result1.get();
Boolean r3 = result1.get();
// 关闭服务
service.shutdownNow();
// 另一种方式启动
// FutureTask<Boolean> task1 = new FutureTask<Boolean>(t1);
// FutureTask<Boolean> task2 = new FutureTask<Boolean>(t2);
// FutureTask<Boolean> task3 = new FutureTask<Boolean>(t3);
//
// new Thread(task1).start();
// new Thread(task2).start();
// new Thread(task3).start();
//
// Boolean r1 = task1.get();
// Boolean r2 = task2.get();
// Boolean r3 = task3.get();
}
}
好处:
- 可以定义返回值
- 可以抛出异常
- 支持泛型
(4)使用线程池
提前创建好多个线程,放入线程池内,使用时直接获取,使用完放入池中。可以避免频繁的创建、销毁,实现重复利用
找到ExecutorService接口的实现类ThreadPoolExecutor,设置相关的属性
执行线程池:
-
service.submint(Callable callable);
-
service.execute(Runnable runnable);
获取返回值:
- service.get()
关闭线程池:
- service.shotdown();
好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间会终止
3.线程状态
(1)线程的生命周期
五大状态:新生状态、就绪状态、运行状态、阻塞状态、死亡状态
注意点:
- 线程一旦死亡,就不能再次启动
- wait()可以释放锁
(2)线程方法
停止线程
- 不建议使用JDK提供的stop()、destroy()方法
- 推荐线程自己停止下来
- 建议使用一个标志位进行终止变量,当flag=false,则终止线程运行。
线程休眠
- sleep存在异常InterruptedException
- sleep时间达到后线程进入就绪状态
- 每个对象都有一个锁,sleep不会释放锁
线程礼让
- yield
- 让当前正在执行的线程暂停,但不阻塞
- 将线程从运行状态转为就绪状态
Join
- join合并线程,等待此线程执行完成后,再执行其他线程,其他线程阻塞
守护线程
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕
public class DaemonTest {
public static void main(String[] args) {
God god = new God();
You you = new You();
Thread thread = new Thread(god);
thread.setDaemon(true); // 默认false表示用户线程,正常的线程都是用户线程
thread.start(); // 上帝线程启动
new Thread(you).start(); // 你
}
}
class God implements Runnable{
@Override
public void run() {
while (true) {
System.out.println("上帝保佑着你");
}
}
}
class You implements Runnable{
@Override
public void run() {
for (int i = 0; i < 30000; i++) {
System.out.println("开心着活着第 " + i + " 天");
}
System.out.println("===goodbye world!===");
}
}
4.线程同步
(1)synchronized
线程的安全问题:
- 多个线程执行的不确定性引起执行结果的不稳定
- 多个线程对某个资源的共享,会造成操作的不完整性,会破坏数据
解决方式:
- 方式一:同步代码块
- synchronized:要求多个线程必须共用同一把锁;其次要将操作共享数据的代码包含在内
- 在实现Runnable接口创建多线程的方式中,我们可以考虑使用this作为同步监视器,因为实现Runnable接口的实现类只有一个
- 在继承Thread类创建多线程的方式中,由于可能会创建多个对象,所以不能使用this充当同步监视器,考虑使用当前类充当同步监视器
- synchronized:要求多个线程必须共用同一把锁;其次要将操作共享数据的代码包含在内
- 方式二:同步方法
- synchronized
- 同步方法涉及到同步监视器,不需要我们显式声明
- 非静态的同步方法:锁是this
- 静态的同步方法:锁是当前类本身
- synchronized
- 方式三:Lock锁 ----JDK5.0新增
- 通过Lock接口的实现类ReentrantLock,显式加锁、释放锁
注意点:
- 通过继承Thread类创建多线程时,需要创建多个对象,则共享资源、同步方法及Lock的实现类都要设置为static,保证多个资源共享同一份资源和同一个同步监视器
- 通过实现Runnable接口创建多线程时,由于只需要创建一个对象,放到多个Thead中,所以共享资源、同步方法及Lock的实现类不需要要设置为static。
单例模式中的线程安全:
// 饿汉式
class Bank{
// 私有化属性
private static final Bank bank = new Bank();
private Bank(){
}
public static Bank getBank(){
return bank;
}
}
// 懒汉式(线程安全)
class Bank2{
private static Bank2 bank = null;
private Bank2(){};
public static Bank2 getBank() {
if (bank == null) {
synchronized (Bank2.class) {
if (bank == null) {
bank = new Bank2();
}
}
}
return bank;
}
}
- 饿汉式中bank用final来修饰,初始化后不能改变
(2)死锁
- 不同线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,形成了线程的死锁。
- 死锁出现后,不会报异常,也不会提示,只是所有线程都处于阻塞状态,无法继续
package com.atlige.deadlockR;
class A {
// 锁是this,A对象实例a
public synchronized void foo(B b) {
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了A实例的foo方法"); // ①
try {
Thread.sleep(200);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用B实例的last方法"); // ③
b.last();
}
public synchronized void last() {
System.out.println("进入了A类的last方法内部");
}
}
class B {
// 锁是b
public synchronized void bar(A a) {
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了B实例的bar方法"); // ②
try {
Thread.sleep(200);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用A实例的last方法"); // ④
a.last();
}
public synchronized void last() {
System.out.println("进入了B类的last方法内部");
}
}
public class DeadLock implements Runnable {
A a = new A();
B b = new B();
public void init() {
Thread.currentThread().setName("主线程");
// 调用a对象的foo方法
a.foo(b);
System.out.println("进入了主线程之后");
}
public void run() {
Thread.currentThread().setName("副线程");
// 调用b对象的bar方法
b.bar(a);
System.out.println("进入了副线程之后");
}
public static void main(String[] args) {
DeadLock dl = new DeadLock();
new Thread(dl).start(); // 新的线程
dl.init(); // 主线程
}
}
5.线程通信
wait和await方法执行后,当前线程就进入了阻塞状态,并释放同步监视器
notify和signal方法执行后,会唤醒一个阻塞的线程。
(1)synchronized
搭配wait()和notify()实现现在阻塞和唤醒
public class printNum2 implements Runnable{
private int num;
@Override
public void run() {
while (true) {
synchronized (this) {
notify();
if (num <= 100) {
System.out.println(Thread.currentThread().getName() + ": " + num);
num++;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
注意点:
- wait()和notify()必须使用在同步代码块或同步方法中,并且方法的调用者必须为同步代码块或同步方法的同监视器,不然会报错 java.lang.IllegalMonitorStateException
(2)Lock锁
搭配await和signal使用,需要一个condition对象
class printNum implements Runnable{
private int num;
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
@Override
public void run() {
while (true) {
try {
lock.lock();
condition.signal();
if (num <= 100) {
System.out.println(Thread.currentThread().getName() + ": " + num);
num++;
condition.await();
} else {
break;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
(3)生产者消费者问题
生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。生产者生成一定量的数据放到缓冲区中,然后重复此过程;与此同时,消费者也在缓冲区消耗这些数据。生产者和消费者之间必须保持同步,要保证生产者不会在缓冲区满时放入数据,消费者也不会在缓冲区空时消耗数据。不够完善的解决方法容易出现死锁的情况,此时进程都在等待唤醒。
当缓冲区已满时,生产者线程执行wait(),使自己处于阻塞的装填,并释放锁,让其他线程执行;
当缓冲区已空时,消费者线程执行wait(),使自己处于阻塞的装填,并释放锁,让其他线程执行。
当生产者向缓冲区放入一个产品后,执行notifyAll(),唤醒其他阻塞的线程,直到执行完所有代码后,释放锁。
当消费者从缓冲区取出一个产品后,执行notifyAll(),唤醒其他阻塞的线程,直到执行完所有代码后,释放锁。
package com.atlige.exer;
import sun.dc.pr.PRError;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class ProduceTest2 {
public static void main(String[] args) {
Storage storage = new Storage();
Producer2 producer2 = new Producer2(storage);
Consumer2 consumer2 = new Consumer2(storage);
new Thread(producer2, "p1").start();
new Thread(producer2, "p2").start();
new Thread(consumer2, "c1").start();
new Thread(consumer2, "c2").start();
}
}
class Storage{
private static final int MAX_SIZE = 10;
private LinkedList<Chicken> chickenList = new LinkedList<>();
public synchronized void produce(Chicken chicken){
// 判断容器是否满,如果已经满了,生产者等待
if (chickenList.size() + 1 > MAX_SIZE){
try {
System.out.println("[生产者" + Thread.currentThread().getName() + "]: 仓库已满");
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
// 未满时,生产产品,并通知消费者消费
chickenList.add(chicken);
// System.out.println(this);
System.out.println("[生产者" + Thread.currentThread().getName() + "]: 生产了一个产品,现库存为:" + chickenList.size());
notifyAll();
}
}
public synchronized void consume(){
// 判断容器里是否有产品,没有产品时等待生产者生产
if (chickenList.size() == 0){
System.out.println("[消费者" + Thread.currentThread().getName() + "]: 仓库为空");
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
// 消费者消费产品,通知生产者继续生产
chickenList.remove();
// System.out.println(this);
System.out.println("[消费者" + Thread.currentThread().getName() + "]: 消费了一个产品,现库存为:" + chickenList.size());
notifyAll();
}
}
}
class Producer2 implements Runnable{
private Storage storage;
public Producer2(Storage storage) {
this.storage = storage;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(1000);
storage.produce(new Chicken());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Consumer2 implements Runnable{
private Storage storage;
public Consumer2(Storage storage){
this.storage = storage;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(1500);
storage.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
lic void run() {
while (true){
try {
Thread.sleep(1000);
storage.produce(new Chicken());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Consumer2 implements Runnable{
private Storage storage;
public Consumer2(Storage storage){
this.storage = storage;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(1500);
storage.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}