1. 基础介绍
线程有四种创建方式,但是我只记录两种,还有的创建方式是通过实现Callable接口,并且重写其中的call方法(有一点要注意,就是只有线程执行完毕才能收到返回值);
- 程序:是为了完成某种任务而使用某一个编程语言编写的一组指令的集合;(可以理解成代码)
- 进程:是指程序的执行过程,或是正在运行的程序,属于一个动态的过程;
- 线程:由进程进一步细化,是一个程序内部的一条执行途径(比如在Java中一个main函数其实就是一个线程,它是从main开始执行);
- 线程分为守护线程和用户线程;守护线程(如垃圾回收机制的线程)用来服务用户线程;
- 多线程:指的就是一个进程同时有多个线程并行执行;
- 并行:多个CPU执行多个任务;
- 并发:一个CPU执行多个任务;(详细的看一看操作系统,通过时间片完成)
注意:一个简单的Java.exe应用程序,运行的话,其中至少有三个线程;一个main(主线程),一个gc()垃圾回收机制(垃圾回收线程,它的垃圾回收是自动的),还有一个异常处理线程;
2.线程的创建和使用
- 第一种创建线程的方法
//自己创建一个类继承Thread类;然后就重写其中的run方法;run方法中写你要执行的动作;
class testThread extends Thread {
// 重写run方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("线程:"+Thread.currentThread().getName()+":" + i);
}
}
}
public class ThreadCreate {
public static void main(String[] args) {
// 创建一个对象调用start方法,不过这个方法是Thread中的;
testThread t = new testThread();
t.start();
for (int j = 0; j < 1000; j++) {
System.out.println("主线程:"+Thread.currentThread().getName()+":" + j);
}
}
}
它的结果就证明了线程之间的运行是并发的,就是通过时间片来管理进行;不是一种顺序执行;
Thread中的常用方法:
方法名 | 方法介绍 |
---|---|
start() | 启动当前线程,调用当前线程的run方法 |
run() | run方法一般都会被重写,将需要执行的操作写入此方法 |
currentThread() | 该方法返回当前执行代码的线程;属于静态方法,无需对象调用; |
getName() | 获得当前线程的名字 |
setName() | 设置当前线程的名字 |
yield() | 释放当前cpu中执行的线程,(简单来说就是用来给同级线程做轮换,当然有时不一定成功,因为可能还是会选中当时释放的线程) |
join() | 该方法主要是用于在A线程中调用B线程,这是A线程就被阻塞了,只有当B线程运行完毕了,A线程才能继续; |
sleep(time) | 这个方法可以使某个线程进入“睡眠状态”,就是让一个线程进行阻塞time毫秒(1000毫秒等于一秒), |
isAlive() | 该方法返回一个boolean值,判断线程是否还存活 |
- yeild()方法运行演示:(每次运行到i%2==1的时候就会释放子线程,但是有时候cpu的运行权又会被子线程重新抢回去 😃)
- join()方法运行演示:(在主线程中调用子线程,直到子线程运行完,才会重新调用主线程)
- 第二种创建线程的方法
就是指写一个类实现Runnable接口,然后重写其中的run方法;需要执行的操作写在run方法中;
public class RunableText implements Runnable {
//重写Runnable中的方法,
@Override
public void run() {
for (int i = 0; i < 10; i++) {
/*
由于这个方法已经不是Thread类中的方法,而是Runnable
接口中的方法,在Runnable中没有getName等方法,所以只能直接通过Thread调用;
主要还是currentThread()是静态的返回一个Thread对象;
*/
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
public static void main(String[] args) {
// 创建一个实现Runnable接口的对象
RunableText runableText = new RunableText();
// 创建一个Thread对象,然后就是放入到Thread的构造方法中生成一个线程;
Thread thread = new Thread(runableText);
// 由于通过Thread类已经生成了一个线程;,于是就可以调用Thread线程中的方法;
thread.setName("子线程");
thread.start();//开启线程
Thread.currentThread().setName("主线程");
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
上述程序运行结果:
2. 线程的调度
注意: 在Java中线程的优先级并非绝对的,尝试之后发现并非优先级高就可以先全部执行,只能说优先级越高,那么执行的概率越大;
-
线程的各种优先级,如下图:(不同的优先级就会有不同的执行顺序;)
第一个是最低的优先级,第二个是默认优先级,第三个是最高优先级;
-
定义优先级的方法
方法名 方法介绍 getPriority() 返回当前线程的优先级值,就像上图一样 setPriority(int) 设置当前线程的优先级值
上述代码执行的结果: -
线程通信,常用的方法(在Object类中)
方法名 方法介绍 wait() 让一个线程陷入等待状态,还能释放它当前所持的锁(暂时不清楚什么是锁) notify() 唤醒正在等待的单个线程 notifyAll() 唤醒所有线程
3. 线程的生命周期
线程的生命周期,是主要记录在线程的类Thread.State,
按照上面的图片稍作解释;
第一个new状态,就是指线程刚刚创立,还没有调用start方法的时候;
第二个 RUNNABLE,则是指线程目前处于可运行状态,在等待需要的资源,就像是操作系统中的就绪;
第三个 BLOCKED,这个不是很理解,等继续学习之后再看看吧,但是在网上看来一下,大概就是类似于阻塞;
第四个 WAITING,这个大概就是理解为使用了wait()方法进入等待状态之后;
第五个TIMED_WAITING,是指使用wait(long)方法,进入等待状态后;
TERMINATED;最后一个就是线程已经终结;生命到了尽头;
4. 线程的同步
有时候,比如卖票的时候,如果只是使用上面的基础线程肯定是不能达到要求的,会出现重票,甚至是负票的现象;
如下面的问题程序:
public class RunTicket implements Runnable {
private static int ticket = 100;
@Override
public void run() {
while (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":" + ticket);
ticket--;
}
}
public static void main(String[] args) {
RunTicket run = new RunTicket();
Thread thread1 = new Thread(run);
Thread thread2 = new Thread(run);
Thread thread3 = new Thread(run);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
运行结果:
那么为什么出票会出现重复?
首先可以知道线程的运行顺序是不规则的,你不能知道cpu并发线程中的哪些线程会运行,哪些线程会停止,当第一个线程对象的ticket(ticket=100)没有运行ticket--时,第二个线程就已经取到了ticket的值(这时ticket=100),这时就会发生所谓的重票现象,因为他们共用一个对象run,所以也就共用一个ticket;
解决办法:
- 同步代码块,使用synchronized对需要同步的代码进行修饰;在这里我们可以直接同步那些使用了ticket的代码,意思就是只有一个对线程对象可以使用,这就是使用同步监视器(或称锁)的方法进行同步;
public class RunTicket implements Runnable {
private int ticket = 100;
//创建一个公用的对象,当锁
Kmdog kmdog = new Kmdog();
@Override
public void run() {
while (true) {
//当然这里就是使用锁的地方,锁也不一定非要这样写,有时候可以用this,考虑使用当前类来做锁;
synchronized (kmdog) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":" + ticket);
ticket--;
} else {
break;
}
}
}
}
public static void main(String[] args) {
RunTicket run = new RunTicket();
Thread thread1 = new Thread(run);
Thread thread2 = new Thread(run);
Thread thread3 = new Thread(run);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
class Kmdog {
}
最重要的是要使用公共的锁;
运行结果:
2. 通过同步方法;也可以达到上述的要求;详细请百度,不做赘述;
杂记,现学即现记(以后把所有东学西记的东西全部总结起来)
百度的时候无意中看到,在子类的构造方法中可以通过super调用父类的普通方法,尝试了一下还真的可以;又get到了一个新东西;
- 父类中的方法要是没有throws异常,那么子类就无法直接throws异常;
- 为什么static不能修饰局部变量,因为它修饰的是全局变量;
- 线程组:在java.lang.ThreadGroup类是用来定义一个线程组的;一个管理线程的类;
- 每个线程都拥有自己独立的栈和程序计数器;多个线程共享一个进程中的方法区;
- 接口与接口之间必须要通过extends继承,而不能通过implements实现;只有类才能实现接口;
我发现其实所有继承了Runnable接口的B接口,都可以通过实现B,来创建线程;比如(RunnableFuture);