java akka main方法结束 不退出_Java 多线程基础(上)

点击上方 程序猿杂货摊,选择 设为星标

优质项目,及时送达

-----

多线程一直是Java 面试中常考的基础知识,之前一直没有系统的学习过,这段时间对着廖雪峰大师的讲义从新把该知识内容整理一遍,该文章系列内容全部来源于廖雪峰官方网站Java 基础教程。

系列目录如下:

1、线程创建

2、线程的状态

3、中断线程

4、守护线程

5、线程同步

6、死锁

7、使用wait和notify

1

线程创建

//方法一:从Thread派生一个自定义类,然后覆写run()方法:class MyThread extends Thread{    @Override    public void run() {        System.out.println("this is MyThread extends Thread ");    }}//方法二:创建Thread实例时,传入一个Runnable实例:class MyRunnable implements Runnable{    public void run() {//        synchronized ()        System.out.println("this is MyRunnable implements Runnable");    }}//用Java8引入的lambda语法进一步简写为:Thread t3 = new Thread(() -> {            System.out.println("this is lambda func");        });        System.out.println("main start....");        t3.start();        t3.join();//join()函数作用是main线程在启动t线程后,可以通过t.join()等待t线程结束后再继续运行,然后才继续往下执行自身线程

使用线程执行的打印语句,和直接在main()方法执行有区别吗?

区别大了去了。我们看以下代码:

public class Main {    public static void main(String[] args) {        System.out.println("main start..."); //Main        Thread t = new Thread() {            //Main            public void run() {                System.out.println("thread run..."); //t                System.out.println("thread end.");   //t            }        };        t.start();                          //Main        System.out.println("main end...");  //Main    }}

我们用蓝色表示主线程,也就是main线程,main线程执行的代码有4行,首先打印main start,然后创建Thread对象,紧接着调用start()启动新线程。当start()方法被调用时,JVM就创建了一个新线程,我们通过实例变量t来表示这个新线程对象,并开始执行。

接着,main线程继续执行打印main end语句,而t线程在main线程执行的同时会并发执行,打印thread run和thread end语句。当run()方法结束时,新线程就结束了。而main()方法结束时,主线程也结束了。

我们再来看线程的执行顺序:

  1. main线程肯定是先打印main start,再打印main end;

  2. t线程肯定是先打印thread run,再打印thread end。

但是,除了可以肯定,main start会先打印外,main end打印在thread run之前、thread end之后或者之间,都无法确定。因为从t线程开始运行以后,两个线程就开始同时运行了,并且由操作系统调度,程序本身无法确定线程的调度顺序。

线程的优先级

可以对线程设定优先级,设定优先级的方法是:

Thread.setPriority(int n) // 1~10, 默认值5

小结:

  • Java用Thread对象表示一个线程,通过调用start()启动一个新线程;

  • 一个线程对象只能调用一次start()方法;

  • 线程的执行代码写在run()方法中;

  • 线程调度由操作系统决定,程序本身无法决定调度顺序;

  • Thread.sleep()可以把当前线程暂停一段时间。

2

线程的状态

在Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:

  • New:新创建的线程,尚未执行;

  • Runnable:运行中的线程,正在执行run()方法的Java代码;

  • Blocked:运行中的线程,因为某些操作被阻塞而挂起;

  • Waiting:运行中的线程,因为某些操作在等待中;

  • Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;

  • Terminated:线程已终止,因为run()方法执行完毕。

Java线程对象Thread的状态包括:New、Runnable、Blocked、Waiting、Timed Waiting和Terminated;

通过对另一个线程对象调用join()方法可以等待其执行结束;

可以指定等待时间,超过等待时间线程仍然没有结束就不再等待;

对已经运行结束的线程调用join()方法会立刻返回。

3

中断线程

如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。

中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。

4cf284673665b6c9aae46f4dfffda5b6.gif
package com.sun;/** * @Auther mashang * @Date 2020-11-26 20:47 * @Version 1.0 *//** * 测试线程中断 */public class testThreadInterrupt {    public static void main(String[] args) throws InterruptedException{        System.out.println("主线程执行开始....");        Thread t = new Thread(){            public void run(){                int i = 0;                while(!isInterrupted()){                    i++;                    System.out.println(i + "   hello");                }            }        };        t.start();        /*         Thread.sleep()和t.sleep() 两种方式不一样,Thread.sleep()是让当前主线程main()休眠10ms,而t.sleep(10)是让t线程休眠10ms         本程序中测试线程中断应该是让主线程main()休眠,不在继续指向下一行代码t.interrupt(),让t.start()先跑一会在中断。         */        Thread.sleep(10);//        t.sleep(10);        t.interrupt();        t.join();        System.out.println("end.....");    }}
4cf284673665b6c9aae46f4dfffda5b6.gif
package com.sun;/** * @Auther mashang * @Date 2020-11-26 20:47 * @Version 1.0 *//** * 测试线程中断 */public class testThreadInterrupt2 {    public static void main(String[] args) throws InterruptedException{        System.out.println("主线程执行开始....");        Thread t = new MyThread();        t.start();        Thread.sleep(1000);        t.interrupt();        t.join();//等待t线程执行完毕        System.out.println("Main end.....");    }}class HelloThread extends Thread{    public void run(){        int i = 0;        while(!isInterrupted()){            i++;            System.out.println(i + "   hello");            try {                Thread.sleep(100);            } catch (InterruptedException e) {                System.out.println("这是HelloThread  异常");                break;            }        }    }}class MyThread extends Thread{    public void run(){        Thread hello = new HelloThread();        hello.start();        try {            hello.join();        } catch (InterruptedException e) {//            e.printStackTrace();            /*            此时的异常是由MyThread 捕获的            java.lang.InterruptedException                at java.lang.Object.wait(Native Method)                at java.lang.Thread.join(Thread.java:1252)                at java.lang.Thread.join(Thread.java:1326)                 - - - - - - - -- -at com.sun.MyThread.run(testThreadInterrupt2.java:51)- - -- - -- - - - -- -             */            System.out.println("这是MyThread 捕获interrupted!");        }        hello.interrupt();//没有这一行,hello线程仍然会继续运行,且JVM不会退出        System.out.println("已经通知hello 执行中断 ");    }}/*代码执行流程,main()主线程执行t.interrupt();中断,此时t线程正在执行hello.join(),等待hello线程执行完毕,故此时t线程会捕获InterruptedException异常,打印"这是MyThread 捕获interrupted!",t线程在执行完毕之前会通知hello线程中断hello.interrupt(),然后主线程会都等待t.join()执行完毕,main()再继续执行。 */

执行结果如下:

主线程执行开始....1   hello2   hello3   hello4   hello5   hello6   hello7   hello8   hello9   hello10   hello这是MyThread 捕获interrupted!已经通知hello 执行中断 这是HelloThread  异常Main end.....

另一个常用的中断线程的方法是设置标志位。我们通常会用一个running标志位来标识线程是否应该继续运行,在外部线程中,通过把HelloThread.running置为false,就可以让线程结束:

/** * 测试线程中断 */public class testThreadInterruptFlag {    public static void main(String[] args) throws InterruptedException{        System.out.println("主线程执行开始....");        HThread hello1 = new HThread();        hello1.start();        Thread.sleep(100);        hello1.running = false;        hello1.join();        System.out.println("main  end.....");    }}class HThread extends Thread{    /*    线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值。     */    public volatile boolean running = true;    @Override    public void run() {        int i = 0;        while (running){            i++;            System.out.println(i+ " +  hello");        }        System.out.println("hello end...");    }}

注意到HelloThread的标志位boolean running是一个线程间共享的变量。线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值。

volatile详解

mashang,公众号:程序猿杂货摊网易云课堂笔记总结1

小结

  • 对目标线程调用interrupt()方法可以请求中断一个线程,目标线程通过检测isInterrupted()标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到InterruptedException;

  • 目标线程检测到isInterrupted()为true或者捕获了InterruptedException都应该立刻结束自身线程;

  • 通过标志位判断需要正确使用volatile关键字;

  • volatile关键字解决了共享变量在线程间的可见性问题。

4

守护线程

Java程序入口就是由JVM启动main线程,main线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。如果有一个线程没有退出,JVM进程就不会退出。所以,必须保证所有线程都能及时结束。

守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。

创建守护线程

Thread t = new TestDaemon();        t.setDaemon(true);        System.out.println(t.isDaemon());        t.start();

小结

  • 守护线程是为其他线程服务的线程;

  • 所有非守护线程都执行完毕后,虚拟机退出;

  • 守护线程不能持有需要关闭的资源(如打开文件等)。

5

线程同步

当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。这个时候,有个单线程模型下不存在的问题就来了:如果多个线程同时读写共享变量,会出现数据不一致的问题。

4cf284673665b6c9aae46f4dfffda5b6.gif
public class TestThreadSyn {    public static void main(String[] args) throws InterruptedException{        System.out.println("main start...");        for(int i = 0;i<1000;i++){            Thread add = new ADDThread();            Thread dec = new DECThread();            add.start();            dec.start();            add.join();            dec.join();            System.out.println("第" +i + "次" +  Counter.counter);        }        System.out.println("main end...");    }}class Counter{    public static final Object lock  = new Object();    public static int counter = 0;}class ADDThread extends Thread{    public void run(){        for(int i = 0;i<10000;i++){            /*            synchronized (Counter.class) 此时用Counter类对象本身也可以锁             */            synchronized (Counter.class){                Counter.counter += 1;            }            /*            synchronized (Counter.lock) 此时用的是Counter类中的实例对象,实例对象可以访问类的静态变量             *///            synchronized (Counter.lock){//                Counter.counter += 1;//            }        }    }}class DECThread extends Thread{    public void run(){        for(int i = 0;i<10000;i++){            synchronized (Counter.class){                Counter.counter -= 1;            }        }    }}//结果:...第993次0第994次0第995次0第996次0第997次0第998次0第999次0main end...

对变量进行读取和写入时,结果要正确,必须保证是原子操作。原子操作是指不能被中断的一个或一系列操作。

例如,对于语句:

n = n + 1;

看上去是一行语句,实际上对应了3条指令:

ILOADIADDISTORE

我们假设n的值是100,如果两个线程同时执行n = n + 1,得到的结果很可能不是102,而是101,原因在于:

┌───────┐ ┌───────┐

│Thread1│ │Thread2│

└───┬───┘ └───┬───┘
│ │
│ILOAD (100) │
│ │ILOAD (100)
│ │IADD
│ │ISTORE (101)
│IADD │
│ISTORE (101)│
▼ ▼

如果线程1在执行ILOAD后被操作系统中断,此刻如果线程2被调度执行,它执行ILOAD后获取的值仍然是100,最终结果被两个线程的ISTORE写入后变成了101,而不是期待的102。

这说明多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待:

┌───────┐     ┌───────┐
│Thread1│ │Thread2│
└───┬───┘ └───┬───┘
│ │
│-- lock -- │
│ILOAD (100) │
│IADD │
│ISTORE (101) │
│-- unlock -- │
│ │-- lock --
│ │ILOAD (101)
│ │IADD
│ │ISTORE (102)
│ │-- unlock --
▼ ▼

通过加锁和解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。

可见,保证一段代码的原子性就是通过加锁和解锁实现的。Java程序使用synchronized关键字对一个对象进行加锁:

synchronized(lock) {    n = n + 1;}
//锁的错误用法public class TestSyn5 {    public static void main(String[] args) throws Exception {        Thread add = new AddThread();        Thread dec = new DecThread();        add.start();        dec.start();        add.join();        dec.join();        System.out.println(Counter5.count);    }}class Counter5 {    public static final Object lock1 = new Object();    public static final Object lock2 = new Object();    public static int count = 0;}class AddThread extends Thread {    public void run() {        for (int i=0; i<10000; i++) {            synchronized(Counter5.lock1) {                Counter5.count += 1;            }        }    }}class DecThread extends Thread {    public void run() {        for (int i=0; i<10000; i++) {            synchronized(Counter5.lock2) {                Counter5.count -= 1;            }        }    }}/*结果并不是0,这是因为两个线程各自的synchronized锁住的不是同一个对象!这使得两个线程各自都可以同时获得锁:因为JVM只保证同一个锁在任意时刻只能被一个线程获取,但两个不同的锁在同一时刻可以被两个线程分别获取。两个线程可以同时操作counter,例如下图 */
┌───────┐    ┌───────┐
│Thread1│ │Thread2│
└───┬───┘ └───┬───┘
│ │
│ILOAD (100) │
│ │ILOAD (100)
│ │IDEC
│ │ISTORE (99)
│IADD │
│ISTORE (101)│
▼ ▼

线程同步实例

package com.sun;/** * @Auther mashang * @Date 2020-11-27 14:50 * @Version 1.0 */public class TestSyn3 implements Runnable{    public static int i =0;    /*    不加static 时,synchronized锁住的对象是this,即当前实例,获得类实例对象的锁就可以访问当前代码块。    但是多个实例同时访问该类变量时不是线程安全的,add()方法对实例对象加锁,是实例就可以访问,    锁是给两个实例加的锁,并没有达到同步的效果     */    public static synchronized void add(){        i++;    }//    public synchronized void add(){//        i++;//    }    public void run(){        for(int i = 0;i<100000;i++){            add();        }    }    public static void main(String[] args) throws InterruptedException {        Thread t1 = new Thread(new TestSyn3());        Thread t2 = new Thread(new TestSyn3());        t1.start();        t2.start();        t1.join();        t2.join();//        System.out.println("public synchronized void add() 执行结果:" + i);        System.out.println("public static synchronized void add() 执行结果:" + i);    }}

不加static执行结果

5e33b8518fb920e704ba63ecf7a457d7.png

加static执行结果

8c08550d7f12edcd78f57feb48091fb5.png

不需要synchronized的操作JVM规范定义了几种原子操作:

  • 基本类型(long和double除外)赋值,例如:int n = m;

  • 引用类型赋值,例如:List list = anotherList。

long和double是64位数据,JVM没有明确规定64位赋值操作是不是一个原子操作,不过在x64平台的JVM是把long和double的赋值作为原子操作实现的。

单条原子操作的语句不需要同步。例如:

public void set(int m) {    synchronized(lock) {        this.value = m;    }}

就不需要同步,对引用也是类似。例如:

public void set(String s) {    this.value = s;}

上述赋值语句并不需要同步,但是,如果是多行赋值语句,就必须保证是同步操作,例如:

class Pair {    int first;    int last;    public void set(int first, int last) {        synchronized(this) {            this.first = first;            this.last = last;        }    }}

有些时候,通过一些巧妙的转换,可以把非原子操作变为原子操作。例如,上述代码如果改造成:

class Pair {    int[] pair;    public void set(int first, int last) {        int[] ps = new int[] { first, last };        this.pair = ps;    }}

就不再需要同步,因为this.pair = ps是引用赋值的原子操作。而语句:

int[] ps = new int[] { first, last };

这里的ps是方法内部定义的局部变量,每个线程都会有各自的局部变量,互不影响,并且互不可见,并不需要同步。

小结

  • 多线程同时读写共享变量时,会造成逻辑错误,因此需要通过synchronized同步;

  • 同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;

  • 注意加锁对象必须是同一个实例;

  • 对JVM定义的单个原子操作不需要同步。

6

死锁

Java的线程锁是可重入的锁。

public class Counter {    private int count = 0;    public synchronized void add(int n) {        if (n < 0) {            dec(-n);        } else {            count += n;        }    }    public synchronized void dec(int n) {        count += n;    }}

观察synchronized修饰的add()方法,一旦线程执行到add()方法内部,说明它已经获取了当前实例的this锁。如果传入的n < 0,将在add()方法内部调用dec()方法。由于dec()方法也需要获取this锁,现在问题来了:

对同一个线程,能否在获取到锁以后继续获取同一个锁?

答案是肯定的。JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。

由于Java的线程锁是可重入锁,所以,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出synchronized块,记录-1,减到0的时候,才会真正释放锁。

死锁

一个线程可以获取一个锁后,再继续获取另一个锁。例如:

public void add(int m) {    synchronized(lockA) { // 获得lockA的锁        this.value += m;        synchronized(lockB) { // 获得lockB的锁            this.another += m;        } // 释放lockB的锁    } // 释放lockA的锁}public void dec(int m) {    synchronized(lockB) { // 获得lockB的锁        this.another -= m;        synchronized(lockA) { // 获得lockA的锁            this.value -= m;        } // 释放lockA的锁    } // 释放lockB的锁}

如何避免死锁呢?答案是:线程获取锁的顺序要一致。即严格按照先获取lockA,再获取lockB的顺序,改写dec()方法如下:

public void dec(int m) {    synchronized(lockA) { // 获得lockA的锁        this.value -= m;        synchronized(lockB) { // 获得lockB的锁            this.another -= m;        } // 释放lockB的锁    } // 释放lockA的锁}

7

使用wait和notify

在Java程序中,synchronized解决了多线程竞争的问题。例如,对于一个任务管理器,多个线程同时往队列中添加任务,可以用synchronized加锁:

多线程协调运行的原则就是:当条件不满足时,线程进入等待状态;当条件满足时,线程被唤醒,继续执行任务。

wait()方法的执行机制非常复杂。首先,它不是一个普通的Java方法,而是定义在Object类的一个native方法,也就是由JVM的C代码实现的。其次,必须在synchronized块中才能调用wait()方法,因为wait()方法调用时,会释放线程获得的锁,wait()方法返回后,线程又会重新试图获得锁。只能在锁对象上调用wait()方法。因为在getTask()中,我们获得了this锁,因此,只能在this对象上调用wait()方法:

public synchronized String getTask() {    while (queue.isEmpty()) {        // 释放this锁:        this.wait();        // 重新获取this锁    }    return queue.remove();}

当一个线程在this.wait()等待时,它就会释放this锁,从而使得其他线程能够在addTask()方法获得this锁。

现在我们面临第二个问题:如何让等待的线程被重新唤醒,然后从wait()方法返回?答案是在相同的锁对象上调用notify()方法。我们修改addTask()如下:

public synchronized void addTask(String s) {    this.queue.add(s);    this.notify(); // 唤醒在this锁等待的线程}

注意到在往队列中添加了任务后,线程立刻对this锁对象调用notify()方法,这个方法会唤醒一个正在this锁等待的线程(就是在getTask()中位于this.wait()的线程),从而使得等待线程从this.wait()方法返回。

4cf284673665b6c9aae46f4dfffda5b6.gif
//完整实例如下:class TaskQueen{    Queuequeue = new LinkedList<>();    public synchronized void addTask(String s){        this.queue.add(s);        this.notifyAll(); 唤醒在this锁等待的所有线程    }    public synchronized String getTask() throws InterruptedException {        while(queue.isEmpty()) {            this.wait();        }        return queue.remove();    }}

注意到wait()方法返回时需要重新获得this锁。

小结

wait和notify用于多线程协调运行:

  • 在synchronized内部可以调用wait()使线程进入等待状态;

  • 必须在已获得的锁对象上调用wait()方法;

  • 在synchronized内部可以调用notify()或notifyAll()唤醒其他等待线程;

  • 必须在已获得的锁对象上调用notify()或notifyAll()方法;

  • 已唤醒的线程还需要重新获得锁后才能继续执行。

994e364e30c1e50032ea40de2f571cc6.png

文案@mashang  编辑@mashang

fd56032894af5a2e659281de3b5a4607.gif

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值