H5W3
当前位置:H5W3 > java > 正文

【Java】Java多线程之线程及其常用方法

创建线程和常用方法

进程与线程的概念

  • 进程

进程是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。

  • 线程

线程是一条执行路径,是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时(并发)运行。
一个正在运行的软件(如迅雷)就是一个进程,一个进程可以同时运行多个任务( 迅雷软件可以同时下载多个文件,每个下载任务就是一个线程), 可以简单的认为进程是线程的集合。

为什么要使用多线程
多线程可以提高程序的效率。
实际生活案例:村长要求喜洋洋在一个小时内打100桶水,可以喜洋洋一个小时只能打25桶水,如果这样就需要4个小时才能完成任务,为了在一个小时能够完成,喜洋洋就请美洋洋、懒洋洋、沸洋洋,来帮忙,这样4只羊同时干活,在一小时内完成了任务。原本用4个小时完成的任务现在只需要1个小时就完成了,如果把每只羊看做一个线程,多只羊即多线程可以提高程序的效率。

并发编程的概念

  • 顺序编程
public class Main {
// 顺序编程:当吃饭吃不完的时候,是不能喝酒的,只能吃完晚才能喝酒
public static void main(String[] args) throws Exception {
// 先吃饭再喝酒
eat();
drink();
}
private static void eat() throws Exception {
System.out.println("开始吃饭...\t" + new Date());
Thread.sleep(5000);
System.out.println("结束吃饭...\t" + new Date());
}
private static void drink() throws Exception {
System.out.println("开始喝酒...\t" + new Date());
Thread.sleep(5000);
System.out.println("结束喝酒...\t" + new Date());
}
}

【Java】Java多线程之线程及其常用方法

  • 并发编程
public class Main {
public static void main(String[] args) {
// 一边吃饭一边喝酒
new EatThread().start();
new DrinkThread().start();
}
}
class EatThread extends Thread{
@Override
public void run() {
System.out.println("开始吃饭?...\t" + new Date());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("结束吃饭?...\t" + new Date());
}
}
class DrinkThread extends Thread {
@Override
public void run() {
System.out.println("开始喝酒?️...\t" + new Date());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("结束喝酒?...\t" + new Date());
}
}

【Java】Java多线程之线程及其常用方法

并发编程,一边吃饭一边喝酒总共用时5秒,比顺序编程更快,因为并发编程可以同时运行,而不必等前面的代码运行完之后才允许后面的代码。
同一个时刻一个CPU只能做一件事情,即同一时刻只能一个线程中的部分代码,假如有两个线程,Thread-0和Thread-1,刚开始CPU说Thread-0你先执行,给你3毫秒时间,Thread-0执行了3毫秒时间,但是没有执行完,此时CPU会暂停Thread-0执行并记录Thread-0执行到哪行代码了,当时的变量的值是多少,然后CPU说Thread-1你可以执行了,给你2毫秒的时间,Thread-1执行了2毫秒也没执行完,此时CPU会暂停Thread-1执行并记录Thread-1执行到哪行代码了,当时的变量的值是多少,此时CPU又说Thread-0又该你,这次我给你5毫秒时间,去执行吧,此时CPU就找出上次Thread-0线程执行到哪行代码了,当时的变量值是多少,然后接着上次继续执行,结果用了2毫秒就Thread-0就执行完了,就终止了,然后CPU说Thread-1又轮到你,这次给你4毫秒,同样CPU也会先找出上次Thread-1线程执行到哪行代码了,当时的变量值是多少,然后接着上次继续开始执行,结果Thread-1在4毫秒内也执行结束了,Thread-1也结束了终止了。CPU在来回改变线程的执行机会称之为线程上下文切换。

线程的状态
创建(new)状态: 准备好了一个多线程的对象,即执行了new Thread(); 创建完成后就需要为线程分配内存
就绪(runnable)状态: 调用了start()方法, 等待CPU进行调度
运行(running)状态: 执行run()方法
阻塞(blocked)状态: 暂时停止执行线程,将线程挂起(sleep()、wait()、join()、没有获取到锁都会使线程阻塞), 可能将资源交给其它线程使用
死亡(terminated)状态: 线程销毁(正常执行完毕、发生异常或者被打断interrupt()都会导致线程终止)

创建线程的方式
1.子类继承Thread类,重写Thread类run方法,new Thread子类创建线程对象,调用线程对象的start()方法。

public class Main {
public static void main(String[] args) {
new MyThread().start();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
}
}

2.实现类实现Runnable接口,重写Runnable接口run()方法,实例化实现类并将此实例作为Thread类的target来创建线程对象,调用线程对象的start()方法。

public class Main {
public static void main(String[] args) {
// 将Runnable实现类作为Thread的构造参数传递到Thread类中,然后启动Thread类
MyRunnable runnable = new MyRunnable();
new Thread(runnable).start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
}
}

3.实现类实现Callable接口,重写Callable带有返回值的call()方法,用FutureTask包装Callable实现类的实例,将FutureTask的实例作为Thread类的target来创建线程对象,调用线程对象的start()方法

public class Main {
public static void main(String[] args) throws Exception {
// 将Callable包装成FutureTask,FutureTask也是一种Runnable
MyCallable callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
// get方法会阻塞调用的线程
Integer sum = futureTask.get();
System.out.println(Thread.currentThread().getName() + Thread.currentThread().getId() + "=" + sum);
}
}
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + " \tstarting...");
int sum = 0;
for (int i = 0; i <= 100000; i++) {
sum += i;
}
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + " \tover...");
return sum;
}
}

三种方式比较:
1.Thread: 继承方式, 不建议使用, 因为Java是单继承的,继承了Thread就没办法继承其它类了,不够灵活
2.Runnable: 实现接口,比Thread类更加灵活,没有单继承的限制
3.Callable: Thread和Runnable都是重写的run()方法并且没有返回值,Callable是重写的call()方法并且有返回值并可以借助FutureTask类来判断线程是否已经执行完毕或者取消线程执行
4.当线程不需要返回值时使用Runnable,需要返回值时就使用Callable

常用方法
1.Thread.currentThread()

public static void main(String[] args) {
Thread thread = Thread.currentThread();
// 线程名称
String name = thread.getName();
// 线程id
long id = thread.getId();
// 线程已经启动且尚未终止
// 线程处于正在运行或准备开始运行的状态,就认为线程是“存活”的
boolean alive = thread.isAlive();
// 线程优先级
int priority = thread.getPriority();
// 是否守护线程
boolean daemon = thread.isDaemon();
// Thread[name=main,id=1,alive=true,priority=5,daemon=false]
System.out.println("Thread[name=" + name + ",id=" + id + ",alive=" + alive + ",priority=" + priority + ",daemon=" + daemon + "]");
}

2.sleep() 与 interrupt()
sleep(): 睡眠指定时间,即让程序暂停指定时间运行,时间到了会继续执行代码,如果时间未到就要醒需要使用interrupt()来随时唤醒
interrupt(): 唤醒正在睡眠的程序,调用interrupt()方法,会使得sleep()方法抛出InterruptedException异常,当sleep()方法抛出异常就中断了sleep的方法,从而让程序继续运行下去

public static void main(String[] args) throws Exception {
Thread thread0 = new Thread(()-> {
try {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t太困了,让我睡10秒,中间有事叫我,zZZ。。。");
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t被叫醒了,又要继续干活了");
}
});
thread0.start();
// 这里睡眠只是为了保证先让上面的那个线程先执行
Thread.sleep(2000);
new Thread(()-> {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t醒醒,醒醒,别睡了,起来干活了!!!");
// 无需获取锁就可以调用interrupt
thread0.interrupt();
}).start();
}

【Java】Java多线程之线程及其常用方法

3.wait() 与 notify()
wait、notify和notifyAll方法是Object类的final方法,这些方法不能被子类重写。因此在程序中可以通过this或者super来调this.wait(), super.wait()。
wait(): 导致线程进入等待阻塞状态,会一直等待直到它被其他线程通过notify()或者notifyAll唤醒。该方法只能在同步方法或同步块内部调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。wait(long timeout): 时间到了自动执行,类似于sleep(long millis)
notify(): 该方法只能在同步方法或同步块内部调用, 随机选择一个(注意:只会通知一个)在该对象上调用wait方法的线程,解除其阻塞状态
notifyAll(): 唤醒所有的wait对象

wait()是让程序暂停执行,线程进入当前实例的等待队列,这个队列属于该实例对象,所以调用notify也必须使用该对象来调用,不能使用别的对象来调用。调用wait和notify必须使用同一个对象来调用。

注意:

  • Object.wait()和Object.notify()和Object.notifyall()必须写在synchronized方法内部或者synchronized块内部
  • 让哪个对象等待wait就去通知notify哪个对象,不要让A对象等待,结果却去通知B对象,要操作同一个对象
public class WaitNotifyTest {
public static void main(String[] args) throws Exception {
WaitNotifyTest waitNotifyTest = new WaitNotifyTest();
new Thread(() -> {
try {
waitNotifyTest.printFile();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
waitNotifyTest.printFile();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t睡觉1秒中,目的是让上面的线程先执行,即先执行wait()");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
waitNotifyTest.notifyPrint();
}).start();
}
private synchronized void printFile() throws InterruptedException {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t等待打印文件...");
this.wait();
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t打印结束。。。");
}
private synchronized void notifyPrint() {
this.notify();
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t通知完成...");
}
}

【Java】Java多线程之线程及其常用方法
notifyAll()
【Java】Java多线程之线程及其常用方法

4.sleep() 与 wait()
Thread.sleep(long millis): 睡眠时不会释放锁

package com.example.demo;
import java.util.Date;
public class Main {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
new Thread(() -> {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t" + i);
try { Thread.sleep(1000); } catch (InterruptedException e) { }
}
}
}).start();
Thread.sleep(1000);
new Thread(() -> {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t" + i);
}
}
}).start();
}
}

因main方法中Thread.sleep(1000)所以上面的线程Thread-0先被执行,当循环第一次时就会Thread.sleep(1000)睡眠,因为sleep并不会释放锁,所以Thread-1得不到执行的机会,所以直到Thread-0执行完毕释放锁对象lock,Thread-1才能拿到锁,然后执行Thread-1;
【Java】Java多线程之线程及其常用方法

object.wait(long timeout): 会释放锁

public class Main {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
new Thread(() -> {
synchronized (object) {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t等待打印文件...");
try {
object.wait(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t打印结束。。。");
}
}).start();
// 先上面的线程先执行
Thread.sleep(1000);
new Thread(() -> {
synchronized (object) {
for (int i = 0; i < 5; i++) {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t" + i);
}
}
}).start();
}
}

【Java】Java多线程之线程及其常用方法

sleep与wait的区别
sleep在Thread类中,wait在Object类中
sleep不会释放锁,wait会释放锁
sleep使用interrupt()来唤醒,wait需要notify或者notifyAll来通知

5.join()

public class JoinTest {
public static void main(String[] args) {
new Thread(new ParentRunnable()).start();
}
}
class ParentRunnable implements Runnable {
@Override
public void run() {
// 线程处于new状态
Thread childThread = new Thread(new ChildRunable());
// 线程处于runnable就绪状态
childThread.start();
try {
// 当调用join时,parent会等待child执行完毕后再继续运行
// 将某个线程加入到当前线程
childThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 5; i++) {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "父线程 running");
}
}
}
class ChildRunable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try { Thread.sleep(1000); } catch (InterruptedException e) {}
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "子线程 running");
}
}
}

程序进入主线程,运行Parent对应的线程,Parent的线程代码分两段,一段是启动一个子线程,一段是Parent线程的线程体代码,首先会将Child线程加入到Parent线程,join()方法会调用join(0)方法(join()方法是普通方法并没有加锁,join(0)会加锁),join(0)会执行while(isAlive()) { wait(0);} 循环判断线程是否处于活动状态,如果是继续wait(0)知道isAlive=false结束掉join(0), 从而结束掉join(), 最后回到Parent线程体中继续执行其它代码。

【Java】Java多线程之线程及其常用方法

6.yield()
交出CPU的执行时间,不会释放锁,让线程进入就绪状态,等待重新获取CPU执行时间,yield就像一个好人似的,当CPU轮到它了,它却说我先不急,先给其他线程执行吧, 此方法很少被使用到

public class Main {
public static void main(String[] args) {
new Thread(new Runnable() {
int sum = 0;
@Override
public void run() {
long beginTime=System.currentTimeMillis();
for (int i = 0; i < 99999; i++) {
sum += 1;
// 去掉该行执行用2毫秒,加上271毫秒
Thread.yield();
}
long endTime=System.currentTimeMillis();
System.out.println("用时:"+ (endTime - beginTime) + " 毫秒!");
}
}).start();
}
}

sleep(long millis) 与 yeid()
sleep(long millis): 需要指定具体睡眠的时间,不会释放锁,睡眠期间CPU会执行其它线程,睡眠时间到会立刻执行
yeid(): 交出CPU的执行权,不会释放锁,和sleep不同的时当再次获取到CPU的执行,不能确定是什么时候,而sleep是能确定什么时候再次执行。两者的区别就是sleep后再次执行的时间能确定,而yeid是不能确定的
yield会把CPU的执行权交出去,所以可以用yield来控制线程的执行速度,当一个线程执行的比较快,此时想让它执行的稍微慢一些可以使用该方法,想让线程变慢可以使用sleep和wait,但是这两个方法都需要指定具体时间,而yield不需要指定具体时间,让CPU决定什么时候能再次被执行,当放弃到下次再次被执行的中间时间就是间歇等待的时间

7.setDaemon(boolean on)
线程分两种:
1.用户线程:如果主线程main停止掉,不会影响用户线程,用户线程可以继续运行。
2.守护线程:如果主线程死亡,守护线程如果没有执行完毕也要跟着一块死(就像皇上死了,带刀侍卫也要一块死),GC垃圾回收线程就是守护线程

public class Main {
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
IntStream.range(0, 5).forEach(i -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\ti=" + i);
});
}
};
thread.start();
for (int i = 0; i < 2; i++) {
System.out.println(Thread.currentThread().getName() + "\ti=" + i);
}
System.out.println("主线程执行结束,子线程仍然继续执行,主线程和用户线程的生命周期各自独立。");
}
}

【Java】Java多线程之线程及其常用方法

public class Main {
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
IntStream.range(0, 5).forEach(i -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\ti=" + i);
});
}
};
thread.setDaemon(true);
thread.start();
for (int i = 0; i < 2; i++) {
System.out.println(Thread.currentThread().getName() + "\ti=" + i);
}
System.out.println("主线程死亡,子线程也要陪着一块死!");
}
}

【Java】Java多线程之线程及其常用方法

参考:
多线程(一):创建线程和线程的常用方法

本文地址:H5W3 » 【Java】Java多线程之线程及其常用方法

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址