最近在学java多线程,先对多线程做一个简单概要,后期再慢慢深入。
下面将从以下几点展开:
- 什么是多线程?为何要引入多线程?线程和进程之间的区别
- 多线程中常用方法小结
- 创建多线程的方式,Thread、Runnable俩种方式之间的区别?通过一个火车票的例子给出
- 线程生命周期
- 守护线程
1.什么是多线程?为何要引入多线程?进程和线程之间的区别
早期计算机,由于没有操作系统,一台计算机只能执行一个程序,并且这个程序能够访问计算机中所有资源。后来引入操作系统,操作系统使得计算机能够同时运行多个程序,并且每个程序是在不同的进程中执行,操作系统为每个进程分配资源。例如计算机中可以同时运行qq,浏览器等程序(进程是动态的,没有运行的程序不能称为进程)。
但是一个程序是一个进程,如何在一个进程中同时执行多个任务?比如说我运行qq,同时进行着和他人聊天以及接收文件,发送文件,如果没有多线程,一次只能干一件事,下一项任务必须等待上一项任务执行完毕。
串行化的工作方式无法使得资源得到充分利用,同时执行效率很低。比如说,在qq中,我先向一个好友发送了一个很大的文件,此时如果没有多线程技术,那么此时,我就需要等待文件发送完毕才能和另一个好友聊天。(很低效的工作方式)。
进程是由操作系统控制,操作系统能够同时运行的进程数量有限(视内存等因素决定,每个进程都需要占据一定的内存空间),线程是属于进程的一部分,可以在进程中创建多个线程,线程间独立执行(如果没有定义同步机制),多个线程共享进程资源。(所以多线程使用不当,容易引发问题,后期将对此做详细说明)。
综上,进程和线程都是为了提高资源利用率,提高工作效率诞生的,进程是由操作系统控制,能同时运行的进程数量有限,进程间相互独立(可以通过一些通信机制交换数据,学习操作系统)。线程是属于进程一部分,共享进程资源。
2.多线程中常用方法小结
线程创建:
- Thread()
- Thread(String name) 指定线程名称
- Thread(Runnable target)
- Thread(Runnable target,String name)
线程的方法:
- void start() : 启动线程
- String getName():返回该线程的名称
- void join():
- void join(long millis):等待该线程终止的最长时间为millis毫秒
- void join(long millis,int nanos)
- static void sleep()
- static void sleep(long millis):让当前线程在指定时间内休眠
- static void sleep(long millis,int nanos)
static void yield():暂定当前正在执行线程,让出cpu资源
获取Thread引用
static Thread currentThread():返回当前正在执行的线程对象引用
3.创建多线程的方式,Thread、Runnable俩种方式之间的区别?
Thread方法创建线程:
/**
*通过继承Thread类,并重写run方法
*/
public class Task extends Thread{
...
public void run(){
...
}
...
}
Thread thread = new Task();//创建Thread对象
thread.start();//线程启动
通过Runnable方法创建:
/**
* 实现Runnable接口,实现run方法
*/
public class Task implements Runnable{
...
public void run(){
...
}
...
}
Thread thread = new Thread(new Task());//创建Thread对象
thread.start();
举例:火车站有五张西安-北京的火车票,分三个窗口卖票。
1.通过Thread方式实现:
public class Main {
public static void main(String[] args){
//定义三个线程模拟三个窗口
Thread t1 = new TicketsThread("窗口1");
Thread t2 = new TicketsThread("窗口2");
Thread t3 = new TicketsThread("窗口3");
t1.start();
t2.start();
t3.start();
}
}
/**
* 定义卖票的任务
*/
class TicketsThread extends Thread{
private static int count = 5;//只有五张票,注意这里定义为static类型
private String name;
public TicketsThread(String name){
this.name = name;
}
@Override
public void run() {
while (count > 0) {
count--;
System.out.println(name + "卖了1张票,剩余票数为" + count);
}
}
}
窗口2卖了1张票,剩余票数为3
窗口3卖了1张票,剩余票数为2
窗口1卖了1张票,剩余票数为3
窗口3卖了1张票,剩余票数为0
窗口2卖了1张票,剩余票数为1
//每次执行结果会不一样,取决于线程抢占cpu的随机性,出现俩次剩余票数为3的情况是由于run操作的非原子性,一个线程刚刚执行完count--操作,另一个线程抢占了cpu,同样执行了count--操作
2.通过实现Runnable接口模拟
public class Main1 {
public static void main(String[] args) {
TicketsThread1 tickets = new TicketsThread1();
Thread t1 = new Thread(tickets, "窗口1");
Thread t2 = new Thread(tickets, "窗口2");
Thread t3 = new Thread(tickets, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
class TicketsThread1 implements Runnable{
private int count = 5;
@Override
public void run() {
while (count > 0){
count--;
System.out.println(Thread.currentThread().getName() + "卖了1张票,剩余票数为:"+count);
}
}
}
窗口1卖了1张票,剩余票数为:4
窗口3卖了1张票,剩余票数为:2
窗口3卖了1张票,剩余票数为:1
窗口3卖了1张票,剩余票数为:0
窗口2卖了1张票,剩余票数为:3
说明:在继承Thread中,将count定义为静态变量,如果不定义为静态变量,每个窗口都可以卖5张票,总共卖出去会是15张票,这是因为定义了三个线程,每个线程虽然共享进程内存资源,但是每个线程有自己的本地内存副本,线程操作自己的本地副本。(这些将会在后期内存可见性中详述)。
在实现Runnable方法中,只初始化了一个任务实例,所以每个线程只对这一个任务实例进行操作。
java中只支持单继承,Runnable方式可以改变单继承的缺陷。
4.线程生命周期
在java程序中,使用new创建一个线程实例,线程调用start()方法,进入就绪状态,(此时线程进入线程队列,等待获取cpu资源),一旦获得cpu资源,线程进入运行状态,执行run方法,执行完毕后,线程消亡。如果在执行过程中,遇到阻塞事件,比如说等待键盘输入,sleep(),则让出cpu资源,进入阻塞状态,当阻塞解除,进入就绪状态。
在API文档中给出线程有6种状态:
- NEW:至今尚未启动的线程
- RUNNABLE:正在执行的线程
- BLOCKED:阻塞
- WAITING:无限期地等待另一个线程来执行某一操作的线程的处于地中状态
- TIMED_WAITING:等待指定时间
- TERMINATED:已退出的线程
5.守护线程
线程分为用户线程和守护线程
用户线程是运行在前台,执行具体任务,我们可以看得见的
守护线程运行在后台,为用户线程提供服务。
守护线程特点:
- 一旦用户线程退出程序,守护线程没有守护对象,也就退出程序。
- 在守护线程中创建的线程也属于守护线程。
- 并不是所有线程都可以设置为守护线程,比如文件操作(一旦用户线程退出jvm,守护线程不能执行完任务退出,必须马上退出)
举例:jvm中一些监测线程,比如监测内存使用情况,GC
数据库连接池一些监测线程