您的位置:首页 > 编程语言 > Java开发

Java基础(七)——多线程

2021-09-29 10:17 591 查看

一、概述

1、介绍

  Java VM 启动的时候会有一个进程Java.exe,该进程中至少有一个线程负责Java程序的执行。而且这个线程运行的代码存在于main方法中,该线程称之为主线程。其实从细节上来说,JVM不止启动了一个线程,其实至少有三个线程。除了main() 主线程,还有 gc() 负责垃圾回收机制的线程,异常处理线程。当然如果发生异常,会影响主线程。
  局部的变量在每一个线程区域中都有独立的一份。

2、程序、进程、线程

  程序(program)是为完成特定任务、用某种语言编写的一组指令的集合 。即一段静态的代码 ,静态对象。
  进程(process)是程序的一次执行过程,或是正在运行的一个程序。是一个动态过程,有它自身的产生、存在和消亡的过程——生命周期。如运行中的QQ、运行中的 MP3播放器。
  线程(thread)是一个程序内部的一条执行路径。由进程可进一步细化为线程。若一个进程同一时间并行执行多个线程,就是支持多线程的。

public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}

class MyThread extends Thread {

@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}

// 匿名方式
public class Main {
public static void main(String[] args) {

new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
}
}
继承Thread类   代码示例:方式二、实现 Runnable 接口
  调用 Thread 类的 start 方法开启线程,会调用当前线程的 run() 方法。

public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread1 = new Thread(myThread);
Thread thread2 = new Thread(myThread);

// 这里开启了两个线程.
thread1.start();
thread2.start();
}
}

class MyThread implements Runnable {

@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}

// 匿名方式
public class Main {
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}).start();
}
实现Runnable接口

  代码示例:方式三、实现 Callable 接口

public class Main {
public static void main(String[] args) throws Exception {
Number number = new Number();

FutureTask<Integer> futureTask = new FutureTask<>(number);
// 通过线程启动这个任务
new Thread(futureTask).start();

final Integer sum = futureTask.get();
System.out.println(sum); // 2550
}
}

class Number implements Callable<Integer> {

@Override
public Integer call() throws Exception {
int sum = 0;
// 求100以内的偶数和
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
sum = sum + i;
}
}
return sum;
}
}
实现Callable接口

  值得注意:
  将futureTask作为Runnable实现类传递,本质为方式二。而调用 Thread 类的 start 方法开启线程,会调用当前线程的 run() 方法。
  当前线程的 run()方法 --> futureTask.run() --> callable.call()。
  实例都是通过构造器初始化的。
  返回值即为 call() 方式的返回值。

// Thread类
@Override
public void run() {
if (target != null) {
target.run(); // futureTask.run()
}
}

  方式四:线程池
  见标签:聊聊并发

3、三种方式的区别

  ①相比继承,实现Runnable接口方式
  好处:避免了单继承的局限性。通过多个线程可以共享同一个接口实现类的对象,天然就能体现多个线程处理共享数据的情况(有共享变量)。参考卖票案例。
  应用:创建了多个线程,多个线程共享数据,则使用实现接口的方式,数据天然的就是共享的。
在定义线程时,建议使用实现接口的方式。

public class Thread extends Object implements Runnable

  ②相比Runnable,Callable功能更强大些:
  比run()方法,call()有返回值;call()可以抛出异常。被外面捕获,获取异常信息;支持泛型的返回值;需要借助FutureTask类,比如获取返回结果。

4、线程有关方法

  void start():启动线程,并执行对象的 run() 方法。
  run():线程在被调度时执行的方法体。
  String getName():返回线程的名称。
  void setName(String name):设置该线程的名称。
  static Thread currentThread():返回当前线程对象 。在 Thread 子类中就是 this ,通常用于主线程和 Runnable 实现类。

  static void yield():线程让步,释放当前CPU的执行权。下一刻可能又立马得到。暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程。若队列中没有同优先级的线程,忽略此方法。

  join():在线程A中调用线程B的join(),此时线程A就进入阻塞状态,直到线程B完全执行完以后,线程A才结束阻塞状态(相当于插队)。低优先级的线程也可以获得执行。

  static void sleep(long millis): 让当前线程睡眠指定毫秒数,在睡眠期间,当前线程是阻塞状态。不会释放锁。令当前活动线程在指定时间段内放弃对 CPU 控制,使其他线程有机会被执行,时间到后重排队。抛出 InterruptedException 异常。

  stop():强制线程生命期结束,不推荐使用。(已过时)。
  boolean isAlive():返回 boolean,判断线程是否还活着。

  代码示例:join() 使用

public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
myThread.setName("线程一");
myThread.start();

Thread.currentThread().setName("主线程");
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
// 主线程拿到CPU执行权到i=40时,会进入阻塞,等 线程一 执行完才结束
if (i == 40) {
myThread.join();
}
}
}
}

class MyThread extends Thread {

@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}

5、线程调度

  调度策略:时间片轮训;抢占式,高优先级的线程抢占CPU。

// 错误示例,结果:交错式打印
class Number1 implements Runnable {

private int number = 1;

public void run() {
while (true) {
synchronized (this) {
if (number <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
} else {
break;
}
}
}
}
}
错误示例   这里需要用到线程的通信,正确方式如下:

/**
* 分析:很自然的想到,线程一打印了1之后,需要让一阻塞,然后让线程二打印2
* 然后将一唤醒,二再阻塞,依次内推。
*/
// 正确示例,结果:交替打印
public class Main {
public static void main(String[] args) {
Number num = new Number();
Thread thread1 = new Thread(num);
Thread thread2 = new Thread(num);

thread1.start();
thread2.start();
}
}

class Number implements Runnable {

private int number = 1;

@Override
public void run() {
while (true) {
// this : num
synchronized (this) {
// 唤醒一个线程
notify();
if (number <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
// 使得调用该方法的线程进入阻塞状态
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}

// 结果
实现交替打印
交替打印

  注意:上面两个线程公用同一把锁 num,this 指num。
  若此时将同步监视器换成

private final Object object = new Object();
synchronized (object) {}

  会报异常"IllegalMonitorStateException"。同步监视器非法

  原因:默认情况下,方法前有一个this,而锁却是Object。

this.notify();
this.wait();

  如果要用Object当锁,需要修改为:

object.notify();
object.wait();

  理解:其实不难理解,解决线程同步问题,是要求要同一把锁。锁是object,而唤醒却是this,就不知道要唤醒谁了呀?应该唤醒跟我(当前线程)共用同一把锁的线程,唤醒别人(别的锁)有什么意义呢?而且本身还是错的。
  形象理解:一个寝室四个人(四个线程)有一个厕所(共享资源),共用厕所(多个线程对共享资源进行访问)有安全隐患,如何解决?加锁。当甲进入厕所时,将厕所门前挂牌(锁)拿走(获得锁),然后使用厕所(此时其他人都进不来,必须在门外等待获得挂牌,即时此时甲在厕所睡着了sleep(),其他人依然要等待,因为甲依然拿着挂牌,线程没有释放锁),使用完毕后,将挂牌挂于门前(线程释放锁),其他三人方可使用,使用前先竞争锁。
若要求两人交替使用厕所,那么当甲使用完毕,通知(notify)乙使用,甲去等待(wait)下一次使用,自然而然,甲需要释放锁。就是甲使用时,乙等待,甲用完了,通知乙(我用完了,你去吧),乙使用时,甲等待,乙用完了,通知甲(我用完了,你去吧)。那么很自然的问题是,甲用完后,通知谁?是通知和我竞争同一个厕所的人,不会去通知隔壁寝室的人(即我用完了,释放出锁,通知竞争这把锁的线程)。
  总结:
  wait():一旦执行此方法,当前线程进入阻塞状态,并释放锁。
  notify():一旦执行此方法,就会唤醒一个被wait()的线程。如果有多个,就唤醒优先级高的,如果优先级一样,则随机唤醒一个。
  notifyAll():一旦执行此方法,会唤醒所有wait()的线程。
  以上三个方法必须使用在同步代码块或同步方法中,这里才有锁。如果是Lock,有别的方式(暂未介绍,可自行百度)。
  以上三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则会出现异常。
  而任何一个类的对象,都可以充当锁。则当锁是object时,根据上一条,以上三个方法调用者就是object,所以定义在java.lang.Object类中。

2、sleep与wait的异同

  相同:都可以使当前线程进入阻塞状态。
  不同:声明位置不同,Thread类中声明sleep(),Object类中声明wait();sleep()随时都可以调用,wait()必须在同步代码块或同步方法中;sleep()不会释放锁,wait()会释放锁;sleep(),超时或者调用interrupt()方法就可以唤醒,wait(),等待其他线程调用对象的notify()或者notifyAll()方法才可以唤醒。

3、生产者与消费者

  见标签:聊聊并发

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: