“等待与希望,乃人生真谛。”这是基督山伯爵最后的箴言,也是我们在漫长人生旅途中的永恒指南。
--大仲马《基督山伯爵》
一、多线程
1、进程
正在运行的程序,是系统进行资源分配和调用的独立单位。
每一个进程都有它自己的内存空间和系统资源。
理解:一个正在运行的软件
2、线程
是进程中的单个顺序控制流,是一条执行路径
一个进程如果只有一条执行路径,则称为单线程程序。
一个进程如果有多条执行路径,则称为多线程程序。
举例:阿里云盘(进程)中多个同时进行的任务,每一个任务可以看作一个线程
3、如何创建一个线程
a. 自定义线程类继承Thread类,重写run方法
b. 自定义线程类实现Runnable接口,实现run方法
4、如何启动一个线程
调用start()方法启动
Thread无参构造方法
Thread() 分配一个新的 Thread对象。
5、注意
1、启动一个线程的时候,若直接调用run方法,仅仅是普通的对象调用方法,按照自上而下的顺序执行,底层不会额外的创建一个线程再执行
2、从执行的结果上来看,java线程之间是抢占式执行的,谁先抢到cpu执行权谁就先执行
3、每次运行的结果顺序不可预测的,完全随机的
4、每个线程都有优先权。 具有较高优先级的线程优先于优先级较低的线程执行。
二、Thread类(第一种多线程的实现方法)
Thread类中的成员方法
1、public final String getName() 获取线程对象的名字
2、设置线程对象名字的方式:
a. 通过父类的有参构造方法,在创建线程对象的时候设置名字
b. 线程对象调用setName(String name)方法,给线程对象设置名字
3、获取线程的等级(线程的调度)
getPriority() 默认优先级都是5
4、设置线程优先级,setPriority(int i),在启动之前设置 [1,10]
注意不是优先级高的一定先执行,只是可能性变高了。
package day19;
*/
class MyThread1 extends Thread {
MyThread1() {
}
//public Thread(String name)
MyThread1(String name) {
super(name);
}
// MyThread1(){
// //super()
// }
@Override
public void run() {
//将来线程启动后需要执行的逻辑
for (int i = 1; i <= 200; i++) {
System.out.println(this.getName() + " - Hello word " + i);
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
//创建一个自己的线程对象
// MyThread1 t1 = new MyThread1("张三"); // Thread-0
MyThread1 t1 = new MyThread1();
t1.setName("张三");
t1.setPriority(1);
System.out.println("t1: "+t1.getPriority());
//创建一个自己的线程对象
// MyThread1 t2 = new MyThread1("李四"); // Thread-1
MyThread1 t2 = new MyThread1();
t2.setName("李四");
t2.setPriority(10);
System.out.println("t2: "+t2.getPriority());
// t1.run();
// t2.run();//想要启动线程需要使用start()方法,不能使用run()方法,run()方法虽然可以运行,但是仅仅是以普通的程序状态去运行,并不是以线程的状态去运行。
t1.start();
t2.start();
}
}
线程控制
(1)线程休眠
public static void sleep(long millis)
package day19;
/*
线程控制,休眠线程
当线程处于休眠状态的时候,该线程就没有cpu执行权了,若这时还有其他的线程,会被抢走cpu执行权。
*/
class SleepThread extends Thread{
@Override
public void run() {
System.out.println(getName()+"睡着了。。。。");
try {
Thread.sleep(5000);//在哪个方法中调用,就是调用该方法的线程对象进行休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName()+"睡醒了。。。。");
}
}
public class ThreadSleepDemo1 {
public static void main(String[] args) {
SleepThread t1 = new SleepThread();
t1.setName("程序");
t1.start();
}
}
(2)线程加入
public final void join()
package day19;
class JoinThread extends Thread{
@Override
public void run() {
for (int i = 1; i < 10; i++) {
System.out.println(getName()+"-"+i);
}
}
}
public class ThreadJoinDemo1 {
public static void main(String[] args) {
JoinThread t1 = new JoinThread();
JoinThread t2 = new JoinThread();
JoinThread t3 = new JoinThread();
t1.setName("小张");
t2.setName("小古");
t3.setName("小季");
t1.start();//t1线程优先运行,其他线程会等待他的结束
try {
t1.join();//其他线程等待该线程执行结束,其他线程之间会进行抢占式的执行
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
t3.start();
}
}
(3)线程礼让
public static void yield()
package day19;
/*
礼让线程
yield()只是为了让运行结果看起来均匀一些
*/
class YieldThread extends Thread{
@Override
public void run() {
for (int i = 1; i < 10; i++) {
System.out.println(getName()+" - "+i);
Thread.yield();
}
}
}
public class ThreadYieldDemo1 {
public static void main(String[] args) {
YieldThread t1 = new YieldThread();
YieldThread t2 = new YieldThread();
t1.setName("小张");
t2.setName("小古");
t1.start();
t2.start();
}
}
(4)后台线程
public final void setDaemon(boolean on)
package day19;
/*
后台进程:
用户线程
守护线程
在启动之前,设置一下,若一个进程中没有用户线程,守护线程也没有必要存在
不同生,但是被守护的线程先死亡,守护线程会一起死亡
假如有很多用户进程,只要有一个用户进程还存在,守护进程就不会死亡
*/
class DaemonThread extends Thread{
@Override
public void run() {
for (int i = 1; i <= 200; i++) {
System.out.println(getName() + " - " + i);
}
}
}
public class ThreadDaemonDemo1 {
public static void main(String[] args) {
DaemonThread t1 = new DaemonThread();
DaemonThread t2 = new DaemonThread();
DaemonThread t3 = new DaemonThread();
t1.setName("刘备");
t2.setName("关羽");
t3.setName("张飞");
t2.setDaemon(true);
t3.setDaemon(true);
t1.start();
t2.start();
t3.start();
}
}
(5)中断线程
public final void stop()
public void interrupt()
package day19;
import javafx.scene.paint.Stop;
/*
stop()方法已被弃用,现在使用的方法是interrupt()方法。
*/
class StopThread extends Thread{
@Override
public void run() {
System.out.println(getName()+"睡着了。。。。");
try {
Thread.sleep(5000);//在哪个方法中调用,就是调用该方法的线程对象进行休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName()+"睡醒了。。。。");
}
}
public class ThreadStopDemo1 {
public static void main(String[] args) {
StopThread t1 = new StopThread();
t1.setName("张三");
t1.start();
try {
Thread.sleep(2000);
// t1.stop();//已经弃用
t1.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
三、Runnable接口(多线程的实现方案2)
实现接口的好处
- 可以避免由于Java单继承带来的局限性。
- 适合多个相同程序的代码去处理同一个资源的情况,把线程同程序的代码,数据有效分离,较好的体现了面向对象的设计思想。
关于电影院卖票程序题目
(1)第一次尝试
package day19.sellTickets;
/*
使用Runnable的方式实现
为了模拟更加真实的售票情况,我们加入延迟
问题;
我们加入了延迟之后,发现
a.有重复售卖同一张票的情况
b.还出现了一个不该出现的票数据,比如第0张票,第-1张票(原因2)
原因:
1.cpu小小的时间片,足以让程序执行很多次
2.线程的执行具有随机性,且是抢占式执行的
现象:线程不安全的现象
如何判断一个程序是否存在线程不安全的现象呢?
三要素(同时满足):
1、是否存在多线程环境?
2、是否存在共享数据?
3、是否存在多条语句操作着共享数据
如何解决线程不安全的现象?
1、同步代码块
2、lock锁
*/
class Window implements Runnable{
private static int tickets=100;
@Override
public void run() {
while(true){
if(tickets>0){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 正在出售第 "+(tickets--)+" 张票 ");
}
}
}
}
public class SellTicketDemo1 {
public static void main(String[] args) {
Window window = new Window();
Thread w1 = new Thread(window,"窗口1");
Thread w2 = new Thread(window,"窗口2");
Thread w3 = new Thread(window,"窗口3");
w1.start();
w2.start();
w3.start();
}
}
(2)第二次改进(解决方案一)
package day19.SellTickets2;
/*
使用Runnable的方式实现
为了模拟更加真实的售票情况,我们加入延迟
问题;
我们加入了延迟之后,发现
a.有重复售卖同一张票的情况
b.还出现了一个不该出现的票数据,比如第0张票,第-1张票(原因2)
原因:
1.cpu小小的时间片,足以让程序执行很多次
2.线程的执行具有随机性,且是抢占式执行的
现象:线程不安全的现象
如何判断一个程序是否存在线程不安全的现象呢?
三要素(同时满足):
1、是否存在多线程环境?
2、是否存在共享数据?
3、是否存在多条语句操作着共享数据
如何解决线程不安全的现象?
1、同步代码块
2、lock锁
解决方案1:加入同步代码块
synchronized(对象){
操作共享数据代码
}
这里的对象,可以是任意一个new出来的对象,但是要保证多个线程之间的是同一个对象。
*/
class Window implements Runnable{
private static int tickets=100;
private Object object = new Object();
@Override
public void run() {
while(true) {
synchronized (object) {
if (tickets > 0) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 正在出售第 " + (tickets--) + " 张票 ");
}
}
}
}
}
public class SellTicketDemo1 {
public static void main(String[] args) {
Window window = new Window();
Thread w1 = new Thread(window,"窗口1");
Thread w2 = new Thread(window,"窗口2");
Thread w3 = new Thread(window,"窗口3");
w1.start();
w2.start();
w3.start();
}
}
(3)第三次改进(解决方案一)(将售票这个动作写成方法)
package day19.SellTickets3;
/*
使用Runnable的方式实现
为了模拟更加真实的售票情况,我们加入延迟
问题;
我们加入了延迟之后,发现
a.有重复售卖同一张票的情况
b.还出现了一个不该出现的票数据,比如第0张票,第-1张票(原因2)
原因:
1.cpu小小的时间片,足以让程序执行很多次
2.线程的执行具有随机性,且是抢占式执行的
现象:线程不安全的现象
如何判断一个程序是否存在线程不安全的现象呢?
三要素(同时满足):
1、是否存在多线程环境?
2、是否存在共享数据?
3、是否存在多条语句操作着共享数据
如何解决线程不安全的现象?
1、同步代码块
2、lock锁
解决方案1:加入同步代码块
synchronized(对象){
操作共享数据代码
}
这里的对象,可以是任意一个new出来的对象,但是要保证多个线程之间的是同一个对象。
synchronized的使用:
1.同步代码块 - 锁对象 - 任意一个对象,前提是多个线程对象共享一个
2.同步方法 - 锁对象 - this
3.同步静态方法 - 锁对象 - 当前类的class文件对象
解决方法2:lock锁
*/
class Window implements Runnable {
private static int tickets = 100;
private Object object = new Object();
private int i = 0;
@Override
public void run() {
while (true) {
if (i % 2 == 0) {
synchronized (Window.class) {
if (tickets > 0) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 正在出售第 " + (tickets--) + " 张票 ");
}
}
} else {
sellTicket();
}
i++;
}
}
//同步方法,将synchronized在方法定义上出现
// public synchronized void sellTicket(){
// if (tickets > 0) {
// try {
// Thread.sleep(50);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// System.out.println(Thread.currentThread().getName() + " 正在出售第 " + (tickets--) + " 张票 ");
// }
// }
//}
public static synchronized void sellTicket() {
if (tickets > 0) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 正在出售第 " + (tickets--) + " 张票 ");
}
}
}
public class SellTicketDemo1 {
public static void main(String[] args) {
Window window = new Window();
Thread w1 = new Thread(window,"窗口1");
Thread w2 = new Thread(window,"窗口2");
Thread w3 = new Thread(window,"窗口3");
w1.start();
w2.start();
w3.start();
}
}
(4)第四次改进(解决方案二)(使用lock锁)
package day19.SellTickets4;
import java.util.concurrent.locks.ReentrantLock;
/*
使用Runnable的方式实现
为了模拟更加真实的售票情况,我们加入延迟
问题;
我们加入了延迟之后,发现
a.有重复售卖同一张票的情况
b.还出现了一个不该出现的票数据,比如第0张票,第-1张票(原因2)
原因:
1.cpu小小的时间片,足以让程序执行很多次
2.线程的执行具有随机性,且是抢占式执行的
现象:线程不安全的现象
如何判断一个程序是否存在线程不安全的现象呢?
三要素(同时满足):
1、是否存在多线程环境?
2、是否存在共享数据?
3、是否存在多条语句操作着共享数据
如何解决线程不安全的现象?
1、同步代码块
2、lock锁
解决方案1:加入同步代码块
synchronized(对象){
操作共享数据代码
}
这里的对象,可以是任意一个new出来的对象,但是要保证多个线程之间的是同一个对象。
synchronized的使用:
1.同步代码块 - 锁对象 - 任意一个对象,前提是多个线程对象共享一个
2.同步方法 - 锁对象 - this
3.同步静态方法 - 锁对象 - 当前类的class文件对象
解决方法2:lock锁,利用ReentrantLock类创建锁对象,要求多个线程对象共享一个
不需要考虑锁对象是谁了?
*/
class Window implements Runnable {
private static int tickets = 1000;
// private Object object = new Object();
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock(); //加锁
if (tickets > 0) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 正在出售第 " + (tickets--) + " 张票 ");
}
lock.unlock(); //释放锁
}
}
}
//同步方法,将synchronized在方法定义上出现
// public synchronized void sellTicket(){
// if (tickets > 0) {
// try {
// Thread.sleep(50);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// System.out.println(Thread.currentThread().getName() + " 正在出售第 " + (tickets--) + " 张票 ");
// }
// }
//}
public class SellTicketDemo1 {
public static void main(String[] args) {
Window window = new Window();
Thread w1 = new Thread(window,"窗口1");
Thread w2 = new Thread(window,"窗口2");
Thread w3 = new Thread(window,"窗口3");
w1.start();
w2.start();
w3.start();
}
}
四、死锁
定义:
死锁是指多个线程在执行过程中,由于竞争资源而造成的一种僵持状态,若无外力作用,这些线程都将无法向前推进。
package day19.dielock;
import java.util.concurrent.locks.ReentrantLock;
public class Locks {
private Locks(){}
public static final ReentrantLock lock1 = new ReentrantLock();
public static final ReentrantLock lock2 = new ReentrantLock();
}
死锁的问题:线程之间存在相互等待的现象
经典的案例:中国人和外国人吃饭的问题
中国人:两个筷子
外国人:一把刀和一把叉
现在:
中国人:一支筷子,一把刀
外国人:一只筷子,一把叉
package day19.dielock;
/*
死锁的问题:线程之间存在相互等待的现象
经典的案例:中国人和外国人吃饭的问题
中国人:两个筷子
外国人:一把刀和一把叉
现在:
中国人:一支筷子,一把刀
外国人:一只筷子,一把叉
*/
class Person extends Thread {
private boolean flag;
public Person(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
synchronized (Locks.lock1) {
System.out.println("if lock1");
//p1
synchronized (Locks.lock2) {
System.out.println("if lock2");
}
}
} else {
synchronized (Locks.lock2) {
System.out.println("else lock2");
//p2
synchronized (Locks.lock1) {
System.out.println("else lock1");
}
}
}
}
}
public class DieLockDome {
public static void main(String[] args) {
Person p1 = new Person(true);
Person p2 = new Person(false);
p1.start();
p2.start();
}
}
五、生产者与消费者的问题
通过前面的所学,可以引出一个十分常用的模型--消费者生产者模型
等待唤醒机制:
共享数据:学生对象(name,age)
生产者线程:对学生对象进行赋值操作
消费者线程:对学生对象进行取值操作
为了方便更好的效果,我们可以让生产者赋值不同的信息
这时候出现了重复取值和姓名与年龄对应不上的情况
检测线程程序是否存在安全问题的三要素:
1、是否存在多线程环境?是 生产者和消费者
2、是否存在共享数据? 是 Student
3、是否存在多条语句操作共享的数据 是
解决方案:
1、加入同步代码块
2、加入lock锁
虽然解决了线程安全的问题,但是结果并不是我们想要的结果。在生产者消费者模型中,消费一次应该通知生产者生产。等待唤醒机制
注意:等待唤醒机制是建立在线程安全基础上设计的。
首先
创建一个产品类
package day19.waitdemo;
public class Student {
private String name;
private int age;
private boolean flag;//boolean类型的默认值在内存是false
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public boolean getFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
然后
创建消费者类和生产者类
ConsumerThread类
package day19.waitdemo;
public class ConsumerThread extends Thread{
private Student s;
public ConsumerThread(Student s) {
this.s = s;
}
@Override
public void run() {
// Student s = new Student();
while(true){
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s){
//消费者消费之前,先判断数据有没有生产,若生产了,就取值打印,若没有生产,就等待,通知生产者生产
if(!s.getFlag()){
try {
s.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(s.getName()+ " - "+s.getAge());
s.setFlag(false);
//通知生产者赋值新的数据
s.notify();
}
}
}
}
ProductThread类
package day19.waitdemo;
public class ProductThread extends Thread{
private Student s;
private int i = 0;
public ProductThread( Student s) {
this.s = s;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s){
//应该先看一看数据有没有被消费,若被消费了,才赋新的值,通知消费者消费。若没有消费,等待消费者消费
if (s.getFlag()){
//若学生对象中的flag成员变量是true,生产者不会去生产
try {
s.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(i%2==0){
s.setName("张三");
s.setAge(18);
}else{
s.setName("李四");
s.setAge(20);
}
i++;
s.setFlag(true);
//赋值之后应该通知消费者进行消费
s.notify();
}
}
}
}
最后
写StudentDemo测试类
package day19.waitdemo;
/*
等待唤醒机制:
共享数据:学生对象(name,age)
生产者线程:对学生对象进行赋值操作
消费者线程:对学生对象进行取值操作
为了方便更好的效果,我们可以让生产者赋值不同的信息
这时候出现了重复取值和姓名与年龄对应不上的情况
检测线程程序是否存在安全问题的三要素:
1、是否存在多线程环境?是 生产者和消费者
2、是否存在共享数据? 是 Student
3、是否存在多条语句操作共享的数据 是
解决方案:
1、加入同步代码块
2、加入lock锁
虽然解决了线程安全的问题,但是结果并不是我们想要的结果。在生产者消费者模型中,消费一次应该通知生产者生产。等待唤醒机制
注意:等待唤醒机制是建立在线程安全基础上设计的。
*/
public class StudentDemo {
public static void main(String[] args) {
Student s = new Student();
ProductThread p1 = new ProductThread(s);
ConsumerThread c1 = new ConsumerThread(s);
p1.start();
c1.start();
}
}
运行的结果(会不断的运行张三-18和李四-20,需要手动停止)