程序、进程、线程的关系
- 程序是指令和数据的有序集合,其本身没有任何的运行意义,是一个静态的概念。
- 进程是执行程序一次执行的过程,是一个动态的概念,是系统分配资源的单位。
- 通常在一个进程中可以包含多个线程,当然一个进程中至少有一个线程,不然没有存在的意义,线程是CPU调度和执行的单位。
线程核心概念
- 线程就是独立的执行路径
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程、gc线程(垃圾回收线程)
- main()称之为主线程,为系统的入口,用于执行整个程序
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
- 线程会带来额外的开销,如cpu调度时间,并发控制开销
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
创建线程的三种方式
第一种创建方式(继承Thread类)
package com.wjr.Demo1;
/**
* @Auther:TrueLies
* @Date:2021/4/25-21:18
* @Description:com.wjr.Demo1
* @version:1.0
*/
//创建线程方式一:继承Thread类,重写run()方法,调用start开启线程
public class Demo extends Thread{
@Override
public void run(){
//run方法线程体
for (int i = 0; i < 20; i++) {
System.out.println("java代码--"+i);
}
}
public static void main(String[] args) {
//main线程,主线程
//创建一个线程对象
Demo demo = new Demo();
//调用start()方法开启线程
demo.start();//调用start方法,同时执行
for (int i = 0; i < 20; i++) {
System.out.println("实习实习实习"+i);
}
}
}
- 注意:线程开启不一定立即执行,由CPU调度执行
第二种创建方式(实现Runnable接口)
package com.wjr.Demo1;
/**
* @Auther:TrueLies
* @Date:2021/4/26-18:34
* @Description:com.wjr.Demo1
* @version:1.0
*/
//创建线程方式2:实现runnable接口,重写run方法,执行线程需要丢入runnable接口实现类,调用start方法
public class TestThread2 implements Runnable{
@Override
public void run(){
//run方法线程体
for (int i = 0; i < 20; i++) {
System.out.println("java代码--"+i);
}
}
public static void main(String[] args) {
//创建一个runnable接口的实现类对象
TestThread2 testThread2 = new TestThread2();
//创建线程对象,通过线程对象来开启我们的线程,代理
// Thread thread = new Thread(testThread2);
// thread.start();
new Thread(testThread2).start();
for (int i = 0; i < 20; i++) {
System.out.println("实习实习实习"+i);
}
}
}
两种方法的区别
- 第一种方法继承了Thread类,第二种方法实现了Runnable接口
- 第一种方法直接调用start方法,第二种方法从Thread类构造器中的实现类对象调用start方法
- 本质一样,因为第一种方法中Thread类也是继承了Runnable接口
- 建议使用实现Runnable接口的方法,避免了单继承的局限性,方便一个对象被多个线程使用
第三种创建方式(实现Callable接口)
下载网络图片:
package com.wjr.Demo2;
import com.wjr.Demo1.TestThread;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;
/**
* @Auther:TrueLies
* @Date:2021/4/26-21:55
* @Description:com.wjr.Demo2
* @version:1.0
*/
public class TestCallable implements Callable<Boolean> {
private String url;//网络图片地址
private String name;//保存的文件名
public TestCallable(String url, String name) {
this.name = name;
this.url = url;
}
//下载图片线程的执行体
@Override
public Boolean call() {
WebDownLoader webDownLoader = new WebDownLoader();
webDownLoader.downloader(url, name);
System.out.println("下载了文件名为:" + name);
return true;
}
//主线程
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable t1 = new TestCallable("https://i2.hdslb.com/bfs/archive/41575b1afec5305fa329d1cdf853373955d4e62a.jpg@380w_240h_100Q_1c.webp","狂神说java");
TestCallable t2 = new TestCallable("https://kuangstudy.oss-cn-beijing.aliyuncs.com/bbs/2021/04/13/kuangstudy5584e809-f8cd-42c5-b8e2-cc2e69a2f9a8.png","狂神");
TestCallable t3 = new TestCallable("https://kuangstudy.oss-cn-beijing.aliyuncs.com/bbs/2021/04/13/kuangstudy4abb3661-55d7-4e27-94bb-f666134b92ba.jpg","狂神SSM整合");
//创建执行服务:
ExecutorService ser = Executors.newFixedThreadPool(3);
//提交执行
Future<Boolean> r1 = ser.submit(t1);
Future<Boolean> r2 = ser.submit(t2);
Future<Boolean> r3 = ser.submit(t3);
//获取结果
boolean rs1 = r1.get();
boolean rs2 = r2.get();
boolean rs3 = r3.get();
//关闭服务
ser.shutdownNow();
}
}
//下载器
class WebDownLoader {
//下载方法
public void downloader(String url, String name) {
try {
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,downloader方法出现问题");
}
}
}
区别
-
实现Callable接口:
-
可以定义返回值
-
可以抛出异常
-
重写call方法
-
打印方式不同:
-
创建执行事务
-
ExecutorService ser = Executors.newFixedThreadPool(3);//3代表线程数
-
提交执行
-
Future<Boolean> r1 = ser.submit(t1);//boolean类型
-
获取结果
-
boolean rs1 = r1.get();
-
关闭服务
-
ser.shutdownNow();
-
-
-
继承Thread类、实现Runnable接口:
- 重写run()方法
- 直接使用start方法开启线程
线程状态
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换
新建状态(NEW)
当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,
并初始化其成员变量的值
就绪状态(RUNNABLE)
当线程对象调用了 start()方法之后,该线程处于就绪状态。 Java 虚拟机会为其创建方法调用栈和程序
计数器,等待调度运行。
运行状态(RUNNING)
如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。
阻塞状态(BLOCKED)
阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
等待阻塞(o.wait->等待对列):
运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue)中。
同步阻塞(lock->锁池)
运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。
其他阻塞(sleep/join)
运行(running)的线程执行 Thread.sleep(long ms)或t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、 join()等待线程终止或者超时或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。
线程死亡(DEAD)
线程会以下面三种方式结束,结束后就是死亡状态。正常结束
- run()或 call()方法执行完成,线程正常结束。异常结束
- 线程抛出一个未捕获的 Exception 或 Error。调用 stop
- 直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。
线程休眠
- sleep(时间):指定当前线程阻塞的毫秒数
- sleep存在异常需要抛出:InterruptedException
- sleep时间达到后,线程进入就绪状态
- sleep可以模拟网络延时、倒计时等
- 每一个对象都有一个锁,sleep不会释放锁
package com.wjr.state;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @Auther:TrueLies
* @Date:2021/4/27-18:06
* @Description:com.wjr.state
* @version:1.0
*/
//模拟倒计时
public class TestSleep {
public static void main(String[] args) {
//打印当前系统时间
Date startTime = new Date(System.currentTimeMillis());//获取系统当前时间
while (true){
try {
Thread.sleep(1000);
System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
startTime = new Date(System.currentTimeMillis());//更新当前时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//模拟倒计时
public static void tenDown() throws InterruptedException {
int num = 10;
while (true){
Thread.sleep(1000);
System.out.println(num--);
if (num<0){
break;
}
}
}
}
线程礼让
- 礼让线程,让当前正在执行的线程暂停,但不阻塞
- 让线程从运行状态转为就绪状态
- 让cpu重新调度,礼让不一定成功,看CPU的调度
package com.wjr.state;
/**
* @Auther:TrueLies
* @Date:2021/4/27-18:15
* @Description:com.wjr.state
* @version:1.0
*/
public class TestYield{
public static void main(String[] args) {
MyYield myYield = new MyYield();
new Thread(myYield,"a").start();
new Thread(myYield,"b").start();
}
}
class MyYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程开始执行");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"线程停止执行");
}
}
Join合并线程
- 在main走到200之前,还会有线程并发得存在
- 在main执行到200之后,join方法会正式进入,等到join的线程走完之后,才会继续走main线程
package com.wjr.state;
/**
* @Auther:TrueLies
* @Date:2021/4/27-18:23
* @Description:com.wjr.state
* @version:1.0
*/
public class TestJoin implements Runnable {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("线程vip来了" + i);
}
}
public static void main(String[] args) throws InterruptedException {
//启动我们的线程
TestJoin testJoin = new TestJoin();
Thread thread = new Thread(testJoin);
thread.start();
//主线程
for (int i = 0; i < 500; i++) {
if (i == 200) {
thread.join();//插队
}
System.out.println("main" + i);
}
}
}
sleep()和wait() 有什么区别?
- 对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于Object 类中的。
- sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态
- 在调用 sleep()方法的过程中, 线程不会释放对象锁。
- 而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
Thread 类中的start() 和 run() 方法有什么区别?
start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程 。
Thread类中的yield方法有什么作用?
Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。