多线程

多线程基础

Java语言内置了多线程支持:一个Java程序实际上一个jvm的进程,jvm进程用一个主线程来执行main()方法,在主线程里又可以启动多个线程。此外,jvm还有负责垃圾回收的其他工作线程。

对于大多数Java程序来说多任务其实就是多线程实现的多任务。

和单线程相比:多线程经常需要都写共享数据,并需要同步。多线程编程的复杂度更高,调试更加困难。

多线程是Java程序最基本的并发模型,读写网络、数据库、Web开发等都需要多线程模型。

创建新线程

创建一个新线程我们需要实例化一个Thread实例,然后调用start()方法启动线程。

public class TestThread {

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

这个线程实际上什么也没做就结束了。我们希望线程能执行一些代码。

  • 方法一:自定义一个类继承Thread类,重写run方法
public class TestThread {
    public static void main(String[] args) {
        Thread thread = new New();
        thread.start(); // 启动新线程
    }
}

class New extends Thread {
    @Override
    public void run() {
        System.out.println("这是一个新线程!");
    }
}

以上使用lambda写法

public class TestThread {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("这是一个新线程");
        });
        thread.start(); // 启动新线程
    }
}
  • 方法二:实现Runable接口
public class TestThread {
    public static void main(String[] args) {
        New2 thread = new New2();
        Thread thread1 = new Thread(thread);
        thread1.start();
        for (int i = 0; i < 30; i++) {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Main方法。。。");
        }
    }
}

class New2 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 30; i++) {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("实现Runable接口");
        }
    }
}

java 用 Thread 对象表示一个线程,通过 start() 方法启动一个新线程;线程的执行代码写在 run() 方法中。线程虽然可以通过 Thread.setPriority(int n) 设置优先级,但是优先级高的并不能保证一定先执行。

线程状态

在java程序中,线程对象调用start()方法启动新线程,在新线程中执行run()方法,run()方法执行完,线程就结束了。Java的线程有以下几种状态:

  • New:新生状态,新创建的线程,尚未执行。
  • Runnable:运行中的线程,正在执行run()方法的线程。
  • Blocked:运行中的线程,因为某些操作被阻塞而挂起。
  • Waiting:运行中的线程,因为某些操作在等待中。
  • Time Waiting:运行中的线程,因为执行了sleep()方法正在计时等待。
  • Terminated:线程一终止,因为run()方法已经执行完毕。

当线程启动后,它可以在 Runable、Blocked、Waiting、Timed Waiting 这几个状态中切换,直到变成 Terminated 状态,线程终止。

线程终止的原因有:

  • 线程正常终止:线程的 run() 方法执行完毕,或执行到 return 语句返回;
  • 线程的意外终止:run() 方法因为未捕获的异常而终止;
  • 线程的强制终止:对某个线程的 Thread 实例调用 stop() 方法强制终止。

一个线程还可以等待另一个线程运行结束。例如,main 线程在启动 a 线程后,调用 a.join() 等待 a 线程结束后在继续运行 main 线程。

public class TestThread {
    public static void main(String[] args) throws InterruptedException {
        Thread a = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("多线程练习" + i + "次");
            }
        });
        a.start();
        a.join();
        System.out.println("main线程执行");
    }
}

如果 a 线程已经结束,调用 a.join() 方法会立即返回。此外,join(long) 重载方法可以指定一个等待时间,超过等待时间后不在继续等待。

中断线程

中断线程就是其他线程给目标线程一个中断信号,该线程收到信号后结束执行 run() 方法。可以通过对目标线程调用 interrupt() 方法,目标线程检测 interrupted 的状态,如果为 true 就结束线程。

public class TestThread {
    public static void main(String[] args) throws InterruptedException {
        Test t = new Test();
        t.start();
        Thread.sleep(5);
        t.interrupt();
        System.out.println("结束");
    }
}

class Test extends Thread {
    public void run() {
        int i = 0;
        while (!isInterrupted()) {
            System.out.println("线程执行了" + i++ + "次。");
        }
    }
}

main 线程对 t 线程调用了 t.interrupt() 方法,t 线程的 while 循环会检测 isInterrupted 的状态。

如果线程处于等待状态,如,t.join() 会让 main 线程进入等待状态,此时对 main 线程调用 interrupt() 方法,join() 方法会立刻抛出 InterruptedException,只要目标线程捕获 InterruptedException ,就说明其他线程对其调用了 interrupt() 方法,该线程应该立刻解说运行。

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

class TestA extends Thread {
    public void run() {
        TestB b = new TestB();
        b.start(); // 启动hello线程
        try {
            b.join(); // 等待hello线程结束
        } catch (InterruptedException e) {
            System.out.println("interrupted!");
        }
        b.interrupt();
    }
}

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

main 线程通过调用 a.interrupt() 从而通知 a 线程中断,而此时 a 线程正位于 b.join() 的等待中,此方法会立刻结束等待并抛出 InterruptedException。由于我们在 a 线程中捕获了 InterruptedException,因此,就可以准备结束该线程。在 a 线程结束前,对 b 线程也进行了 interrupt() 调用通知其中断。如果去掉这一行代码,可以发现 b 线程仍然会继续运行,且 JVM 不会退出。

另外一个中断线程的方法就是设置标志位。我们通常会用一个 Boolean 来标志一个线程是否需要停止。

public class TestThread {
    public static void main(String[] args) throws InterruptedException {
        TestA a = new TestA();
        a.start();
        Thread.sleep(2);
        a.flag = false;
        System.out.println("end");
    }
}

class TestA extends Thread {
    public volatile boolean flag = true;
    public void run() {
        while (flag) {
            System.out.println("线程执行中...");
        }
        System.out.println("结束");
    }
}

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

守护线程

java 程序的入口就是由 jvm 启动的 main 线程,main 线程又可以启动其他线程,当所有的线程都结束后,jvm 退出,程序结束。但是有一些线程是无限循环的,比如定时任务、心跳等线程。如果这个线程不结束 jvm 就无法结束。那要怎么办呢?答案就是使用守护线程(Daemon Thread)。

守护线程是指为其他线程服务的线程。在 jvm 中,所有非守护线程都执行完毕后,无论有没有守护线程 jvm 都会推出。jvm 的推出不必关心守护线程是否已经结束。

守护线程的创建和普通的线程创建方法一样,只是在调用 start() 方法前,调用 setDaemon(true) 把线程设置为守护线程。在守护线程的代码中,不能有任何需要关闭的资源,因为守护线程结束时没有机会关闭这些资源。

线程同步

多个线程同时运行时,线程的调度由操作系统决定,任何一个线程都有可能在某个时候被操作系统暂停,然后在某个时间段后开启。这种情况下如果有多个线程读写共享变量,就会出现数据不一致的问题。如下,a 线程 +1 1000次,b 线程 -1 1000次,理论上组后的结果应该是 0。但实际上最后的结果大多不是 0。这就是因为多线程读写共享变量导致数据不一致。

public class TestThread {
    public static void main(String[] args) throws InterruptedException {
        TestA a = new TestA();
        TestB b = new TestB();
        a.start();
        b.start();
        a.join();
        b.join();
        System.out.println(Counter.counter);
    }
}
class Counter {
    public static int counter = 0;
}
class TestA extends Thread {
    public void run() {
        for (int i = 0; i < 1000; i++) {
            Counter.counter += 1;
        }
    }
}
class TestB extends Thread {
    public void run() {
        for (int i = 0; i < 1000; i++) {
            Counter.counter -=1;
        }
    }
}

造成这种情况是因为 a 线程取到值(假如是100)后还没来得及 +1 操作,线程就被暂停了,然后 b 线程取到的值也是 100 ,b 线程对他进行了 -1 操作并赋值给了变量,此时变量的值是 99。然后 a 线程又获得资源进行 +1 操作获得值是 101,并赋值给变量,变量最终值为 101,这明显是错误的,进行了 +1,-1 操作后变量的值应该还是100。

这就说明在多线程操作时,要保证逻辑的准确,在一个线程执行时,其他的线程必须等待,等待其完成后才可以执行其他的线程。这就是原子性。那怎么保证原子性呢?办法就是加锁。通过加锁保证一个线程在执行的时候,不会有其他的线程操作同一个数据,直到解锁。这种被加了锁的代码块被称为临界区(Critical Section),临界区的代码任何时候都只有一个线程执行。

保证一段代码的原子性就是通过加锁实现的,在 Java 中通过 synchronized 关键字对一个对象加锁。

synchronized(lock) {
    n = n+1;
}

synchronized 保证了代码块只能由一个线程执行。我们把上边的代码加上锁在看下。

public class TestThread {
    public static void main(String[] args) throws InterruptedException {
        TestA a = new TestA();
        TestB b = new TestB();
        a.start();
        b.start();
        a.join();
        b.join();
        System.out.println(Counter.counter);
    }
}
class Counter {
    public static final Object lock = new Object();
    public static int counter = 0;
}
class TestA extends Thread {
    public void run() {
        for (int i = 0; i < 1000; i++) {
            synchronized (Counter.lock) {
                Counter.counter += 1;
            }
        }
    }
}
class TestB extends Thread {
    public void run() {
        for (int i = 0; i < 1000; i++) {
            synchronized (Counter.lock) {
                Counter.counter -=1;
            }
        }
    }
}

上边的代码中 Conter.lock 实例作为锁,两个线程在执行各自的代码块儿时,需要先获得锁,才能进入代码块执行,执行结束后在 synchronized 的语句块执行完毕后自动释放锁,下一个线程就可以再次获得锁执行代码块。但是 synchroized 会降低代码的执行效率。

概括一下如何使用 synchronized :

  1. 找出改变共享变量的线程代码块;
  2. 选择一个共享实例作为锁(多个线程代码块必须使用同一把锁才能保证原子性);
  3. 给线程代码块加锁。

同步方法

Java 程序依靠 synchronized 对线程进行同步,使用 synchronized 的时候,锁住哪个对象非常重要。

让线程自己锁对象往往使得代码逻辑混乱,不利于封装。我们可以把 synchronized 的逻辑封装起来。比如下面的计数器:

class Counter {
    private int count = 0;

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

    public void dec(int i) {
        synchronized (this) {
            count -= i;
        }
    }
    
    public int get() {
        return count;
    }
}

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

也可以把 synchronized 关键字提到方法上,让 synchronized 修饰这个方法。

class Counter {
    private int count = 0;

    public synchronized void add(int i) {
        count += i;
    }

    public synchronized void dec(int i) {
        count -= i;
    }

    public int get() {
        return count;
    }
}

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

死锁

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;
    }
}

执行 add() 的时候已经获得了 this 的锁,但是 add() 方法里面又调用了 dec() 的方法,由于 dec() 方法也需要获取 this 的锁,那 dec() 方法是否可以获取到 this 的锁呢?答案是可以获取。jvm 允许一个线程重复获取一个锁,这种可以被同一个线程反复获取的锁就是可重入锁。

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

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的锁
}

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

  • 线程1:进入 add() ,获得 lockA;
  • 线程2:进入 dec() ,获得 lockb。

随后:

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

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

死锁发生后,没有任何机制能解除死锁,只能强制结束 jvm。因此,在编写多线程时一定要防止死锁,死锁一旦形成,就只能强制结束进程。所以我们获取锁的顺序要一致。严格按照先获取 lockA ,再获取 lockB 的顺序。

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(lockA) { // 获得lockA的锁
        this.value -= m;
        synchronized(lockB) { // 获得lockB的锁
            this.another -= m;
        } // 释放lockB的锁
    } // 释放lockA的锁
}

使用wait和notify

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

public class TaskQueue {
    Queue<String> queue = new LinkedList<>();

    public synchronized void addTask(String s) {
        this.queue.add(s);
    }

但是 synchronized 并没有解决多线程的协调问题。以上边的 TaskQueue 为例,我们再编写一个 getTask() 方法取出队列的第一个任务:

public class TaskQueue {
    Queue<String> queue = new LinkedList<>();

    public synchronized void addTask(String s) {
        this.queue.add(s);
    }
    
    public synchronized String getTask() {
        while (this.queue.isEmpty()) {
            
        }
        return this.queue.remove();
    }
}

上述代码看上去没有问题: getTask() 内部先判断队列是否为空,如果为空就循环等待,知道一个线程往队列中放入一个任务, while() 循环退出,就可以返回队列的元素。但实际上 while 循环永远不会退出,因为线程在执行 getTask() 入口获取了 this 锁,其他线程根本无法调用 addTask() ,因为 addTask() 执行条件也是获取 this 锁。因此,此代码会在getTasd() 方法死循环。

我们在回头来想想我们当初想要实现的目标:

  • 线程1可以调用 addTask() 方法不断往队列中添加任务;
  • 线程2可以调用 getTask() 方法从队列中获取任务,如果队列为空则等待,直到队列中被放入了新的任务。

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

对上述代码我们进行改造:

public class TaskQueue {
    Queue<String> queue = new LinkedList<>();

    public synchronized void addTask(String s) {
        this.queue.add(s);
    }

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

当一个线程执行到 getTask() 方法的 while 循环时,必定已经获取到了 this 锁,此时,线程执行 while 条件判断,如果条件成立(队列为空),线程将执行 this.wait(),进入等待。

这里的关键是 wait() 方法必须是在当前获取的锁对象上调用,这里获取的是 this 锁,因此调用 this.wait().

调用 wait() 方法后,线程进入等待状态,wait() 方法不会返回,直到将来某个时刻线程从等待状态被其他线程唤醒后才会返回。

有些仔细的童鞋会指出:即使线程在getTask()内部等待,其他线程如果拿不到this锁,照样无法执行addTask(),肿么办?

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

因此,只能在锁对象上调用wait()方法。因为在getTask()中,我们获得了this锁,因此,只能在this对象上调用wait()方法,当 this.wait() 等待时,它就会释放 this 锁,从而使其他线程能够在 addTask() 方法获得 this 锁。

现在我们面临第二个问题:如何将进入等待的 getTask() 方法从等待状态唤醒。答案是在相同的锁对象上调用 notify() 方法。我们修改 addTask() 如下:

public synchronized void addTask(String s) {
    this.queue.add(s);
    this.notify();// 唤醒this锁等待的线程
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值