多线程
一. 进程
是正在运行的程序
- 是系统进行资源分配和调用的独立单位;
- 每个进程都有它自己的内存空间和系统资源
二. 线程
是进程中的耽搁顺序控制流,是一条执行路径
1.一个进程如果只有一条执行路径,则成为单线程程序
2.多线程,一个进程如果有多条执行路径,则成为多线程程序
多线程的实现方式
- 方式1
- 定义一个类,继承于Thread类
- 在这个类中重写run()方法
- 创建类对象
- 启动线程
两个小问题
- 为什么要重写run()方法?
因为run() 方法是为了封装被线程执行的代码 - run()和strart()的区别
前者封装线程执行代码,直接调用,相当于普通方法的调用;后者启动线程,然后由JVM调用此线程的run()方法
设置和获取线程名称
- setName()
- getName()
- currentThread().getName() 获取当前线程的名称
/*
设置线程名称
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
my1.setName("飞机");
my2.setName("高铁");
*/
/*
带参数的设置线程名称
MyThread my1 = new MyThread("飞机");
MyThread my2 = new MyThread("高铁");
*/
// my1.start();
// my2.start();
// 获取当前线程的名称 System.out.println(Thread.currentThread().getName()); // main
三. 线程调度
线程优先级高,仅仅表示它获取CPU时间片相对多一些,并不表示它一定最先执行,是概率事件。
线程的调度模型
- 分时调度模型:所有线程轮有使用CPU的使用权,平均分配每个线程占用CPU的时间片。
- 抢占式调度模型:优先让优先级高的线程使用CPU,如果优先级相同,那么会随机选择一个。优先级高的获取的CPU时间片相对多一些。
Java使用的是抢占式的调度模型
Thread类设置和获取线程优先级的方法
- getPriority() int
- setPriority(int newPriority)
Thread.MAX_PRIORITY 最大10
Thread.MIN_PRIORITY 最小1
Thread.NORM_PRIORITY 默认值5
优先级高不一定在最前面,只是获取CPU时间片相对多一些
ThreadPriority tp1 = new ThreadPriority();
ThreadPriority tp2 = new ThreadPriority();
ThreadPriority tp3 = new ThreadPriority();
tp1.setName("高铁");
tp2.setName("飞机");
tp3.setName("汽车");
// 获取优先级
System.out.println(tp1.getPriority()); // 5
System.out.println(tp2.getPriority()); // 5
System.out.println(tp3.getPriority()); // 5
System.out.println("--------");
// 查看优先级的区间
System.out.println(Thread.MAX_PRIORITY); // 10
System.out.println(Thread.MIN_PRIORITY); // 1
System.out.println(Thread.NORM_PRIORITY); // 5
// 设置优先级
tp1.setPriority(10);
tp2.setPriority(1);
tp3.setPriority(6);
// 运行
tp1.start();
tp2.start();
tp3.start();
四. 线程控制
- sleep(long) 暂停
- join() 这个线程结束,才能向下执行
- setDaemon(boolean) 当运行的程序只剩下守护线程时,Java虚拟机也将退出,比如垃圾回收机制就是守护线程;
a. 当java虚拟机中没有非守护线程在运行的时候,java虚拟机会关闭。
b. 当所有常规线程运行完毕以后,守护线程不管运行到哪里,虚拟机都会退出运行。
c. 所以你的守护线程最好不要写一些会影响程序的业务逻辑。否则无法预料程序到底 会出现什么问题
五. 线程的生命周期
方式二
实现Runnable
- 定义一个了类,实现Runnable接口
- 在类中重写run() 方法
- 创建类对象
- 创建Thread类对象,把类对象作为构造方法的参数
- 启动线程
两种方式小结:
方案: - 继承Thread类
- 实现Runnable接口
相比于继承Thread类,实现Runnable接口的好处
- 避免了Java单继承的局限性
- 避免多个相同的程序代码去处理同一个资源,把线程和程序的代码,数据有效分离,较好的体现了面向对象的设计思想
案例
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
public class SellTicket implements Runnable {
private int tickets = 100;
@Override
public void run() {
int k = 110;
while (k >= 0) {
if (tickets >= 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 卖了票,目前剩票" + tickets--);
} else {
System.out.println("票已经卖光了");
}
k --;
}
}
}
测试类
// 创建卖票对象
SellTicket sellTicket = new SellTicket();
// 创建线程
Thread th1 = new Thread(sellTicket,"窗口1");
Thread th2 = new Thread(sellTicket,"窗口2");
Thread th3 = new Thread(sellTicket,"窗口3");
// 时间到,开始售票
th1.start();
th2.start();
th3.start();
但是测试出来会出现bug
六. 同步代码块
synchronized(任意对象){}
改进上面代码如下
public class SellTicket implements Runnable {
private int tickets = 100;
private int runTimes = 110;
private Object obj = new Object();
@Override
public void run() {
while (runTimes >= 0) {
synchronized (obj) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 卖了票,目前剩票" + tickets--);
} else {
System.out.println("票已经卖光了");
}
runTimes--;
}
}
}
}
同步代码块的优缺点
优点:解决了多线程的数据安全问题。
缺点:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
七. 同步方法
把synchronized 加到方法上
格式:
- 修饰符 synchronized 返回值类型 方法名(){}
同步方法的锁对象是: this
八. 同步静态方法
格式
- 修饰符 static synchronized 返回值类型 方法名(){}
同步静态方法的锁对象是:类名.class
private static int tickets = 100;
private Object obj = new Object();
@Override
public void run() {
while (true) {
// 同步方法锁
sellTickets();
// 同步静态方法锁
sellTickets_class();
}
}
// 同步方法锁,锁对象是 this
private synchronized void sellTickets() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--;
}
}
// 同步今天方法锁的锁对象是 SellTicket.class
private static synchronized void sellTickets_class() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--;
}
}
九. 线程安全的类
/*
以下是线程安全的替代类,如果不考虑同步,还是使用StringBuilder 、ArrayList 、HashMap
*/
StringBuilder sbu = new StringBuilder();
StringBuffer sbf = new StringBuffer();
ArrayList<String> arr = new ArrayList<>();
Vector<String> ve = new Vector<>();
HashMap<String, String> map = new HashMap<>();
Hashtable<String, String> table = new Hashtable<>();
/*
但是数组和map的依然被替代了,替代的是Collections
*/
List<String> strings = Collections.synchronizedList(arr);
Set<Object> objects = Collections.synchronizedSet(new HashSet<>());
Map<String, String> map1 = Collections.synchronizedMap(map);
JDK5之后提供了新的锁对象 Lock
十. Lock
它是一个接口,所有要用他的实现类ReentrantLock的实例
- lock() 加锁
- unlock() 释放锁
private int tickets = 100;
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
lock.lock();
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--;
}
} finally {
lock.unlock();
}
}
}
十一. 线程等待
为了体现java生产者和消费者,在生产和消费过程中的等待和唤醒,在Object类中有等待和唤醒的方法
- wait() 导致当前线程等待,直到另一个线程调用该对象的notify() 或者notifyAll()
- notify() 唤醒单个线程
- notifyAll() 唤醒正在等待监视器中的的所有线程
特殊的错误 IllegalMonitorStateException
在线程中调用wait方法的时候 要用synchronized锁住对象,确保代码段不会被多个线程调用
案例
送奶工人送牛奶到奶箱中,程序员从奶箱中拿牛奶,要求:工人送一瓶,程序员取一瓶,用多线程实现;
生产者消费者案例中包含的类:
1:奶箱类(Box):定义一个成员变量,表示第x瓶奶,提供存储牛奶和获取牛奶的操作
2:生产者类(Producer):实现Runnable接口,重写run()方法,调用存储牛奶的操作
3:消费者类(Customer):实现Runnable接口,重写run()方法,调用获取牛奶的操作
4:测试类(BoxDemo):里面有main方法,main方法中的代码步骤如下
A:创建奶箱对象,这是共享数据区域
B:创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作
C:创建消费者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用获取牛奶的操作
D:创建2个线程对象,分别把生产者对象和消费者对象作为构造方法参数传递
E:启动线程
public class BoxDemo {
public static void main(String[] args) {
// 创建盒子对象
Box box = new Box();
// 创建生产者和消费者,因为他们都对盒子做操作,所以盒子作为参数
Producter producter = new Producter(box);
Consumer consumer = new Consumer(box);
// 创建两个线程,开始存牛奶和取牛奶
Thread t1 = new Thread(producter);
Thread t2 = new Thread(consumer);
// 开始
t1.start();
t2.start();
}
}
public class Box {
private int milk; // 记录第几瓶奶
private boolean state = false; // 记录箱子中是否有奶
// 盒子具有放入牛奶的功能
public synchronized void put(int i){
// 如果有奶,就等待
if (state) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果没奶,就放入
this.milk = i+1;
System.out.println("生产者放入第" + this.milk + "瓶奶");
// 通知,其他线程,已经有奶了,可以拿了
state = true;
notifyAll();
}
// 盒子具有被拿出牛奶的功能
public synchronized void get() {
// 如果没有奶了,就等待
if (!state) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果有奶,就取走
System.out.println("消费者拿走第" + milk + "瓶奶");
// 通知其他线程,可以放奶了
state = false;
notifyAll();
}
}
public class Producter implements Runnable{
private Box box;
public Producter(Box box) {
this.box = box;
}
@Override
public void run() {
// 放入牛奶
for (int i = 0; i < 5; i++) {
box.put(i);
}
}
}
public class Consumer implements Runnable{
private Box box;
public Consumer(Box box) {
this.box = box;
}
// 消费者 取走牛奶
@Override
public void run() {
// 因为不知道什么时候放奶,所以一直取
while (true) {
box.get();
}
}
}