java的多线程,
java的多线程,
多线程
进程:进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程
什么是多线程呢?即就是一个程序中有多个线程在同时执行。
通过下图来区别单线程程序与多线程程序的不同:
单线程:
多线程:
主线程
回想我们以前学习中写过的代码,当我们在dos命令行中输入java空格类名回车后,启动JVM,并且加载对应的class文件。虚拟机并会从main方法开始执行我们的程序代码,一直把main方法的代码执行结束。如下代码演示:
/*
*进程:进程就是系统中运行的一个应用程序,我们可以打开任务管理器查看进程那一栏
*
*线程:进程中包含一个或多个线程,线程控制进程的执行
*
*也可以说应用程序中有一个或多个线程
*
*main方法其实就是一个线程:所以main方法顺序执行
*main方法所在的线程叫 主线程
*/
public class Demo01 {
public static void main(String[] args) {
System.out.println(1/0);
}
}
我们可以看到 System.out.println(1/0)抛出异常, Exception in thread main,说明该异常是从main线程中抛出的,说明main方法在一个线程中,这个线程就是主线程
Thread类概述
该如何创建线程呢?通过API中搜索,查到Thread类。通过阅读Thread类中的描述。Thread是程序中的执行线程。Java 虚拟机允许应用程序并发地运行多个执行线程。
继续阅读,发现创建新执行线程有两种方法。
创建线程的步骤:
1 定义一个类继承Thread。
2 重写run方法。
3 创建子类对象,就是创建线程对象。
4 调用start方法,开启线程并让线程执行,同时还会告诉jvm去调用run方法。
/*回想自定义异常:
* Throwable
* Exception
* Error
* 自己定义一个类继承Exception/RuntimeException
* class AgeException extends Exception{
*
* }
* class Frog extends Animal{
*
* }
* 自定义线程思想跟上面一致
* class 类名 extends Thread{
* //重写run方法
* public void run(){
* //线程要干的事(线程要执行的代码)
* }
* }
* 执行结果分析:
* 发现每次打印顺序不同,但是main线程中的代码和自定义线程中的代码一定执行完
*/
public class Demo02 {
public static void main(String[] args) {
ThreadDemo01 td=new ThreadDemo01();
//td.run();//不会开启任何自定义线程,这句话仅仅相当于创建对象调用方法
td.start();
for(int i=0;i<10;i++){
System.out.println("main..."+i);
}
}
}
public class ThreadDemo01 extends Thread{
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println("run..."+i);
}
}
}
思考:线程对象调用 run方法和调用start方法区别?
线程对象调用run方法不开启线程。仅是对象调用方法。线程对象调用start开启线程,并让jvm调用run方法在开启的线程中执行。
继承Thread类原理
我们为什么要继承Thread类,并调用其的start方法才能开启线程呢?
继承Thread类:因为Thread类用来描述线程,具备线程应该有功能。那为什么不直接创建Thread类的对象呢?如下代码:
Thread t1 = new Thread();
t1.start();//这样做没有错,但是该start调用的是Thread类中的run方法,而这个run方法没有做什么事情,更重要的是这个run方法中并没有定义我们需要让线程执行的代码。
Thread t1 = new Thread();
t1.start();//这样做没有错,但是该start调用的是Thread类中的run方法,而这个run方法没有做什么事情,更重要的是这个run方法中并没有定义我们需要让线程执行的代码。
创建线程的目的是什么?
是为了建立程序单独的执行路径,让多部分代码实现同时执行。也就是说线程创建并执行需要给定线程要执行的任务。
对于之前所讲的主线程,它的任务定义在main函数中。自定义线程需要执行的任务都定义在run方法中。
Thread类run方法中的任务并不是我们所需要的,只有重写这个run方法。既然Thread类已经定义了线程任务的编写位置(run方法),那么只要在编写位置(run方法)中定义任务代码即可。所以进行了重写run方法动作。
多线程的内存图解
每个线程执行的代码都会在一个栈中,CPU在多个线程的run方法的代码中做着随机切换动作
Thread类的方法
开启的线程都会有自己的独立运行栈内存,那么这些运行的线程的名字是什么呢?该如何获取呢?既然是线程的名字,按照面向对象的特点,是哪个对象的属性和谁的功能,那么我们就去找那个对象就可以了。查阅Thread类的API文档发现有个方法是获取当前正在运行的线程对象。还有个方法是获取当前线程对象的名称。既然找到了,我们就可以试试。
public class ThreadDemo02 extends Thread{
@Override
public void run() {//由于Thread类中run方法没有抛出任何异常
//子类继承Thread类重写run方法也不能抛出任何异常
/* try{
Thread.sleep(1000);//1s=1000ms
}catch(InterruptedException e){
}*/
for(int i=0;i<10;i++){
System.out.println(getName()+"..."+i);
}
}
}
创建线程方式—实现Runnable接口
创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后创建Runnable的子类对象,传入到某个线程的构造方法中,开启线程。
为何要实现Runnable接口,Runable是啥玩意呢?继续API搜索。
查看Runnable接口说明文档:Runnable接口用来指定每个线程要执行的任务。包含了一个 run 的无参数抽象方法,需要由接口实现类重写该方法。
创建线程的步骤。
1、定义类实现Runnable接口。
2、覆盖接口中的run方法。。
3、创建Thread类的对象
4、将Runnable接口的子类对象作为参数传递给Thread类的构造函数。
5、调用Thread类的start方法开启线程。
public class RunnableDemo01 implements Runnable{
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"..."+i);
}
}
}
/*创建线程的第二种方式:
* 实现Runnable接口
* class 类名 implements Runnable{
* public void run(){
* //线程执行的代码
* }
* }
* Thread类中的构造方法:
* Thread(Runnable target)
*/
public class Demo01 {
public static void main(String[] args) {
RunnableDemo01 rd=new RunnableDemo01();
/*
* new Thread()是开启一个线程
* 传入的rd:告诉线程执行的代码
* start():JVM调用run方法
*
*/
new Thread(rd).start();
for(int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+"..."+i);
}
}
}
实现Runnable的原理
为什么需要定一个类去实现Runnable接口呢?继承Thread类和实现Runnable接口有啥区别呢?
实现Runnable接口,避免了继承Thread类的单继承局限性。覆盖Runnable接口中的run方法,将线程任务代码定义到run方法中。
创建Thread类的对象,只有创建Thread类的对象才可以创建线程。线程任务已被封装到Runnable接口的run方法中,而这个run方法所属于Runnable接口的子类对象,所以将这个子类对象作为参数传递给Thread的构造函数,这样,线程对象创建时就可以明确要运行的线程的任务。
实现Runnable的好处
第二种方式实现Runnable接口避免了单继承的局限性,所以较为常用。实现Runnable接口的方式,更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务。继承Thread类,线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,有又有线程任务。实现runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦。
线程的匿名内部类使用
使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。
/*
*匿名内部类
* new 父类名/父接口名(){
* //重写父类或父接口的方法
* };
* 上面的创建匿名内部类完成两个操作:
* 1.底层会创建一个父类或父接口的匿名子类或匿名实现类
* 2.还会创建这个子类或实现类的对象
* class 匿名 extends 父类{
*
* }
* new 匿名();
*/
public class Demo02 {
public static void main(String[] args) {
//对继承Thread类使用匿名内部类方式
Thread t=new Thread(){ //多态
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(getName()+"..."+i);
}
}
};
t.start();
}
}
public class Demo02 {
public static void main(String[] args) {
//对实现Runnable接口采用匿名内部类方式
Runnable r=new Runnable() {
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"..."+i);
}
}
};
new Thread(r).start();
}
}
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “功夫熊猫3”,本次电影的座位共100个(本场电影只能卖100张票)。
需要窗口,采用线程对象来模拟;需要票,Runnable接口子类来模拟
/*多线程卖票
* 用三个售票窗口卖100张票
* 从第100张一直卖到第1张
* 例如:
* A窗口 B窗口 C窗口
* 100 98 96
* 99 97 95
* .......
* 用线程模拟售票窗口
* 线程执行的代码卖票通过打印卖第几张票来模拟
*
*/
public class Demo01 {
public static void main(String[] args) {
//1.创建Runnable的子类对象
Ticket t=new Ticket();
//2.创建三个线程模拟三个窗口,让它们去卖这100张票
new Thread(t).start();//Thread-0
new Thread(t).start();//Thread-1
new Thread(t).start();//Thread-2
}
}
public class Ticket implements Runnable{
//存储卖的票数
private int ticket=100;
@Override
public void run() {
while(true){
//打印卖第几张票
/* try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}*/
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"卖第"
+ticket--+"张");//后置自减,先使用再自减
}
}
}
}
1 结果
出现0票,-1票
把睡眠放在if(ticket>0)后面也能有错票产生
/*我们需要通过同步代码块来保证共享数据的安全
*同步代码块
* synchronized(锁对象){//锁对象可以是任意对象
* //需要保证安全的代码
* //我们一般把涉及共享数据代码放在里面
*
* }
* 注意事项:保证所有的线程使用同一个锁对象
*
*/
public class Ticket implements Runnable{
//存储卖的票数
private int ticket=100;
Object obj=new Object();//做为锁对象
@Override
public void run() {
while(true){
//打印卖第几张票
/* try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}*/
synchronized(obj){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"卖第"
+ticket--+"张");//后置自减,先使用再自减
}
}
}
}
}
/*第二种保证安全的机制:
* 同步方法格式:
* 修饰符 synchronized 返回值类型 方法名(){
* //代码
* }
* 同步方法也相当于同步代码块,同步方法的锁对象是this
* 等效代码
* 修饰符 返回值类型 方法名(){
* synchronized(this){
* //代码
*
* }
* }
*/
public class Ticket02 implements Runnable {
// 存储卖的票数
private int ticket = 100;
@Override
public void run() {
//Thread-0
while (true) {
// 打印卖第几张票
method01();
}
}
private synchronized void method01() {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "卖第"
+ ticket-- + "张");// 后置自减,先使用再自减
}
}
}
线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
我们详细的解释一下为什么要使用线程池?
在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。
线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。
通常,线程池都是通过线程池工厂创建,再调用线程池中的方法获取线程,再通过线程去执行任务方法。
代码演示:
package com.whhp.thread04;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/*创建线程的时机:当我们new Thread()调用start()方法时候会创建线程
*销毁线程的时机:当线程中的run方法执行完,线程会被销毁
*创建和销毁动作都比较消耗资源
*
*线程池
*
*/
public class Demo01 {
public static void main(String[] args) {
//创建一个线程池,线程池中有三个线程
ExecutorService es=Executors.newFixedThreadPool(3);
//向线程池中提交任务
es.submit(new Task());
}
}
class Task implements Runnable{
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"..."+i);
}
}
相关文章
- 暂无相关文章
用户点评