Java多线程(一站式学习就在这里)

36 篇文章 0 订阅

一、什么是线程?

        现代操作系统(Windows,macOS,Linux)都可以执行多任务,即同时运行多个任务。CPU执行代码都是一条一条顺序执行的,但是,即使是单核CPU,也可以同时运行多个任务。因为 操作系统执行多任务实际上就是让CPU对多个任务轮流交替执行 。例如,让浏览器执行0.001秒,让QQ执行0.001秒,再让音乐播放器执行0.001秒,在人看来,CPU就是在同时执行多个任务。即使是多核CPU,因为通常任务的数量远远多于CPU的核数,所以任务也是交替执行的。
        在计算机中,我们把一个任务称为一个进程,某些进程内部还需要同时执行多个子任务,我们吧这些子任务成为 线程 它是计算机操作系统调度执行的最小任务单元
进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。

常用的Linux等操作系统都采用抢占式多任务,如何调度线程完全由操作系统决定,程序自己不能决定什么时候执行,以及执行多长时间。

多线程

        Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动多个线程。因此,对于大多数Java程序来说,我们说多任务,实际上是说如何使用多线程实现多任务。
        和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。

二、Thread线程的创建方式

2.1 通过 Thread 实例或继承 Thread 实例的方式

public class Main {
    public static void main(String[] args) {
        //直接创建 Thread 实例
        Thread t = new Thread();
        t.start(); // 启动新线程
    }
}
//自定义类继承 Thread
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!");
    }
}

2.2 创建Thread实例 传入Runnable

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!");
    }
}

2.3 线程优先级

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

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

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

2.4 注意事项

        需要注意启动一个线程,调用的是 start() 方法。调用 run() 方法是无效的,直接调用 run() 方法,实际相当于调用了一个普通的 Java 方法5s d,当前线程没有任何变化,也不会启动新线程。

三、线程的生命周期 (状态)

3.1 线程声明周期中的6种状态:

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

 生命周期中没有Running状态,因为运行时是不需要保存CPU状态的。

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

 线程终止的原因有:

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

四、线程中的关键字

  • sleep()           :阻塞睡眠
  • join()              :阻塞当前线程,让目标线程优先执行
  • yield()            :目标线程礼让执行
  • wait()/notify() :等待/唤醒 执行
  • interrupt()      :中断执行

4.1 sleep() 阻塞睡眠

会让线程休息指定时间,时间超时,就继续执行。
它会让出 CPU 执行时间,其他线程都可能进行竞争执行。此时,如果当前线程持有锁,则锁不会被释放。
try { 
    Thread.sleep(2000); 
} catch (InterruptedException e) { 
    e.printStackTrace(); 
}

4.2 join() 阻塞并优先执行

会让指定的目标线程优先执行完成,然后再继续执行当前线程 ,此操作会阻塞当前线程。
当线程 B  需要依赖线程 A 执行返回的结果时,可以在线程 B 中调用 A.join() ,等待线程 A 执行完成,然后线程 B 在继续执行。还可以使用 A.join(long) 设置超时等待时间,如果线程 A 一直未完成,在超时之后,则线程 B 将继续执行。
final Thread threadA = new Thread(new Runnable() {
    @Override
    public synchronized void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("线程A---正在运行 " + i);
        }
    }
});
Thread threadB = new Thread(new Runnable() {
    @Override
    public synchronized void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("线程B---正在运行 " + i);
            if(i == 0){
                try {
                    System.out.println("让线程A---优先执行完成---然后线程B---再继续执行 " + i);
                    threadA.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
});
threadA.start();
threadB.start();

4.3 yield() 礼让执行

线程内部调用 Thread.yield() 之后,会让出 CPU,当在下次竞争中获得 CPU 执行时间之后,会继续执行。
调用 Thread.yield() 之后,线程 A 只是会被打回就绪状态,有可能马上开始恢复运行,也有可能会等待一段时间。
注意:让出的执行时间只会分配给 具有相同优先级的 B 线程或者优先级更高的线程 C。
Thread threadA = new Thread(new Runnable() {
    @Override
    public synchronized void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("线程A正在运行 " + i);
            if (i == 10) {
                System.out.println("线程A-------礼让----线程B----优先执行 --- " + i);
                Thread.yield();
            }
        }
    }
});
Thread threadB = new Thread(new Runnable() {
    @Override
    public synchronized void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("线程B正在运行 " + i);
        }
    }
});
threadA.start();
threadB.start();

4.4 wait() notify() 等待/唤醒执行

它们是 Object 的方法,需要在 synchronized 中调用,线程 A 调用 object.wait() 之后会释放锁,线程 B 中调用 object.notify()  之后,线程 A 便恢复执行。
//使用 wait() notify() 实现交替打印

final Object object = new Object();
final Thread threadA = new Thread(new Runnable() {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            synchronized (object){
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for(int j= 0;j < 15;j++){
                    System.out.println("线程A---正在运行---" + j);
                }
                object.notify();
            }
        }
    }
});
Thread threadB = new Thread(new Runnable() {
    @Override
    public void run() {
        for(int i= 0;i < 10;i++){
            synchronized (object) {
                for (int j = 0; j < 10; j++) {
                    System.out.println("线程B---正在运行---" + j);
                }
                object.notify();
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
});
threadA.start();
threadB.start();

5.5 interrupted 中断线程执行

表示运行中的线程 A ,可以在线程 B 中调用 A.interrupted() 来实现线程 A 的中断操作。
isInterrupted() 是中断状态,通过线程 A 的 isInterrupted() 来获取线程 A 的状态。
// 在其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,
// 如果是,就立刻结束运行。
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        t.start();
        Thread.sleep(1); // 暂停1毫秒
        t.interrupt(); // 中断t线程
        t.join(); // 等待t线程结束
        System.out.println("end");
    }
}

class MyThread extends Thread {
    public void run() {
        int n = 0;
        while (! isInterrupted()) {
            n ++;
            System.out.println(n + " hello!");
        }
    }
}

注意 

如果线程处于等待状态,例如: t.join()会让main线程进入等待状态,此时,如果在main线程调用interrupt()join()方法会立刻抛出InterruptedException,因此,目标线程只要捕获到join()方法抛出的InterruptedException,就说明有其他线程对其调用了interrupt()方法,通常情况下该线程应该立刻结束运行。这种情况下,由于调用了 A.interrupted() 之后,会抛出异常,如果使用catch(InterruptedException) 来捕获的话,A.isInterrupted() 状态会被清除,也就是 A.isInterrupted() 恒返回false。

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

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

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

5.6 设置标志位 中断线程

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

public class Main {
    public static void main(String[] args)  throws InterruptedException {
        HelloThread t = new HelloThread();
        t.start();
        Thread.sleep(1);
        t.running = false; // 标志位置为false
    }
}

class HelloThread extends Thread {
    public volatile boolean running = true;
    public void run() {
        int n = 0;
        while (running) {
            n ++;
            System.out.println(n + " hello!");
        }
        System.out.println("end!");
    }
}

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

5.6 Java内存模型之共享变量 Volatile

线程之间通信的两种方式:共享内存消息传递
Java内存模型是共享内存的并发模型,线程间主要是通过 读/写 共享变量,来完成隐式通信。
共享变量包括: 实例域静态域数组元素,因为它们都是放在堆内存中的,所有线程都能进行访问,它们是可共享的。
        为什么要对线程间共享的变量用关键字volatile声明呢?这涉及到  Java的内存模型, 即在JVM中,变量的值保存在主内存中,但是,当线程访问该变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!

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

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

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

volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。

六、线程同步锁之 Synchronize、ReentrantLock

        当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。所以在多线程运行的时候,就会出现一个问题: 如果多个线程同时读写共享变量,会出现数据不一致的问题。
        这是因为当多个线程对一个变量进行读取和写入的时候,想要结果正确,就必须保证是原子操作。
        

原子操作

原子操作是指不能被中断的一个或一系列操作。
接下来我们通过一个例子来讲述原子操作的实现机制:
n = n + 1;

对于上面的这行代码,看上去是一行语句,但实际上,这行代码是需要由 3条 CPU 指令来完成解读执行的:

ILOAD
IADD
ISTORE

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

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

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

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

6.1 synchronized

Java程序使用 synchronized关键字对一个对象进行加锁:
下面的代码表示用 lock 实例作为锁,获取锁与释放锁的时机。
synchronized(lock) { // 获取锁
    ...
    n = n + 1;
} // 释放锁

synchronized 的使用方式:

public class Main {
    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(Counter.count);
    }
}

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

class AddThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            synchronized(Counter.lock) {
                Counter.count += 1;
            }
        }
    }
}

class DecThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            synchronized(Counter.lock) {
                Counter.count -= 1;
            }
        }
    }
}

synchronized 使用方式概括:

  • 找出修改共享变量的线程代码块;
  • 选择一个共享实例作为锁;
  • 使用synchronized(lockObject) { ... }
synchronized 的缺点:
  • 由于代码块无法并发执行,带来的性能下降;
  • 加锁和解锁的操作需要消耗一定的时间,所以会降低程序的执行效率;

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

public void add(int m) {
    synchronized (obj) {
        if (m < 0) {
            throw new RuntimeException();
        }
        this.value += m;
    } // 无论有无异常,都会在此释放锁
}
synchronized 注意事项:
1. synchronized 锁住的必须是同一个对象,否则就会在多个线程中同时获得锁。因此,在使用 synchronized 的时候,获取到的是哪个锁,是非常重要的,锁对象不对,代码逻辑就不对。
2. 针对不同的共享变量的操作,应该使用不同的锁,否则由于无法并发执行,从而影响执行效率。
不需要 synchronized 操作的场景

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

  • 基本类型(longdouble除外)赋值,例如:int n = m
  • 引用类型赋值,例如:List<String> list = anotherList

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

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

6.2 ReentrantLock

从Java 5开始,引入了一个高级的处理并发的java.util.concurrent包,它提供了大量更高级的并发功能,能大大简化多线程程序的编写。Java语言直接提供了synchronized关键字用于加锁,但这种锁一是很重,二是获取时必须一直等待,没有额外的尝试机制。

java.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加锁:

public class Counter {
    private int count;

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

//替换之后
public class Counter {
    private final Lock lock = new ReentrantLock();
    private int count;

    public void add(int n) {
        lock.lock();
        try {
            count += n;
        } finally {
            lock.unlock();
        }
    }
}

ReentrantLock 的用法是,首先获取锁 new ReentrantLock();然后需要在 finally 语句块儿中正确释放锁。

顾名思义,ReentrantLock是可重入锁,它和synchronized一样,一个线程可以多次获取同一个锁。和synchronized不同的是,ReentrantLock可以尝试获取锁:

if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try {
        ...
    } finally {
        lock.unlock();
    }
}

上述代码在尝试获取锁的时候,最多等待1秒。如果1秒后仍未获取到锁,tryLock()返回false,程序就可以做一些额外处理,而不是无限等待下去。所以,使用ReentrantLock比直接使用synchronized更安全,线程在tryLock()失败的时候不会导致死锁 (死锁会在文章下面讲到)。

6.3 synchronized中的类锁和对象锁

对象锁:锁住的是对象实例,也就是锁住的是一个对象的内存空间,适用于对象实例方法上 ,或者一个对象实例上的一种锁;

类锁:锁住的是class类,class类对象只有一个,适用于一个类的class对象上 ,或者类的静态方法上;

6.3.1 对象锁

让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装。更好的方法是把synchronized逻辑封装起来。例如,我们编写一个计数器如下:

public class Counter {
    private int count = 0;

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

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

    public int get() {
        return count;
    }
}

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

var c1 = Counter();
var c2 = Counter();

// 对c1进行操作的线程:
new Thread(() -> {
    c1.add();
}).start();
new Thread(() -> {
    c1.dec();
}).start();

// 对c2进行操作的线程:
new Thread(() -> {
    c2.add();
}).start();
new Thread(() -> {
    c2.dec();
}).start();

当我们锁住的是对象 this 实例时,可以用synchronized修饰这个方法。下面两种写法是等价的:

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

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

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

6.3.2 类锁

再思考一下,如果对一个静态方法添加synchronized修饰符,它锁住的是哪个对象呢?我们知道静态方法是是没有 this 实例的,因为static方法是针对类而不是实例的。因此,对static方法添加synchronized,锁住的是该类的 Class 实例,为此下面的两种方式是等价的:

public synchronized static void test(int n) {
    ...
}

public class Counter {
    public static void test(int n) {
        synchronized(Counter.class) {
            ...
        }
    }
}

注意读取一个变量是不需要同步的,但是如果是读取多行,则是需要进行同步处理的:

public class Counter {
    private int count;

    public int get() {
        // 不需要同步处理
        return count;
    }
    ...
}


public class Counter {
    private int first;
    private int last;

    public Pair get() {
        // 需要做同步处理
        Pair p = new Pair();
        p.first = first;
        p.last = last;
        return p;
    }
    ...
}

6.3.3 类锁与对象锁的区别

我们通过实际举例来介绍一下 二者之间的不同:

6.3.3.1 对象锁的使用方式:

public class SynchronizedDemo {
    //同步方法,对象锁
    public synchronized void syncMethod() {
        
    }
    //同步块,对象锁
    public void syncThis() {
        synchronized (this) {
           
        }
    }
}

6.3.3.2 类锁的使用方式:

public class SynchronizedDemo {
    //同步 class 对象,类锁
    public void syncClassMethod() {
        synchronized (SynchronizedDemo.class) {
            
        }
    }
    //同步静态方法,类锁
    public static synchronized void syncStaticMethod(){

    }
}

6.4 线程安全知识进阶

       接下来,我们通过多种场景,包括多个线程使用同一个对象锁和不同的两个对象锁,以及与类锁混合调用时,来一起了解一下什么情况下是线程安全的,什么情况下是线程不安全的。

首先完我们看以下代码: 该类中包含:对象锁、类锁、普通方法,我们接下来,分情况来看一下在不同组合调用的情况下,是否是线程安全的。

public class SynchronizedObj {
    private int ticket = 10;
    //同步方法,对象锁
    public synchronized void syncMethod() {
        for (int i = 0; i < 1000; i++) {
            ticket--;
            System.out.println(Thread.currentThread().getName() 
                                             + "剩余的票数:" + ticket);
        }
    }
    //同步块,对象锁
    public void syncThis() {
        synchronized (this) {
            for (int i = 0; i < 1000; i++) {
                ticket--;
                System.out.println(Thread.currentThread().getName() 
                                              + "剩余的票数:" + ticket);
            }
        }
    }
    //同步class对象,类锁
    public void syncClassMethod() {
        synchronized (SynchronizedObj.class) {
            for (int i = 0; i < 50; i++) {
                ticket--;
                System.out.println(Thread.currentThread().getName()
                                                + "剩余的票数:" + ticket);
            }
        }
    }
    
    //普通方法
    public void somethMethod() {
        for (int i = 0; i < 50; i++) {
             ticket--;
             System.out.println(Thread.currentThread().getName()+"剩余的票数:" + ticket);
        }
    }
}

情况一:

使用三个线程,同时操作同一个共享变量对象中的 同步方法(对象锁)、同步块(对象锁)、普通方法,这种情况是互不影响的,即线程安全的:

public static void main(String[] args){

    final SynchronizedObj synchronizedDemo = new SynchronizedObj();

    //线程一
    new Thread() {
        @Override
        public void run() {
            synchronizedDemo.syncMethod();
        }
    }.start();

    //线程二,多个线程同时访问同一个对象锁时,线程二需要等待线程一释放对象锁之后,才能进行访问。
    new Thread() {
        @Override
        public void run() {
            synchronizedDemo.syncThis();
        }
    }.start();

    //线程三,线程三访问的是对象实例里面的不带同步的普通方法。因此不会受对象锁的限制,随时可以进行访问。
    new Thread() {
        @Override
        public void run() {
            synchronizedDemo.somethMethod();
        }
    }.start();
}

情况二:

使用两个线程,同时操作两个共享变量对象实例中的同步方法时,这种情况也是互不影响的,即线程安全的:

public static void main(String[] args){
    final SynchronizedObj synchronizedDemo1 = new SynchronizedObj();
    final SynchronizedObj synchronizedDemo2 = new SynchronizedObj();
    //线程一
    new Thread() {
        @Override
        public void run() {
            synchronizedDemo1.syncMethod();
        }
    }.start();
    //线程二
    new Thread() {
        @Override
        public void run() {
            synchronizedDemo2.syncMethod();
        }
    }.start();
}

情况三:

使用两个线程,同时操作同一个共享变量对象实例中的对象锁同步方法、类锁同步方法时,这种情况是会产生影响的,即线程不安全的:

public static void main(String[] args){

    final SynchronizedObj synchronizedDemo = new SynchronizedObj();

    //线程一
    new Thread() {
        @Override
        public void run() {
            synchronizedDemo.syncMethod();
        }
    }.start();

    //线程二
    new Thread() {
        @Override
        public void run() {
            synchronizedDemo.syncClassMethod();
        }
    }.start();
}

情况四:

使用两个线程,同时操作不同的两个共享变量对象实例中的同一个 类锁 同步方法时,访问时是互不影响的,即线程安全的:

public static void main(String[] args){

    final SynchronizedObj synchronizedDemo1 = new SynchronizedObj();
    final SynchronizedObj synchronizedDemo2 = new SynchronizedObj();

    //线程一
    new Thread() {
        @Override
        public void run() {
            synchronizedDemo1.syncClassMethod();
        }
    }.start();

    //线程二
    new Thread() {
        @Override
        public void run() {
            synchronizedDemo2.syncClassMethod();
        }
    }.start();
}

七、死锁现象

在理解死锁之前,我们先来了解一下可重入锁,Java的线程锁是可重入的锁,什么是可重入锁呢?

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

看下面的例子:

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锁。这就是可重入锁。

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

来看个例子:

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的顺序。

八、线程锁的种类

  • 乐观锁/悲观锁
  • 独享锁/共享锁
  • 互斥锁/读写锁
  • 可重入锁
  • 公平锁/非公平锁
  • 分段锁
  • 偏向锁/轻量级锁/重量级锁
  • 自旋锁

8.1 乐观锁与悲观锁

乐观锁与悲观锁并不是特指某两种类型的锁,它们是从看待并发同步的角度,抽象出来的一个概念。

8.1.1 乐观锁

顾名思义,就是每次去拿数据的时候都很乐观,总是认为别人不会对所需数据进行修改,所以不会对操作上锁,但是在更新 回写到 主内存的时候,会对是否有人对其进行过修改做一个判断。

乐观锁适用于 多读的并发场景,吞吐量高。

典型应用: CAS 原子操作。

8.1.2 悲观锁

顾名思义,总是做最坏的打算,总是认为别人会对所需数据进行修改,所以每次操作数据的时候,都会上锁。

悲观锁适用于 多写的并发场景。

典型应用:   synchronized 关键字。

8.2 独享锁/共享锁

独享锁是指该锁一次只能被一个线程所持有。理所当然,synchronized、ReentrantLock 就是一种独享锁了。

共享锁是指该锁可以被多个线程所持有。比如 Lock 的另一个实现类 ReadWriteLock,它的写锁是一个独享锁,它的读锁则是一个共享锁

public class Counter {
    private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
    private final Lock rlock = rwlock.readLock();
    private final Lock wlock = rwlock.writeLock();
    private int[] counts = new int[10];

    public void inc(int index) {
        wlock.lock(); // 加写锁
        try {
            counts[index] += 1;
        } finally {
            wlock.unlock(); // 释放写锁
        }
    }

    public int[] get() {
        rlock.lock(); // 加读锁
        try {
            return Arrays.copyOf(counts, counts.length);
        } finally {
            rlock.unlock(); // 释放读锁
        }
    }
}

把读写操作分别用读锁和写锁来加锁,在读取时,多个线程可以同时获得读锁,这样就大大提高了并发读的执行效率。

8.3 互斥锁/读写锁

上面讲的独享锁/共享锁是一种广义的说法,互斥锁/读写锁就是具体的实现:

  • 互斥锁在Java中的具体实现就是ReentrantLock。
  • 读写锁在Java中的具体实现就是ReadWriteLock。

8.4 可重入锁

Java的线程锁是可重入的锁,什么是可重入锁呢?

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

看下面的例子:

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锁。

8.5 公平锁/非公平锁

公平锁 是指多个线程会按照申请锁的顺序来获取锁。

非公平锁 是指多个线程获取锁的顺序是不固定的。

ReetrantLock 可以通过构造函数来设置,是公平锁还是非公平锁。ReetrantLock默认是非公平锁,Synchronized 也是一种非公平锁。非公平锁的优点在于吞吐量比公平锁大。

8.6 分段锁

分段锁并不是具体的一种锁,它是一种设计思想,目的是实现高效的并发操作。比如: ConcurrentHashMap 就是通过分段锁的形式来实现高并发操作的。ConcurrentHashMap 的分段锁是 Segment,它类似于 HashMap 的结构,即内部拥有一个 Entry 数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。

当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。

分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

8.7 偏向锁/轻量级锁/重量级锁

这三种锁实际指的是 synchronized 状态的三个阶段。是从 Java 5 引入的 锁状态升级机制,目的是实现高效的 synchronized。

偏向锁:是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

轻量级锁: 是指偏向锁状态下,被另一个线程所访问,此时的偏向锁就会升级为 轻量级锁。在轻量级锁状态下,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

重量级锁:重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。

 8.8 自旋锁

在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

九、线程池

Java 语言内置了多线程支持,启动一个新线程非常方便,但是创建线程是需要操作系统资源的,频繁创建和销毁大量线程,需要消耗大量的时间。如果复用一组线程,而不是一个任务对应一个新的线程,是可以减少CPU资源的消耗以及时间的消耗的。这种能接收大量小任务并进行分发处理的就是线程池

简单地说,线程池内部维护着若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配其中一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么线程池中再增加一个新线程进行处理。 

ExecutorService

Java标准库提供了 ExecutorService 接口表示线程池,典型用法如下:

// 创建固定大小的线程池:  (包含三个线程)
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任务:
executor.submit(new Task("task1"));
executor.submit(new Task("task2"));


class Task implements Runnable {
    private final String name;

    public Task(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println("start task " + name);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        System.out.println("end task " + name);
    }
}

newFixedThreadPool 是指设置一个固定数量的线程池,这个后面会讲到,我们先来看一下 ThreadPoolExecutor。

ThreadPoolExecutor

ExecutorService是个接口,而 ThreadPoolExecutor 则是具体的实现类。我们可以通过它的构造方法来配置线程池的参数:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {
}
参数解释:
corePoolSize         :线程池中核心线程的数量;
maximumPoolSize :线程池中最大线程数;当任务超过这个最大值式,就会被阻塞。最大线程数=核心线程 + 非核心线程数;只有核心线程不够用时,才创建非核心线程,执行完任务后,非核心线程会被销毁;
keepAliveTime       :非核心线程数的超时时长,当非核心线程执行的时间超过这个设定值时,就会被回收。如果想同时对核心线程起作用,则需要将 allowCoreThreadTimeOut 设置为true。
unit                         :keepAliveTime 对应的时间单位 TimeUnit;
workQueue            :线程池中的任务队列;我们提交的 Runnable 任务,就被存储在这个集合中;
RejectedExecutionHandler   :缓冲池对已拒绝任务的处理策略;

线程池 ThreadPoolExecutor 的优势

1. 线程复用 降低性能开销
     复用已经创建好的线程,避免频繁创建和销毁,从而导致性能的消耗。
2. 合理分配线程资源
     控制线程的并发数,合理使用系统资源,提高应用性能。
3. 线程管理
     可以有效的控制线程的执行,比如:定时执行、取消执行等。

线程池ThreadPoolExecutor的分配规则

1. 如果提交到线程池中的任务数量未达到核心线程数量时,则会启动核心线程去执行任务;

2. 如果提交到线程池中的任务数已经达到核心线程数时,且任务队列 workQueue未满,则将新任务放入队列中等待执行。

3. 如果提交到线程池中的任务数已经达到核心线程数,但是未超过线程池规定最大值,且 workQueue 已满,则会开启一个非核心线程来执行新添加的任务。

4. 如果提交到线程池中的任务数已经超过线程池规定的最大值则会拒绝执行该任务,采取拒绝策略,并抛出  RejectedExecutionExeption 异常

拒绝策略 RejectedExecutionHandler

  • AbortPolicy
  • DiscardPolicy
  • DiscardOldestPolicy
  • CallerRunsPolicy
  • 自定义

◇AbortPolicy
该策略是线程池的默认策略。使用该策略时,如果线程池队列满了的情况下,就丢掉这个任务并且抛出RejectedExecutionException异常。
源码如下:

 public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
      //不做任何处理,直接抛出异常
       throw new RejectedExecutionException(
                 "Task " + r.toString() + " rejected from " + e.toString()
           );
}

◇DiscardPolicy

这个策略是AbortPolicy的silent版本,如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常。

源码如下:

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    //就是一个空的方法
}

◇DiscardOldestPolicy

这个策略从字面上也很好理解,丢弃最老的。也就是说如果队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列。

因为队列是队尾进,队头出,所以队头元素是最老的,因此每次都是移除对头元素后再尝试入队。

源码如下:

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
        //移除队头元素
        e.getQueue().poll();
        //再尝试入队
        e.execute(r);
    }
}

CallerRunsPolicy

使用此策略,如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行。就像是个急脾气的人,我等不到别人来做这件事就干脆自己干。

源码如下:

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
       //直接执行run方法
       r.run();
    }
}

自定义
如果以上策略都不符合业务场景,那么可以自己定义一个拒绝策略,只要实现RejectedExecutionHandler接口,并且实现rejectedExecution方法就可以了。具体的逻辑就在rejectedExecution方法里去定义就OK了。
例如:我定义了我的一个拒绝策略,叫做MyRejectPolicy,里面的逻辑就是打印处理被拒绝的任务内容

public class MyRejectPolicy implements RejectedExecutionHandler{
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        //Sender是我的Runnable类,里面有message字段
        if (r instanceof Sender) {
            Sender sender = (Sender) r;
            //直接打印
            System.out.println(sender.getMessage());
        }
    }
}

这几种策略没有好坏之分,只是适用不同场景,具体哪种合适根据具体场景和业务需要选择,如果需要特殊处理就自己定义好了。

线程池分类

线程池根据不同的特性分为四种,它们都是直接或者间接通过ThreadPoolExecutor来实现的。

  • FixedThreadPool
  • CachedThreadPool
  • ScheduledThreadPool
  • SingleThreadExecutor

9.1 FixedThreadPool

     通过Executors对象的newFixedThreadPool() 方法创建;线程数固定,全都是核心线程,
     没有超时机制,排队任务无限制,所以响应较快,不用担心线程会被回收。
     适用场景:执行长期任务,性能好很多。
// newFixedThreadPool 源码
// 参数nThreads,即为我们设置的核心线程数量
public static ExecutorService newFixedThreadPool(int nThreads){ 
    return new ThreadPoolExecutor(
                                  nThreads, 
                                  nThreads, 
                                  0L, 
                                  TimeUnit.MILLISECONDS, 
                                  new LinkedBlockingQueue<Runnable>());
 } 

// 使用方式
ExecutorService mExecutor = Executors.newFixedThreadPool(5);
Runnable myRunnable = new Runnable() { 
    @Override public void run() { 
        Log.i("myRunnable", "run"); 
    } 
}; 
mExecutor.execute(myRunnable);

9.2 CachedThreadPool

通过 Executors 对象的 newCachedThreadPool() 方法来创建;它是一个数量不限的线程池,它所拥有的线程都是非核心线程,即不需要指定固定核心线程数量。当有新的任务来时,如果没有空闲的线程,则直接创建新的线程,不会去排队而是直接执行,并且超时时间都是 60s 。所以它适合执行大量耗时小的任务。由于设置了超时时间为 60s,所以当线程空闲一定时间时,就会被系统回收,所以理论上,该线程池不会有占用系统资源的无用线程的情况。

适用场景:执行很多短期异步的任务

// CachedThreadPool 源码
public static ExecutorService newCachedThreadPool(){ 
    return new ThreadPoolExecutor( 
                                  0, 
                                  Integer.MAX_VALUE, 
                                  60L, 
                                  TimeUnit.SECONDS, 
                                  new SynchronousQueue<Runnable>() 
                                 );
 }

//使用方式
ExecutorService mExecutor = Executors.newCachedThreadPool();

9.3 ScheduledThreadPool

有一种任务需要定期反复执行,例如,每秒刷新证券价格。这种任务本身固定,需要反复执行的,可以使用 ScheduledThreadPool。放入ScheduledThreadPool的任务可以定期反复执行。它可以设置核心线程数,且可以有数量不限的非核心线程数,但是它里面的非核心线程超时时间为 0s,就是非核心线程数一旦执行完就会立刻被回收。

适用场景:适合用于执行定时任务和固定周期的重复任务。

// 源码
// 参数corePoolSize 为核心线程数量
public static ScheduledThreadPool newScheduledThreadPool(int corePoolSize){ 
    return new ScheduledThreadPoolExecutor(corePoolSize); 
}

public ScheduledThreadPoolExecutor(int corePoolSize){ 
    super(
          corePoolSize, 
          Integer.MAX_VALUE, 
          0, 
          NANOSECONDS, 
          new DelayedWorkQueue()
         ); 
}

//使用方式
ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);
// 1秒后执行一个 一次性任务
ses.schedule(new Task("one-time"), 1, TimeUnit.SECONDS);
// 2秒后开始执行定时任务,每3秒执行:
ses.scheduleAtFixedRate(new Task("fixed-rate"), 2, 3, TimeUnit.SECONDS);
// 2秒后开始执行定时任务,以3秒为间隔执行:
ses.scheduleWithFixedDelay(new Task("fixed-delay"), 2, 3, TimeUnit.SECONDS);

 FixedRate和FixedDelay的区别

 FixedRate是指任务总是以固定时间间隔触发,不管任务执行多长时间:

FixedDelay是指,上一次任务执行完毕后,等待固定的时间间隔,再执行下一次任务:

Java标准库还提供了一个java.util.Timer类,这个类也可以定期执行任务,但是,一个Timer会对应一个Thread,所以,一个Timer只能定期执行一个任务,多个定时任务必须启动多个Timer,而一个ScheduledThreadPool就可以调度多个定时任务,所以,我们完全可以用ScheduledThreadPool取代旧的Timer

9.4 SingleThreadExecutor

通过 Executors 对象的 newSingleThreadExecutorl() 方法来创建;只有一个核心线程,它确保所有任务进来都要排队顺序执行。它的意义在于,统一所有外界任务到同一线程中,让调用者忽略线程同步的问题。

适用场景:一个任务一个任务的执行的场景

public static ExecutorService newSingleThreadExecutor(){ 
    return new FinalizableDelegatedExecutorService(
                                     new ThreadPoolExecutor( 
                                             1, 
                                             1, 
                                             0L,                                                                                                                                                                                                                                 
                                             TimeUnit.MILLISECONDS, 
                                             new LinkedBlockingQueue<Runnable>()
                                     )
    ); 
}

线程池相关API:

  • shutDown() ——— 关闭线程池,会执行完已提交的任务;
  • shutDownNow() ——— 关闭线程池,并尝试结束已提交的任务;
  • awaitTermination() ------ 等待指定的时间关闭线程池;
  • allowCoreThreadTimeOut(boolean flag) ——— 允许核心线程在闲置超时时进行回收;
  • execute() ——— 提交任务,无返回值;
  • submit() ——— 提交任务,最终还是调用了execute()方法,不同的是,submit() 会返回一个 Future对象;我们还可以通过返回的Future对象里的cancel方法,取消一个Future的执行。
  • beforeExecute() ——— 任务执行前执行的方法;
  • afterExecute() ——— 任务执行结束后执行的方法;
  • terminated() ——— 线程池关闭后执行的方法;

十、Java中 Concurrent 并发集合

Java标准库的 java.util.concurrent包提供的线程安全的集合如下:

interfacenon-thread-safethread-safe
ListArrayListCopyOnWriteArrayList
MapHashMapConcurrentHashMap
SetHashSet / TreeSetCopyOnWriteArraySet
QueueArrayDeque / LinkedListArrayBlockingQueue / LinkedBlockingQueue
DequeArrayDeque / LinkedListLinkedBlockingDeque

十一、Atomic 原子操作类

Java标准库中的 java.util.concurrent 包下,除了提供底层锁、并发集合外,还提供了一组基于CAS实现原子操作的封装类,这些原子操作类,位于 java.util.concurrent.atomic 包中。

在Java语言中,++i和i++操作并不是线程安全的,在使用的时候,不可避免的会用到 synchronized关键字。而AtomicInteger则通过一种线程安全的加减操作接口。

我们以AtomicInteger为例,它提供的主要操作有:

  • 获取当前值:                                get()
  • 获取当前的值,并设置新的值:   getAndSet(int newValue)
  • 获取当前的值,并自增:              getAndIncrement()
  • 获取当前的值,并自减:              getAndDecrement()
  • 获取当前的值,并加上预期的值:getAndAdd(int delta)
  • 增加值并返回新值:                      addAndGet(int delta)
  • 加1后返回新值:                           incrementAndGet()
  • 用CAS方式设置:                         compareAndSet(int expect, int update)

Atomic类是通过 无锁的方式 实现的 线程安全访问。它的主要原理是利用了CAS。

下面我们来通过一个例子对比一下 AtomicInteger 的优势:

// 普通线程同步
class Test2 {
        private volatile int count = 0;

        public synchronized void increment() {
            count++; //若要线程安全执行执行count++,需要加锁
        }

        public int getCount() {
            return count;
        }
}

//使用 AtomicInteger
class Test2 {
        private AtomicInteger count = new AtomicInteger();

        public void increment() {
           // 使用AtomicInteger之后,不需要加锁,也可以实现线程安全。
           count.incrementAndGet();
        }
       
       public int getCount() {
           return count.get();
        }
}

从上面的例子中我们可以看出:使用AtomicInteger是非常的安全的.而且因为AtomicInteger由硬件提供原子操作指令实现的。在非激烈竞争的情况下,开销更小,速度更快。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值