多线程详解
多线程详解
多线程是java学习中重要的一部分,我们会通过多线程实现同时操作同一资源的程序
进程和线程
在了解多线程之前我们先学习一些基本知识:
进程:是正在运行的程序
- 是系统进行资源分配和调用的独立单位
- 每个进程都有它自己的内存空间和系统资源
线程:是进程中的单个顺序控制流,是一条执行路径
- 单线程:一个进程如果只有一条执行路径,则称为单线程程序
- 多线程:一个进程如果有多条执行路径,则被成为多线程程序
多线程的实现方法
方法1:继承Thread类
- 定义一个类MyThread继承Thread类
- 在MyThread类中重写run()方法
- 创建MyThread类的对象
- 启动线程
两个小问题:
- 为什么要重写run()方法?
- 因为run()是用来封装被线程执行的代码
- run()方法和start()方法的区别?
- run():封装线程执行的代码,直接调用,相当于普通方法的调用
- start():启动线程,然后由JVM调用此线程的run()方法
下面给出示例代码:
public class Demo1 {
public static void main(String[] args) {
//首先创建对象
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
//调用线程(注意:使用start方法启动线程,其中start方法会执行run方法)
my1.start();
my2.start();
}
}
public class MyThread extends Thread {
//重写run方法,实现线程化
@Override
public void run() {
for (int i=0;i<500;i++){
System.out.println(i);
}
}
}
方法2:实现Runnable接口
- 定义一个类MyRunnable实现Runnable接口
- 在MyRunnable类里重写run方法
- 创建MyRunnable类的对象
- 创建Thread类的对象,参数是MyRunnable对象
- 启动线程
采用接口的好处:
- 避免了Java单继承的局限性
- 适合多个相同的程序的代码去处理同一个资源的情况,把线程和程序的代码以及数据有效分离,较好得体现了面向对象的设计思想
下面给出示例代码:
public class Demo1 {
public static void main(String[] args) {
//首先创造MyRunnable类
MyRunnable my = new MyRunnable();
//然后创建Thread对象
Thread t1 = new Thread(my);
Thread t2 = new Thread(my);
//可以采用带线程名的方法创造对象
Thread t3 = new Thread(my,"吕小布");
//下面正常运行即可
t1.start();
t2.start();
t3.start();
}
}
public class MyRunnable implements Runnable{
//重写run方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//这里没有继承Thread,所以不能直接使用getName
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
设置和获得多线程名称
Thread类中设置和获得多线程名称的方法:
- void setName(String name):将此线程的名称更改为参数name
- String getName():返回此线程的名称
- 通过构造方法也可以设置线程名称
如何获得main()方法的线程名称?
- public static Thread currentThread():返回当前正在执行的线程对象的引用
下面给出示例代码:(setName和getName)
public class Demo1 {
public static void main(String[] args) {
//首先创建对象
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
//我们可以通过setName()方法改变线程名
my1.setName("吕小布");
my2.setName("吕大布");
//调用线程(注意:使用start方法启动线程,其中start方法会执行run方法)
my1.start();
my2.start();
}
}
public class MyThread extends Thread{
//重写run方法,实现线程化
@Override
public void run() {
for (int i=0;i<500;i++){
//我们可以采用getName()方法获得线程名
System.out.println(getName() + ":" + i);
}
}
}
下面给出示例代码:(通过构造方法重写线程名以及获得main的线程名)
public class Demo1 {
public static void main(String[] args) {
//我们可以直接采用带参构造创造有线程名的线程
//但注意:需要在MyThread里重写构造方法并super父类name
MyThread my1 = new MyThread("吕小布");
MyThread my2 = new MyThread("吕大布");
//调用线程(注意:使用start方法启动线程,其中start方法会执行run方法)
my1.start();
my2.start();
//这里再多讲一个Thread的Static方法
//currentThread()是static方法,可以获得其当前运行的线程
System.out.println(Thread.currentThread().getName());
}
}
public class MyThread extends Thread{
//重写构造方法
public MyThread(){
}
public MyThread(String name){
super(name);
}
//重写run方法,实现线程化
@Override
public void run() {
for (int i=0;i<10;i++){
//我们可以采用getName()方法获得线程名
System.out.println(getName() + ":" + i);
}
}
}
线程调度
线程有两种调度模型:
- 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
- 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么随机选择一个,优先级高的线程获取的CPU时间片相对较高
Java采用的是抢占式调度模型
假如计算机只有一个CPU,那么CPU在某一时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性的。
Thread类中设置和获取优先级的方法:
- public final int getPriority():返回此线程的优先级
- public final void setPriority(int newPriority):更改此线程的优先级
线程默认优先级是5,线程优先级范围是1~10;线程优先级高仅仅代表线程获得时间片的几率高。
下面给出示例代码:
public class Demo1 {
public static void main(String[] args) {
//首先创建对象
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
MyThread my3 = new MyThread();
//我们可以通过setName()方法改变线程名
my1.setName("刘备");
my2.setName("关羽");
my3.setName("张飞");
//我们先来查看未设置时优先级度数以及最高最低优先级(最低1,最高10,默认5)
System.out.println(my1.getPriority());
System.out.println(Thread.NORM_PRIORITY);
System.out.println(Thread.MIN_PRIORITY);
System.out.println(Thread.MAX_PRIORITY);
//然后我们通过修改优先级控制线程顺序
my1.setPriority(10);
my2.setPriority(5);
my3.setPriority(1);
//调用线程(注意:使用start方法启动线程,其中start方法会执行run方法)
my1.start();
my2.start();
my3.start();
}
}
public class MyThread extends Thread{
//重写run方法,实现线程化
@Override
public void run() {
for (int i=0;i<10;i++){
//我们可以采用getName()方法获得线程名
System.out.println(getName() + ":" + i);
}
}
}
线程控制
下面给出线程控制相关方法:
方法名 | 说明 |
---|---|
static void sleep(long millis) | 使当前正在执行的线程停留指定的毫秒数 |
void join() | 等待这个线程死亡 |
void setDaemon(boolean on) | 将该线程标记为守护线程,当运行的线程都是守护线程时,程序终止 |
下面给出示例代码:(sleep方法)
public class Demo1 {
public static void main(String[] args) {
//首先创建对象
Thread1 my1 = new Thread1();
Thread1 my2 = new Thread1();
Thread1 my3 = new Thread1();
//我们可以通过setName()方法改变线程名
my1.setName("刘备");
my2.setName("孙权");
my3.setName("曹操");
//调用线程(注意:使用start方法启动线程,其中start方法会执行run方法)
//这里数据出来就会每隔1s出来一次,且每次不是同个进程连续执行
my1.start();
my2.start();
my3.start();
}
}
//这里我们讲解sleep
public class Thread1 extends Thread{
//重写run方法,实现线程化
@Override
public void run() {
for (int i=0;i<500;i++){
//我们可以采用getName()方法获得线程名
System.out.println(getName() + ":" + i);
//我们在输出后,让他们等到1s(注意这里单位是ms)
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
下面给出示例代码:(join方法)
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
//首先创建对象
Thread2 my1 = new Thread2();
Thread2 my2 = new Thread2();
Thread2 my3 = new Thread2();
//我们可以通过setName()方法改变线程名
my1.setName("刘备");
my2.setName("孙权");
my3.setName("曹操");
//调用线程(注意:使用start方法启动线程,其中start方法会执行run方法)
my1.start();
//这里使用join()方法,我们使用join之后,只有这个进程结束之后,才继续运行其他部分
my1.join();
my2.start();
my3.start();
}
}
public class Thread2 extends Thread{
//重写run方法,实现线程化
@Override
public void run() {
for (int i=0;i<10;i++){
//我们可以采用getName()方法获得线程名
System.out.println(getName() + ":" + i);
}
}
}
下面给出示例代码:(setDaemon方法)
public class Demo3 {
public static void main(String[] args) {
//首先创建对象
Thread3 my2 = new Thread3();
Thread3 my3 = new Thread3();
//我们可以通过setName()方法改变线程名
my2.setName("关羽");
my3.setName("张飞");
//我们设置主线程main改名为刘备
Thread.currentThread().setName("刘备");
//我们把关羽和张飞设置为守护线程
my2.setDaemon(true);
my3.setDaemon(true);
//调用线程(注意:使用start方法启动线程,其中start方法会执行run方法)
my2.start();
my3.start();
//我们给主线程设置一些动作
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
//然后当我们运行后会发现,主线程完全运行,然后守护线程逐渐死亡不运行完毕
}
}
public class Thread3 extends Thread{
//重写run方法,实现线程化
@Override
public void run() {
for (int i=0;i<100;i++){
//我们可以采用getName()方法获得线程名
System.out.println(getName() + ":" + i);
}
}
}
线程生命周期
这里我们给出示例图(来自b站黑马程序员)
案例:卖票
需求:某电影院目前正在上映国产大片,共100张票,而它有三个窗口卖票,请设计一个程序模拟
下面给出示例代码:
public class SellTicketDemo {
public static void main(String[] args) {
//创建SellTicket对象
SellTicket st = new SellTicket();
//创建售卖机
Thread sell1 = new Thread(st,"售卖机1号");
Thread sell2 = new Thread(st,"售卖机2号");
Thread sell3 = new Thread(st,"售卖机3号");
//开始运行即可
sell1.start();
sell2.start();
sell3.start();
}
}
public class SellTicket implements Runnable{
//这里设置共有ticket
public static int ticket = 100;
//重构方法
@Override
public void run() {
//因为程序一直运行,需要在死循环里进行
while (true){
//当票还有时,售卖,ticket减一
if (ticket>0){
System.out.println(Thread.currentThread().getName() + "正在售卖第" + (101-ticket) + "张票");
ticket--;
}
}
}
}
这里我们稍微给出编译结果:
售卖机2号正在售卖第1张票
售卖机2号正在售卖第2张票
售卖机1号正在售卖第1张票
售卖机3号正在售卖第1张票
售卖机1号正在售卖第4张票
售卖机2号正在售卖第3张票
售卖机1号正在售卖第6张票
结论:
我们会发现相同的票会出现很多次,这是因为当一个线程运行时,另一个进程可能也同步运行,他们先后顺序执行输出操作,未执行ticket--的操作,导致出现多次相同票。
多线程安全问题
多线程出现问题的环境:
- 是否是多线程环境
- 是否存在共享数据
- 是否有多条语句操作共享数据
那么我们如何解决多线程安全问题呢?
- 基本思想:让程序没有安全问题的环境
如何实现?
- 把多条语句操作共享数据的代码锁起来,让任意时刻只有一个线程执行
- Java提供了同步代码块和同步方法来解决
同步代码块
锁多条语句操作共享数据,可以通过同步代码块来实现:
synchronized(任意对象){
多条语句操作共享数据的代码
}
这里我们同样给出同步方法的格式:
//同步方法(这里的对象是 this)
public synchronized 返回类型 方法名(){
}
//同步静态方法(这里的对象是 类名.class)
public static synchronized 返回类型 方法名(){
}
同步的好处和弊端:
- 好处:解决了多线程的数据安全问题
- 弊端:当线程很多时,因为每个线程都要判断是否上锁,降低程序运行速率
下面给出代码示例:(同步代码块)
public class SellTicketDemo {
public static void main(String[] args) {
//创建SellTicket对象
Demo78.SellTicket st = new SellTicket();
//创建售卖机
Thread sell1 = new Thread(st,"售卖机1号");
Thread sell2 = new Thread(st,"售卖机2号");
Thread sell3 = new Thread(st,"售卖机3号");
//开始运行即可
sell1.start();
sell2.start();
sell3.start();
}
}
public class SellTicket implements Runnable{
//这里设置共有ticket
public static int ticket = 100;
//注意:这里需要创建private的Object对象,使所以线程共用一把锁
private Object obj = new Object();
//重构方法
@Override
public void run() {
//因为程序一直运行,需要在死循环里进行
while (true){
//采用同步代码块来实现锁方法
synchronized (obj){
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "正在售卖第" + (101-ticket) + "张票");
ticket--;
}
}
}
}
}
下面给出代码示例:(同步方法)
public class SellTicketDemo {
public static void main(String[] args) {
//创建SellTicket对象
SellTicket st = new SellTicket();
//创建售卖机
Thread sell1 = new Thread(st,"售卖机1号");
Thread sell2 = new Thread(st,"售卖机2号");
Thread sell3 = new Thread(st,"售卖机3号");
//开始运行即可
sell1.start();
sell2.start();
sell3.start();
}
}
public class SellTicket implements Runnable{
//这里设置共有ticket
public static int ticket = 100;
private Object obj = new Object();
//重构方法
@Override
public void run() {
//因为程序一直运行,需要在死循环里进行
while (true){
//采用同步方法来实现锁方法
//这里可以采用SellMethod方法中任意一个
sellMethod3();
}
}
//sellMethod1:直接把内容带入方法中,在方法中使用同步代码块
private void sellMethod1() {
synchronized (obj){
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "正在售卖第" + (101-ticket) + "张票");
ticket--;
}
}
}
//sellMethod2:直接把方法变成同步方法(这里的锁是class本身,即this)
private synchronized void sellMethod2() {
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "正在售卖第" + (101-ticket) + "张票");
ticket--;
}
}
//sellMethod3:直接把方法变成静态同步方法(这里的锁是class本身,即类名.class,即SellTicket.class)
private static synchronized void sellMethod3() {
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "正在售卖第" + (101-ticket) + "张票");
ticket--;
}
}
}
线程安全的类
我们给出一些线程安全的类,你可以在多线程的编译过程中直接使用以下类:
- StringBuffer
- 线程安全,可变的字符序列
- JDK5之后由StringBuilder代替,在无多线程情况下使用StringBuilder,在多线程情况下使用StringBuffer
- Vector
- 线程安全,数组
- JDK1.2之后由ArrayList代替,在无多线程情况下使用ArrayList,在多线程情况下使用Vector
- Hashtable
- 该类实现了一个哈希表,它将键映射到值;任何非null对象都可以作为键或值
- JDK1.2之后由HashMap代替,在无多线程情况下使用HashMap,在多线程情况下使用Hashtable
Lock锁
同步代码块和同步方法等同于Lock锁,但我们无法清晰看出锁的印记
于是Java给出Lock类清晰给出加锁和释放锁的方法
- void lock():获得锁
- void unlcok():释放锁
Lock是接口,不能直接实例化,所以我们采用它的实现类ReentrantLock来实例化
ReentrantLock构造方法:
- ReentrantLock():创建一个Reentrantlock实例
下面给出代码示例:
public class SellTicketDemo {
public static void main(String[] args) {
//创建SellTicket对象
SellTicket st = new SellTicket();
//创建售卖机
Thread sell1 = new Thread(st,"售卖机1号");
Thread sell2 = new Thread(st,"售卖机2号");
Thread sell3 = new Thread(st,"售卖机3号");
//开始运行即可
sell1.start();
sell2.start();
sell3.start();
}
}
public class SellTicket implements Runnable{
//这里设置共有ticket
public static int ticket = 100;
//设置锁Lock(Lock是接口,所以需要采用继承类ReentrantLock来实现)
public Lock lock = new ReentrantLock();
//重构方法
@Override
public void run() {
//因为程序一直运行,需要在死循环里进行
while (true){
//这里锁上
lock.lock();
//当票还有时,售卖,ticket减一
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "正在售卖第" + (101-ticket) + "张票");
ticket--;
}
//这里开锁
lock.unlock();
}
}
}
生产者消费者模式概述
生产者消费者模式一个非常经典的多线程协作模式
实际上主要包含两类线程:
- 一类是生产者线程用于生产数据
- 一类是消费者线程用于消费数据
同时需要一个公共仓库
- 生产者生产数据后放置于共享数据区,并不关心消费者行为
- 消费者只需要从共享数据区获得数据,并不关心生产者行为
为了体现生产和消费的等待和唤醒状态,Java提供了Object类的一些方法:
方法名 | 说明 |
---|---|
void wait() | 导致当前线程等待,直到其他线程采用唤醒方法(notify或notifyAll) |
void notify() | 唤醒正在等待对象监视器的单个线程 |
void notifyAll() | 唤醒正在等待对象监视器的所有线程 |
生产者消费者模式案例
生产者消费者案例包含的类:
- 奶箱类(Box):定义一个成员变量,表示第X瓶奶,提供存储牛奶和获取牛奶的方法
- 生产者类(Producer):实现Runnable接口,重写run方法,调用存储牛奶的方法
- 消费者类(Customer):实现Runnable接口,重写run方法,调用获得牛奶的方法
- 测试类(BoxDemo):里面包含main方法
- 创建奶箱对象,这里是共享数据区域
- 创建生产者对象,把奶箱作为参数传递
- 创建消费者对象,把奶箱作为参数传递
- 创建两个线程对象,分别把生产者对象和消费者对象作为参数传递
- 启动线程
下面给出示例代码:
public class BoxDemo {
public static void main(String[] args) {
//创造一个奶盒
Box b = new Box();
//创造生产者对象,把奶盒当作公共资源放进去
Producer p = new Producer(b);
//创造顾客对象,把奶盒当作公共资源放进去
Customer c = new Customer(b);
//创造线程
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
//开始线程运行
t1.start();
t2.start();
}
}
public class Box {
//定义成员变量,表示第x瓶奶
private int milk;
//定义Box状态
private boolean state = false;
//提供存储牛奶的方法(要使用synchronized指定一次只能执行一个)
public synchronized void put(int milk){
//如果还有奶,暂时不需要存入,我们就将这个线程挂起
if (state){
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//如果没有奶,我们需要把第x瓶奶放入
this.milk = milk;
System.out.println("送奶工将第" +this.milk + "瓶奶放入奶箱");
//然后我们修改奶瓶状态,使其他线程苏醒
state = true;
notifyAll();
}
//提供获得牛奶的方法(要使用synchronized指定一次只能执行一个)
public synchronized void get(){
//如果没有奶,我们需要等待,将这个线程挂起
if (!state){
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//如果有奶,我们取奶
System.out.println("用户拿到第" + this.milk + "瓶奶");
//然后我们修改状态,并使其他线程苏醒
state = false;
notifyAll();
}
}
public class Producer implements Runnable{
private Box b;
public Producer(Box b) {
this.b = b;
}
@Override
public void run() {
//依次放入i瓶奶
for (int i = 1; i <= 5; 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();
}
}
}
结束语
好的,关于Java相关的多线程我们就讲解到这里