Java 多线程?什么鬼哦?

多线程

强烈推荐廖雪峰老师的网站

线程与进程的区别

在计算机中,我们把一个任务称为一个进程。在进程内部可以同时执行多个子任务,我们把子任务称为线程。

即:一个进程至少包含一个线程。

Java多线程

一个Java程序就是一个JVM进程,该进程用一个主线程来执行main()方法,在main()方法内部又可以存在多个线程。

创建线程

线程的创建

public class Main{
	public static void main(String[] args) {
        Thread thread = new Thread();
        t.start(); //启动线程t
    }    
}

使用线程执行任务

方法一

做一个Thread类的子类,覆盖run()方法。

public class Main{
    public static void main(String[] args) {
        Thread thread = new ThreadTest();
        t.start();
    }
}

class ThreadTest extends Thread {
    @Override
    public void run() {
        System.out.println("Start!");
    }
}

执行start方法后会自动调用run方法输出Start

方法二

通过Runnable接口实现。

public class Main{
    public static void main(String[] args) {
        Thread thread = new ThreadTest(new Runnable());
        t.start();
    }
}

class ThreadTest implements Runnable{
    @Override
    public void run() {
        System.out.println("Start!");
    }
}

性质

  1. 可以看到,调用run()方法时,并没有使用t.run()的方式。

    如果使用了这种方式,实质上只是调用了一个普通的Java方法打印语句完成任务,并没有新建一个线程,所有的操作都是在main线程中执行的。

    必须调用Threadstart()方法才能启动新线程。其内部调用了private native void start0()方法。native修饰符表示这个方法由JVM虚拟机内部的C语言代码实现。

  2. 线程之间是并发执行的。即线程之间的调度顺序不能由程序本身确定。

    比如:

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

    除了可以确定首先输出main run外,不能确定语句的输出顺序。模拟并发执行的效果,可以调用Thread.sleep()方法,强迫当前线程暂定一段时间。

线程的优先级

Thread.setPriority(int n); // 级别1-10, 默认5

注意,优先级高的线程只是会被操作系统调度更频繁,并不能通过设置优先级来保证 优先级高的线程一定会先执行。

如果想要使得某个线程在另一个线程结束后运行,可以使用join()方法。

class Main {
    public static void main(String[] args) {
        System.out.println("Main Run");
        Thread t = new Thread() {
            public void run () {
                System.out.println("Thread Run");
                System.out.println("Thread End");
            }
        };
        try {
            t.join();
            // 可以指定时间,当指定时间后线程未结束则不再等待。
            // 如果在指定时间内完成,线程仍会继续等待,指定到达指定时间
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Main End");
    }
}

线程的状态

Java线程有以下状态:

  1. New:新创建的线程,尚未执行;
  2. Runnable:运行中的线程,随时被CPU调度;
  3. Blocked:运行中的线程,因某些操作被阻塞而挂起;
  4. Waiting:运行中的线程,因某些操作而在等待中;
  5. Timed Waiting:运行中的线程,因执行sleep()方法而计时等待;
  6. Terminated:线程已终止,因run()方法执行完毕。

在Java程序中,一个线程对象只能调用一次start()方法。

启动后,线程在Runnable, Blocked, Waiting, Timed Waiting 状态间切换,直到变成Terminated状态。

线程终止的原因

  1. 正常终止:run()方法执行完毕;
  2. 意外终止:run()方法碰到未捕获的异常;
  3. 强制终止:对线程实例调用stop()方法(不推荐)。

线程中断

方法一

在一个线程执行一个任务的过程中,我们可能需要打断这个线程并执行其它线程,这时我们就需要使用interrupt()方法。

class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new ThreadTest();
        // t线程启动
        t.start();
        // 当前线程暂停, 执行t线程
        Thread.sleep(1000);
        // 暂停结束,二者并行, 打断t线程
        t.interrupt();
        // 等待t线程结束
        t.join();
        System.out.println("Main End");
    }
}

class ThreadTest extends Thread {
    @Override
    public void run() {
        // 新建p线程
        Thread p = new ThreadTest2();
        // p线程启动
        p.start();
        try {
            // 等待p线程结束
            p.join();
        } catch (InterruptedException e) {
            System.out.println("T was Interrupted!");
        }
        p.interrupt();
    }
}

class ThreadTest2 extends Thread {
    @Override
    public void run() {
        int n = 0;
        // 在未被中止前持续循环
        while (!isInterrupted()) {
            System.out.println(n++ + "asdf");
            try {
                // 当前线程暂停
                Thread.sleep(100);
            } catch (InterruptedException e) {
//                System.out.println("P was Interrupted!");
                // 被中止后break跳出循环
                break;
            }
        }
    }
}

main线程调用t.interrupt()方法中断t线程,此时t线程中正在等待p线程结束,t.interrupt()方法会立刻结束t线程并抛出InterruptedException异常,在t线程结束前,对p线程进行了p.interrupt()方法调用使p线程中断。

如果没有进行该操作,p线程会持续运行,JVM也不会退出。有些时候,我们不得不使得一些线程无限循环,那么当我们要退出时,谁来关闭这些线程?

因此有了 守护线程

方法二

我们可以通过设置标志位来判断线程是否继续运行。

class Main {
    public static void main(String[] args) throws InterruptedException {
        // 注意这里是子类的引用
        ThreadTest t = new ThreadTest();
        // 启动线程t
        t.start();
        // 当前线程暂停
        Thread.sleep(1000);
        // 改变标志位的值
        t.running = false;
    }
}

class ThreadTest extends Thread {
    // 标志位
    public volatile boolean running = true;

    @Override
    public void run() {
        int n = 0;
        while (running) {
            System.out.println(n++ + "asdf");
        }
        System.out.println("T End");
    }
}

注意到这里使用了volatile关键字对running进行标记,该关键字用于标记线程间的共享变量,确保每个线程都能读取到更新后的变量值。

线程间共享变量

为什么要用该关键字呢?

这涉及到Java的内存模型。在JVM中,变量的值保存在主内存中。当线程访问变量时,会先获取一个副本,并保存在该线程的工作内存中。

如果线程修改了变量,JVM会在某个时刻把修改后的值写到主内存中,这个时刻是不确定的!因此,会出现变量更改后,某个线程访问到的变量依然是更改前的变量值的情况。

所以我们需要使用关键字volatile告诉JVM:

  1. 每次访问变量时,总是获取主内存的最新值;
  2. 每次修改变量后,立刻写到主内存中。

守护线程

当某个用户线程无限循环时,我们不能正常结束JVM, 但是我们很需要这个无限循环的线程,比如Java的垃圾回收线程,怎么做?守护线程。

守护线程用来服务于用户线程。在JVM中,所有用户线程结束后,无论是否有守护线程运行,虚拟机都会退出。

守护线程的创建

Thread t = new ThreadTest();
t.setDaemon(true);
t.start();

性质

  1. 不能将已经启动的线程设置为守护线程,即

    t.start();
    t.setDaemon(true); // 将会抛出异常
    

    是错误的,会抛出IllegalThreadStateException异常。

  2. 在守护线程中产生的新线程也是Daemon的, 即也是守护线程;

  3. 守护线程不能访问文件等固有资源。因为它可能会在任何时候中断;

  4. Java自带的多线程框架,比如ExecutorService,会将守护线程转换为用户线程,所以如果要使用后台线程就不能用Java的线程池。

线程同步

多个线程同时运行时,线程的调度有操作系统决定,因此任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。那么,如果多个线程同时读写共享变量,就会出现数据不一致的问题。

如:

class Main {
    public static void main(String[] args) throws Exception {
        ThreadAdd add = new ThreadAdd();
        ThreadDec dec = new ThreadDec();
        add.start();
        dec.start();
        add.join();
        dec.join();
        System.out.println(Counter.count);
    }
}

class Counter {
    public static int cnt = 0;
}

class ThreadAdd extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            Counter.cnt++;
        }
    }
}

class ThreadDec extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            Counter.cnt--;
        }
    }
}

这个程序的结果是多少?0?

实际上每次运行的结果都是不一样的。

原子操作

对变量进行读取和写入时,必须保证操作是原子操作,即不能被中断的操作。

JVM规定以下操作为原子操作:

  1. 基本类型赋值;
  2. 引用类型赋值。

加锁解锁

如果操作是非原子操作,那么在若干个线程同时执行的过程中,该操作可能会中断导致得到错误的结果。

为了避免这种情况的发生,我们需要使用加锁和解锁的操作。

加锁解锁操作可以保证若干条指令总是在一个线程执行,不会有其它线程进入此指令区间。即使在执行中,线程被中断,其它线程也会因为无法获得锁, 导致无法进入此指令区间。只有执行线程将锁释放,即解锁后,其它线程才有机会获得锁并执行。

这种加锁和解锁之间的代码块我们称之为临界区,任何时候临界区中最多只有一个线程执行。

加锁和解锁可以看作将非原子操作实现成原子操作。易得,原子操作是不需要加锁的。

加锁解锁的实现

我们通过synchronized关键字对一个对象进行加锁。

即:

  1. 找出修改共享变量的线程代码块;
  2. 选择一个共享实例作为锁;
  3. 使用synchronized(lockObject) {...}.

举个例子就是这样的:

synchronized(lock) { //加锁
    n += 1;
} //自动解锁

上面的代码经过加锁操作后,改写为:

class Main {
    public static void main(String[] args) throws Exception {
        ThreadAdd add = new ThreadAdd();
        ThreadDec dec = new ThreadDec();
        add.start();
        dec.start();
        add.join();
        dec.join();
        System.out.println(Counter.count);
    }
}

class Counter {
    public static final Object lock = new Object();
    public static int cnt = 0;
}

class ThreadAdd extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            synchronized(Counter.lock) {
                Counter.cnt++;
            }
        }
    }
}

class ThreadDec extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            synchronized(Counter.lock) {
                Counter.cnt--;
            }
        }
    }
}

其中的代码:

synchronized(Counter.lock) {
//....
}

其中用Counter.lock实例作为锁,两个线程在临界区先获得锁,再执行操作。

执行结束且语句块结束后,会自动释放锁。

参考资料

  1. https://www.liaoxuefeng.com/wiki/1252599548343744/1255943750561472
  2. https://zhuanlan.zhihu.com/p/28049750
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值