多线程
(一)实现多线程
1.进程
进程:是正在运行的程序
- 是系统进行资源分配和调用的独立单位
- 每一个进程都有它自己的内存空间和系统资源
2.线程
线程:是进程中的单个顺序控制流,是一条执行路径
- 单线程:一个进程如果只有一条执行路径,则称为单线程程序
- 多线程:一个进程如果有多条执行路径,则称为多线程程序
举例
- 记事本程序(调整页面大小时不能同时修改文本中的内容)
- 扫雷程序(玩的同时不影响计时)
3.多线程的第一种实现方式
方式1:继承Thread类
- 定义一个类MyThread继承Thread类
- 在MyThread类中重写run()方法
- 创建MyThread类的对象
- 启动线程
两个小问题:
- 为什么要重写run()方法?
因为run()是用来封装被线程执行的代码
- run()方法和start()方法的区别?
run():封装线程执行的代码,直接调用,相当于普通方法的调用
start():启动线程;然后由JVM调用此线程的run()方法
代码演示:
定义Mythread类:
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
//重写run的目的:将自定义代码存储在run方法,让线程运行。
}
创建测试类:
public class ThreadDemo {
public static void main(String[] args) {
MyThread mt = new MyThread();
MyThread mt2 = new MyThread();
mt.start();
mt2.start();
}
}
4.设置和获取线程名称
Thread类中设置和获取线程名称的方法
- void setName(String name):将此线程的名称更改为等于参数name
- String getName():返回次线程的名称
- 通过构造方法也可以设置线程名称
如何获取main()方法所在的线程名称?
- public static Thread currentThread():返回对当前正在执行的线程对象的引用
代码演示:
定义MyThread类:
public class MyThread extends Thread {
public MyThread() {}
public MyThread(String name){//给出带参构造方法
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + ": " + i);
}
}
//重写run的目的:讲自定义代码存储在run方法,让线程运行。
}
定义测试类:
public class ThreadDemo {
public static void main(String[] args) {
//无参构造
MyThread mt = new MyThread();
MyThread mt2 = new MyThread();
//给线程设置名称
mt.setName("火车");
mt2.setName("飞机");
//带参构造
MyThread mt3 = new MyThread("小汽车");
MyThread mt4 = new MyThread("轮船");
//启动线程
mt.start();
mt2.start();
mt3.start();
mt4.start();
//获取main方法所在的线程名称
System.out.println(Thread.currentThread().getName()); //main
}
}
5.线程调度
线程有两种调度模型
- 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
- 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么随机会选择一个,优先级高的线程获取的CPU时间片相对多一些
java使用的是抢占式调度模型
假如计算机只有一个CPU,那么CPU在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的
Thread类中设置和获取线程优先级的方法
- public final int getPriority():返回此线程的优先级
- public final void setPriority(int newPriority):更改此线程的优先级
注意:即使设置了优先级,也不一定优先级大的也不一定优先,因为设置优先级的大小仅仅只是提高了抢占到CPU时间片的概率,只有多次运行或者次数比较多的时候才能看到想要的效果
代码演示:
public class ThreadDemo {
public static void main(String[] args) {
//无参构造
MyThread mt = new MyThread();
MyThread mt2 = new MyThread();
MyThread mt3 = new MyThread();
//给线程设置名称
mt.setName("火车");
mt2.setName("飞机");
mt3.setName("小汽车");
//获得此线程的优先级
System.out.println(mt.getPriority());//5
System.out.println(mt2.getPriority());//5
System.out.println(mt3.getPriority());//5
System.out.println(Thread.MAX_PRIORITY);//最大优先级为10 被final和static修饰的成员变量
System.out.println(Thread.MIN_PRIORITY);//最小优先级为1
System.out.println(Thread.NORM_PRIORITY);//默认的优先级为5
//设置线程优先级
mt.setPriority(10);
mt2.setPriority(5);
mt3.setPriority(1);
//启动线程
mt.start();
mt2.start();
mt3.start();
}
}
5.线程控制
代码演示:
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + ": " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyThread mt = new MyThread();
MyThread mt2 = new MyThread();
MyThread mt3 = new MyThread();
mt.setName("关羽");
mt2.setName("张飞");
mt3.setName("刘备");
//启动线程
mt.start();
try {
//void join()
mt.join();//只有当这个线程结束时,其他线程才会继续执行
} catch (InterruptedException e) {
e.printStackTrace();
}
mt2.start();
mt3.start();
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyThread mt = new MyThread();
MyThread mt2 = new MyThread();
mt.setName("关羽");
mt2.setName("张飞");
//设置主线程
Thread.currentThread().setName("刘备");
//设置守护线程 void setDaemon(boolean on)
mt.setDaemon(true);
mt2.setDaemon(true);
//启动线程
mt.start();
mt2.start(); //当主线程结束时,守护线程也会结束
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
6.线程的生命周期
7.多线程的第二种实现方式
实现Runnable接口
- 定义一个MyRunnable类实现Runnable接口
- 重写MyRunnable中的run()方法
- 创建MyRunnable类的对象
- 创建Thread类的对象,并把MyRunnable类的对象作为构造方法的参数
- 启动线程
多线程的实现方案有两种:
- 继承Thread类
- 实现Runnable接口
相比继承Thread类,实现Runnable接口的好处
- 避免了Java单继承的局限性
- 适合多个相同程序的代码去处理同一个资源的情况,把线程和代码、数据有效分离,较好的体现了面向对象的设计思想
代码演示:
定义MyRunnable类
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++)
//因为getName()是Thread类的特有方法,因为前面第一种方法是继承了Thread类才可以使用
//此处想要使用getName()方法,就要先获取当前线程Thread.currentThread() 获取到了线程才可以使用其getName()方法
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
测试类:
public class RunnableDemo {
public static void main(String[] args) {
//创建MyRunnable类的对象
MyRunnable mr = new MyRunnable();
//创建Thread类的对象
Thread t = new Thread(mr,"飞机");
Thread t2 = new Thread(mr,"火车");
//启动线程
t.start();
t2.start();
}
}
(二)线程同步
1.案例(卖票)
定义卖票类(SellTicket):
public class SellTicket implements Runnable {
private int tickets = 100;
@Override
public void run() {
while(true){
//下面代码会导致卖票出现两种问题 :1.相同的票卖了多次 2.出现了负数的票
if (tickets > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第"+tickets+"张票被"+Thread.currentThread().getName()+"卖出");
tickets--;
} else {
System.out.println(Thread.currentThread().getName()+"显示票已卖完");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
创建测试类:
public class SellTicketDemo {
public static void main(String[] args) {
//创建卖票类对象
SellTicket st = new SellTicket();
//创建线程对象
Thread t1 = new Thread(st,"第1窗口");
Thread t2 = new Thread(st,"第2窗口");
Thread t3 = new Thread(st,"第3窗口");
//启动线程
t1.start();
t2.start();
t3.start();
}
}
卖票出现了问题:
- 相同的票卖了多次
- 出现了负数的票
问题原因:
- 线程执行的随机性导致的
2.卖票案例数据安全问题的解决
为什么出现问题?(这也是判断多线程程序是否会有数据安全问题的标准)
- 是否是多线程环境
- 是否有共享数据
- 是否有多条语句操作共享数据
如何解决多线程安全问题?
- 基本思想:让程序没有安全问题的环境
怎么实现?
- 把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
- Java提供了同步代码块的方式来解决
3.同步代码块
锁多条语句操作共享数据,可以使用同步代码块实现
- 格式:
synchronized(任意对象) {
多条语句操作共享数据的代码
}
- synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成一把锁
改进定义的Sellticket类
public class SellTicket implements Runnable {
private int tickets = 1000;
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {//此处加上同一把锁,当线程进来先看是否持有锁,有则可以进去执行,只有当前一个线程执行完代码块之后,下一个线程才可以进来
//下面代码会导致卖票出现两种问题 :1.相同的票卖了多次 2.出现了负数的票
if (tickets > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第" + tickets + "张票被" + Thread.currentThread().getName() + "卖出");
tickets--;
} else {
System.out.println(Thread.currentThread().getName() + "显示票已卖完");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
4.线程同步方法
代码演示:
public class SellTicket implements Runnable {
private static int tickets = 100;
private Object obj = new Object();
private int index = 0;
@Override
public void run() {
while (true) {
if (index % 2 == 0) {
synchronized (SellTicket.class) {//此处加上同一把锁,当线程进来先看是否持有锁,有则可以进去执行,只有当前一个线程执行完代码块之后,下一个线程才可以进来
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//下面两行代码会导致卖票出现两种问题 :1.相同的票卖了多次 2.出现了负数的票
System.out.println("第" + tickets + "张票被" + Thread.currentThread().getName() + "卖出");
tickets--;
}
}
} else {
// synchronized (obj) {//此处加上同一把锁,当线程进来先看是否持有锁,有则可以进去执行,只有当前一个线程执行完代码块之后,下一个线程才可以进来
// if (tickets > 0) {
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//
// //下面两行代码会导致卖票出现两种问题 :1.相同的票卖了多次 2.出现了负数的票
// System.out.println("第" + tickets + "张票被" + Thread.currentThread().getName() + "卖出");
// tickets--;
// }
// }
sellTicket();
}
index++;
}
}
private static synchronized void sellTicket() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//下面两行代码会导致卖票出现两种问题 :1.相同的票卖了多次 2.出现了负数的票
System.out.println("第" + tickets + "张票被" + Thread.currentThread().getName() + "卖出");
tickets--;
}
}
}
5.线程安全的类
代码演示:
import java.util.*;
public class ThreadDemo2 {
public static void main(String[] args) {
StringBuffer sf = new StringBuffer();
StringBuilder sb = new StringBuilder();
Vector<String> v = new Vector<>();
ArrayList<String> array = new ArrayList<>();
Hashtable<String,String> ht = new Hashtable<>();
HashMap<String,String> hm = new HashMap<>();
//集合通常使用以下Collections工具类的方法来保证线程同步
List<String> list = Collections.synchronizedList(new ArrayList<>());
}
}
6.Lock锁
代码演示:
定义卖票类:(测试类省略参考以前案例)
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SellTicket implements Runnable {
private int tickets = 100;
private Lock lock = new ReentrantLock(); //创建Lock类对象
@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();//在此处释放锁
}
}
}
}
(三)生产者消费者
1.概述
2.生产者消费者案例
定义奶箱类(Box):
public class Box {
private int milk; //奶箱中奶的数量
private boolean flag = false; //奶箱的状态 ,默认开始奶箱为空的
public synchronized void put(int milk) {
//当奶箱有奶,就等待消费
if (flag) {
try {
wait(); //wait()方法需要在线程同步里使用 所以方法加synchronized
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//当奶箱没有奶,就生产
this.milk = milk;
System.out.println("生产了" + milk + "瓶奶");
//生产完改变奶箱的状态
flag = true;
//唤醒其他线程
notifyAll();
}
public synchronized void get() {
//当奶箱没有奶,就等待生产
if (!flag) { //此时假设flag为false 只有if里面为true时才执行,所以加一个逻辑非!来执行代码块内容
try {
wait(); //wait()方法需要在线程同步里使用 所以方法加synchronized
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//当奶箱有奶,就消费
System.out.println("用户拿到了第" + milk + "瓶奶");
//消费完,改变奶箱的状态
flag = false;
//唤醒其他线程启动
notifyAll();
}
}
定义生产者类:
public class Producer implements Runnable {
private Box b;
public Producer(Box b) {
this.b = b;
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
b.put(i);
}
}
}
定义消费者类:
public class Customer implements Runnable{
private Box b;
public Customer(Box b) {
this.b = b;
}
@Override
public void run() {
while(true) {
b.get();
}
}
}
定义测试类:
public class BoxDemo {
public static void main(String[] args) {
//创建奶箱对象 这是共享区域
Box b = new Box();
//创建生产者对象
Producer p = new Producer(b);
//创建消费者对象
Customer c = new Customer(b);
//创建两个线程对象
Thread td1 = new Thread(p,"生产者");
Thread td2 = new Thread(c,"消费者");
//启动线程
td1.start();
td2.start();
}
}