多线程

创建新线程

Java语言内置了多线程支持。当Java程序启动的时候,实际上是启动了一个JVM进程,然后,JVM启动主线程来执行main()方法。在main()方法中,我们又可以启动其他线程。

Thread的实例方法start()启动线程

实现方式有以下几种:

 public class Main {
        public static void main(String[] args) {
            Thread t = new MyThread();
            t.start(); // 启动新线程
        }
    }
    
    class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("start new thread!");
        }
    }

上述代码使用的是继承Thread类来完成多线程的创建。

使用的类:Thread

使用方法:Thread的实例方法 start()

 public class Main {
        public static void main(String[] args) {
            Thread t = new Thread(new MyRunnable());
            t.start(); // 启动新线程
        }
    }
    
    class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("start new thread!");
        }
    }

上述代码使用的是实现Runnable类来完成多线程的创建。

使用的类/接口:Thread Runnable

使用的方法:Thread的实例方法 start()

public class Main {

    public static void main(String[] args) throws Exception {
        // 实现实现Callable接口的类
        MyCallable callable = new MyCallable();
        /*
        *FutureTask一个可取消的异步计算,FutureTask 实现了Future的基本方法,
        *提空 start cancel 操作,可以查询计算是否已经完成,并且可以获取计算的结果。
         */
        FutureTask<Integer> ft = new FutureTask<>(callable);
        Thread thread = new Thread(ft);
        thread.start();
        Integer value = ft.get(); // 获取返回值
        System.out.println(value); // 1
    }
}

// 相比于Runnable,Callable有反回值,通过泛型来定义返回的类型
class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        return 1;
    }
}

上述代码实现了Callable接口来完成多线程的创建

使用的接口/类:Callable,Thread,FutureTask

使用的方法:FutureTask的实例方法get(),Thread的实例方法start()

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("start new thread!");
        });
        t.start(); // 启动新线程
    }
}

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable(){
            public void run(){
                System.out.println("start new thread!")
            }
        });
        t.start(); // 启动新线程
    }
}

上述代码使用了匿名类,第一个是Java8引入的lambda语法,第二个是普通的匿名类语法

使用的类/接口:Thread Runnable

使用的方法:Thread的实例方法 start()

线程执行与main方法执行的区别

public class Main {
    public static void main(String[] args) {
        System.out.println("main start...");
        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 start...
*main end...
*thread run...
*thread end.
*/

public class Main {
    public static void main(String[] args) {
        System.out.println("main start...");
        Thread t = new Thread() {
            public void run() {
                System.out.println("thread run...");
                System.out.println("thread end.");
            }
        };
        t.start();
        try{ // 异常捕获
             Thread.sleep(10) // 毫秒为单位
        }catch(InterruptedException e){}
       
        System.out.println("main end...");
    }
}

/* 
*打印的顺序 
*main start...
*thread run...
*thread end.
*main end...
*/
Thread的静态方法setPriority()设置优先级

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

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

优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。

Thread的静态方法yield()让出线程
public void run(){
	 Thread.yield();
}

静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。

多线程的状态

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

  • New(初始化):新创建的线程,尚未执行;
  • Runnable(运行):运行中的线程,正在执行run()方法的Java代码;
  • Blocked(阻塞):运行中的线程,因为某些操作被阻塞而挂起;
  • Waiting(等待):运行中的线程,因为某些操作在等待中;
  • Timed Waiting(超时等待):运行中的线程,因为执行sleep()方法正在计时等待;
  • Terminated(终止):线程已终止,因为run()方法执行完毕。

当线程启动后,它可以在RunnableBlockedWaitingTimed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。

线程终止的原因有:

  • 线程正常终止:run()方法执行到return语句返回;
  • 线程意外终止:run()方法因为未捕获的异常导致线程终止;
  • 对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)。

join方法

public class Create {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("main start");
        Thread thread = new Thread(){
            @Override
            public void run() {
                System.out.println("thread start");
                System.out.println("thread end");
            }
        };
        thread.start();
        thread.join(); // 可以设置失效时间(毫米)
        System.out.println("main end");
    }
}

/* 
*打印的顺序 
*main start...
*thread run...
*thread end.
*main end...
*/

main线程对线程对象t调用join()方法时,主线程将等待变量t表示的线程运行结束,即join就是指等待该线程结束,然后才继续往下执行自身线程。所以,上述代码打印顺序可以肯定是main线程先打印startt线程再打印hellomain线程最后再打印end

如果t线程已经结束,对实例t调用join()会立刻返回。此外,join(long)的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。

中断线程

如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。(比如:用户下载一个100M的文件,网速太慢,用户点击取消,就是中断了线程)

使用Thread的实例方法interrupt()
public class Create {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("main start");
        Thread thread = new Thread(){
            @Override
            public void run() {
                int count = 0;
               	while(true){
                    count++;
                    System.out.println(count);
                }
            }
        };
        thread.start();
        Thread.sleep(1000);
        thread.interrupt(); // 中断
        System.out.println("main end");
    }
}

述代码,main线程通过调用t.interrupt()方法中断t线程,但是要注意,interrupt()方法仅仅向t线程发出了“中断请求”,至于t线程是否能立刻响应,要看具体代码。t线程要调用而t线程的while循环会检测isInterrupted(),isInterrupted()返回true会直接中断线程的执行

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        t.start();
        Thread.sleep(1000);
        t.interrupt(); // 中断t线程
        t.join(); // 等待t线程结束
        System.out.println("end");
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        Thread hello = new HelloThread();
        hello.start(); // 启动hello线程
        try {
            hello.join(); // 等待hello线程结束
        } catch (InterruptedException e) {
            System.out.println("interrupted!"); // 中断,抛出异常
        }
        hello.interrupt();
    }
}

class HelloThread extends Thread {
    @Override
    public void run() {
        int n = 0;
        while (!isInterrupted()) {
            n++;
            System.out.println(n + " hello!");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                break;
            }
        }
    }
}

如果线程处于等待状态,就是main线程调用了 t.join()等待t线程结束,如果之前调用 t.interrupt()中断t线程的话;t线程内部调用

hello.join()方法来等待hello线程结束时,会立刻抛出InterruptedException异常,捕获异常,就可以准备结束该线程了

t线程结束前,对hello线程也进行了interrupt()调用通知其中断。如果去掉这一行代码,可以发现hello线程仍然会继续运行,且JVM不会退出。

设置标志位进行中断
public class Main {
    public static void main(String[] args) throws InterruptedException {
        HelloThread  t = new HelloThread();
        t.start();
        Thread.sleep(10);
        t.running = false;
    }
}
class HelloThread extends Thread {

    public volatile boolean running = true;
    @Override
    public void run() {
        int n = 0;
        while (running) {
            n++;
            System.out.println(n + " hello!");
        }
    }
}

上述代码中,在HelloThread内部申明了running全局变量,由volatile修饰后,成为了线程共享的变量,线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值。

volatile的作用

这涉及到Java的内存模型。在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!

这会导致如果一个线程更新了某个变量,另一个线程读取的值可能还是更新前的。例如,主内存的变量a = true,线程1执行a = false时,它在此刻仅仅是把变量a的副本变成了false,主内存的变量a还是true,在JVM把修改后的a回写到主内存之前,其他线程读取到的a的值仍然是true,这就造成了多线程之间共享的变量不一致。

因此,volatile关键字的目的是告诉虚拟机:

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

守护线程

Java程序入口就是由JVM启动main线程,main线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。

如果有一个线程没有退出,JVM进程就不会退出。所以,必须保证所有线程都能及时结束。

但是有一种线程的目的就是无限循环,例如,一个定时触发任务的线程:

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

如上代码,调用Thread实例方法setDaemon(true),将此线程变成守护线程

线程同步

当线程各自运行时,无法手动干预线程的调度,因此,任何一个线程都有可能被系统干预暂停,然后在某个时间段在此运行

所以,当多个线程同时读写共享变量,会出现数据不一致的情况。

并发问题:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new AddRunnable());
        Thread thread2 = new Thread(new DecRunnable());
        thread.start();
        thread2.start();
        thread.join();
        thread2.join();
        System.out.println(Counter.count);
    }
}

class Counter {
    public static Integer count = 0;
}

class AddRunnable implements Runnable{

    @Override
    public void run(){
        for(int i = 0; i < 100; i++){
            Counter.count += 1;
        }
    }
}

class DecRunnable implements Runnable{

    @Override
    public void run(){
        for(int i = 0; i < 100; i++){
            Counter.count -= 1;
        }
    }
}

根据正常单线程模型下的运行结果:0,但是在多线程情况下,每次运行结果都是不一样的

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

假设 i=100 ,线程一与线程二要i++,这可能造成线程一与线程二执行完后最后的数是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 --
        ▼             ▼

加锁和解锁就能保证三条指令能在一个线程间执行完成,不会有其他线程会进入此指令区间。

即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。

只有执行线程将锁释放后,其他线程才有机会获得锁并执行。

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

使用synchronized关键字进行同步
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new AddRunnable());
        Thread thread2 = new Thread(new DecRunnable());
        thread.start();
        thread2.start();
        thread.join();
        thread2.join();
        System.out.println(Counter.count);
    }
}

class Counter {
    public final static Object lock = new Object();
    public static Integer count = 0;
}

class AddRunnable implements Runnable{

    @Override
    public void run(){
        for(int i = 0; i < 100; i++){
            synchronized (Counter.lock){ // 获取锁
                Counter.count += 1;
            } // 释放锁
        }
    }
}

class DecRunnable implements Runnable{

    @Override
    public void run(){
        for(int i = 0; i < 100; i++){
            synchronized (Counter.lock){ // 获取锁
                Counter.count -= 1;
            } // 释放锁
        }
    }
}

synchronized保证了代码块在任意时刻最多只有一个线程能执行。

使用synchronized解决了多线程同步访问共享变量的正确性问题。但是,它的缺点是带来了性能下降。因为synchronized代码块无法并发执行。此外,加锁和解锁需要消耗一定的时间,所以,synchronized会降低程序的执行效率。

我们来概括一下如何使用synchronized

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

在使用synchronized的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁

错误使用synchronized
public class Main {
        public static void main(String[] args) throws Exception {
            var add = new AddThread();
            var dec = new DecThread();
            add.start();
            dec.start();
            add.join();
            dec.join();
            System.out.println(Counter.count);
        }
    }
    
    class Counter {
        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(Counter.lock1) {
                    Counter.count += 1;
                }
            }
        }
    }
    
    class DecThread extends Thread {
        public void run() {
            for (int i=0; i<10000; i++) {
                synchronized(Counter.lock2) {
                    Counter.count -= 1;
                }
            }
        }
    }

上述代码结果不是0,虽然使用了synchronized。当时两个线程锁住的不是同一个对象!这使得两个线程各自都可以同时获得锁:因为JVM只保证同一个锁在任意时刻只能被一个线程获取,但两个不同的锁在同一时刻可以被两个线程分别获取。

因此,使用synchronized的时候,获取到的是哪个锁非常重要。锁对象如果不对,代码逻辑就不对。

public class Main {

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[]{new AddThread(),new AddThread2(),new DecThread(),new DecThread2()};
        for (Thread thread : threads) {
            thread.start();
            thread.join();
        }
        System.out.println(Counter.count1);
        System.out.println(Counter.count2);
    }
}

class Counter {
    public final static Object lock = new Object(); // 同步锁
    public static Integer count1 = 0;
    public static Integer count2 = 0;
}

class AddThread extends Thread {
    @Override
    public void run() {
        for(int i = 0; i < 100; i++){
            synchronized (Counter.lock){
                Counter.count1++;
            }
        }
    }
}

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

class AddThread2 extends Thread {
    @Override
    public void run() {
        for(int i = 0; i < 100; i++){
            synchronized (Counter.lock){
                Counter.count2++;
            }
        }
    }
}

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

上述四个线程对两个变量进行读写,但使用的是同一个Counter.lock对象锁,这就造成了原本可以并发执行的Counter.studentCount += 1Counter.teacherCount += 1,现在无法并发执行了,执行效率大大降低。

实际上,需要同步的线程可以分成两组:AddThreadDecThreadAddThread2DecThread2,组之间不存在竞争,因此,应该使用两个不同的锁

AddThreadDecThread使用Counter.lock

synchronized(Counter.lock) {
  ...
}

AddThread2DecThread2使用Counter.lock2

synchronized(Counter.lock2) {
  ...
}

这样才能最大化地提高执行效率。

不需要synchronized的操作

JVM规范定义了几种原子操作:

  • 基本类型(longdouble除外)赋值,例如:int n = m; (x64平台)jvm规定了64位也是原子性的
  • 引用类型赋值,例如:List<String> list = anotherList

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

public class Main{
    
	public Integer id;
    
    public void set (Integer id){
        this.id = id;
    }
}

但是,如果是多行赋值语句,就必须保证是同步操作,

public class Main{
    
	public Integer id;
	
	public String name;
    
    public void set (Integer id,String name){
        this.id = id;
        this.name = name;
    }
}

有些时候,通过一些巧妙的转换,可以把非原子操作变为原子操作。

public class Main{
	
    public Object[] idAndName;
    
    public void set (Integer id,String name){
        Object[] idAndName = new Object[]{id,name};
        this.idAndName = idAndName;
    }
}

this.idAndName = idAndName是引用赋值的原子操作

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

为什么多行赋值是不安全的?

当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。

当执行完第一个赋值操作后,此时该线程被操作系统暂停。另外一个线程开始执行时,修改了第二个赋值操作的引用,然后第二个线程也被暂停,第一个线程继续执行后继续进行第二个变量的赋值操作。结果就会造成第二个线程中的变量被第一个线程中设置的替换了。

同步方法

让线程自己选择锁对象往往会使逻辑变的混乱,而且不利于封装。更好的方法是将synchronized封装起来。

public class Counter {

    private Integer count = 0;

    public void add(){
        synchronized (this){
            count++;
        }
    }

    public void dec(){
        synchronized (this){
            count--;
        }
    }

    public int get() {
        return count;
    }
}

这样一来,线程调用add()方法与dec()方法时它不必关心同步逻辑,因为synchronized代码块在add()dec()方法内部。并且,我们注意到,synchronized锁住的对象是this,即当前实例,这又使得创建多个Counter实例的时候,它们之间互不影响,可以并发执行:

public class Main {

    public static void main(String[] args) throws Exception {
        Counter counter = new Counter();
        Counter counter2 = new Counter();

        // counter实例
        new Thread(()->{
            counter.add();
        }).start();

        new Thread(()->{
            counter.dec();
        }).start();

        // counter2实例
        new Thread(()->{
            counter2.add();
        }).start();

        new Thread(()->{
            counter2.dec();
        }).start();

        System.out.println(counter.get());
        System.out.println(counter2.get());
    }
}

如果一个类可以被多线程访问并且保证数据安全,那么这个类就是线程安全。Counter类就是线程安全的,类似Math这种只提供静态方法的也是线程安全的,除了线程安全的类,大多数类都是线程不安全的。如ArrayList,但是只是读取而不写入那么没有线程安全问题

上述的代码其实可以改进

public class Counter {

    private Integer count = 0;

    public synchronized void add(){ // 锁住this
            count++;
    } // 解锁

    public synchronized  void dec(){ // 锁住this
            count--;
    } // 解锁

    public int get() {
        return count;
    }
}

因此,用synchronized修饰的方法就是同步方法,它表示整个方法都必须用this实例加锁。

如果是静态方法加synchronized的话。锁住的是jvm自动创建的Class对象

get()方法因为是读取一个变量的,所以不需要同步,但如果是:

public class Counter {
	private Integer id;
    private Integer count;
    
    public Num get(){
        Num num = new Num();
        num.id = this.id;
        num.count = this.count;
        return num;
    }
}

就必须要同步了。

死锁

当一个对象拿到锁之后,可以调用其他需要这个锁的对象,就是锁可以重复使用(可重入锁)

public class Main {

    public static void main(String[] args) throws Exception {
        Counter counter = new Counter();
        // counter实例
        new Thread(()->{
            counter.add();
        }).start();
        System.out.println(counter.get());
    }
}

class Counter {

    private Integer count = 0;

    public void add(){
        synchronized (this){
            count++;
            dec(); // 调用需要this锁的dec()方法
        }
    }

    public void dec(){
        synchronized (this){
            count--;
        }
    }

    public int get() {
        return count;
    }
}

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

死锁

public class Main {

    public static void main(String[] args) {
        MyClass myclass = new MyClass();

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                myclass.add(10);
            }
        });

        Thread thread2 = new Thread(() ->{
           myclass.dec(10);
        });

        thread.start();
        thread2.start();
    }
}


class MyClass {

    private final Object lockOne = new Object();
    private final Object lockTwo = new Object();
    private Integer count = 0;

    public  void add(Integer num){
        synchronized (lockOne){ // 获取lockOne锁
            try {
                Thread.sleep(1000); // 睡眠,等待lockTwo锁被拿区
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (Integer i = 1; i <= num; i++) {
                this.count+=i;
            }
            synchronized (lockTwo){ // 拿取lockTwo锁(通过等待,成功让lockTwo被其他线程获取)
                dec(count);
            }

        }
    }

    public void dec(Integer num){

        synchronized (lockTwo){ // 拿取lockTwo锁
            for (Integer i = 1; i <= num; i++) {
                this.count-=i;
            }
            synchronized (lockOne){ // 拿取lockOne锁(锁已经被获取)
                add(count);
            }

        }

    }
}

在获取多个锁的时候,不同线程获取多个不同对象的锁可能导致死锁。对于上述代码,线程1和线程2如果分别执行add()dec()方法时:

  • 线程1:进入add(),获得lockAOne
  • 线程2:进入dec(),获得lockTwo

随后:

  • 线程1:准备获得lockTwo,失败,等待中;
  • 线程2:准备获得lockAOne,失败,等待中。

此时,两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,这就是死锁。

死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。

因此,在编写多线程应用时,要特别注意防止死锁。因为死锁一旦形成,就只能强制结束进程。

解决:线程获取锁的顺序要一致。