第一章 线程
什么是线程?
- 线程(Thread)是一个程序内部的一条执行路径。
- 我们之前启动程序执行后,main方法的执行其实就是一条单独的执行路径。
- 程序中如果只有一条执行路径,那么这个程序就是单线程的程序。
public static void main(String[] args){
//代码.....
for(int i = 0; i < 10; i++){
System.out.println(i);
}
//代码.....
}
1.1多线程
什么是多线程?
多线程是指从软硬件上实现多条执行流程的技术。
一般来说,消息通信,淘宝,京东系统都离不开多线程
1.2多线程的创建
1.2.1 Thread类
Java是通过java.lang.Thread类来代表线程。
按照面向对象的思想,Thread类应该提供了实现多线程的方式。
步骤如下:
- 继承Thread类
- 重写run方法
- 创建线程对象
- 调用start()方法启动
构造方法:
- public Thread() :分配一个新的线程对象。
- public Thread(String name) :分配一个指定名字的新的线程对象。
- public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
- public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。
常用方法:
- public String getName() :获取当前线程名称。
- public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
- public void run() :此线程要执行的任务在此处定义代码。
- public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
- public static Thread currentThread() :返回对当前正在执行的线程对象的引用。
代码如下:
自定义线程类:
/**
* 类继承Thread 表示该类是一个线程类
* 需要重写run方法,该方法就是多线程要执行的功能
*/
public class MyThread01 extends Thread{
public void run(){
for (int i = 0;i < 10; i++){
System.out.println("打游戏...."+i);
}
}
}
public class MyThread02 extends Thread{
public void run(){
for (int i = 0;i<=10;i++){
System.out.println("听歌...."+i);
}
}
}
测试类:
/**
* 多线程可以让多个任务同时(并发 和 并行 )执行,实现步骤
* 1.自定义类,继承Thread 类;
* 2.在类中重写重号run方法;
* 3.在调用的位置,实例化线程类的对象,然层调用start方法,启动线程自动执行run方法
*/
public class ThreadTest01 {
public static void main(String[] args) {
//实例化线程类的对象
MyThread01 t1 = new MyThread01();
MyThread02 t2 = new MyThread02();
//启动线程,实现了多线程任务,多个任务交替执行
t1.start();//自动调用run方法
t2.start();
}
}
运行结果如下:
使用Thread类创建多线程的优点以及缺点:
优点:编码简单
缺点:线程类已经继承Thread,无法继承其他类,不利于扩展。
1.2.2 Runnable接口
采用 java.lang.Runnable 也是非常常见的一种,我们只需要重写run方法即可。
设计该接口的目的是为希望在活动时执行代码的对象提供一个公共协议。例如,Thread
类实现了 Runnable
。激活的意思是说某个线程已启动并且尚未停止。
步骤如下:
- 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
- 创建MyRunnable任务对象
- 把MyRunnable任务对象交给Thread处理。
- 调用线程对象的start()方法启动线程
常用方法:
- run()使用实现接口
Runnable
的对象创建一个线程时,启动该线程将导致在独立执行的线程中调用对象的run
方法。
代码如下:
public class MyRunnable01 implements Runnable{
public void run(){
for (int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"Runnable方式执行听歌..."+i);
}
}
}
/**
* Runnable接口实现多线程的步骤:
* 1.自定义类实现 Runnable 接口;
* 2.重写run方法,里面放入多线程执行的功能:
* 3.创建Runnable接口实现类的对象:
* 4.把Runnable接口实现类的对象作为参数,创建线程对象(把接口对象加入到线程执行中去);
* 5.线程类对象启动线程---- 自动调用run 方法
*/
public class ThreadTest02 {
public static void main(String[] args) {
MyRunnable01 r1 = new MyRunnable01();
//把上面的接口实现类对象,加入到线程中
Thread t1=new Thread(r1,"库洛米");
t1.start();
}
}
运行结果如下:
使用Runnable()接口创建多线程优点以及缺点:
优点:线程任务类只是实现接口,可以继承类和实现接口,扩展性强。
缺点:编程多一层对象包装,如果线程有执行结果不可以直接返回的。
1.2.3 实现Runnable接口(匿名内部类形式)
使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。
步骤如下:
- 创建Runnable的匿名内部类对象。
- 交给Thread处理。
- 调用线程对象的start()启动线程。
举例如下:
package com.hp.test16;
public class UnNameTest {
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++){
System.out.println("库洛米:"+i);
}
}
};
new Thread(r).start();
for (int i = 0; i < 10; i++){
System.out.println("玉桂狗:"+i);
}
}
}
运行结果如下:
1.3Thread和Runnable的区别
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
扩充:在 java 中,每次程序运行至少启动 2 个线程。一个是 main 线程,一个是垃圾收集线程。因为每当使用 java命令执行一个类的时候,实际上都会启动一个 JVM ,每一个 JVM 其实在就是在操作系统中启动了一个进程
第二章 线程安全
2.1 线程安全
线程安全问题:
- 多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。
线程安全问题出现的原因:
- 存在多线程并发
- 同时访问共享资源
- 存在修改共享资源
举个例子:
我们来分析一下:
- 需要提供一个卖票类,创建一个类对象代表卖票的数量。
- 需要定义一个线程类,线程类可以处理电影票。
- 创建2个线程对象,传入一个卖票类。
- 启动2个线程,去卖同种电影票。
卖票类:
public class Ticket implements Runnable{
/**
* 执行卖票操作
*/
private int ticket = 50;
@Override
public void run(){
//获取当前程序对应执行线程的 名称
String tn = Thread.currentThread().getName();
while (true){
//1.获得票的变量
if (ticket>0){
//2.模拟卖票aqq
System.out.println(tn+"正在卖第"+ticket+"张票");
//模拟出票,需要一段时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//把票的总数-1
ticket--;
}else {
//票卖完了,结束循环
break;
}
}
}
}
测试类:
public class TreadTest01 {
public static void main(String[] args) {
//实例化卖票对象
Ticket ticket = new Ticket();
//定义线程,指定线程执行卖票程序,启动线程
Thread t1 = new Thread(ticket,"窗口1");
Thread t2 = new Thread(ticket,"窗口2");
t1.start();
t2.start();
}
}
运行结果如下:
2.2 线程同步
- 同步代码块。
- 同步方法。
- 锁机制。
2.3 同步代码块
- 作用:把出现线程安全问题的核心代码给上锁。
- 原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。
格式如下:
synchronized(同步锁){
操作共享资源的代码(核心代码)
锁对象的规范要求规范上:
- 建议使用共享资源作为锁对象。
- 对于实例方法建议使用this作为锁对象。
- 对于静态方法建议使用字节码(类名.class)对象作为锁对象
使用同步代码块解决代码:
public class Ticket implements Runnable {
/**
* 执行卖票操作
*/
private int ticket = 50;
Object lock = new Object();
@Override
public void run() {
//获取当前程序对应执行线程的 名称
String tn = Thread.currentThread().getName();
while (true) {
synchronized (lock) {
//1.获得票的变量
if (ticket > 0) {
//2.模拟卖票aqq
System.out.println(tn + "正在卖第" + ticket + "张票");
//模拟出票,需要一段时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//把票的总数-1
ticket--;
} else {
//票卖完了,结束循环
break;
}
}
}
}
}
2.4 同步方法
- 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
- 如果方法是实例方法:同步方法默认用this作为的锁对象。但是代码要高度面向对象!
- 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
同步代码块与同步方法的区别:
- 同步代码块的范围更小
- 同步方法的范围更大
2.5 Lock锁
- 为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便。
- Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。
- Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象。
- public void lock() :加同步锁。
- public void unlock() :释放同步锁。
public class Ticket03 implements Runnable{
/**
* 执行卖票操作
*/
private int ticket = 50;
//定义lock锁对象
Lock lock = new ReentrantLock();
@Override
public void run(){
//获取当前程序对应执行线程的 名称
String tn = Thread.currentThread().getName();
while (true){
//锁定
lock.lock();
try {
//1.获得票的变量
if (ticket>0){
//2.模拟卖票aqq
System.out.println(tn+"正在卖第"+ticket+"张票");
//模拟出票,需要一段时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//把票的总数-1
ticket--;
}else {
//票卖完了,结束循环
break;
}
}catch (Exception e){
e.printStackTrace();
} finally {
//解除锁定
lock.unlock();
}
}
}
}
第三章 线程状态
3.1 线程状态概述
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中 java.lang.Thread.State 这个枚举中给出了六种线程状态:
1.新建状态(New)
新创建了一个线程对象,但还没有调用start()方法。实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了新建状态。
2.就绪状态(Runnable)
新建状态的线程,调用线程的start()方法,此线程进入就绪状态。
3.运行状态(Running)
当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法。
4.阻塞状态(Blocked)
线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。
5.等待状态/计时等待(Waiting/Timed_Waiting)
线程进入等待状态有三种方式:
cpu调度给优先级更高的线程
线程要等待获得资源或者信号
时间片的轮转,时间片到了,进入等待状态
在可执行状态下,如果调用 sleep()、 wait()等方法,线程都将进入等待状态。
sleep() 和 wait() 的区别:
sleep方法是Thread类里面的,主要的意义就是让当前线程停止执行,让出CPU给其他的线程,但是不会释放对象锁资源以及监控的状态,当指定的时间到了之后又会自动恢复运行状态。
wait方法是Object类里面的,主要的意义就是让线程放弃当前的对象的锁,进入等待此对象的等待锁定池,只有针对此对象调动notify方法后本线程才能够进入对象锁定池准备获取对象锁进入运行状态。
6.终止状态(Terminated)
表示该线程已经执行完毕。当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
3.2 Timed_Waiting(计时等待)
3.3 BLOCKED(锁阻塞)
3.4 Waiting(无限等待)
![](https://i-blog.csdnimg.cn/blog_migrate/55803563abf449f1ac89bea920eb1043.png)
第四章 等待唤醒机制
4.1 线程通信
所谓线程通信就是线程间相互发送数据,线程间共享一个资源即可实现线程通信。
线程通信常见形式
- 通过共享一个数据的方式实现。
- 根据共享数据的情况决定自己该怎么做,以及通知其他线程怎么做。
线程通信实际应用场景
- 生产者与消费者模型:生产者线程负责生产数据,消费者线程负责消费生产者产生的数据。
- 要求:生产者线程生产完数据后唤醒消费者,然后等待自己,消费者消费完该数据后唤醒生产者,然后等待自己。
线程通信的前提:
线程通信通常是在多个线程操作同一个共享资源的时候需要进行通信,且要保证线程安全。
4.2 等待唤醒机制
等待唤醒机制是多个线程间的一种协作机制。谈到线程经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是全部,线程间也会有协作机制。
就是在一个线程进行了规定操作后,就进入等待状态(wait()),等待其他线程执行完他们的指定代码过后,再将其唤醒(notify());在有多个线程进行等待时,如果需要,可以使用notifyAll()来唤醒所有等待的线程。
wait、notify就是线程间的一种协作机制。
等待唤醒中的方法:
- wait:线程不再活动,不再参与调度,进入wait set中,因此不会浪费cpu资源,也不会竞争锁,这时的线程状态即是waiting。它还要等着别的线程执行一个特别的动作,也是“通知notify”在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列(ready queue)中
- notify:选取所通知对象的wait set中一个线程释放;例如,餐厅有位置后,等候就餐最久的顾客最先入座。
- notifyAll:释放所通知对象的wait set上的全部线程。
注意:哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以需要再次常识去获取锁(很可能面临其他线程的竞争),成功后才能在当初调用wait方法之后的地方恢复执行。
总结:如果能获取锁,线程就从waitiing状态变成runnable状态;
否则,从wait set出来,又进入entry set,线程就从waiting状态又变成blocked状态。
调用wait和notify方法需要注意的细节:
1.wait方法与notify方法必须要由同一个锁对象调用。因为对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
2.wait方法与notify方法是属于Object类的方法的。因为锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
3.wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为必要通过锁对象调用这两个方法。
案例生产者消费者模型
我们就拿买牛奶和喝牛奶来举例子:
奶箱类 ( Box) : 定义一个成员变量, 表示第 x 瓶奶, 提供存储牛奶和获取牛奶的操作
生产者类 (Producer) : 实现 Runnable 接口, 重写 run() 方法, 调用存储牛奶的操作
消费者类 (Customer) : 实现 Runnable 接口, 重写 run() 方法, 调用获取牛奶的操作
测试类 ( BoxTest) : 里面有 main 方法。
main 方法中的代码步骤如下:
- ①创建奶箱对象, 这是共享数据区域
- ②创建生产者对象, 把奶箱对象作为构造方法参数传递, 因为在这个类中要调用存储牛奶的操作
- ③创建消费者对象, 把奶箱对象作为构造方法参数传递, 因为在这个类中要调用获取牛奶的操作
- ④创建 2 个线程对象, 分别把生产者对象和消费者对象作为构造方法参数传递
- ⑤启动线程
代码实现:
奶箱类 ( Box) :
public class Box {
//定义一个成员变量,表示第几瓶奶
private int milk;
//定义一个成员变量,表示奶箱的状态
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+"瓶奶了哦");
//生产完毕之后,修改奶箱的状态
state = true;
//唤醒其他等待的线程
notify();
}
public synchronized void get(){
//如果没有牛奶,就等待生产
if (!state){
try {
wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
//如果有牛奶,就消费牛奶
System.out.println("我要喝第"+this.milk+"瓶奶了哦");
//消费完毕后,修改奶箱状态
state = false;
//唤醒其他等待的线程
notify();
}
}
生产者类 (Producer) :
public class Producer implements Runnable {
private Box b;
public Producer(Box b) {
this.b = b;
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
b.put(i);
}
}
}
消费者类 (Customer) :
public class Customer implements Runnable{
private Box b;
public Customer(Box b){
this.b = b;
}
@Override
public void run() {
while (true){
b.get();
}
}
}
测试类 ( BoxTest) :
public class BoxTest {
public static void main(String[] args) {
//创建奶箱对象, 这是共享数据区域
Box b = new Box();
//创建生产者对象, 把奶箱对象作为构造方法参数传递, 因为在这个类中要调用存储牛奶的操作
Producer p = new Producer(b);
//创建消费者对象, 把奶箱对象作为构造方法参数传递, 因为在这个类中要调用获取牛奶的操作
Customer c = new Customer(b);
//创建 2 个线程对象, 分别把生产者对象和消费者对象作为构造方法参数传递
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
//启动线程
t1.start();
t2.start();
}
}
运行结果如下: